Skip to content

Remote Attestation TLS (RA-TLS)

Choose file-based TLS or Nitro attestation TLS for HTTPS deployments

Containment Chamber supports two TLS modes: file for traditional certificate-based HTTPS, and ratls (remote attestation TLS) for deployments in AWS Nitro Enclaves.

When TLS is enabled, the signer API runs two listeners:

Port Protocol Purpose
9000 (default) HTTP Health probes only (/upcheck, /healthcheck)
9443 (default) HTTPS All API routes (encrypted)

The HTTP listener stays up for Kubernetes liveness and readiness probes. It does not serve /publicKeys, /sign, chamber, auth, or key-manager routes while TLS is enabled. Those APIs move to the HTTPS listener. The Prometheus metrics listener (:3000) remains separate and is always HTTP-only regardless of TLS mode.

direction: down
vc: "Validator Client\n(Lighthouse, Teku, Prysm...)"
ingress: "Ingress / Load Balancer"
cc: "Containment Chamber" {
style.fill: "#FFF6EF"
style.stroke: "#D35F0A"
style.font-color: "#170206"
http: "HTTP :9000\n/upcheck · /healthcheck"
https: "HTTPS :9443\n/publicKeys · /sign"
metrics_listener: "HTTP :3000\n/metrics"
}
probes: "Kubelet" {
style.fill: "#FEEC8C"
style.stroke: "#D35F0A"
style.font-color: "#170206"
live: "Liveness → /upcheck"
ready: "Readiness → /healthcheck"
}
metrics: "Prometheus Scraper"
vc.style.fill: "#FFF6EF"
vc.style.stroke: "#D35F0A"
vc.style.font-color: "#170206"
ingress.style.fill: "#F6F8FA"
ingress.style.stroke: "#6B7280"
ingress.style.font-color: "#170206"
cc.http.style.fill: "#FEEC8C"
cc.http.style.stroke: "#D35F0A"
cc.http.style.font-color: "#170206"
cc.https.style.fill: "#CAF2E6"
cc.https.style.stroke: "#13A477"
cc.https.style.font-color: "#170206"
cc.metrics_listener.style.fill: "#CAF2E6"
cc.metrics_listener.style.stroke: "#13A477"
cc.metrics_listener.style.font-color: "#170206"
metrics.style.fill: "#CAF2E6"
metrics.style.stroke: "#13A477"
metrics.style.font-color: "#170206"
probes.live.style.fill: "#FEEC8C"
probes.live.style.stroke: "#D35F0A"
probes.live.style.font-color: "#170206"
probes.ready.style.fill: "#FEEC8C"
probes.ready.style.stroke: "#D35F0A"
probes.ready.style.font-color: "#170206"
vc -> ingress -> cc.https: "TLS (encrypted)"
probes.live -> cc.http: "/upcheck" {style.stroke-dash: 3}
probes.ready -> cc.http: "/healthcheck" {style.stroke-dash: 3}
metrics -> cc.metrics_listener: "/metrics" {style.stroke-dash: 3}
Mode Certificate source Rotation Use case
disabled Default; no HTTPS
file PEM files on disk File replacement + poll cert-manager, Vault PKI, self-signed
ratls Auto-generated, bound to enclave attestation Automatic AWS Nitro Enclave deployments

File mode reads a certificate and private key from disk and polls for changes on a configurable interval. Cert rotation doesn’t require a restart — replace the files atomically and the signer picks them up on the next poll.

Generate a self-signed certificate (dev/test)

Section titled “Generate a self-signed certificate (dev/test)”
Terminal window
openssl req -x509 -newkey rsa:4096 \
-keyout tls.key -out tls.crt \
-days 365 -nodes \
-subj "/CN=containment-chamber"
tls:
mode: file
listen_port: 9443
file:
cert_path: /etc/certs/tls.crt
key_path: /etc/certs/tls.key
reload_interval_seconds: 60 # 0 = disable polling

Or via environment variables:

