Skip to content

Configuration Reference

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.

A fully commented example configuration file is available at config.example.yaml. Copy it and customize for your deployment:

Terminal window
curl -O https://raw.githubusercontent.com/unforeseen-consequences/containment-chamber/main/config.example.yaml
cp config.example.yaml config.yaml
# Edit config.yaml to match your setup
containment-chamber server -c config.yaml
Full 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 settings
server:
listen_address: "0.0.0.0" # CLI: --server-listen-address
listen_port: 9000 # CLI: --server-listen-port
# Metrics endpoint
metrics:
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: --network
network: mainnet
# Key sources: filesystem, DynamoDB, or both
key_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 queues
signing:
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: null

Configuration values are resolved in the following precedence order (highest wins):

  1. CLI flags — e.g., --server-listen-port 9001
  2. Environment variables — e.g., CONTAINMENT_SERVER__LISTEN_PORT=9001
  3. Config file — e.g., config.yaml
  4. Built-in defaults

To start with a config file:

Terminal window
containment-chamber server -c config.yaml

Or use CLI flags directly:

Terminal window
containment-chamber server \
--key-sources-filesystem-paths ./keystores/raw \
--key-sources-filesystem-paths ./keystores/pbkdf2 \
--anti-slashing-backend sqlite \
--anti-slashing-sqlite-path ./slashing.sqlite

All configuration options can be set via environment variables using the CONTAINMENT_ prefix. Nested keys use __ (double underscore) as the separator.

