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.

Documentation menu

On each target

Pair the enforcer (once)
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 behind the ALB
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

services/enforcer/integrations/aws/alb/main.tf
# 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.