Plan a production signer from install through deployment, auth, verification, and hardening
This guide takes you from a fresh machine to a production-ready Containment Chamber signer. It links to detailed reference pages at each step so you can dive deeper without losing the thread.
If you only need a local development signer, follow the Quick Start instead. Quick Start is stateless and Web3Signer-compatible; this guide shows the state-backed path you need before relying on runtime auth policies, tokens, or chamber key generation.
Install
Section titled “Install”docker pull ghcr.io/unforeseen-consequences/containment-chamber:latestdocker run --rm ghcr.io/unforeseen-consequences/containment-chamber:latest --versioncurl -LO https://github.com/unforeseen-consequences/containment-chamber/releases/latest/download/containment-chamber-linux-amd64chmod +x containment-chamber-linux-amd64sudo mv containment-chamber-linux-amd64 /usr/local/bin/containment-chambercontainment-chamber --versionChoose one method and verify the binary or container image works.
See Installation for build-from-source, cargo install, and shell completions.
Choose a deployment model
Section titled “Choose a deployment model”| Model | Best For | Complexity | Docs |
|---|---|---|---|
| Docker Compose | Single-node production, fastest path | Low | Docker |
| Kubernetes | Multi-instance, automated rollouts | Medium | Kubernetes |
| Bare Metal + systemd | Max control, existing metal | Medium | Bare Metal |
| Nitro Enclave | AWS, hardware-rooted trust | High | Enclave |
This guide assumes Docker Compose for concreteness. Adapt the file paths and commands if you chose a different model.
Configure
Section titled “Configure”Create a production-ready config.yaml:
server: listen_address: "0.0.0.0" listen_port: 9000
key_sources: filesystem: paths: - /keystores
# Custody: static ceremony — KMS keys, Shamir threshold, root-token recipients.# Nitro builds compile this in; non-Nitro builds read it here.ceremony: generation: 1 kms_threshold: 2 kms_keys: - arns: ["arn:aws:kms:us-east-1:123456789012:key/aaa-key-id"] - arns: ["arn:aws:kms:us-east-1:123456789012:key/bbb-key-id"] - arns: ["arn:aws:kms:us-west-2:123456789012:key/ccc-key-id"] retired_kms_keys: [] root_token_recipients: - "age1qz..." # age X25519 public key of the admin who will decrypt the root token
signer_state: backend: dynamodb table: containment-state refresh_interval_seconds: 1
anti_slashing: backend: postgres url: "postgresql://cc:env:DB_PASSWORD@postgres:5432/slashing"
metrics: listen_address: "0.0.0.0" listen_port: 3000Create the DynamoDB state table before starting the signer, or omit signer_state and skip the auth/key-generation steps if you intentionally want a stateless Web3Signer-compatible deployment.
See Configuration Reference for every option.
Load keys
Section titled “Load keys”Place your EIP-2335 keystores (JSON files) in a keystores/ directory alongside your config.yaml. The directory layout is:
keystores/├── 0x8f1e...a2b3.json # keystore file├── 0x8f1e...a2b3.txt # password file (same basename)└── ...This filesystem setup loads existing keys; it does not enable chamber-side key generation. To use /api/v1/chamber/keys/generate, configure DynamoDB Key Source plus signer_state, then enable chamber.keys.generate.enabled.
Deploy
Section titled “Deploy”Start the signer and its dependencies:
docker compose up -dIf you used the Docker Compose example, this also starts PostgreSQL and creates the slashing-protection database automatically.
Boot and auto-init
Section titled “Boot and auto-init”If the state table is empty and a ceremony: block is configured, the chamber auto-initializes on first boot:
- Generates a random 256-bit master key.
- Shamir-splits it M-of-N and KMS-wraps each share under the configured keys.
- Self-tests the wrapping (attested KMS decrypt + Shamir combine + commitment verify) before any durable write.
- Writes the
MASTER_KEYrow and creates the root management token, age-encrypted toroot_token_recipients, stored inROOT_TOKEN_BOOTSTRAP. - Transitions to
Unsealed.
On every subsequent boot the chamber auto-unseals by reconstructing the master key from the MASTER_KEY row.
Retrieve the root token
Section titled “Retrieve the root token”The root management token is written once at auto-init, age-encrypted to the recipients in root_token_recipients. Read it out of DynamoDB and decrypt it locally:
# Read the age-encrypted ciphertext from DynamoDBaws dynamodb get-item \ --table-name containment-state \ --key '{"pk": {"S": "ROOT_TOKEN_BOOTSTRAP"}}' \ --query 'Item.ciphertext.B' \ --output text | base64 -d > root_token.age
# Decrypt with your age identity (the private key for the recipient in root_token_recipients)age -d -i /path/to/age-identity.txt root_token.ageStore the plaintext root token as ROOT_TOKEN in your shell. Token secrets are shown (decrypted) only once — keep it in a secure credential store.
Set up auth
Section titled “Set up auth”With the root token in hand, create a signing policy and a client token for your validator:
# Create a policy that allows signing only for your keyscontainment-chamber operator auth policy create \ --name validator-signing \ --rules '[{"effect":"allow","scopes":["sign","public_keys"],"keys":["0x8f1e...a2b3","0xabcd...1234"]}]' \ --token env:ROOT_TOKEN \ --signer-url http://localhost:9000 \ --allow-plaintext-signer
# Create a client token bound to that policycontainment-chamber operator auth token create \ --policies validator-signing \ --ttl-seconds 86400 \ --token env:ROOT_TOKEN \ --signer-url http://localhost:9000 \ --allow-plaintext-signerSave the client token for your validator — it is shown only once. In production, add --bound-cidrs <validator-egress-cidr> when the validator client’s source network is stable.
See Auth Policies & Tokens for the full model and additional scopes. For the request lifecycle and how policies are evaluated, see API Concepts.
Verify
Section titled “Verify”Check health, loaded keys, and metrics:
# Livenesscurl http://localhost:9000/upcheck
# Loaded keyscurl -H "Authorization: Bearer $TOKEN" \ http://localhost:9000/api/v1/eth2/publicKeys
# Prometheus metricscurl http://localhost:3000/metricsConnect your validator client. For Lighthouse:
lighthouse vc \ --beacon-node http://localhost:5052 \ --web3-signer-url http://localhost:9000 \ --web3-signer-token $TOKENSee Validator Clients for Teku, Prysm, Lodestar, and Nimbus examples.
Harden
Section titled “Harden”Before you point real validators at this signer, complete the production hardening checklist:
- Set up TLS termination or RA-TLS for enclave deployments
- Configure log forwarding and alerting on signing error rates
- Review the security model and threat assumptions
- Set up automated backups for the anti-slashing database
- Keep age private keys for root-token decryption in offline cold storage — they are your break-glass credential
See Production Hardening for the complete checklist.
Next Steps
Section titled “Next Steps”- Monitor: Set up Prometheus/Grafana dashboards using the metrics on port 3000 — see Observability
- Scale: Run multiple instances behind a load balancer with the PostgreSQL anti-slashing backend
- Rotate custody keys: Update the
ceremony:block (newkms_keys/kms_threshold, bumpgeneration) and redeploy — the watcher reconciles theMASTER_KEYrow automatically on the next boot - Upgrade: Follow the Upgrading guide for safe version bumps with zero downtime

