ThinkingRoot Docs
Security & Billing

Secrets encryption

Sealed boxes — the gateway can encrypt, only the provisioner can decrypt.

Project secrets are encrypted with libsodium sealed boxes (via the pure-Rust dryoc crate). The design splits the keypair across two services so the component that accepts secrets can never read them.

The asymmetric split

                 public key                         secret key
                     │                                  │
   you ──plaintext──▶ Gateway ──seal──▶ ciphertext ──▶ Provisioner ──unseal──▶ engine env
                     (encrypt-only)      (project_secrets)  (decrypt-at-spawn)
  • The gateway holds only the public key. It seals plaintext on write and stores the ciphertext in project_secrets. It is mathematically unable to decrypt — sealed boxes are anonymous public-key encryption.
  • The provisioner holds the secret key. At container spawn it unseals each secret and injects it as an environment variable into the project's engine.
  • A short hash of the public key is stored as envelope_key_id so key rotation is trackable.

What's never exposed

  • Plaintext is never persisted — only ciphertext lands in the database.
  • The API never returns plaintextGET on secrets lists names and timestamps only. The Console shows a new secret's value once, at creation, and never again.
  • Secrets aren't written to the engine's volume — they exist only as process environment variables inside the container.

Lifecycle implication

Because secrets are injected at spawn, adding or rotating a secret takes effect when the engine next starts — there's no live reload. The Connectors and Secrets guides call this out where it matters.

Local parity

Self-hosted engines read secrets from ~/.config/thinkingroot/secrets.toml (mode 0600) instead of a vault, exposing the same ctx.env names — so Root Functions behave identically in both environments.