Terminal window
# server.listen_port → CONTAINMENT_SERVER__LISTEN_PORT
CONTAINMENT_SERVER__LISTEN_PORT=9001
# server.listen_address → CONTAINMENT_SERVER__LISTEN_ADDRESS
CONTAINMENT_SERVER__LISTEN_ADDRESS="127.0.0.1"
# anti_slashing.url → CONTAINMENT_ANTI_SLASHING__URL
CONTAINMENT_ANTI_SLASHING__URL="postgresql://user:pass@db/slashing"
# anti_slashing.backend → CONTAINMENT_ANTI_SLASHING__BACKEND
CONTAINMENT_ANTI_SLASHING__BACKEND=postgres
# network → CONTAINMENT_NETWORK
CONTAINMENT_NETWORK=mainnet

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: false
OptionTypeDefaultCLI FlagEnv VarDescription
server.listen_addressstring"0.0.0.0"--server-listen-addressCONTAINMENT_SERVER__LISTEN_ADDRESSBind address for the HTTP signing server.
server.listen_portinteger9000--server-listen-portCONTAINMENT_SERVER__LISTEN_PORTPort for the HTTP signing server.
server.request_timeout_secondsinteger30--server-request-timeout-secondsCONTAINMENT_SERVER__REQUEST_TIMEOUT_SECONDSPer-request timeout for signing HTTP handlers.
server.graceful_shutdown_timeout_secondsinteger25--server-graceful-shutdown-timeout-secondsCONTAINMENT_SERVER__GRACEFUL_SHUTDOWN_TIMEOUT_SECONDSGraceful shutdown timeout before force-closing requests.
server.cors_allowed_originslist of strings, or nullnull--server-cors-allowed-originsCONTAINMENT_SERVER__CORS_ALLOWED_ORIGINSCORS allowed origins. Omit or null to disable. Use ["*"] to allow all origins.
server.max_request_body_bytesinteger2097152CONTAINMENT_SERVER__MAX_REQUEST_BODY_BYTESMaximum request body size in bytes (2 MiB default). Requests exceeding this limit are rejected with 413.
server.seccompbooleanfalseCONTAINMENT_SERVER__SECCOMPEnable seccomp system call filter on Linux.
server.trust_proxy_headersbooleanfalseCONTAINMENT_SERVER__TRUST_PROXY_HEADERSConsult 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_cidrslist of strings[]CONTAINMENT_SERVER__TRUSTED_PROXY_CIDRSCIDR 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_secondsinteger30--server-health-check-poll-interval-secondsCONTAINMENT_SERVER__HEALTH_CHECK__POLL_INTERVAL_SECONDSInterval between background polls of the anti-slashing backend for the /healthcheck readiness probe.
server.health_check.poll_timeout_msinteger2000--server-health-check-poll-timeout-msCONTAINMENT_SERVER__HEALTH_CHECK__POLL_TIMEOUT_MSPer-poll timeout budget (milliseconds) for the /healthcheck anti-slashing backend probe.
server.ceremony_rate_limit.max_attemptsinteger5CONTAINMENT_SERVER__CEREMONY_RATE_LIMIT__MAX_ATTEMPTSMaximum consecutive failed passphrase attempts on unauthenticated ceremony endpoints before lockout.
server.ceremony_rate_limit.lockout_secondsinteger60CONTAINMENT_SERVER__CEREMONY_RATE_LIMIT__LOCKOUT_SECONDSLockout duration (seconds) after exceeding max_attempts on ceremony endpoints.
OptionTypeDefaultCLI FlagEnv VarDescription
metrics.listen_addressstring"0.0.0.0"--metrics-listen-addressCONTAINMENT_METRICS__LISTEN_ADDRESSBind address for the Prometheus metrics server.
metrics.listen_portinteger3000--metrics-listen-portCONTAINMENT_METRICS__LISTEN_PORTPort for the Prometheus metrics endpoint.
metrics.refresh_interval_secondsinteger30--metrics-refresh-interval-secondsCONTAINMENT_METRICS__REFRESH_INTERVAL_SECONDSHow often metrics are refreshed.
OptionTypeDefaultCLI FlagEnv VarDescription
networkstring"mainnet"--networkCONTAINMENT_NETWORKEthereum network name (mainnet, hoodi, sepolia).
OptionTypeDefaultEnv VarDescription
key_sources.filesystem.pathslist of stringsCONTAINMENT_KEY_SOURCES__FILESYSTEM__PATHSDirectories containing keystore configuration files.
key_sources.filesystem.keystore_load_concurrencyinteger128CONTAINMENT_KEY_SOURCES__FILESYSTEM__KEYSTORE_LOAD_CONCURRENCYParallel encrypted keystore decryption workers.
key_sources.filesystem.raw_load_concurrencyinteger128CONTAINMENT_KEY_SOURCES__FILESYSTEM__RAW_LOAD_CONCURRENCYParallel raw key loading workers.
OptionTypeDefaultEnv VarDescription
key_sources.dynamodb.tablestringCONTAINMENT_KEY_SOURCES__DYNAMODB__TABLEDynamoDB table name for validator keys.
key_sources.dynamodb.status_filterlist of strings["active"]CONTAINMENT_KEY_SOURCES__DYNAMODB__STATUS_FILTERValidator statuses to load from DynamoDB.
key_sources.dynamodb.refresh_interval_secondsinteger1CONTAINMENT_KEY_SOURCES__DYNAMODB__REFRESH_INTERVAL_SECONDSHow often to refresh keys from DynamoDB (0 disables).
key_sources.dynamodb.max_concurrent_readsinteger16CONTAINMENT_KEY_SOURCES__DYNAMODB__MAX_CONCURRENT_READSParallel DynamoDB read workers for key loading.
OptionTypeDefaultCLI FlagEnv VarDescription
signer_state.backendenum--signer-state-backendCONTAINMENT_SIGNER_STATE__BACKENDSigner-state backend. Currently supports dynamodb.
signer_state.tablestring--signer-state-dynamodb-tableCONTAINMENT_SIGNER_STATE__TABLEDynamoDB table name for signer state (master key shares, unseal ceremony, operator credentials). Required when key_sources.dynamodb is configured.
signer_state.refresh_interval_secondsinteger1--signer-state-refresh-interval-secondsCONTAINMENT_SIGNER_STATE__REFRESH_INTERVAL_SECONDSHow 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_minutesinteger30--signer-state-unseal-timeout-minutesCONTAINMENT_SIGNER_STATE__UNSEAL_TIMEOUT_MINUTESHow long operators have to submit all unseal shares before progress resets. Set to 0 for no timeout.
OptionTypeDefaultCLI FlagEnv VarDescription
signing.max_concurrent_jobsinteger2000--signing-max-concurrent-jobsCONTAINMENT_SIGNING__MAX_CONCURRENT_JOBSMaximum concurrent signing operations.
signing.queue_buffer_sizeinteger4000--signing-queue-buffer-sizeCONTAINMENT_SIGNING__QUEUE_BUFFER_SIZEQueue buffer size for incoming signing requests.
signing.priority.enabledbooleanfalse--signing-priority-enabledCONTAINMENT_SIGNING__PRIORITY__ENABLEDEnable priority signing pool for high-priority operations.
signing.priority.concurrencyinteger50--signing-priority-concurrencyCONTAINMENT_SIGNING__PRIORITY__CONCURRENCYConcurrency limit for priority signing pool.
signing.priority.operationslist of strings[]--signing-priority-operationsCONTAINMENT_SIGNING__PRIORITY__OPERATIONSList of operation types to route to priority pool (e.g., BLOCK_V2).
OptionTypeDefaultCLI FlagEnv VarDescription
anti_slashing.backendenum"sqlite"--anti-slashing-backendCONTAINMENT_ANTI_SLASHING__BACKENDAnti-slashing backend (noop, sqlite, postgres, dynamodb).
anti_slashing.pathstring"./slashing_protection.sqlite"--anti-slashing-sqlite-pathCONTAINMENT_ANTI_SLASHING__PATHSQLite database path (sqlite backend only).
anti_slashing.urlstring--anti-slashing-postgres-urlCONTAINMENT_ANTI_SLASHING__URLPostgreSQL connection URL (postgres backend only).
anti_slashing.pool_sizeinteger64--anti-slashing-postgres-pool-sizeCONTAINMENT_ANTI_SLASHING__POOL_SIZEPostgreSQL connection pool size.
anti_slashing.force_ipv4booleanfalse--anti-slashing-postgres-force-ipv4CONTAINMENT_ANTI_SLASHING__FORCE_IPV4Force PostgreSQL DNS resolution to IPv4 only.
anti_slashing.tablestring--anti-slashing-dynamodb-tableCONTAINMENT_ANTI_SLASHING__TABLEDynamoDB table name (dynamodb backend only).
anti_slashing.max_concurrent_writesinteger256--anti-slashing-dynamodb-max-concurrent-writesCONTAINMENT_ANTI_SLASHING__MAX_CONCURRENT_WRITESApplication-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.
OptionTypeDefaultEnv VarDescription
http.key_manager_api.enabledbooleanfalseCONTAINMENT_HTTP__KEY_MANAGER_API__ENABLEDEnable the Key Manager API.
http.key_manager_api.max_concurrent_readsinteger50CONTAINMENT_HTTP__KEY_MANAGER_API__MAX_CONCURRENT_READSMaximum concurrent Key Manager API reads (GET/DELETE).
http.key_manager_api.max_concurrent_writesinteger10CONTAINMENT_HTTP__KEY_MANAGER_API__MAX_CONCURRENT_WRITESMaximum concurrent Key Manager API writes (POST).
http.key_manager_api.request_timeout_secondsinteger30CONTAINMENT_HTTP__KEY_MANAGER_API__REQUEST_TIMEOUT_SECONDSKey Manager API request timeout in seconds.
http.key_manager_api.max_items_per_requestinteger100CONTAINMENT_HTTP__KEY_MANAGER_API__MAX_ITEMS_PER_REQUESTMaximum items per Key Manager API import/delete request.
OptionTypeDefaultEnv VarDescription
chamber.keys.list.max_concurrent_requestsinteger16CONTAINMENT_CHAMBER__KEYS__LIST__MAX_CONCURRENT_REQUESTSTower concurrency cap for GET /api/v1/chamber/keys.
chamber.keys.generate.enabledbooleanfalseCONTAINMENT_CHAMBER__KEYS__GENERATE__ENABLEDMount POST /api/v1/chamber/keys/generate. Requires key_sources.dynamodb (enforced at boot).
chamber.keys.generate.max_items_per_requestinteger100CONTAINMENT_CHAMBER__KEYS__GENERATE__MAX_ITEMS_PER_REQUESTMaximum keys per keygen request.
chamber.keys.generate.max_concurrent_requestsinteger16CONTAINMENT_CHAMBER__KEYS__GENERATE__MAX_CONCURRENT_REQUESTSTower concurrency cap. Per-request batch parallelism is derived from std::thread::available_parallelism().
chamber.keys.generate.request_timeout_secondsinteger600CONTAINMENT_CHAMBER__KEYS__GENERATE__REQUEST_TIMEOUT_SECONDSKeygen request timeout in seconds.
chamber.keys.generate.backup.enabledbooleanfalseCONTAINMENT_CHAMBER__KEYS__GENERATE__BACKUP__ENABLEDEnable age-encrypted mnemonic backup. Requires non-empty recipients.
chamber.keys.generate.backup.recipientslist of stringsCONTAINMENT_CHAMBER__KEYS__GENERATE__BACKUP__RECIPIENTSage public key recipients for the BIP-39 mnemonic backup.
chamber.keys.import.enabledbooleanfalseCONTAINMENT_CHAMBER__KEYS__IMPORT__ENABLEDMount POST /api/v1/chamber/keys. Memory-only imports (storage.persist=false) work without a mutable backend.
chamber.keys.import.max_items_per_requestinteger100CONTAINMENT_CHAMBER__KEYS__IMPORT__MAX_ITEMS_PER_REQUESTMaximum keys per import request.
chamber.keys.import.max_concurrent_requestsinteger16CONTAINMENT_CHAMBER__KEYS__IMPORT__MAX_CONCURRENT_REQUESTSTower concurrency cap for chamber import.
chamber.keys.import.request_timeout_secondsinteger600CONTAINMENT_CHAMBER__KEYS__IMPORT__REQUEST_TIMEOUT_SECONDSChamber import request timeout in seconds.
chamber.keys.lifecycle.enabledbooleanfalseCONTAINMENT_CHAMBER__KEYS__LIFECYCLE__ENABLEDMount 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_requestinteger500CONTAINMENT_CHAMBER__KEYS__LIFECYCLE__MAX_ITEMS_PER_REQUESTMaximum keys per PATCH/DELETE batch.
chamber.keys.lifecycle.max_concurrent_requestsinteger16CONTAINMENT_CHAMBER__KEYS__LIFECYCLE__MAX_CONCURRENT_REQUESTSTower concurrency cap for chamber lifecycle.
chamber.keys.lifecycle.request_timeout_secondsinteger600CONTAINMENT_CHAMBER__KEYS__LIFECYCLE__REQUEST_TIMEOUT_SECONDSChamber lifecycle request timeout in seconds.
OptionTypeDefaultEnv VarDescription
static_authobjectCONTAINMENT_STATIC_AUTHStateless-only static authorization: named policies, client tokens that reference them, and an optional anonymous binding. Rejected when signer_state is configured.
static_auth.policieslist of objects[]CONTAINMENT_STATIC_AUTH__POLICIESNamed policies: each has a name and rules (same PolicyRule schema as API policies — effect allow/deny, optional scopes, keys, operations).
static_auth.anonymous.policieslist of strings[]CONTAINMENT_STATIC_AUTH__ANONYMOUS__POLICIESNamed policies applied to token-less requests. Empty/absent = anonymous denied (deny-by-default).
static_auth.tokenslist of objects[]CONTAINMENT_STATIC_AUTH__TOKENSClient 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).
OptionTypeDefaultEnv VarDescription
tee.platformenum"none"CONTAINMENT_TEE__PLATFORMTEE 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.regionstring"us-east-1"CONTAINMENT_TEE__NITRO__AWS__REGIONAWS 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.endpointslist of objects[]CONTAINMENT_TEE__NITRO__EGRESS__ENDPOINTSEgress 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).
OptionTypeDefaultEnv VarDescription
opentelemetry.enabledbooleanfalseCONTAINMENT_OPENTELEMETRY__ENABLEDEnable OpenTelemetry OTLP export.
opentelemetry.endpointstring"http://localhost:4317"CONTAINMENT_OPENTELEMETRY__ENDPOINTOTLP gRPC endpoint URL.
opentelemetry.service_namestring"containment-chamber"CONTAINMENT_OPENTELEMETRY__SERVICE_NAMEService name attached to traces.
OptionTypeDefaultEnv VarDescription
logging.levelstring"info"CONTAINMENT_LOGGING__LEVELLog level filter (EnvFilter syntax supported).
logging.formatenum"text"CONTAINMENT_LOGGING__FORMATLog output format: text or json.
logging.log_colorbooleanautoCONTAINMENT_LOGGING__LOG_COLORANSI color output (auto-detected when unset).
server:
listen_address: "0.0.0.0"
listen_port: 9000

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

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

