Mutual TLS Between Tile Services and Upstream APIs
A tile platform is rarely self-contained. The renderer calls a geocoding service, an elevation API, a partner’s feature service, a licensing check. Each of those calls carries a credential, and a static API key in an environment variable is a credential that leaks in logs, survives in images, and cannot be revoked for one caller without revoking it for all. Mutual TLS replaces it with a certificate that proves who the caller is at the transport layer, cannot be replayed from a log line, and expires whether or not anyone remembers to rotate it. This guide extends TLS Certificate Automation within Network Security and Access Control.
What mutual TLS actually changes
In ordinary TLS the client verifies the server. In mutual TLS both directions verify, so the server also demands a certificate from the client and validates it against a certificate authority it trusts. Three consequences follow, and the third is the one that decides whether this is worth the effort.
The credential cannot be copied out of a log. A bearer token appears in headers, proxies and traces; a private key never travels on the wire, so an attacker with a full request capture still cannot impersonate the client.
Revocation is per identity and immediate. Each caller has its own certificate. Removing one caller is revoking one certificate, not rotating a shared secret and redeploying every consumer of it.
Expiry is enforced by the protocol. This is the double edge. A certificate that expires stops working, in production, at a moment determined months earlier by someone who has probably moved on. Mutual TLS without automated renewal is a scheduled outage, and the automation is not optional in the way it is for a server certificate that at least produces a browser warning first.
Prerequisites and environment assumptions
Terraform 1.6 or later with hashicorp/aws pinned at ~> 5.60. A private certificate authority — a managed one, or an internal authority whose issuance you can automate; a manually operated authority reintroduces exactly the human step this design removes. Somewhere to store issued certificates and keys that the workload can read at start-up, which is a secrets manager rather than a container image. And an upstream service that supports client certificate validation; if the upstream is a third party that only offers API keys, mutual TLS is not available for that hop and the honest answer is to say so rather than to build something that looks like it.
Decide the certificate lifetime deliberately. Short lifetimes — days rather than years — are better security and demand more reliable automation. The workable rule is that lifetime should be several times the renewal interval, so a single failed renewal is a warning rather than an outage, and monitoring should alert on time-to-expiry rather than on renewal failure alone.
Step-by-step implementation
-
Create a private certificate authority for internal service identity. It should be distinct from whatever issues your public server certificates, because the trust decisions are different: this authority’s certificates say “this is a service in our platform”, and nothing outside the platform should trust it.
-
Issue one certificate per calling service, not per environment. The subject common name should identify the service —
tile-renderer.prod— because that string is what the upstream will authorise against, and a certificate shared across services makes per-caller revocation impossible. -
Store the certificate and key in a secrets manager and mount at start-up. The private key must not be in the image, in the task definition, or in an environment variable that appears in a console. Fetching at start-up also means a rotated certificate is picked up on the next deployment or restart.
-
Configure the upstream to require and authorise the client certificate. Requiring a certificate is authentication; checking which certificate is authorisation, and only the second one distinguishes an allowed caller from any other holder of a certificate from the same authority. Match on the subject or on a certificate attribute, and treat “signed by our authority” alone as insufficient.
-
Automate renewal with headroom and alert on expiry. A renewal job that runs at one third of the certificate lifetime gives two failures’ worth of slack. The alert must be on remaining validity, because a renewal job that has silently stopped running reports no failures at all.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.60" }
}
}
resource "aws_acmpca_certificate_authority" "internal" {
type = "ROOT"
certificate_authority_configuration {
key_algorithm = "RSA_2048"
signing_algorithm = "SHA256WITHRSA"
subject {
common_name = "Spatial Platform Internal CA"
organization = "Example Mapping"
}
}
# Distinct from whatever issues public server certificates: this authority
# says "a service inside our platform", and nothing outside should trust it.
usage_mode = "SHORT_LIVED_CERTIFICATE"
}
# One certificate per CALLING SERVICE. Sharing one across services makes
# per-caller revocation impossible, which is most of the point.
resource "aws_acm_certificate" "tile_renderer_client" {
domain_name = "tile-renderer.prod.internal"
certificate_authority_arn = aws_acmpca_certificate_authority.internal.arn
key_algorithm = "RSA_2048"
options {
certificate_transparency_logging_preference = "DISABLED"
}
lifecycle {
create_before_destroy = true
}
}
# The private key is fetched at start-up, never baked into an image and never
# placed in an environment variable that a console will render.
resource "aws_secretsmanager_secret" "client_cert" {
name = "tls/client/tile-renderer"
tags = {
Service = "tile-renderer"
Rotation = "automated"
}
}
# Alert on REMAINING VALIDITY, not on renewal failure: a renewal job that has
# silently stopped running produces no failures at all.
resource "aws_cloudwatch_metric_alarm" "client_cert_expiry" {
alarm_name = "tile-renderer-client-cert-expiry"
comparison_operator = "LessThanThreshold"
evaluation_periods = 1
threshold = 10 # days
metric_name = "DaysToExpiry"
namespace = "AWS/CertificateManager"
period = 86400
statistic = "Minimum"
dimensions = { CertificateArn = aws_acm_certificate.tile_renderer_client.arn }
alarm_actions = [var.oncall_topic_arn]
}
variable "oncall_topic_arn" { type = string }
# Upstream side. Requiring a certificate is AUTHENTICATION; checking which
# certificate is AUTHORISATION, and only the second distinguishes an allowed
# caller from any other holder of a certificate from the same authority.
server {
listen 443 ssl;
server_name elevation-api.internal;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
ssl_client_certificate /etc/ssl/internal-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
location / {
# "Signed by our CA" is not enough — every service in the platform has
# one of those. Authorise the specific subject.
if ($ssl_client_s_dn !~ "CN=tile-renderer\.prod\.internal") {
return 403;
}
proxy_set_header X-Client-Identity $ssl_client_s_dn;
proxy_pass http://elevation_backend;
}
}
Verification
Prove both halves separately. Connect from the renderer with its certificate and confirm the call succeeds. Then connect without a certificate and confirm the upstream refuses — that proves ssl_verify_client is actually on rather than configured and not reloaded. Then, most importantly, connect with a different valid certificate from the same authority and confirm the upstream refuses it too. That third test is the one that distinguishes authentication from authorisation, and it is the one most often skipped.
Confirm the private key is not reachable anywhere it should not be: not in the image layers, not in the task definition, not in an environment variable listed by the console. Finally, confirm the renewal automation works by observing a real renewal rather than trusting the schedule — the first renewal is where a misconfigured issuance permission shows up, and it is far better to see it while the old certificate has three weeks left.
Preventing recurrence
- Alarm on days remaining, per certificate. The certificate that expires is always the one nobody had an alarm on, and a per-certificate alarm derived from the module means new services get one automatically.
- Keep issuance in code. A certificate issued by hand during an incident has no renewal automation attached and will expire silently in a year.
- Record the authorised subject alongside the upstream configuration. The authorisation rule is a string match, and a service rename that changes the subject breaks the call in a way whose error message points at TLS rather than at the rename.
- Rehearse revocation. Revoking one caller’s certificate and confirming that only that caller stops working is the test that proves the per-identity property you adopted mutual TLS for.
Frequently Asked Questions
Is a certificate signed by our authority enough to authorise a caller?
No, and treating it as enough is the most common mistake in a mutual TLS deployment. Every service in the platform holds such a certificate, so validation alone lets any of them call any upstream. Authorise on the subject or on a specific certificate attribute in addition to validating the chain.
What lifetime should client certificates have?
Short, provided renewal is automated — days to weeks rather than years. The rule that matters is that the lifetime should be several times the renewal interval, so one failed renewal is a warning and not an outage.
Can I use mutual TLS with a third-party API?
Only if they support it. Many do not, and the honest response is to keep an API key for that hop while confining it: store it in the secrets manager, rotate it on a schedule as described in Rotating PostGIS Credentials in Terraform Without Downtime, and keep it out of logs.
Does mutual TLS replace network controls?
No. It authenticates the caller; it does not stop an unauthorised host from reaching the endpoint and consuming resources trying. Keep the security group and routing controls in place — the layers answer different questions and both are cheap.
Related
- TLS Certificate Automation — the parent topic covering certificate lifecycle
- Automating ACM Certificates for Tile CDN Endpoints — the public server-certificate counterpart
- Secrets Management for Spatial Pipelines — where the client key is stored and how it reaches the workload
- Encryption and Key Management for Spatial Data — the in-transit chain this hop belongs to