Skip to content

Key Management

The Chamber Key Management API is the operator-facing lifecycle surface over runtime and DynamoDB keys. Five HTTP verbs under /api/v1/chamber/keys/*, each independently gated:

MethodPathOperationScopeEnable flag
GET/api/v1/chamber/keysListchamber_keys_listAlways mounted when key_sources.dynamodb is configured
POST/api/v1/chamber/keys/generateGeneratechamber_keys_generatechamber.keys.generate.enabled
POST/api/v1/chamber/keysImportchamber_keys_importchamber.keys.import.enabled
PATCH/api/v1/chamber/keysActivate / deactivatechamber_keys_patchchamber.keys.lifecycle.enabled
DELETE/api/v1/chamber/keysDeactivate or removechamber_keys_deletechamber.keys.lifecycle.enabled

All routes require an authenticated client token with the listed scope. All can return 503 Service Unavailable when the signer is sealed because the handler calls require_unsealed_signer before key work starts.

chamber:
keys:
list:
max_concurrent_requests: 16
generate:
enabled: true
max_items_per_request: 100
max_concurrent_requests: 16
request_timeout_seconds: 600
backup:
enabled: false
# recipients: ["age1..."]
import:
enabled: true
max_items_per_request: 100
max_concurrent_requests: 16
request_timeout_seconds: 600
lifecycle:
enabled: true
max_items_per_request: 500
max_concurrent_requests: 16
request_timeout_seconds: 600

The generated Configuration Reference is the source of truth for defaults.

FieldTypeDefaultNotes
list.max_concurrent_requestsNonZero usize16Tower concurrency cap. List is always mounted when key_sources.dynamodb is configured
generate.enabledboolfalseBoot validation refuses true without key_sources.dynamodb
generate.max_items_per_requestNonZero usize100Maximum count per request
generate.max_concurrent_requestsNonZero usize16Tower request-level cap. Per-request batch parallelism is std::thread::available_parallelism() and not operator-configurable
generate.request_timeout_secondsNonZero u64600Per-request timeout
generate.backup.enabledboolfalseWhen true, requires non-empty recipients (boot-validated)
generate.backup.recipientsVec<String>[]age public keys (age1...) for the BIP-39 mnemonic backup
import.enabledboolfalseMemory-only imports work without a mutable backend; persistent imports require key_sources.dynamodb
import.max_items_per_requestNonZero usize100Hard cap on combined keystores + raw_keys length
import.max_concurrent_requestsNonZero usize16Tower request-level cap
import.request_timeout_secondsNonZero u64600Per-request timeout (long because scrypt + BLS per key)
lifecycle.enabledboolfalseMemory-key deletes work without a mutable backend; PATCH on memory keys returns a per-key error
lifecycle.max_items_per_requestNonZero usize500Higher than import because PATCH/DELETE is one UpdateItem per key with no crypto
lifecycle.max_concurrent_requestsNonZero usize16Tower request-level cap
lifecycle.request_timeout_secondsNonZero u64600Per-request timeout

The key-management routes are documented interactively in the API Reference. This page focuses on behavior that matters operationally.

Derives BLS validator keys via BIP-39 mnemonic → EIP-2333 → bls::Keypair, builds and self-verifies the DepositData, optionally encrypts the mnemonic with age to one or more recipients, and persists each key as KeyStatus::Inactive.

Generated keys start inactive so that:

  1. Operators submit deposit data to the Ethereum deposit contract.
  2. The validator appears on the beacon chain after the activation queue.
  3. Only then does the operator activate signing.

Signing against an inactive key fails at the GVR/key-resolution boundary — the key is in DynamoDB but is not loaded into the live signer cache.

When chamber.keys.generate.backup.enabled = true, the BIP-39 mnemonic for each generated key is encrypted with each recipient’s age public key and stored alongside the key in DynamoDB. To recover: decrypt with the corresponding age private key (kept offline), re-derive via EIP-2333, re-import. This is independent of KMS — your last-resort recovery path if the master key becomes unreconstructable.

Generation is all-or-nothing for derivation and deposit-data construction. Persistence failures are reported per key in the response.

Accepts EIP-2335 keystores and raw hex BLS keys. Decrypts in parallel under a CPU semaphore, then either writes through to DynamoDB or inserts only into the in-memory DashMap.

Import supports two storage modes:

storage.persistKeySourcePersists across restart?Requires key_sources.dynamodb
true (default)DynamoDByesyes
falseMemorynono (works even on a filesystem-only signer)

Memory mode is also the path the Key Manager API takes — different surface, identical outcome for KeySource::Memory keys.

Returns metadata for every loaded key. Always mounted when key_sources.dynamodb is configured.

The signer’s in-memory cache only contains active keys. Inactive keys are retrieved directly from DynamoDB when requested, so operators can audit generated-but-not-yet-activated keys without making them signable.

Patch applies per-key status transitions:

  • activeinactive — removes the key from the in-memory signing cache. The DynamoDB row stays; the encrypted blob is untouched. Subsequent re-activation round-trips through the same decryption path
  • inactiveactive — decrypts the DynamoDB row with the master key and inserts the keypair into the signing cache
  • PATCH on a KeySource::Memory key returns a per-key error (memory keys have no DynamoDB row to flip)
  • PATCH on a missing pubkey returns not_found per-key, not 404 overall

For each pubkey:

  • KeySource::Dynamodb → marks the DynamoDB row inactive and removes the key from the in-memory signing cache. The encrypted row remains for later reactivation or audit.
  • KeySource::Memory → removes from the in-memory DashMap only (no DynamoDB row exists)
  • KeySource::Filesystem → returns not_active per-key (filesystem keys are immutable from HTTP; remove the file and restart)

Every chamber.keys.* route is gated by:

  1. The configured auth scope (see Access Control)
  2. require_unsealed_signer — returns 503 if the signer is in any state other than Unsealed or AwaitingRotation
  3. Tower middleware stack: LoadShedLayerConcurrencyLimitLayer(max_concurrent_requests)TimeoutLayer(request_timeout_seconds)

The 503-sealed response is documented on every chamber.keys endpoint in the OpenAPI spec.