Skip to main content

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.yml as GIG_MASTER_KEY: ${GIG_MASTER_KEY:?...} — compose fails fast if unset.
warning

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/rand on 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_KEY orphans existing ciphertexts. If you must rotate now, re-enter credentials after changing the key.
note

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.