Auth Policies
Auth policies let you run a single Containment Chamber instance for multiple clients while controlling exactly which routes and operations each client can access. Each client authenticates with a unique token, and policies define what that token is allowed to do.
By default, all routes are open without authentication. When auth_policies is configured, requests must include HTTP Basic auth. Requests without a valid token are rejected (unless an unauthenticated_policy is defined).
Token Format
Section titled “Token Format”Tokens must follow these rules:
- Minimum 16 characters
- Alphanumeric characters and dashes only (
a-z,A-Z,0-9,-) - Use the
env:prefix to load a token from an environment variable at startup:If the referenced environment variable is not set, startup fails with a clear error.token: "env:VC1_TOKEN" # resolved from $VC1_TOKEN
Client Integration
Section titled “Client Integration”Clients embed the token in the signer URL using HTTP Basic auth:
http://policy-name:your-token-here@signer:9000The username is the policy name (the key under auth_policies in config). The password is the token. The signer uses the username to look up the correct policy, then validates the token against it.
For single-policy deployments, validator clients that don’t support custom usernames can use any non-empty string — but it must match a policy name in the config. A common pattern is to name your policy x for simplicity:
auth_policies: x: token: "env:VC_TOKEN" allowed_scopes: [sign, public_keys]http://x:my-token@signer:9000This works with all major validator clients: Lighthouse, Teku, Prysm, Lodestar, and Nimbus.
API Scopes
Section titled “API Scopes”Scopes control which routes a policy can access. Omit allowed_scopes and denied_scopes to allow all routes.
| Scope | Grants access to | Description |
|---|---|---|
sign | POST /api/v1/eth2/sign/{pubkey} | BLS signing operations |
public_keys | GET /api/v1/eth2/publicKeys | List loaded validator public keys |
list_keys | GET /eth/v1/keystores | List keystores via Key Manager API |
import_keystores | POST /eth/v1/keystores | Import keystores via Key Manager API |
delete_keys | DELETE /eth/v1/keystores | Remove keystores via Key Manager API |
chamber_keys_generate | POST /api/v1/chamber/keys/generate | Generate new validator keys in DynamoDB |
chamber_keys_write | POST /api/v1/chamber/keys | Import keys into DynamoDB or memory |
chamber_keys_list | GET /api/v1/chamber/keys | List all loaded keys with source and status |
chamber_keys_patch | PATCH /api/v1/chamber/keys | Change key status (active/inactive) |
chamber_keys_delete | DELETE /api/v1/chamber/keys | Delete keys from DynamoDB or memory |
chamber_seal | POST /api/v1/chamber/seal | Emergency seal — zeroize master key, block all signing |
chamber_rotate | POST /api/v1/chamber/rotate/kms | Rotate KMS keys (re-split master key with new ARNs) |
POST /api/v1/chamber/rotate/unseal | Start operator rotation (new passphrases) | |
DELETE /api/v1/chamber/rotate/unseal | Cancel in-progress operator rotation | |
POST /api/v1/chamber/rotate/mode | Switch between kms_only and unseal modes | |
DELETE /api/v1/chamber/unseal | Discard accumulated unseal shares | |
chamber_status | GET /api/v1/chamber/status | View current seal state, key counts, enclave info |
GET /api/v1/chamber/attestation | Retrieve Nitro Enclave attestation document |
The following endpoints use dedicated tokens instead of auth policy scopes:
| Endpoint | Auth | Token source |
|---|---|---|
POST /api/v1/chamber/init | Setup token (one-time) | Printed to stdout on first boot |
DELETE /api/v1/chamber/init | Setup token | Same as above |
POST /api/v1/chamber/unseal/register | Registration token (per-operator) | Returned in the init response |
POST /api/v1/chamber/unseal | Passphrase (no token) | Operator’s passphrase is the authentication |
Valid Signing Operations
Section titled “Valid Signing Operations”The following operation names can be used in allowed_signing_operations and denied_signing_operations:
| Operation | Description |
|---|---|
AGGREGATION_SLOT | Aggregation slot selection |
AGGREGATE_AND_PROOF | Aggregate attestation proof |
ATTESTATION | Attestation signing |
BLOCK_V2 | Block proposal |
RANDAO_REVEAL | RANDAO reveal |
SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF | Sync committee contribution and proof |
SYNC_COMMITTEE_MESSAGE | Sync committee message |
SYNC_COMMITTEE_SELECTION_PROOF | Sync committee selection proof |
VALIDATOR_REGISTRATION | Validator registration |
VOLUNTARY_EXIT | Voluntary exit |
Configuration Examples
Section titled “Configuration Examples”Each policy is a named entry under auth_policies with a token and optional restrictions.
By default, Containment Chamber runs without any auth policies. When no auth_policies block is present, all routes are open without tokens.
This is the simplest setup — no auth configuration needed:
server: listen_address: "0.0.0.0" listen_port: 9000
key_sources: filesystem: paths: - ./keystores
antislashing: backend: sqlite path: ./slashing-protection.sqlite
# No auth_policies = all requests allowed (Web3Signer compatible)A full-access operator token with access to all routes and operations:
auth_policies: operator: token: "env:OPERATOR_TOKEN" # No restrictions — full access to all scopes and operationsCombine with a restricted validator client token for separation of duties:
auth_policies: operator: token: "env:OPERATOR_TOKEN" # Full access
vc1: token: "env:VC1_TOKEN" allowed_scopes: - sign - public_keys allowed_keys: - "0xabc123..." - "0xdef456..."A read-only monitoring token that can list keys but cannot sign:
auth_policies: monitoring: token: "env:MONITORING_TOKEN" allowed_scopes: - public_keys - list_keysThis token can call GET /api/v1/eth2/publicKeys and GET /eth/v1/keystores, but signing and key management operations return HTTP 403.
Restrict a token to signing only — no key management access:
auth_policies: vc1: token: "env:VC1_TOKEN" allowed_scopes: - sign - public_keys allowed_keys: - "0xabc123..." - "0xdef456..." denied_signing_operations: - "VOLUNTARY_EXIT"
exit-admin: token: "env:EXIT_ADMIN_TOKEN" allowed_scopes: - sign allowed_keys: - "0xabc123..." - "0xdef456..." allowed_signing_operations: - "VOLUNTARY_EXIT"Each tenant can only sign for their own validators. Requests for other keys return HTTP 403.
The unauthenticated_policy controls what happens to requests that arrive without any auth token:
# Allow unauthenticated access to signing and public key listing onlyunauthenticated_policy: allowed_scopes: - sign - public_keys# Allow unauthenticated signing but block voluntary exitsunauthenticated_policy: allowed_scopes: - sign - public_keys denied_signing_operations: - "VOLUNTARY_EXIT"# Combine with authenticated policiesauth_policies: exit-admin: token: "env:EXIT_ADMIN_TOKEN" allowed_scopes: - sign allowed_signing_operations: - "VOLUNTARY_EXIT"unauthenticated_policy: allowed_scopes: - sign - public_keys denied_signing_operations: - "VOLUNTARY_EXIT"To reject all unauthenticated requests, omit unauthenticated_policy entirely. When auth_policies are configured, requests without a valid token receive HTTP 401.
Policy Evaluation Rules
Section titled “Policy Evaluation Rules”Default behavior: When no auth_policies and no unauthenticated_policy are configured, auth is disabled — all requests are allowed regardless of whether a token is sent.
| Named policies | Unauthenticated policy | No token | Valid credentials | Invalid credentials |
|---|---|---|---|---|
| ❌ None | ❌ None | ✅ Allow (auth off) | ✅ Allow (auth off) | ✅ Allow (auth off) |
| ❌ None | ✅ Set | ✅ Check unauthenticated | ✅ Check unauthenticated | ✅ Check unauthenticated |
| ✅ Set | ❌ None | ❌ 401 | ✅ Check policy | ❌ 403 |
| ✅ Set | ✅ Set | ✅ Check unauthenticated | ✅ Check policy | ❌ 403 |
When no named policies are configured, tokens are ignored — there’s nothing to validate them against. All requests are evaluated against the unauthenticated_policy (if set) or allowed unconditionally (if not set).
When named policies ARE configured, sending an unrecognized or invalid token always results in 403 — tokens are never silently downgraded to unauthenticated access.
Once a policy is matched, the request is checked against:
allowed_scopesordenied_scopes— which API routes are accessibleallowed_keys— which validator public keys can be signed (signing scope only)allowed_signing_operationsordenied_signing_operations— which signing operations are permitted
Key Visibility
Section titled “Key Visibility”The two key listing endpoints have different visibility rules:
GET /api/v1/eth2/publicKeys (Web3Signer API)
Section titled “GET /api/v1/eth2/publicKeys (Web3Signer API)”Returns only keys the token can sign with. This endpoint is designed for validator clients — showing keys the client can’t sign would cause it to accept validator duties and then fail, resulting in missed attestations and penalties.
The response is filtered by:
- The token must have the
public_keysscope (otherwise 403) - The token must also have the
signscope — keys you can’t sign with are excluded - If
allowed_keysis set, only those keys are returned
| Token scopes | allowed_keys | Result |
|---|---|---|
sign + public_keys | Not set | All loaded keys |
sign + public_keys | ["0xabc...", "0xdef..."] | Only those 2 keys |
public_keys only | Any | Empty list (can’t sign = can’t see) |
sign only | Any | 403 (no public_keys scope) |
GET /api/v1/chamber/keys (Chamber API)
Section titled “GET /api/v1/chamber/keys (Chamber API)”Returns all loaded keys regardless of allowed_keys. This endpoint is for operators managing the signer — they need full visibility to import, delete, and monitor key status. Requires the chamber_keys_list scope.
Behavior When Sealed
Section titled “Behavior When Sealed”When the signer is sealed (master key not in memory), endpoints respond differently:
| Endpoint | Behavior when sealed |
|---|---|
GET /upcheck | 200 with seal status text (e.g., "sealed", "kms_unsealed"). Always returns 200 — load balancers see the process as healthy regardless of seal state. Only returns 503 if the anti-slashing backend is down. |
GET /api/v1/eth2/publicKeys | 200 with empty list []. No keys are loaded when sealed, so there’s nothing to return. No error. |
POST /api/v1/eth2/sign/{id} | 503 "signer is sealed". The seal check runs before auth — even a valid token gets 503. |
GET /api/v1/chamber/keys | 503 "signer is sealed". Same as signing — requires an unsealed signer. |
| Chamber seal endpoints | Work in all states — they manage the seal lifecycle. |
Two Auth Mechanisms
Section titled “Two Auth Mechanisms”The signer uses two different authentication mechanisms:
Auth policies (HTTP Basic)
Section titled “Auth policies (HTTP Basic)”Used by: signing endpoints, key manager, chamber keys API.
Format: Authorization: Basic base64(policy-name:token)
Tokens and scopes are configured in auth_policies. The policy name is the username in the Basic auth header.
Setup and registration tokens (Bearer)
Section titled “Setup and registration tokens (Bearer)”Used by: chamber seal/unseal ceremony endpoints (/api/v1/chamber/init, /api/v1/chamber/unseal/register).
Format: Authorization: Bearer token
These are one-time tokens generated at runtime — the setup token is logged on first boot, registration tokens are returned in the init response. They are not configured in auth_policies.
The operator CLI subcommands use the correct format automatically:
--auth-token→ sent as Bearer for seal operations, not applicable for auth policy endpoints--setup-token→ sent as Bearer--registration-token→ sent as Bearer- Passphrase-based auth (unseal) → no token needed