Key sources control where validator keys are loaded from. You can use filesystem, DynamoDB, or both simultaneously.

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

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

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


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]

EIP-3076 slashing protection backend configuration. PostgreSQL is recommended for production deployments as it supports multi-instance setups with full surround vote detection.

BackendValueMulti-InstanceSurround DetectionUse Case
PostgreSQLpostgresProduction (recommended)
SQLitesqliteDevelopment / single instance
DynamoDBdynamodb✅ (implicit)AWS deployments
NoopnoopTesting only
anti_slashing:
backend: postgres
url: "postgresql://user:password@localhost:5432/slashing?sslmode=require"
pool_size: 64
force_ipv4: false

TLS is enabled by default. Append ?sslmode=disable to the URL to disable it. The AWS RDS CA bundle is included in the Docker image.

anti_slashing:
backend: sqlite
path: ./slashing_protection.sqlite
anti_slashing:
backend: noop

Hot-load and remove EIP-2335 keystores at runtime via the /eth/v1/keystores endpoint. Disabled by default.

http:
key_manager_api:
enabled: true

Access 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 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 (effect allow/deny, optional scopes/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 a secret (env:VAR_NAME injected from the environment, or a clear-text value — operator’s choice), the named policies it is bound to, and an optional token_bound_cidrs list restricting the token to peer IPs in those ranges (validated at startup like API tokens — 0.0.0.0/0, ::/0, IPv4 /<8, IPv6 /<16 are 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 IPs

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

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


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 timeout

See Seal & Unseal for the full init ceremony and operational procedures.


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


Structured logging configuration for console output.

logging:
level: "info"
format: "text"
log_color: true

HTTPS 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.
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
tls:
mode: atls
listen_port: 9443
atls:
cert_validity_seconds: 86400 # 24 hours
rotation_interval_seconds: 3600 # rotate every hour

See Attested TLS for the full setup guide, including cert rotation and client-side attestation verification.


Recommended configuration values by validator count:

Parameter1K validators10K validators100K+ validators
signing.max_concurrent_jobs100–500500–20002000+ (default: 2000)
signing.queue_buffer_size1000–30003000–40004000+ (default: 4000)
anti_slashing.postgres.pool_size16–3232–6464+ (default: 64)
CPU cores1–22–44–8+

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

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.

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 second

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 bastion

Validation 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 IP

The 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=...}.

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-For is not consulted for authz decisions (it continues to drive client_ip= in audit logs). A leaked token cannot escape its CIDR binding by forging X-Forwarded-For.
  • IPv4-mapped IPv6 peers (::ffff:a.b.c.d) are canonicalized before matching so a token bound to 10.0.0.0/8 matches both 10.0.1.5 and ::ffff:10.0.1.5.
  • validate_safe_cidrs rejects unsafe ranges (0.0.0.0/0, ::/0, IPv4 /<8, IPv6 /<16): API-minted tokens are rejected at mint time with 400 Bad Request; static_auth tokens 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.