AWS ALB
The ALB terminates mutual TLS against a trust store holding the client CA bundle and forwards the leaf in X-Amzn-Mtls-Clientcert-Leaf. mt-enforcer runs in-path on each target, re-validates the leaf, authorizes the request and proxies allowed requests to the origin on loopback.
On each target
mt-enforcer pair --api https://api.mutualtrust.io --state /var/lib/mt-enforcer
# shows a pairing code; approve it in the portal (Services > your service > Pair gateway)mt-enforcer proxy --state /var/lib/mt-enforcer \
--peer-source forwarded --format aws-alb \
--trusted-proxies 10.0.1.0/24,10.0.2.0/24 \
--listen 0.0.0.0:8443 --admin 0.0.0.0:9464 \
--origin http://127.0.0.1:9001--trusted-proxies lists the ALB subnets; requests from anywhere else get 403 untrusted_forwarder before the header is read. In passthrough mode the enforcer reads X-Amzn-Mtls-Clientcert instead; when both headers are present they must carry the same leaf.
Terraform
# Mutual Trust with an AWS Application Load Balancer.
#
# The ALB terminates mutual TLS in verify mode against a trust store holding the
# client CA bundle from the signed manifest, and forwards the leaf in
# X-Amzn-Mtls-Clientcert-Leaf. An ALB cannot call an external authorizer, so
# mt-enforcer runs in-path on every target (proxy mode, peer source forwarded):
# it accepts requests only from the ALB subnets, re-validates the leaf against
# the signed bundle, authorizes each request and proxies allowed requests to the
# origin on loopback.
#
# Before applying:
# mt-enforcer pair --api https://api.mutualtrust.io --state ./mt-state
# mt-enforcer bundle --state ./mt-state --out client-ca.pem
# Re-run `mt-enforcer bundle` and apply again when the space adds or rotates an issuer.
terraform {
required_version = ">= 1.6"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.80" }
}
}
variable "vpc_id" { type = string }
variable "alb_subnet_ids" { type = list(string) }
variable "alb_subnet_cidrs" { type = list(string) } # --trusted-proxies on the enforcer
variable "target_instance_ids" { type = list(string) }
variable "server_certificate_arn" { type = string }
variable "bundle_bucket" { type = string }
resource "aws_s3_object" "client_ca" {
bucket = var.bundle_bucket
key = "mutualtrust/client-ca.pem"
source = "${path.module}/client-ca.pem"
etag = filemd5("${path.module}/client-ca.pem")
}
resource "aws_lb_trust_store" "mt" {
name = "mt-orders"
ca_certificates_bundle_s3_bucket = var.bundle_bucket
ca_certificates_bundle_s3_key = aws_s3_object.client_ca.key
}
resource "aws_security_group" "alb" {
name = "mt-orders-alb"
vpc_id = var.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Targets accept the enforcer ports only from the ALB. Together with
# --trusted-proxies this is what makes the ALB certificate header trustworthy.
resource "aws_security_group" "targets" {
name = "mt-orders-targets"
vpc_id = var.vpc_id
ingress {
description = "mt-enforcer proxy"
from_port = 8443
to_port = 8443
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
ingress {
description = "mt-enforcer readiness (/readyz)"
from_port = 9464
to_port = 9464
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # control plane: signed policy, ACKs, receipts
}
}
resource "aws_lb" "mt" {
name = "mt-orders"
load_balancer_type = "application"
subnets = var.alb_subnet_ids
security_groups = [aws_security_group.alb.id]
drop_invalid_header_fields = true
}
resource "aws_lb_target_group" "enforcer" {
name = "mt-orders-enforcer"
port = 8443
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
port = "9464"
path = "/readyz" # 200 only while signed policy is fresh
matcher = "200"
}
}
resource "aws_lb_target_group_attachment" "enforcer" {
for_each = toset(var.target_instance_ids)
target_group_arn = aws_lb_target_group.enforcer.arn
target_id = each.value
port = 8443
}
resource "aws_lb_listener" "mtls" {
load_balancer_arn = aws_lb.mt.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.server_certificate_arn
mutual_authentication {
mode = "verify" # "passthrough" also works: the enforcer then reads X-Amzn-Mtls-Clientcert
trust_store_arn = aws_lb_trust_store.mt.arn
ignore_client_certificate_expiry = false
advertise_trust_store_ca_names = "on"
}
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.enforcer.arn
}
}
# On each target, next to an origin bound to 127.0.0.1:9001 (systemd unit, ECS
# sidecar container or the Helm chart in integrations/kubernetes):
output "enforcer_command" {
value = join(" ", [
"mt-enforcer proxy --state /var/lib/mt-enforcer",
"--peer-source forwarded --format aws-alb",
"--trusted-proxies ${join(",", var.alb_subnet_cidrs)}",
"--listen 0.0.0.0:8443 --admin 0.0.0.0:9464",
"--origin http://127.0.0.1:9001",
])
}Trust store and revocation
mt-enforcer bundle writes the verified client CA bundle for the trust store; re-run it and apply when the space adds or rotates an issuer. Revocation does not depend on the ALB: the enforcer applies the manifest's deny set (revoked serials, suspended and withdrawn grants) on every request.
Health checks
The target group checks /readyz on the admin port, which is 200 only while the enforcer holds fresh signed policy, so the ALB stops routing to a target that would fail closed.