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.
What TLS Changes
Section titled “What TLS Changes”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}Modes at a Glance
Section titled “Modes at a Glance”| 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
Section titled “File Mode”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)”openssl req -x509 -newkey rsa:4096 \ -keyout tls.key -out tls.crt \ -days 365 -nodes \ -subj "/CN=containment-chamber"Configure
Section titled “Configure”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 pollingOr via environment variables:
CONTAINMENT_TLS__MODE=fileCONTAINMENT_TLS__LISTEN_PORT=9443CONTAINMENT_TLS__FILE__CERT_PATH=/etc/certs/tls.crtCONTAINMENT_TLS__FILE__KEY_PATH=/etc/certs/tls.keyKubernetes: mount a TLS Secret
Section titled “Kubernetes: mount a TLS Secret”Create a Secret from your cert and key, then mount it into the pod:
apiVersion: v1kind: Secretmetadata: name: containment-chamber-tlstype: kubernetes.io/tlsdata: 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-tlsWith 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.
Cert rotation
Section titled “Cert rotation”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 Mode
Section titled “RA-TLS Mode”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:
- The server is running in a genuine AWS Nitro Enclave
- The enclave image, kernel, parent IAM role, and EIF signing certificate match the expected PCR measurements (PCR0, PCR1, PCR2, PCR3, PCR8)
- 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.
Configuration
Section titled “Configuration”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 hourNo cert files to manage. The signer generates and rotates certs automatically.
How cert rotation works
Section titled “How cert rotation works”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.
Client-side verification (TOFU model)
Section titled “Client-side verification (TOFU model)”Clients that support RA-TLS verify the attestation document before trusting the connection:
- The client receives the TLS certificate during the handshake
- It extracts the attestation document from the X.509 extension
- 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)
- It checks the PCR measurements match the expected enclave binary
- 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:
containment-chamber operator status \ --signer-url https://localhost:9443 \ --danger-skip-attestation-verificationTroubleshooting
Section titled “Troubleshooting”“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.
Next Steps
Section titled “Next Steps”- Nitro Enclave Overview — when to use RA-TLS and how it fits into the enclave trust model
- Enclave Deployment — configure vsock, egress endpoints, and attested KMS
- Production Hardening — network isolation and certificate rotation policies

