Move all hardcoded credentials out of tracked compose/env files into the
git-crypt-encrypted secrets/ directory, using each app's supported mechanism:
- env_file -> secrets/*.env: mealie, navidrome, karakeep, meilisearch,
baserow, maloja, valheim, photoprism, komf, openldap, penpot, vaultwarden
- file:///run/secrets: authentik email password
- jelu DB password appended to existing secrets/jelu.env
Untrack root .env (interpolated ${VAR} secrets) and add sanitized
.env.example template; gitignore /.env.
Move unreferenced orphan files (mediawiki/rtorrent/snibox .env) into
secrets/ to preserve values while encrypting them.
Add SECURITY.md documenting the secrets conventions and a rotation
checklist. NOTE: all migrated values remain in prior git history and
must be rotated at their providers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.5 KiB
Security: secrets handling & rotation checklist
How secrets are stored in this repo
secrets/— git-crypt encrypted (see.gitattributes:secrets/** filter=git-crypt). All real credentials live here. The working-tree copies are plaintext (git-crypt only encrypts the committed blobs), so Docker reads them normally.- Root
.env— git-ignored (/.env) and not committed. It holds non-secret config (ports, paths, domains) plus a few${VAR}values interpolated across services. Use.env.exampleas the template; fill in the blanked secret values locally. - Tracked compose /
*.envfiles — must contain no secret values. Pull secrets in via one of:env_file: - secrets/<svc>.env(universal)FILE__VARNAME=/run/secrets/<name>(LinuxServer.io images, e.g. calibre-web-automated)VARNAME_FILE=/run/secrets/<name>(miniflux, gitea-runner, …)file:///run/secrets/<name>(authentik)
Before you commit
# git-crypt must be unlocked so secrets/ files encrypt on commit
git-crypt status | grep -i 'not encrypted' || echo "all secrets/ files encrypted"
Quick scan for accidental plaintext secrets in tracked files:
git ls-files | grep -vE '^secrets/|^\.env\.example$' | xargs grep -nIE \
'(PASSWORD|SECRET|TOKEN|API_?KEY|CLIENT_SECRET|MASTER_KEY)=[^ ]' 2>/dev/null \
| grep -vE '_FILE|/run/secrets/|file:///|\$\{|=\s*$'
Rotation checklist
All values below were committed to git history in plaintext before the 2026-06-12
migration. Migrating them to secrets/ only protects them going forward — each
must be rotated at its provider, then history scrubbed (see bottom).
Tick each once the credential has been regenerated and the new value written to the
corresponding secrets/ file.
External / high priority (reachable beyond the LAN)
- Gmail app password —
EMAIL_PASSWORDin.env(reused by vaultwarden, mealie, baserow SMTP). Regenerate at Google Account → App passwords. - ProtonMail SMTP token —
secrets/authentik_email_password. Regenerate in Proton → SMTP submission. - mealie OIDC client secret —
secrets/mealie.env. Rotate themealieprovider in Authentik. - Last.fm API key + secret —
secrets/navidrome.env. Reissue at last.fm/api/accounts. - Meilisearch master key —
secrets/meilisearch.env(used by karakeep + meilisearch). Generate a new random key. - karakeep
NEXTAUTH_SECRET—secrets/karakeep.env.openssl rand -base64 36. - vaultwarden
ADMIN_TOKEN—secrets/vaultwarden.env. Regenerate withvaultwarden hash(Argon2). - jelu
GOOGLE_API_KEY—secrets/jelu.env. Rotate in Google Cloud console.
Internal (LAN-only, still rotate — kralovna is reused widely)
- Postgres password —
POSTGRES_PASSWORDin.env(kralovna). - MySQL/MariaDB passwords —
MYSQL_PASSWORD,MYSQL_ROOT_PASSWORDin.env(kralovna). - baserow DB password —
secrets/baserow.env. - photoprism admin + DB passwords —
secrets/photoprism.env. - jelu DB password —
secrets/jelu.env. - komf komga password + Kavita API key —
secrets/komf.env. - openldap admin + readonly passwords —
secrets/openldap.env. - maloja force password —
secrets/maloja.env. - valheim server password —
secrets/valheim.env. - penpot postgres password —
secrets/penpot.env.
Orphaned services (moved to secrets/; rotate if still running anywhere)
- mediawiki MySQL password —
secrets/mediawiki.env. - rtorrent RPC2 password —
secrets/rtorrent.env. - snibox
SECRET_KEY_BASE—secrets/snibox.env.
Scrubbing git history
After rotating, remove the old plaintext values from history so the leaked secrets become useless even to someone with an old clone:
# Using git-filter-repo (recommended). Removes the old tracked paths entirely.
git filter-repo --invert-paths \
--path .env \
--path mediawiki.env --path rtorrent.env --path snibox.env
# Then force-push and have every clone re-clone (rewritten history diverges):
git push --force --all
git push --force --tags
For surgical edits to lines inside files that stay tracked (e.g. a secret that lived
in docker-compose.yml), use git filter-repo --replace-text <file> with
old==>***REMOVED*** rules, or BFG's --replace-text.
Rotation is what actually neutralizes a leak. History scrubbing is best-effort — assume anything ever pushed is already compromised and rotate regardless.