Skip to content

Zero to Production

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.

Terminal window
docker pull ghcr.io/unforeseen-consequences/containment-chamber:latest
docker run --rm ghcr.io/unforeseen-consequences/containment-chamber:latest --version

Choose one method and verify the binary or container image works.

See Installation for build-from-source, cargo install, and shell completions.

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.

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: 3000

Create 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.

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.

Start the signer and its dependencies:

Terminal window
docker compose up -d

If you used the Docker Compose example, this also starts PostgreSQL and creates the slashing-protection database automatically.

If the state table is empty and a ceremony: block is configured, the chamber auto-initializes on first boot:

  1. Generates a random 256-bit master key.
  2. Shamir-splits it M-of-N and KMS-wraps each share under the configured keys.
  3. Self-tests the wrapping (attested KMS decrypt + Shamir combine + commitment verify) before any durable write.
  4. Writes the MASTER_KEY row and creates the root management token, age-encrypted to root_token_recipients, stored in ROOT_TOKEN_BOOTSTRAP.
  5. Transitions to Unsealed.

On every subsequent boot the chamber auto-unseals by reconstructing the master key from the MASTER_KEY row.

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:

Terminal window
# Read the age-encrypted ciphertext from DynamoDB
aws 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.age

Store the plaintext root token as ROOT_TOKEN in your shell. Token secrets are shown (decrypted) only once — keep it in a secure credential store.

With the root token in hand, create a signing policy and a client token for your validator:

Terminal window
# Create a policy that allows signing only for your keys
containment-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 policy
containment-chamber operator auth token create \
--policies validator-signing \
--ttl-seconds 86400 \
--token env:ROOT_TOKEN \
--signer-url http://localhost:9000 \
--allow-plaintext-signer

Save 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.

Check health, loaded keys, and metrics:

Terminal window
# Liveness
curl http://localhost:9000/upcheck
# Loaded keys
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:9000/api/v1/eth2/publicKeys
# Prometheus metrics
curl http://localhost:3000/metrics

Connect your validator client. For Lighthouse:

Terminal window
lighthouse vc \
--beacon-node http://localhost:5052 \
--web3-signer-url http://localhost:9000 \
--web3-signer-token $TOKEN

See Validator Clients for Teku, Prysm, Lodestar, and Nimbus examples.

Before you point real validators at this signer, complete the production hardening checklist:

See Production Hardening for the complete checklist.

  • 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 (new kms_keys / kms_threshold, bump generation) and redeploy — the watcher reconciles the MASTER_KEY row automatically on the next boot
  • Upgrade: Follow the Upgrading guide for safe version bumps with zero downtime