Complete reference for all Containment Chamber configuration options, environment variables, and CLI flags.
Containment Chamber is configured through a YAML file, environment variables, and CLI flags. This page documents every available option.
Example Configuration File
Section titled “Example Configuration File”A fully commented example configuration file is available at config.example.yaml. Copy it and customize for your deployment:
curl -O https://raw.githubusercontent.com/unforeseen-consequences/containment-chamber/main/config.example.yamlcp config.example.yaml config.yaml# Edit config.yaml to match your setupcontainment-chamber server -c config.yamlFull config.example.yaml
# Copy this file to config.yaml and customize for your deployment.## Usage:# containment-chamber server -c config.yaml## CLI flags override values from this file.# Environment variables with CONTAINMENT_ prefix also override (use __ for nesting).# Example: CONTAINMENT_ANTI_SLASHING__URL="postgresql://..." (env var names are automatically lowercased)
# Server settingsserver: listen_address: "0.0.0.0" # CLI: --server-listen-address listen_port: 9000 # CLI: --server-listen-port
# Metrics endpointmetrics: listen_address: "0.0.0.0" # CLI: --metrics-listen-address listen_port: 3000 # CLI: --metrics-listen-port refresh_interval_seconds: 30 # CLI: --metrics-refresh-interval-seconds
# Ethereum signing configuration# Network name used for genesis-validators-root validation. Signing remains fork-agnostic.# CLI: --networknetwork: mainnet
# Key sources: filesystem, DynamoDB, or bothkey_sources: filesystem: paths: - ./keystores/raw - ./keystores/pbkdf2 - ./keystores/scrypt keystore_load_concurrency: 128 raw_load_concurrency: 128 # DynamoDB key source (optional, requires AWS credentials) # KMS key ARNs and threshold are provided during the init ceremony via the API, # not in this config file. See the Seal & Unseal guide for the init ceremony. # dynamodb: # table: containment-keys # refresh_interval_seconds: 1
# Signer state is required when key_sources.dynamodb is configured.# signer_state:# backend: dynamodb# table: containment-state# refresh_interval_seconds: 1# unseal_timeout_minutes: 30
# Signing concurrency and priority queuessigning: max_concurrent_jobs: 2000 queue_buffer_size: 4000 # priority: # enabled: true # concurrency: 50 # operations: [BLOCK_V2]
# Anti-slashing backend# postgres is recommended for production (multi-instance safe)anti_slashing: backend: postgres url: "postgresql://user:password@localhost:5432/slashing?sslmode=require" pool_size: 64 # Force DNS resolution to IPv4 only. Enable for musl/scratch Docker images # with IPv6 routing issues (e.g., NAT64 on Kubernetes). Default: false. force_ipv4: false # TLS is enabled by default. Add ?sslmode=disable to the URL to disable. # AWS RDS CA is baked into the Docker image.# Alternative backends:# anti_slashing:# backend: sqlite# path: ./slashing_protection.sqlite# anti_slashing:# backend: noop # WARNING: no slashing protection
# ──────────────────────────────────────────────────────────────# Key Manager API — hot-load/remove EIP-2335 keystores at runtime# Disabled by default.# ──────────────────────────────────────────────────────────────http: key_manager_api: enabled: false # max_concurrent_reads: 50 # max_concurrent_writes: 10## To enable:# http:# key_manager_api:# enabled: true## State-backed auth (policies + tokens) is managed via `operator auth`, not the config file.# See the Signing Auth guide for details: /chamber-api/auth/## Stateless deployments declare auth in config via static_auth (rejected when signer_state is set):## static_auth:# policies:# - name: attester# rules:# - effect: allow# scopes: [sign, public_keys]# anonymous: # named policies applied to token-less requests (optional)# policies: [attester]# tokens: # static client tokens (optional)# - secret: env:CC_ATTESTER_TOKEN # "env:VAR" injects from env, or a clear-text value# policies: [attester]# token_bound_cidrs: ["10.0.0.0/8"] # optional: restrict this token to source CIDRs
# OpenTelemetry OTLP tracing export# opentelemetry:# enabled: false# endpoint: "http://localhost:4317"# service_name: "containment-chamber"
# Logging# logging:# level: "info"# format: "text"# log_color: nullConfiguration Layering
Section titled “Configuration Layering”Configuration values are resolved in the following precedence order (highest wins):
- CLI flags — e.g.,
--server-listen-port 9001 - Environment variables — e.g.,
CONTAINMENT_SERVER__LISTEN_PORT=9001 - Config file — e.g.,
config.yaml - Built-in defaults
To start with a config file:
containment-chamber server -c config.yamlOr use CLI flags directly:
containment-chamber server \ --key-sources-filesystem-paths ./keystores/raw \ --key-sources-filesystem-paths ./keystores/pbkdf2 \ --anti-slashing-backend sqlite \ --anti-slashing-sqlite-path ./slashing.sqliteEnvironment Variables
Section titled “Environment Variables”All configuration options can be set via environment variables using the CONTAINMENT_ prefix. Nested keys use __ (double underscore) as the separator.
# server.listen_port → CONTAINMENT_SERVER__LISTEN_PORTCONTAINMENT_SERVER__LISTEN_PORT=9001
# server.listen_address → CONTAINMENT_SERVER__LISTEN_ADDRESSCONTAINMENT_SERVER__LISTEN_ADDRESS="127.0.0.1"
# anti_slashing.url → CONTAINMENT_ANTI_SLASHING__URLCONTAINMENT_ANTI_SLASHING__URL="postgresql://user:pass@db/slashing"
# anti_slashing.backend → CONTAINMENT_ANTI_SLASHING__BACKENDCONTAINMENT_ANTI_SLASHING__BACKEND=postgres
# network → CONTAINMENT_NETWORKCONTAINMENT_NETWORK=mainnetServer
Section titled “Server”HTTP server settings for the signing API.
server: listen_address: "0.0.0.0" listen_port: 9000 seccomp: false # Enable seccomp syscall filter (Linux only). Default: falseServer
Section titled “Server”| Option | Type | Default | CLI Flag | Env Var | Description |
|---|---|---|---|---|---|
server.listen_address | string | "0.0.0.0" | --server-listen-address | CONTAINMENT_SERVER__LISTEN_ADDRESS | Bind address for the HTTP signing server. |
server.listen_port | integer | 9000 | --server-listen-port | CONTAINMENT_SERVER__LISTEN_PORT | Port for the HTTP signing server. |
server.request_timeout_seconds | integer | 30 | --server-request-timeout-seconds | CONTAINMENT_SERVER__REQUEST_TIMEOUT_SECONDS | Per-request timeout for signing HTTP handlers. |
server.graceful_shutdown_timeout_seconds | integer | 25 | --server-graceful-shutdown-timeout-seconds | CONTAINMENT_SERVER__GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS | Graceful shutdown timeout before force-closing requests. |
server.cors_allowed_origins | list of strings, or null | null | --server-cors-allowed-origins | CONTAINMENT_SERVER__CORS_ALLOWED_ORIGINS | CORS allowed origins. Omit or null to disable. Use ["*"] to allow all origins. |
server.max_request_body_bytes | integer | 2097152 | “ | CONTAINMENT_SERVER__MAX_REQUEST_BODY_BYTES | Maximum request body size in bytes (2 MiB default). Requests exceeding this limit are rejected with 413. |
server.seccomp | boolean | false | “ | CONTAINMENT_SERVER__SECCOMP | Enable seccomp system call filter on Linux. |
server.trust_proxy_headers | boolean | false | “ | CONTAINMENT_SERVER__TRUST_PROXY_HEADERS | Consult X-Forwarded-For for the client IP when the TCP peer is in trusted_proxy_cidrs. Default uses the peer IP verbatim. |
server.trusted_proxy_cidrs | list of strings | [] | “ | CONTAINMENT_SERVER__TRUSTED_PROXY_CIDRS | CIDR ranges whose peers may set X-Forwarded-For. Only consulted when trust_proxy_headers is true. Unsafe CIDRs are rejected at startup. |
server.health_check.poll_interval_seconds | integer | 30 | --server-health-check-poll-interval-seconds | CONTAINMENT_SERVER__HEALTH_CHECK__POLL_INTERVAL_SECONDS | Interval between background polls of the anti-slashing backend for the /healthcheck readiness probe. |
server.health_check.poll_timeout_ms | integer | 2000 | --server-health-check-poll-timeout-ms | CONTAINMENT_SERVER__HEALTH_CHECK__POLL_TIMEOUT_MS | Per-poll timeout budget (milliseconds) for the /healthcheck anti-slashing backend probe. |
server.ceremony_rate_limit.max_attempts | integer | 5 | “ | CONTAINMENT_SERVER__CEREMONY_RATE_LIMIT__MAX_ATTEMPTS | Maximum consecutive failed passphrase attempts on unauthenticated ceremony endpoints before lockout. |
server.ceremony_rate_limit.lockout_seconds | integer | 60 | “ | CONTAINMENT_SERVER__CEREMONY_RATE_LIMIT__LOCKOUT_SECONDS | Lockout duration (seconds) after exceeding max_attempts on ceremony endpoints. |
Metrics
Section titled “Metrics”| Option | Type | Default | CLI Flag | Env Var | Description |
|---|---|---|---|---|---|
metrics.listen_address | string | "0.0.0.0" | --metrics-listen-address | CONTAINMENT_METRICS__LISTEN_ADDRESS | Bind address for the Prometheus metrics server. |
metrics.listen_port | integer | 3000 | --metrics-listen-port | CONTAINMENT_METRICS__LISTEN_PORT | Port for the Prometheus metrics endpoint. |
metrics.refresh_interval_seconds | integer | 30 | --metrics-refresh-interval-seconds | CONTAINMENT_METRICS__REFRESH_INTERVAL_SECONDS | How often metrics are refreshed. |
Network
Section titled “Network”| Option | Type | Default | CLI Flag | Env Var | Description |
|---|---|---|---|---|---|
network | string | "mainnet" | --network | CONTAINMENT_NETWORK | Ethereum network name (mainnet, hoodi, sepolia). |
Key Sources — Filesystem
Section titled “Key Sources — Filesystem”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
key_sources.filesystem.paths | list of strings | — | CONTAINMENT_KEY_SOURCES__FILESYSTEM__PATHS | Directories containing keystore configuration files. |
key_sources.filesystem.keystore_load_concurrency | integer | 128 | CONTAINMENT_KEY_SOURCES__FILESYSTEM__KEYSTORE_LOAD_CONCURRENCY | Parallel encrypted keystore decryption workers. |
key_sources.filesystem.raw_load_concurrency | integer | 128 | CONTAINMENT_KEY_SOURCES__FILESYSTEM__RAW_LOAD_CONCURRENCY | Parallel raw key loading workers. |
Key Sources — DynamoDB + KMS
Section titled “Key Sources — DynamoDB + KMS”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
key_sources.dynamodb.table | string | — | CONTAINMENT_KEY_SOURCES__DYNAMODB__TABLE | DynamoDB table name for validator keys. |
key_sources.dynamodb.status_filter | list of strings | ["active"] | CONTAINMENT_KEY_SOURCES__DYNAMODB__STATUS_FILTER | Validator statuses to load from DynamoDB. |
key_sources.dynamodb.refresh_interval_seconds | integer | 1 | CONTAINMENT_KEY_SOURCES__DYNAMODB__REFRESH_INTERVAL_SECONDS | How often to refresh keys from DynamoDB (0 disables). |
key_sources.dynamodb.max_concurrent_reads | integer | 16 | CONTAINMENT_KEY_SOURCES__DYNAMODB__MAX_CONCURRENT_READS | Parallel DynamoDB read workers for key loading. |
Signer State
Section titled “Signer State”| Option | Type | Default | CLI Flag | Env Var | Description |
|---|---|---|---|---|---|
signer_state.backend | enum | — | --signer-state-backend | CONTAINMENT_SIGNER_STATE__BACKEND | Signer-state backend. Currently supports dynamodb. |
signer_state.table | string | — | --signer-state-dynamodb-table | CONTAINMENT_SIGNER_STATE__TABLE | DynamoDB table name for signer state (master key shares, unseal ceremony, operator credentials). Required when key_sources.dynamodb is configured. |
signer_state.refresh_interval_seconds | integer | 1 | --signer-state-refresh-interval-seconds | CONTAINMENT_SIGNER_STATE__REFRESH_INTERVAL_SECONDS | How often each instance checks state rows and reloads auth policies/tokens. Set to 0 to disable both state and auth refresh. |
signer_state.unseal_timeout_minutes | integer | 30 | --signer-state-unseal-timeout-minutes | CONTAINMENT_SIGNER_STATE__UNSEAL_TIMEOUT_MINUTES | How long operators have to submit all unseal shares before progress resets. Set to 0 for no timeout. |
Concurrency
Section titled “Concurrency”| Option | Type | Default | CLI Flag | Env Var | Description |
|---|---|---|---|---|---|
signing.max_concurrent_jobs | integer | 2000 | --signing-max-concurrent-jobs | CONTAINMENT_SIGNING__MAX_CONCURRENT_JOBS | Maximum concurrent signing operations. |
signing.queue_buffer_size | integer | 4000 | --signing-queue-buffer-size | CONTAINMENT_SIGNING__QUEUE_BUFFER_SIZE | Queue buffer size for incoming signing requests. |
signing.priority.enabled | boolean | false | --signing-priority-enabled | CONTAINMENT_SIGNING__PRIORITY__ENABLED | Enable priority signing pool for high-priority operations. |
signing.priority.concurrency | integer | 50 | --signing-priority-concurrency | CONTAINMENT_SIGNING__PRIORITY__CONCURRENCY | Concurrency limit for priority signing pool. |
signing.priority.operations | list of strings | [] | --signing-priority-operations | CONTAINMENT_SIGNING__PRIORITY__OPERATIONS | List of operation types to route to priority pool (e.g., BLOCK_V2). |
Anti-Slashing
Section titled “Anti-Slashing”| Option | Type | Default | CLI Flag | Env Var | Description |
|---|---|---|---|---|---|
anti_slashing.backend | enum | "sqlite" | --anti-slashing-backend | CONTAINMENT_ANTI_SLASHING__BACKEND | Anti-slashing backend (noop, sqlite, postgres, dynamodb). |
anti_slashing.path | string | "./slashing_protection.sqlite" | --anti-slashing-sqlite-path | CONTAINMENT_ANTI_SLASHING__PATH | SQLite database path (sqlite backend only). |
anti_slashing.url | string | — | --anti-slashing-postgres-url | CONTAINMENT_ANTI_SLASHING__URL | PostgreSQL connection URL (postgres backend only). |
anti_slashing.pool_size | integer | 64 | --anti-slashing-postgres-pool-size | CONTAINMENT_ANTI_SLASHING__POOL_SIZE | PostgreSQL connection pool size. |
anti_slashing.force_ipv4 | boolean | false | --anti-slashing-postgres-force-ipv4 | CONTAINMENT_ANTI_SLASHING__FORCE_IPV4 | Force PostgreSQL DNS resolution to IPv4 only. |
anti_slashing.table | string | — | --anti-slashing-dynamodb-table | CONTAINMENT_ANTI_SLASHING__TABLE | DynamoDB table name (dynamodb backend only). |
anti_slashing.max_concurrent_writes | integer | 256 | --anti-slashing-dynamodb-max-concurrent-writes | CONTAINMENT_ANTI_SLASHING__MAX_CONCURRENT_WRITES | Application-level cap on concurrent DynamoDB anti-slash writes (dynamodb backend only). Sized at ~1/8 of signing.max_concurrent_jobs; raise it proportionally when increasing signing concurrency. |
Key Manager API
Section titled “Key Manager API”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
http.key_manager_api.enabled | boolean | false | CONTAINMENT_HTTP__KEY_MANAGER_API__ENABLED | Enable the Key Manager API. |
http.key_manager_api.max_concurrent_reads | integer | 50 | CONTAINMENT_HTTP__KEY_MANAGER_API__MAX_CONCURRENT_READS | Maximum concurrent Key Manager API reads (GET/DELETE). |
http.key_manager_api.max_concurrent_writes | integer | 10 | CONTAINMENT_HTTP__KEY_MANAGER_API__MAX_CONCURRENT_WRITES | Maximum concurrent Key Manager API writes (POST). |
http.key_manager_api.request_timeout_seconds | integer | 30 | CONTAINMENT_HTTP__KEY_MANAGER_API__REQUEST_TIMEOUT_SECONDS | Key Manager API request timeout in seconds. |
http.key_manager_api.max_items_per_request | integer | 100 | CONTAINMENT_HTTP__KEY_MANAGER_API__MAX_ITEMS_PER_REQUEST | Maximum items per Key Manager API import/delete request. |
Chamber Keys
Section titled “Chamber Keys”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
chamber.keys.list.max_concurrent_requests | integer | 16 | CONTAINMENT_CHAMBER__KEYS__LIST__MAX_CONCURRENT_REQUESTS | Tower concurrency cap for GET /api/v1/chamber/keys. |
chamber.keys.generate.enabled | boolean | false | CONTAINMENT_CHAMBER__KEYS__GENERATE__ENABLED | Mount POST /api/v1/chamber/keys/generate. Requires key_sources.dynamodb (enforced at boot). |
chamber.keys.generate.max_items_per_request | integer | 100 | CONTAINMENT_CHAMBER__KEYS__GENERATE__MAX_ITEMS_PER_REQUEST | Maximum keys per keygen request. |
chamber.keys.generate.max_concurrent_requests | integer | 16 | CONTAINMENT_CHAMBER__KEYS__GENERATE__MAX_CONCURRENT_REQUESTS | Tower concurrency cap. Per-request batch parallelism is derived from std::thread::available_parallelism(). |
chamber.keys.generate.request_timeout_seconds | integer | 600 | CONTAINMENT_CHAMBER__KEYS__GENERATE__REQUEST_TIMEOUT_SECONDS | Keygen request timeout in seconds. |
chamber.keys.generate.backup.enabled | boolean | false | CONTAINMENT_CHAMBER__KEYS__GENERATE__BACKUP__ENABLED | Enable age-encrypted mnemonic backup. Requires non-empty recipients. |
chamber.keys.generate.backup.recipients | list of strings | — | CONTAINMENT_CHAMBER__KEYS__GENERATE__BACKUP__RECIPIENTS | age public key recipients for the BIP-39 mnemonic backup. |
chamber.keys.import.enabled | boolean | false | CONTAINMENT_CHAMBER__KEYS__IMPORT__ENABLED | Mount POST /api/v1/chamber/keys. Memory-only imports (storage.persist=false) work without a mutable backend. |
chamber.keys.import.max_items_per_request | integer | 100 | CONTAINMENT_CHAMBER__KEYS__IMPORT__MAX_ITEMS_PER_REQUEST | Maximum keys per import request. |
chamber.keys.import.max_concurrent_requests | integer | 16 | CONTAINMENT_CHAMBER__KEYS__IMPORT__MAX_CONCURRENT_REQUESTS | Tower concurrency cap for chamber import. |
chamber.keys.import.request_timeout_seconds | integer | 600 | CONTAINMENT_CHAMBER__KEYS__IMPORT__REQUEST_TIMEOUT_SECONDS | Chamber import request timeout in seconds. |
chamber.keys.lifecycle.enabled | boolean | false | CONTAINMENT_CHAMBER__KEYS__LIFECYCLE__ENABLED | Mount PATCH+DELETE on /api/v1/chamber/keys. Per-key source-dispatched: memory keys can be deleted without a backend; persistent keys need one. |
chamber.keys.lifecycle.max_items_per_request | integer | 500 | CONTAINMENT_CHAMBER__KEYS__LIFECYCLE__MAX_ITEMS_PER_REQUEST | Maximum keys per PATCH/DELETE batch. |
chamber.keys.lifecycle.max_concurrent_requests | integer | 16 | CONTAINMENT_CHAMBER__KEYS__LIFECYCLE__MAX_CONCURRENT_REQUESTS | Tower concurrency cap for chamber lifecycle. |
chamber.keys.lifecycle.request_timeout_seconds | integer | 600 | CONTAINMENT_CHAMBER__KEYS__LIFECYCLE__REQUEST_TIMEOUT_SECONDS | Chamber lifecycle request timeout in seconds. |
Auth Policies
Section titled “Auth Policies”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
static_auth | object | — | CONTAINMENT_STATIC_AUTH | Stateless-only static authorization: named policies, client tokens that reference them, and an optional anonymous binding. Rejected when signer_state is configured. |
static_auth.policies | list of objects | [] | CONTAINMENT_STATIC_AUTH__POLICIES | Named policies: each has a name and rules (same PolicyRule schema as API policies — effect allow/deny, optional scopes, keys, operations). |
static_auth.anonymous.policies | list of strings | [] | CONTAINMENT_STATIC_AUTH__ANONYMOUS__POLICIES | Named policies applied to token-less requests. Empty/absent = anonymous denied (deny-by-default). |
static_auth.tokens | list of objects | [] | CONTAINMENT_STATIC_AUTH__TOKENS | Client tokens: each has a secret (env:VAR to inject from the environment, or a clear-text value), policies (named policies the token is bound to), and an optional token_bound_cidrs list restricting the token to peer IPs in those ranges (validated at startup, same as API tokens). |
TEE — Nitro Enclave
Section titled “TEE — Nitro Enclave”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
tee.platform | enum | "none" | CONTAINMENT_TEE__PLATFORM | TEE platform. nitro enables AWS Nitro Enclave mode; none (default) runs as a plain binary with no vsock or NSM attestation. When nitro, a tee.nitro: section must also be present. |
tee.nitro.aws.region | string | "us-east-1" | CONTAINMENT_TEE__NITRO__AWS__REGION | AWS region used inside the Nitro Enclave. Must be set explicitly because the EC2 IMDS endpoint (169.254.169.254) is unreachable from inside the enclave. |
tee.nitro.egress.endpoints | list of objects | [] | CONTAINMENT_TEE__NITRO__EGRESS__ENDPOINTS | Egress endpoints the enclave reaches via the parent’s vsock-proxy fleet. Each entry requires: hostname (string), loopback (unique IPv4 in 127.0.0.0/8, not 127.0.0.0/127.0.0.1/127.255.255.255), vsock_port (unique u16, must not collide with ingress or reserved ports 7000-7002), and optional port (default 443). |
OpenTelemetry
Section titled “OpenTelemetry”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
opentelemetry.enabled | boolean | false | CONTAINMENT_OPENTELEMETRY__ENABLED | Enable OpenTelemetry OTLP export. |
opentelemetry.endpoint | string | "http://localhost:4317" | CONTAINMENT_OPENTELEMETRY__ENDPOINT | OTLP gRPC endpoint URL. |
opentelemetry.service_name | string | "containment-chamber" | CONTAINMENT_OPENTELEMETRY__SERVICE_NAME | Service name attached to traces. |
Logging
Section titled “Logging”| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
logging.level | string | "info" | CONTAINMENT_LOGGING__LEVEL | Log level filter (EnvFilter syntax supported). |
logging.format | enum | "text" | CONTAINMENT_LOGGING__FORMAT | Log output format: text or json. |
logging.log_color | boolean | auto | CONTAINMENT_LOGGING__LOG_COLOR | ANSI color output (auto-detected when unset). |
server: listen_address: "0.0.0.0" listen_port: 9000Metrics
Section titled “Metrics”Prometheus metrics endpoint configuration. Metrics are served on a separate port from the signing API.
metrics: listen_address: "0.0.0.0" listen_port: 3000 refresh_interval_seconds: 30Network
Section titled “Network”The Ethereum network name. Used to validate genesis_validators_root on signing requests. Signing remains fork-agnostic because fork data comes from each request.
network: mainnetKey Sources
Section titled “Key Sources”Key sources control where validator keys are loaded from. You can use filesystem, DynamoDB, or both simultaneously.
Filesystem
Section titled “Filesystem”Paths to directories containing key configuration YAML files. Multiple directories can be specified to load keys from different locations or formats (raw hex, PBKDF2, Scrypt).
key_sources: filesystem: paths: - ./keystores/raw - ./keystores/pbkdf2 - ./keystores/scrypt keystore_load_concurrency: 128 raw_load_concurrency: 128DynamoDB + KMS
Section titled “DynamoDB + KMS”Store validator keys in AWS DynamoDB encrypted under the chamber master key. The master key is reconstructed from KMS-wrapped Shamir shares. Supports key generation and import via dedicated API endpoints.
key_sources: dynamodb: table: containment-keys refresh_interval_seconds: 1
signer_state: backend: dynamodb table: containment-state
chamber: keys: generate: enabled: false max_items_per_request: 100 backup: enabled: false recipients: [] # age public keys for offline mnemonic recovery import: enabled: false lifecycle: enabled: falseKMS key ARNs and the KMS threshold are submitted during the init ceremony, not stored in the config file. See DynamoDB Key Source and Seal & Unseal Guide for the full setup.
Concurrency
Section titled “Concurrency”Controls backpressure for signing request processing.
signing.max_concurrent_jobs— Maximum number of signing operations processed simultaneously.signing.queue_buffer_size— Size of the request queue buffer. Requests beyond this limit receive HTTP 503.signing.priority.enabled— Enable a dedicated priority pool for high-priority operations like block signing.signing.priority.concurrency— Concurrency limit for the priority pool.signing.priority.operations— List of operation types to route to the priority pool (e.g.,[BLOCK_V2]).
signing: max_concurrent_jobs: 2000 queue_buffer_size: 4000 # priority: # enabled: true # concurrency: 50 # operations: [BLOCK_V2]Anti-Slashing
Section titled “Anti-Slashing”EIP-3076 slashing protection backend configuration. PostgreSQL is recommended for production deployments as it supports multi-instance setups with full surround vote detection.
Backend Options
Section titled “Backend Options”| Backend | Value | Multi-Instance | Surround Detection | Use Case |
|---|---|---|---|---|
| PostgreSQL | postgres | ✅ | ✅ | Production (recommended) |
| SQLite | sqlite | ❌ | ✅ | Development / single instance |
| DynamoDB | dynamodb | ✅ | ✅ (implicit) | AWS deployments |
| Noop | noop | — | — | Testing only |
PostgreSQL (recommended)
Section titled “PostgreSQL (recommended)”anti_slashing: backend: postgres url: "postgresql://user:password@localhost:5432/slashing?sslmode=require" pool_size: 64 force_ipv4: falseTLS is enabled by default. Append ?sslmode=disable to the URL to disable it. The AWS RDS CA bundle is included in the Docker image.
SQLite
Section titled “SQLite”anti_slashing: backend: sqlite path: ./slashing_protection.sqliteanti_slashing: backend: noopKey Manager API
Section titled “Key Manager API”Hot-load and remove EIP-2335 keystores at runtime via the /eth/v1/keystores endpoint. Disabled by default.
http: key_manager_api: enabled: trueAccess to Key Manager API routes (/eth/v1/keystores) is controlled by auth policies (managed via containment-chamber operator auth) using the list_keys, import_keystores, and delete_keys scopes. See Auth Policies below.
Auth Policies
Section titled “Auth Policies”Auth policies are managed via containment-chamber operator auth, not in the config file. Policies control access to all routes. When policies exist, requests must include an Authorization: Bearer <token> header. Tokens are bound to one or more policies.
Bootstrap a management token via the containment-chamber operator generate-root-token ceremony. The management token has full access to all scopes and can be used to create policies and client tokens with operator auth.
For the complete API reference, policy evaluation rules, and detailed examples, see the Auth Policies guide.
Static Auth (config-file-based, stateless-only)
Section titled “Static Auth (config-file-based, stateless-only)”static_auth declares named policies, static client tokens, and an optional anonymous binding, using the same PolicyRule model as API-managed policies — one representation everywhere. It is stateless-only: configurations that also set signer_state are rejected at startup, so state-backed deployments use the runtime auth API instead.
policies— named, reusable rule-sets (effectallow/deny, optionalscopes/keys/operations).anonymous.policies— named policies applied to requests with no Bearer token. Empty/absent ⇒ anonymous denied (deny-by-default).tokens— client tokens, each with asecret(env:VAR_NAMEinjected from the environment, or a clear-text value — operator’s choice), the namedpoliciesit is bound to, and an optionaltoken_bound_cidrslist restricting the token to peer IPs in those ranges (validated at startup like API tokens —0.0.0.0/0,::/0, IPv4/<8, IPv6/<16are rejected). Secrets are HMAC-hashed at boot and never persisted.
Example: allow unauthenticated signing but block voluntary exits, and issue one env-injected client token:
static_auth: policies: - name: anonymous rules: - effect: allow scopes: [sign, public_keys] - effect: deny operations: [VOLUNTARY_EXIT] anonymous: policies: [anonymous] tokens: - secret: env:CC_VC_TOKEN policies: [anonymous] token_bound_cidrs: ["10.0.0.0/8"] # optional: restrict the token to these peer IPsAPI Scopes
Section titled “API Scopes”All available scopes for use in policy rules:
sign, public_keys, list_keys, import_keystores, delete_keys, chamber_keys_generate, chamber_keys_import, chamber_keys_list, chamber_keys_patch, chamber_keys_delete, chamber_seal, chamber_rotate, chamber_status, auth_policies_manage, auth_tokens_manage
Valid Signing Operation Names
Section titled “Valid Signing Operation Names”AGGREGATION_SLOT, AGGREGATE_AND_PROOF, ATTESTATION, BLOCK_V2, RANDAO_REVEAL, SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF, SYNC_COMMITTEE_MESSAGE, SYNC_COMMITTEE_SELECTION_PROOF, VALIDATOR_REGISTRATION, VOLUNTARY_EXIT
Canary Keys
Section titled “Canary Keys”Canary keys are validator public keys that should never sign in normal operation. When a canary key signs, the signer logs a warning and increments the containment_canary_signing_total metric. Signing is not blocked.
canary_keys: - "0x1234..." - "0x5678..."See Production Hardening for usage guidance.
DynamoDB Unseal Timeout
Section titled “DynamoDB Unseal Timeout”When using the DynamoDB key source with the seal/unseal feature, unseal_timeout_minutes controls how long a partial unseal (shares submitted but threshold not yet met) stays valid before expiring. Expired partial unseals require operators to resubmit their shares.
signer_state: backend: dynamodb table: containment-state unseal_timeout_minutes: 30 # 0 = no timeoutSee Seal & Unseal for the full init ceremony and operational procedures.
OpenTelemetry
Section titled “OpenTelemetry”OpenTelemetry OTLP tracing export. Disabled by default.
opentelemetry: enabled: true endpoint: "http://localhost:4317" service_name: "containment-chamber"The endpoint should point to an OTLP gRPC collector (e.g., Jaeger, Grafana Tempo, or the OpenTelemetry Collector).
Logging
Section titled “Logging”Structured logging configuration for console output.
logging: level: "info" format: "text" log_color: trueHTTPS listener configuration. TLS is disabled by default. When enabled, a second listener starts on port 9443 alongside the existing HTTP listener on port 9000.
Two modes are available:
file— reads a PEM certificate and private key from disk, polling for changes on a configurable interval. Works with any PKI: self-signed certs, cert-manager, Vault PKI.atls— generates an ephemeral certificate bound to an NSM attestation document. For AWS Nitro Enclave deployments only.
File mode
Section titled “File mode”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 pollingAttestation TLS mode
Section titled “Attestation TLS mode”tls: mode: atls listen_port: 9443 atls: cert_validity_seconds: 86400 # 24 hours rotation_interval_seconds: 3600 # rotate every hourSee Attested TLS for the full setup guide, including cert rotation and client-side attestation verification.
Scaling Recommendations
Section titled “Scaling Recommendations”Recommended configuration values by validator count:
| Parameter | 1K validators | 10K validators | 100K+ validators |
|---|---|---|---|
signing.max_concurrent_jobs | 100–500 | 500–2000 | 2000+ (default: 2000) |
signing.queue_buffer_size | 1000–3000 | 3000–4000 | 4000+ (default: 4000) |
anti_slashing.postgres.pool_size | 16–32 | 32–64 | 64+ (default: 64) |
| CPU cores | 1–2 | 2–4 | 4–8+ |
Anti-Slashing Backend at Scale
Section titled “Anti-Slashing Backend at Scale”For 100K+ validators, DynamoDB anti-slashing is recommended over PostgreSQL:
- Multi-instance safe (no single point of failure)
- No connection pool limits
- Scales automatically with DynamoDB on-demand billing
DynamoDB Shard Count
Section titled “DynamoDB Shard Count”The DynamoDB key source uses 16 shards for parallel key loading. This is a compile-time constant and cannot be changed via configuration. 16 shards supports up to ~100K keys efficiently.
Key Refresh Interval
Section titled “Key Refresh Interval”For large deployments, set key_sources.dynamodb.refresh_interval_seconds to a non-zero value to periodically reload keys added via the API without restarting:
key_sources: dynamodb: refresh_interval_seconds: 1 # reload every secondCeremony CIDR Filter
Section titled “Ceremony CIDR Filter”The seven unauthenticated chamber ceremony endpoints (init, unseal, unseal/init, unseal/register, unseal/credentials, generate-root-token, attestation) can be restricted to a CIDR allowlist via server.ceremony_allowed_cidrs.
server: ceremony_allowed_cidrs: - "10.0.0.0/8" # operator VPC - "192.168.0.0/16" # staging bastionValidation is strict: 0.0.0.0/0, ::/0, IPv4 prefixes shorter than /8, and IPv6 prefixes shorter than /16 are rejected at startup.
An empty list is backward-compatible — no filter is applied. In that mode the server emits a tracing::warn! event at startup:
ceremony CIDR filter disabled (ceremony_allowed_cidrs is empty); ceremony routes are reachable from any source IPThe containment_ceremony_cidr_filter_enabled gauge reports 0 when empty, 1 otherwise — wire an alert if you expect the filter to always be active.
Rejections are logged (never exposed to the caller) as a structured audit event with outcome="ceremony_cidr_reject" and counted in containment_cidr_rejections_total{layer="ceremony",reason=...}.
Token-Bound CIDRs
Section titled “Token-Bound CIDRs”Any client auth token — minted via the API in a state-backed deployment, or declared under static_auth.tokens in a stateless deployment — may carry a token_bound_cidrs list. When non-empty, the token is only usable from a TCP peer IP in the listed ranges. Enforcement runs inside AuthContext::require_scope / require_signing / filter_public_keys, so every route gated by one of the 15 ApiScopes — plus lookup-self and renew-self — is CIDR-enforced automatically.
For state-backed deployments, CIDR binding is supplied either via the containment-chamber operator auth token create --token-bound-cidrs 10.0.0.0/8,192.168.0.0/16 CLI flag or in the token-creation request body; for stateless deployments it is declared under static_auth.tokens[].token_bound_cidrs. See the Auth guide and API Reference for the request schema.
Rules:
- Management (root) tokens are never CIDR-bound — they are the break-glass path and binding them would block operator recovery.
- Authorization uses the raw TCP peer IP only.
X-Forwarded-Foris not consulted for authz decisions (it continues to driveclient_ip=in audit logs). A leaked token cannot escape its CIDR binding by forgingX-Forwarded-For. - IPv4-mapped IPv6 peers (
::ffff:a.b.c.d) are canonicalized before matching so a token bound to10.0.0.0/8matches both10.0.1.5and::ffff:10.0.1.5. validate_safe_cidrsrejects unsafe ranges (0.0.0.0/0,::/0, IPv4/<8, IPv6/<16): API-minted tokens are rejected at mint time with400 Bad Request;static_authtokens are rejected at startup as a configuration error (fail-closed) before any listener binds.
Recommended refresh interval for security-sensitive deployments: keep the default signer_state.refresh_interval_seconds = 1 so a narrowed CIDR binding (implemented today as revoke + re-mint) is effective within about one second across the cluster.
Next Steps
Section titled “Next Steps”- Auth Policies & Tokens — create policies and tokens after configuring the server
- Observability — set up metrics and logging with the values you just configured
- Production Hardening — apply the ceremony CIDR filter and token security checklist