Vault & Encryption
Credentials at rest are protected by envelope encryption (internal/vault, DESIGN.md decision #15). Every secret is encrypted under its own fresh data-encryption key (DEK); each DEK is wrapped by a master key-encryption key (KEK) that the gateway loads at startup and that never touches the database. A database dump without the KEK is useless.
All cryptography is XChaCha20-Poly1305 (libsodium primitives via golang.org/x/crypto/chacha20poly1305).
GIG_MASTER_KEY
The KEK comes from the GIG_MASTER_KEY environment variable: a hex-encoded 32-byte key.
GIG_MASTER_KEY=$(openssl rand -hex 32)
- Required. The gateway refuses to start without it (and rejects keys that don't decode to exactly 32 bytes).
- It exists only in the gateway process's memory and your deployment environment — never in SQLite/Postgres, never in a sandbox, never in logs.
- It is consumed by
docker-compose.ymlasGIG_MASTER_KEY: ${GIG_MASTER_KEY:?...}— compose fails fast if unset.
Compose environment variables are visible via docker inspect on the host. The *_FILE Docker-secrets convention is currently wired only for the OIDC client secret (GIG_OIDC_CLIENT_SECRET_FILE); DESIGN.md plans the same pattern for GIG_MASTER_KEY, but today it must be set as a plain environment variable. Whatever you do: back up GIG_MASTER_KEY separately from the database. Lose the key and every stored credential is permanently unrecoverable — by design.
What's stored in the vault
The vault encrypts the secrets that the rest of the system must never see in plaintext at rest:
- User API keys / tokens for Tier 1 servers — decrypted in gateway memory only at the moment the egress proxy injects them into an outbound request.
- Tier 2 secrets (database strings and similar) destined for sandbox environments — the manifest schema and store carry the target env var, but the Tier 2 env-injection flow itself is still planned (see Security Model).
- Credentials are managed per user via the REST API and dashboard; ciphertext lives in the SQLite store alongside non-secret metadata (Postgres is the design-level second driver).
What is not in the vault: the KEK itself, placeholders (deliberately non-secret sentinels), and manifests.
Ciphertext layout
Encrypt generates a fresh random 32-byte DEK per secret, seals the plaintext under the DEK, seals the DEK under the KEK, and concatenates:
[1 byte version = 1]
[24 byte nonce_kek]
[4 byte big-endian length of wrapped DEK]
[wrapped DEK = Seal(KEK, nonce_kek, DEK)]
[24 byte nonce_dek]
[ciphertext = Seal(DEK, nonce_dek, plaintext)]
Properties that fall out of this design:
- Per-secret DEKs mean no nonce-reuse pressure on the KEK and no single key encrypting bulk data — the KEK only ever wraps 32-byte DEKs.
- XChaCha20-Poly1305's 24-byte nonces are large enough to generate randomly with negligible collision risk; both nonces are fresh from
crypto/randon every encryption. - Authenticated encryption (Poly1305) means tampered or truncated ciphertext fails decryption loudly (
vault: malformed or unsupported ciphertext/vault: unwrap DEK).
Key rotation
The design (DESIGN.md decision #15) calls for versioned key IDs in the ciphertext header for rotation. The current implementation carries a single leading version byte (1), which gives the format room to evolve; an automated rotation workflow (re-wrapping DEKs under a new KEK) is not yet implemented.
What this means operationally today:
- The version byte lets a future gateway recognize and migrate old ciphertexts.
- Envelope encryption makes rotation cheap when it lands: rotating the KEK only requires re-wrapping each secret's small DEK — the bulk ciphertext never needs re-encryption.
- Until a rotation tool ships, changing
GIG_MASTER_KEYorphans existing ciphertexts. If you must rotate now, re-enter credentials after changing the key.
Honest status: encryption, decryption, and the versioned format are implemented and tested; multi-key rotation tooling is a documented design intent, not a shipped feature.
How the pieces connect
GIG_MASTER_KEY (env, hex) ──decode──▶ KEK (32 bytes, gateway memory only)
│
user saves a credential │ wraps
via dashboard / REST API ▼
plaintext ──Seal(DEK)──▶ ciphertext + wrapped DEK ──▶ SQLite store
│
sandbox makes HTTPS call │ unwraps + opens
with placeholder token ▼
proxy injects RealSecret ◀── Decrypt, in-memory, at request time
The decrypted secret exists transiently in the gateway process while the proxy rewrites the request header; it is never written back anywhere and never crosses into a Tier 1 sandbox.
See Configuration for the full environment variable reference and Security Model for how the vault fits into the overall trust boundaries.