This project is security-sensitive. Some configurations are intended only for development, trusted networks, or local break-glass operation.
Deployment stance
- Treat
KMS.Client.Localcalls withoutsession:as trusted application/admin execution. - Treat the HTTP API as an admin surface. All non-health HTTP API routes require the configured bearer token.
- Do not expose the HTTP API to an untrusted network without TLS and API-token protection.
- Disable request-body logging in proxies, app logs, APM tools, and error reporters.
- Disable Erlang crash dump writing in production with
ERL_CRASH_DUMP_BYTES=0.
Risky configurations
HTTP API token required
If config :kms, :api has no :api_token, all non-health HTTP API routes fail closed with api_token_required.
Deployments must set KMS_API_TOKEN / config :kms, :api, api_token: ... and send it as:
Authorization: Bearer <token>Crash dumps
Production startup fails unless crash dump writing is disabled with ERL_CRASH_DUMP_BYTES=0 or ERL_CRASH_DUMP_SECONDS=0.
Crash dumps can expose process stack/heap data and runtime metadata from code paths that handle plaintext key material. Treat any KMS crash dump as a secret spill. See Crash dumps.
Plain HTTP
KMS.API can run Bandit with plain HTTP inside the :kms OTP application. Plain HTTP is unsafe across untrusted networks because bearer tokens, session secrets, and request bodies can be intercepted.
Use TLS directly or terminate TLS at a trusted proxy. Ensure proxy/access logs do not capture request bodies or bearer tokens.
SQLite database file
SQLite is the default persistence backend. Protect KMS_DATABASE_PATH or $KMS_DATA_DIR/kms.sqlite3 plus -wal/-shm sidecars with strict filesystem permissions, backups, and host-level access controls. The database contains encrypted key material, principals, session verifiers, factors, bindings, fallbacks, and WebAuthn state.
Local RMK provider
KMS.RMK.Local stores the root material key in a local file. This is useful for development and simple single-node operation, but production deployments should explicitly decide whether local file-key custody is acceptable.
Set the key path with KMS_RMK_LOCAL_KEY_PATH or config :kms, KMS.RMK.Local, key_path: .... Development/test may auto-create the key file with 0600 permissions. Production requires an explicit key path and never auto-creates this key; startup fails if the path is missing or the configured key file is missing, unreadable, or empty.
Protect the key file with host-level controls, backups, and strict file permissions. Loss of this key can make stored KEK material unrecoverable. Disclosure can expose encrypted secrets.
MFA factors
The built-in one_time_code factor returns the generated code in the API challenge response. This is useful for tests and development only. It is not production MFA when the same remote caller receives the code.
Use the built-in totp provider for authenticator-app based MFA. TOTP seeds are stored RMK-encrypted in factor config, and TOTP session challenges never return codes or seeds. TOTP is still phishable; prefer WebAuthn/passkeys when phishing resistance is required.
Break-glass recovery
Fallback private keys are offline break-glass secrets. The compiled HTTP router exposes no fallback operations, including public-key registry and envelope metadata calls.
Run all fallback operations only from trusted code with remote execution access to the running KMS instance, or from another trusted in-process runtime. Never route fallback private keys through ordinary HTTP clients, proxies, or logs.
Fallback envelopes use version 1 HKDF-style derivation with context info bound to fallback purpose, target resource id, fallback public-key id, curve, recipient public key, and ephemeral public key. This prevents ciphertext replay between KEK/principal fallback rows. Unsupported fallback versions and pre-release SHA256(shared_secret) payloads fail closed.
KMS.AsymmetricCrypto is HPKE-aligned ECDH envelope encryption, not full RFC 9180 HPKE. It uses ephemeral ECDH, the ephemeral public key as the encapsulated key, HKDF-style key derivation, AES-GCM with AAD, and caller :info context binding. The default envelope key size remains 16 bytes for compatibility; callers may pass key_size: 16 | 24 | 32 (or symmetric_key_size) and may opt into kdf: :hpke_labeled for RFC 9180-inspired labeled extract/expand domain separation. Envelopes provide recipient confidentiality and AEAD integrity/context binding, but not sender authentication.
Inspect/log redaction
Secret-bearing schemas mark sensitive fields with Ecto redact: true, including KEK master keys, principal passwords/private keys, session secrets/challenges, provider config, and fallback encrypted payloads. Remote session handles redact bearer secrets and challenges from inspect/1 and JSON encoding. HTTP factor responses omit provider config.
Redaction reduces accidental leakage in logs and changeset inspection. Do not log request bodies, bearer tokens, plaintext secrets, crash dumps, or APM payloads.
Session bearer credentials
Session secrets are bearer credentials. Store and transmit them only over trusted paths.
KMS stores session secret verifiers as versioned HMAC-SHA256 values keyed by KMS_SESSION_HASH_KEY / :kms, :session_hash_key. Production requires this key to be configured and high entropy. Legacy unsalted SHA-256 session hashes are accepted only so pre-existing sessions can expire naturally.
Login/password rate limits
KMS enforces fixed-window failed password attempt rate limits by default:
:session_login_rate_limit[:principal]limits attempts per{kind, ref}.:session_login_rate_limit[:source]limits attempts per remote source when the caller suppliessource:; HTTP session creation passes the connection remote IP automatically.
Default configuration is conventional and simple:
config :kms, :session_login_rate_limit,
principal: [limit: 5, window_ms: :timer.minutes(5)],
source: [limit: 50, window_ms: :timer.minutes(5)]Set config :kms, :session_login_rate_limit, false to disable built-in enforcement when the embedding application provides its own rate-limiting controls. Successful password authentication clears the principal login counter; source counters remain until the window expires to resist username spraying.
Session lifetime
Sessions have three lifetime controls:
:session_max_ttl_mscaps caller-requestedttl_msvalues.:session_idle_timeout_mscaps the idle window extended by refresh/nudge.:session_max_lifetime_msis an absolute lifetime from session creation; after this, forced re-authentication is required.
refresh_session/2 and /api/sessions/refresh rotate the session bearer secret and invalidate the old secret. nudge_session/2 and /api/sessions/nudge extend the idle expiry for an active session without decrypting private key material.
High-entropy secrets
KMS rejects low-entropy secrets at encryption/session creation time. This applies to RMK PDK secrets, passphrase-session passphrases, and factor-provider secrets used to protect pending sessions.
Use randomly generated high-entropy values. Human passphrases such as open sesame are rejected before new encrypted material is written.