Proposal: encrypt secrets at rest via SOPS (opt-in) #233

Closed
opened 2026-06-01 05:16:18 +02:00 by nickorlabs · 1 comment
nickorlabs commented 2026-06-01 05:16:18 +02:00 (Migrated from github.com)

Background

Today secrets in Odysseus (OpenAI key, admin password seed, SearXNG signing key, MCP OAuth secrets, IMAP passwords, etc.) live as plaintext values in .env. The file is gitignored, so they don't leak via git — but they sit unencrypted on disk and any process that can read the file (host backup, container exec, a misconfigured rsync, a leaked snapshot) reveals every secret simultaneously.

PR #22 already moved the SearXNG signing key out of source into ${SEARXNG_SECRET} — the obvious next step is making the .env itself safe at rest for users who want it.

Proposal

Add opt-in support for SOPS with age keys. Existing users see zero change — the feature is only active when a secrets.env.enc file is present at boot.

Scope: what gets encrypted vs stays plaintext

Stays in .env (plaintext, gitignored) Moves to secrets.env.enc (SOPS-encrypted, committable)
APP_PORT, LLM_HOST, SEARXNG_INSTANCE, ALLOWED_ORIGINS, CHROMADB_BIND, LOCALHOST_BYPASS OPENAI_API_KEY, ODYSSEUS_ADMIN_PASSWORD, SEARXNG_SECRET, INTERNAL_TOOL_TOKEN, MCP OAuth client secrets, IMAP passwords

Only true secrets move; non-secret config benefits from staying diff-able in PRs.

Why SOPS specifically

  • Stays line-by-line .env-compatible — encrypts values, keeps keys in cleartext, so structural diffs still work
  • No daemon / no infra — single binary, age private key lives in ~/.config/sops/age/keys.txt
  • The encrypted file is safe to commit — solves "how do I sync secrets between machines" too
  • Drop-in via sops exec-env — wraps process startup with decrypt-and-source; no plaintext on container disk, no application code change

Naming note

The codebase already has routes/vault_routes.py (Bitwarden/Vaultwarden integration — a runtime feature). To avoid collision I'm using "encrypted secrets at rest" / "SOPS" / secrets.env.enc, not "vault" anywhere.

Proposed implementation (minimal)

  1. Add sops binary to the Dockerfile (~10MB Go binary)
  2. Add .sops.yaml (creation rule) and secrets.env.example (format reference)
  3. Update docker/entrypoint.sh: if secrets.env.enc exists, wrap gosu with sops exec-env — decrypted secrets become env vars JIT, plaintext never touches disk
  4. Update .gitignore to mirror the .env / !.env.example pattern for secrets.env
  5. Add a "Encrypting secrets at rest" section to SECURITY.md with the age-keygen → sops -e → docker-compose mount workflow

Backwards-compatible: a user without secrets.env.enc runs exactly as today.

Open questions for the maintainer

  1. Appetite at all — is this a direction you want to take, or do you prefer to keep secrets management out-of-tree (let users wrap with their own SOPS / Doppler / etc. without core support)?
  2. Bundling sops in the image — OK to add the binary unconditionally, or prefer it behind a build ARG / Compose profile so non-users don't pull it?
  3. Key backend — age (proposed, modern + simple) vs GPG vs cloud-KMS abstraction (more flexible but more deps). Happy to support whichever you prefer.

PR with concrete code coming as a follow-up to this issue — easier to discuss against real diffs. Close this freely if the direction isn't wanted; the PR is self-contained and easy to drop.

## Background Today secrets in Odysseus (OpenAI key, admin password seed, SearXNG signing key, MCP OAuth secrets, IMAP passwords, etc.) live as plaintext values in `.env`. The file is gitignored, so they don't leak via git — but they sit unencrypted on disk and any process that can read the file (host backup, container exec, a misconfigured rsync, a leaked snapshot) reveals every secret simultaneously. PR #22 already moved the SearXNG signing key out of source into `${SEARXNG_SECRET}` — the obvious next step is making the `.env` itself safe at rest for users who want it. ## Proposal Add **opt-in** support for [SOPS](https://github.com/getsops/sops) with age keys. Existing users see zero change — the feature is only active when a `secrets.env.enc` file is present at boot. ### Scope: what gets encrypted vs stays plaintext | Stays in `.env` (plaintext, gitignored) | Moves to `secrets.env.enc` (SOPS-encrypted, committable) | |---|---| | `APP_PORT`, `LLM_HOST`, `SEARXNG_INSTANCE`, `ALLOWED_ORIGINS`, `CHROMADB_BIND`, `LOCALHOST_BYPASS` | `OPENAI_API_KEY`, `ODYSSEUS_ADMIN_PASSWORD`, `SEARXNG_SECRET`, `INTERNAL_TOOL_TOKEN`, MCP OAuth client secrets, IMAP passwords | Only true secrets move; non-secret config benefits from staying diff-able in PRs. ### Why SOPS specifically - Stays line-by-line `.env`-compatible — encrypts values, keeps keys in cleartext, so structural diffs still work - No daemon / no infra — single binary, age private key lives in `~/.config/sops/age/keys.txt` - The encrypted file is safe to commit — solves "how do I sync secrets between machines" too - Drop-in via `sops exec-env` — wraps process startup with decrypt-and-source; **no plaintext on container disk**, no application code change ### Naming note The codebase already has `routes/vault_routes.py` (Bitwarden/Vaultwarden integration — a runtime feature). To avoid collision I'm using "encrypted secrets at rest" / "SOPS" / `secrets.env.enc`, not "vault" anywhere. ## Proposed implementation (minimal) 1. Add `sops` binary to the Dockerfile (~10MB Go binary) 2. Add `.sops.yaml` (creation rule) and `secrets.env.example` (format reference) 3. Update `docker/entrypoint.sh`: if `secrets.env.enc` exists, wrap `gosu` with `sops exec-env` — decrypted secrets become env vars JIT, plaintext never touches disk 4. Update `.gitignore` to mirror the `.env` / `!.env.example` pattern for `secrets.env` 5. Add a "Encrypting secrets at rest" section to `SECURITY.md` with the age-keygen → sops -e → docker-compose mount workflow Backwards-compatible: a user without `secrets.env.enc` runs exactly as today. ## Open questions for the maintainer 1. **Appetite at all** — is this a direction you want to take, or do you prefer to keep secrets management out-of-tree (let users wrap with their own SOPS / Doppler / etc. without core support)? 2. **Bundling `sops` in the image** — OK to add the binary unconditionally, or prefer it behind a build ARG / Compose profile so non-users don't pull it? 3. **Key backend** — age (proposed, modern + simple) vs GPG vs cloud-KMS abstraction (more flexible but more deps). Happy to support whichever you prefer. PR with concrete code coming as a follow-up to this issue — easier to discuss against real diffs. Close this freely if the direction isn't wanted; the PR is self-contained and easy to drop.
pewdiepie-archdaemon commented 2026-06-01 16:02:32 +02:00 (Migrated from github.com)

There is a related PR in #236 for optional encrypted-at-rest secrets via SOPS. Leaving this open until that approach is reviewed, because secret storage touches setup, backups, Docker/native installs, and migration behavior.

There is a related PR in #236 for optional encrypted-at-rest secrets via SOPS. Leaving this open until that approach is reviewed, because secret storage touches setup, backups, Docker/native installs, and migration behavior.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sleepy/odysseus#233
No description provided.