Credentials are sealed before they reach the database by WaylandYang · Pull Request #284 · deeplethe/utopia · GitHub
Skip to content

Credentials are sealed before they reach the database - #284

Merged
WaylandYang merged 1 commit into
devfrom
feat/sealed-secrets
Sep 4, 2026
Merged

WaylandYang merged 1 commit into
devfrom
feat/sealed-secrets

Conversation

@WaylandYang

Copy link
Copy Markdown
Contributor

SECURITY.md listed this as the first thing to fix before 1.0: LLM API keys, Ask-the-Data connection strings, source tokens and push keys were plain text in Postgres. Now they are sealed before they reach the database.

Mechanism (utopia_core::secrets): AES-256-GCM, a fresh nonce per value, stored as enc:v1: + base64. The key never enters the database: UTOPIA_SECRET_KEY (32 bytes, hex or base64), or secret.key generated in the data directory on first start (0600 on unix). The threat model is "a database leak is not a credential leak" — a dump or a read-only role sees ciphertext; someone with both the data directory and the database is server access control's problem, not this layer's.

Where it applies — the store seals on write and opens on read at four choke points, so callers see plaintext as before:

  • llm_settings.chat_api_key / embed_api_key
  • data_sources.conn_string (the credential-free summary opens first; a value sealed with another key shows as such instead of a garbled URL)
  • sources.config for the keys in SOURCE_SECRET_KEYS, and sources.ingest_token (push verification already compared in Rust after fetching by id, so it keeps working)

Upgrade: rows written by earlier versions are read as plaintext and sealed on the next start (sealing::backfill, idempotent, runs every boot). A wrong key does not fall back to plaintext — reading such a row is an error that names UTOPIA_SECRET_KEY, because handing a garbled key to an external API is worse than failing.

Not in this cut: key rotation (a re-encryption pass with the old and the new key; the format is versioned for it).

Docs: SECURITY.md / SECURITY.zh-CN.md move the item from "before a public network" to "what is in place"; .env.example and docker-compose.yml carry UTOPIA_SECRET_KEY.

Tests: five unit tests on the sealer (round trip, fresh nonce, idempotent seal, tamper detection, key parsing) and a_secret_is_sealed_at_rest (store, DB): each of the four places is ciphertext under raw SQL and plaintext through the store, a None in the settings upsert keeps the old key, and a legacy plaintext row reads fine and is sealed by the backfill. Verified in the isolated env: first boot generates data/secret.key, backfills the existing rows, and chat still reaches the model.

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
@WaylandYang
WaylandYang merged commit b9ba34b into dev Sep 4, 2026
@WaylandYang
WaylandYang deleted the feat/sealed-secrets branch September 4, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant