You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Support optional encrypted-at-rest secrets via SOPS
Adds opt-in SOPS support so deployers who want it can encrypt true
secrets (OPENAI_API_KEY, ODYSSEUS_ADMIN_PASSWORD, SEARXNG_SECRET, MCP
OAuth secrets, IMAP passwords, etc.) at rest instead of leaving them
plaintext in .env. Non-secret config (APP_PORT, LLM_HOST, etc.) stays
in .env so PR diffs remain reviewable.
Uses SOPS's idiomatic in-place encryption convention: secrets.env is
encrypted in place with 'sops -e -i secrets.env' and the encrypted file
itself is safe to commit. To edit later: 'sops secrets.env' opens
$EDITOR with decrypted contents and re-encrypts on save.
The feature is only active when /app/secrets.env is present at
container start; existing deployments behave exactly as before. If
secrets.env is present but NOT SOPS-encrypted (detected via the
sops_version marker), the entrypoint refuses to start — that shape is
almost always a packaging mistake.
- Dockerfile: pin sops v3.13.1 (~10MB static Go binary), arch-aware
- docker/entrypoint.sh: wrap gosu with 'sops exec-env' when encrypted
secrets.env exists; fail loudly if sops is missing, no SOPS_AGE_KEY
[_FILE] is set, or the file is plaintext
- .sops.yaml: creation rule with placeholder age public key
- secrets.env.example: documents the in-place encryption workflow
- .gitignore: ignore transient decrypt artifacts only — encrypted
secrets.env is intentionally committable
- SECURITY.md: new 'Encrypting Secrets At Rest' section with the
age-keygen -> 'sops -e -i' -> compose-mount workflow
Verified end-to-end with the entrypoint exec-ing a sentinel-loaded
container:
positive (encrypted file mounted, key bound) -> sentinel in env
negative (no secrets.env) -> entrypoint unchanged
refuse (plaintext secrets.env present) -> exit 1 with hint
Proposed in #233. Naming chosen to avoid colliding with the existing
runtime Bitwarden integration at routes/vault_routes.py.
Copy file name to clipboardExpand all lines: SECURITY.md
+51Lines changed: 51 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,6 +23,57 @@ Security fixes are handled on the default branch until formal releases are cut.
23
23
- Treat shell, model-serving, MCP, email, calendar, and vault features as privileged admin functionality.
24
24
- Common internal-only ports are Odysseus `7000`, SearXNG `8080`, ntfy `8091`, ChromaDB `8100`, Ollama `11434`, and local model/provider APIs such as `8000-8020`.
25
25
26
+
## Encrypting Secrets At Rest (Optional)
27
+
28
+
By default, secret values (API keys, admin password seed, SearXNG signing key, etc.) live in `.env` as plaintext. The file is gitignored, but any process that reads it (container exec, host backup, snapshot) reveals every secret. For deployments where that's a concern, Odysseus supports [SOPS](https://github.com/getsops/sops) with age keys to encrypt secrets at rest.
29
+
30
+
This is **opt-in** — the feature is only active when an encrypted `secrets.env` is present at container start. Existing deployments behave exactly as before.
31
+
32
+
### Setup
33
+
34
+
1. Install [`age`](https://github.com/FiloSottile/age) on your host and generate a key:
35
+
36
+
```bash
37
+
mkdir -p ~/.config/sops/age
38
+
age-keygen -o ~/.config/sops/age/keys.txt
39
+
age-keygen -y <~/.config/sops/age/keys.txt # prints your age public key
40
+
```
41
+
42
+
2. Paste the printed public key into `.sops.yaml` (replace the `age1REPLACE...` placeholder).
43
+
44
+
3. Copy the example, fill in real secret values, then encrypt in place:
45
+
46
+
```bash
47
+
cp secrets.env.example secrets.env
48
+
$EDITOR secrets.env # fill in real values
49
+
sops -e -i secrets.env # encrypt in place — file is now safe to commit
50
+
```
51
+
52
+
To edit later: `sops secrets.env` opens `$EDITOR` with decrypted contents and re-encrypts on save.
53
+
54
+
4. Provide the age **private** key to the container by mounting it and pointing `SOPS_AGE_KEY_FILE` at it (`docker-compose.yml`):
At container start, the entrypoint runs `sops exec-env` to inject secrets as env vars JIT — plaintext never touches the container filesystem. If `secrets.env` is present but **not** SOPS-encrypted, the entrypoint refuses to start (it's almost always a packaging mistake).
66
+
67
+
### What to put in `secrets.env` vs `.env`
68
+
69
+
Only true secrets belong in `secrets.env`. Non-secret configuration stays in `.env` because it benefits from plaintext diffs and PR review:
0 commit comments