{{ message }}
Repository navigation
Credentials are sealed before they reach the database - #284
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: WaylandYang <wayland0916@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 asenc:v1:+ base64. The key never enters the database:UTOPIA_SECRET_KEY(32 bytes, hex or base64), orsecret.keygenerated 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_keydata_sources.conn_string(the credential-free summary opens first; a value sealed with another key shows as such instead of a garbled URL)sources.configfor the keys inSOURCE_SECRET_KEYS, andsources.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 namesUTOPIA_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.exampleanddocker-compose.ymlcarryUTOPIA_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, aNonein 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 generatesdata/secret.key, backfills the existing rows, and chat still reaches the model.🤖 Generated with Claude Code