Terminal window
CONTAINMENT_TLS__MODE=file
CONTAINMENT_TLS__LISTEN_PORT=9443
CONTAINMENT_TLS__FILE__CERT_PATH=/etc/certs/tls.crt
CONTAINMENT_TLS__FILE__KEY_PATH=/etc/certs/tls.key

Create a Secret from your cert and key, then mount it into the pod:

apiVersion: v1
kind: Secret
metadata:
name: containment-chamber-tls
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded cert>
tls.key: <base64-encoded key>

Reference it in the Pod spec:

spec:
containers:
- name: containment-chamber
volumeMounts:
- name: tls
mountPath: /etc/certs
readOnly: true
env:
- name: CONTAINMENT_TLS__MODE
value: file
- name: CONTAINMENT_TLS__FILE__CERT_PATH
value: /etc/certs/tls.crt
- name: CONTAINMENT_TLS__FILE__KEY_PATH
value: /etc/certs/tls.key
volumes:
- name: tls
secret:
secretName: containment-chamber-tls

With cert-manager, annotate the Secret and let the controller rotate it. Point cert_path and key_path at the mounted volume path and set reload_interval_seconds: 60 — the signer will pick up renewed certs without a restart.

Replace the cert and key files atomically (write to a temp file, then mv) and wait up to reload_interval_seconds for the signer to reload. The old cert continues serving existing connections until the poll fires.


RA-TLS generates an ephemeral ECDSA P-256 TLS key pair at startup and on each configured certificate rotation. It binds the TLS public key hash (SPKI) into an AWS Nitro Secure Module (NSM) attestation document, then embeds that document in the X.509 certificate as a custom extension.

Clients that perform attestation verification get three guarantees from the TLS handshake:

  1. The server is running in a genuine AWS Nitro Enclave
  2. The enclave image, kernel, parent IAM role, and EIF signing certificate match the expected PCR measurements (PCR0, PCR1, PCR2, PCR3, PCR8)
  3. The TLS session goes to that specific enclave — not an interceptor

RA-TLS requires the signer compiled with the nitro feature and must run inside an actual enclave. See the Enclave deployment guide for infrastructure setup.

tls:
mode: ratls
listen_port: 9443
max_connections: 512 # cap on concurrent TLS sessions (applies to file + ratls modes)
ratls:
cert_validity_seconds: 86400 # cert lifetime: 24 hours
rotation_interval_seconds: 3600 # rotate the RA-TLS cert/key every hour

No cert files to manage. The signer generates and rotates certs automatically.

Every rotation_interval_seconds, the signer generates a new ephemeral key pair, obtains a fresh NSM attestation document binding the new public key hash, and starts serving the updated certificate to new connections. Active TLS sessions continue using the previous cert until they complete — there’s no connection drop.

direction: right
gen: "Key generation\n(ephemeral ECDSA)" {
style.fill: "#FFF6EF"
style.stroke: "#D35F0A"
style.font-color: "#170206"
keypair: "New key pair"
spki: "SPKI hash"
keypair -> spki
}
attest: "NSM attestation" {
style.fill: "#FEEC8C"
style.stroke: "#D35F0A"
style.font-color: "#170206"
doc: "Attestation document\n(PCR0 · PCR1 · PCR2 · PCR3 · PCR8 · SPKI hash)"
spki -> doc: "bound into"
}
cert: "X.509 certificate" {
style.fill: "#CAF2E6"
style.stroke: "#13A477"
style.font-color: "#170206"
pubkey: "TLS public key"
ext: "Custom extension\n(attestation document)"
}
tls: "TLS listener\n:9443"
tls.style.fill: "#CAF2E6"
tls.style.stroke: "#13A477"
tls.style.font-color: "#170206"
gen.keypair.style.fill: "#FFF6EF"
gen.keypair.style.stroke: "#D35F0A"
gen.keypair.style.font-color: "#170206"
gen.spki.style.fill: "#FFF6EF"
gen.spki.style.stroke: "#D35F0A"
gen.spki.style.font-color: "#170206"
attest.doc.style.fill: "#FEEC8C"
attest.doc.style.stroke: "#D35F0A"
attest.doc.style.font-color: "#170206"
cert.pubkey.style.fill: "#CAF2E6"
cert.pubkey.style.stroke: "#13A477"
cert.pubkey.style.font-color: "#170206"
cert.ext.style.fill: "#CAF2E6"
cert.ext.style.stroke: "#13A477"
cert.ext.style.font-color: "#170206"
timer.style.fill: "#F6F8FA"
timer.style.stroke: "#6B7280"
timer.style.font-color: "#170206"
keypair -> cert.pubkey: "public key"
doc -> cert.ext: "embedded"
cert -> tls: "served to clients"
timer: "Rotation timer\n(every N seconds)" {style.stroke-dash: 3}
timer -> gen: "triggers" {style.stroke-dash: 3}

RA-TLS Cert Rotation vs KMS Recipient Keys

Section titled “RA-TLS Cert Rotation vs KMS Recipient Keys”

RA-TLS certificate rotation and KMS RecipientAttestation use different keys for different jobs.

Mechanism Key type Lifetime Purpose
RA-TLS listener certificate ECDSA P-256 TLS key pair Startup, then tls.ratls.rotation_interval_seconds Proves the HTTPS endpoint is the attested enclave
KMS RecipientAttestation RSA-2048 recipient key pair One KMS decrypt request Lets KMS return plaintext encrypted only to that decrypt call

The RA-TLS key pair is the TLS server identity. It is rotated by the background certificate-rotation task before the certificate expires.

The KMS RSA key pair is not the TLS identity and is not timer-based. For each attested KMS decrypt, the enclave creates a fresh RSA public key, asks NSM for an attestation document containing that key, sends it to KMS as RecipientInfo, and decrypts the returned CMS envelope with the matching private key. The key is then dropped.

See AWS KMS Permissions for the KMS-side RecipientAttestation policy.

Clients that support RA-TLS verify the attestation document before trusting the connection:

  1. The client receives the TLS certificate during the handshake
  2. It extracts the attestation document from the X.509 extension
  3. It verifies the attestation document offline — validating the COSE_Sign1 envelope against the AWS Nitro Root CA G1 certificate pinned in the binary at build time (no network call to AWS)
  4. It checks the PCR measurements match the expected enclave binary
  5. It records those PCR values — future connections from a different binary are rejected

The first connection establishes trust. Subsequent connections verify the PCRs match. A cert from a different binary (even with a valid attestation) won’t be accepted.

Skip attestation verification (debugging only)

Section titled “Skip attestation verification (debugging only)”

During local development or testing outside a Nitro Enclave, you can disable attestation verification on the client side:

Terminal window
containment-chamber operator status \
--signer-url https://localhost:9443 \
--danger-skip-attestation-verification

“certificate file not found”

Check that cert_path and key_path are absolute paths and the signer process can read them. In Docker and Kubernetes, verify the volume is mounted at the expected path and the file names match exactly.

“TLS handshake failed” on the validator client side

The client doesn’t trust the signer’s certificate authority. For self-signed certs, either add the cert to the client’s trust store or configure the client to skip cert verification (acceptable in private networks, not for public endpoints).

RA-TLS: “attestation verification failed”

The PCR measurements in the attestation document don’t match what the client expects. This happens after a binary update (PCRs change) or when running outside a Nitro Enclave. Use --danger-skip-attestation-verification for local development only; binary upgrades require the client to re-establish trust via a new TOFU handshake.

KMS decrypt fails with attestation rejected

This is not an RA-TLS certificate problem. KMS decrypt uses a separate per-request RSA recipient key and attestation document. Check the KMS key policy conditions for kms:RecipientAttestation:*, especially PCR0, PCR3, and PCR8.

HTTPS listener not starting

Check the startup logs for TLS initialization errors. Common causes: cert and key are from different key pairs, the key file has wrong permissions (must be readable by the signer process), or the cert PEM is malformed.

Port 9443 already in use

Set tls.listen_port to a free port and update your Kubernetes Service, ingress rules, and validator client configuration to match.