diff --git a/.env b/.env.example similarity index 96% rename from .env rename to .env.example index 178df5d..824e9fd 100644 --- a/.env +++ b/.env.example @@ -21,16 +21,16 @@ PHOTOS_STORAGE_PATH=/srv/dev-disk-by-uuid-2d34f1a9-4284-4cad-ae9a-f1ef36244201/p EMAIL_ADMIN=lukas@kucharczyk.xyz EMAIL_FROM=kucharczyk.lukas@gmail.com EMAIL_HOST=smtp.gmail.com -EMAIL_PASSWORD=sebrubdsgkuptcjr +EMAIL_PASSWORD= EMAIL_PORT=587 POSTGRES_HOST=postgres POSTGRES_USER=lukas -POSTGRES_PASSWORD=kralovna +POSTGRES_PASSWORD= POSTGRES_PORT=5432 MYSQL_HOST=mariadb MYSQL_USER=lukas -MYSQL_PASSWORD=kralovna -MYSQL_ROOT_PASSWORD=kralovna +MYSQL_PASSWORD= +MYSQL_ROOT_PASSWORD= MYSQL_PORT=3306 PUID=1000 PGID=100 diff --git a/.gitignore b/.gitignore index e50883d..6d2defd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -git-crypt-key \ No newline at end of file +git-crypt-key +# Real environment file with secrets; use .env.example as the template +/.env \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ad8bb47 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,91 @@ +# 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.example`** as the template; fill in the blanked secret values + locally. +- **Tracked compose / `*.env` files** — must contain **no secret values**. Pull secrets + in via one of: + - `env_file: - secrets/.env` (universal) + - `FILE__VARNAME=/run/secrets/` (LinuxServer.io images, e.g. calibre-web-automated) + - `VARNAME_FILE=/run/secrets/` (miniflux, gitea-runner, …) + - `file:///run/secrets/` (authentik) + +## Before you commit + +```sh +# 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: + +```sh +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_PASSWORD` in `.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 the `mealie` provider 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 with `vaultwarden 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_PASSWORD` in `.env` (`kralovna`). +- [ ] **MySQL/MariaDB passwords** — `MYSQL_PASSWORD`, `MYSQL_ROOT_PASSWORD` in `.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: + +```sh +# 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 ` 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. diff --git a/baserow.env b/baserow.env index 42efe82..b68e2da 100644 --- a/baserow.env +++ b/baserow.env @@ -2,7 +2,7 @@ BASEROW_PUBLIC_URL=https://baserow.${DOMAIN} DATABASE_HOST=${POSTGRES_HOST} DATABASE_NAME=baserow DATABASE_USER=baserow -DATABASE_PASSWORD=S@8rBtSApf@YpNLXS!2hr2F$ +# DATABASE_PASSWORD provided via secrets/baserow.env EMAIL_SMTP=1 EMAIL_SMTP_HOST=${EMAIL_HOST} EMAIL_SMTP_PASSWORD=${EMAIL_PASSWORD} diff --git a/docker-compose.yml b/docker-compose.yml index 9ec1c9a..f639445 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ secrets: file: secrets/gitea_runner_token.txt authentik_secret_key: file: secrets/authentik_secret_key + authentik_email_password: + file: secrets/authentik_email_password email_host: file: secrets/email_host email_username: @@ -288,7 +290,7 @@ services: OIDC_PROVIDER_NAME: Authentik OIDC_CONFIGURATION_URL: https://authentik.kucharczyk.xyz/application/o/mealie/.well-known/openid-configuration OIDC_CLIENT_ID: asDhzvutfxxpgwaaz0Jjr6SNpEtZo8GKjjs1WzUU - OIDC_CLIENT_SECRET: iIgP3aaF1t0sTd8JPwXrCYmd3Ycc5hhfQROdHN7ByDU81gFJiNbRQ1OrTU7e9yzuPAyqLShRQ2Ve7ov03maHpQtyZzZ2FBdb0OHCkoS4brVuV8uZ4cnVPCzwLEO9bk9U + # OIDC_CLIENT_SECRET provided via secrets/mealie.env OIDC_SIGNUP_ENABLED: false OIDC_USER_GROUP: mealie-users OIDC_ADMIN_GROUP: mealie-admins @@ -297,6 +299,7 @@ services: ALLOW_PASSWORD_LOGIN: false env_file: - mealie.env + - secrets/mealie.env volumes: - "${DOCKER_STORAGE_PATH}/mealie/data/:/app/data" networks: @@ -332,6 +335,7 @@ services: - ${DOCKER_STORAGE_PATH}/valheim/data:/opt/valheim env_file: - valheim.env + - secrets/valheim.env ports: - ${VALHEIM_EXTERNAL_PORT}:${VALHEIM_INTERNAL_PORT} cap_add: @@ -403,9 +407,10 @@ services: # caddy.@api_expiry.status: "3xx" # caddy.forward_auth_0.handle_response_0: "path /api/*" # caddy.forward_auth_0.handle_response_1: "replace_status 401" + env_file: + - secrets/navidrome.env environment: - ND_LASTFM_APIKEY: 29e22ee836a0cb51cfaacb72d605e30d - ND_LASTFM_SECRET: 10aa58294eeffa142685e78a0cd78ad6 + # ND_LASTFM_APIKEY / ND_LASTFM_SECRET provided via secrets/navidrome.env ND_DEEZER_ENABLED: true ND_DEVACTIVITYPANEL: true ND_ENABLESHARING: true @@ -427,6 +432,7 @@ services: - "${MALOJA_EXTERNAL_PORT}:${MALOJA_INTERNAL_PORT}" env_file: - maloja.env + - secrets/maloja.env user: "${PUID}:${PGID}" volumes: - "${DOCKER_STORAGE_PATH}/maloja:/data" @@ -606,6 +612,7 @@ services: - mariadb env_file: - photoprism.env + - secrets/photoprism.env volumes: - "${PHOTOS_STORAGE_PATH}/import:/photoprism/import" - "${PHOTOS_STORAGE_PATH}/originals:/photoprism/originals" @@ -651,6 +658,7 @@ services: - postgres env_file: - baserow.env + - secrets/baserow.env volumes: - "${DOCKER_STORAGE_PATH}/baserow:/baserow/data" restart: unless-stopped @@ -715,7 +723,7 @@ services: # PUSH_INSTALLATION_KEY= - PUSH_RELAY_URI=https://api.bitwarden.eu - PUSH_IDENTITY_URI=https://identity.bitwarden.eu - - ADMIN_TOKEN=$$argon2id$$v=19$$m=65540,t=3,p=4$$aWJ2cVRvYUsySkM3M01TMTJJMnZqbUF0Wm1qRWhvd1B6Sk50Q1hwck96dz0$$FKjZ36E54pX2e0AE9OaDpiH43TyAyfVwr3IvracbqEA + # ADMIN_TOKEN provided via secrets/vaultwarden.env - SMTP_HOST=${EMAIL_HOST} - SMTP_FROM=${EMAIL_FROM} - SMTP_FROM_NAME="Bitwarden (bw.kucharczyk.xyz)" @@ -821,12 +829,14 @@ services: - 3003:3000 env_file: - .env + - secrets/meilisearch.env + - secrets/karakeep.env environment: LOG_LEVEL: debug MEILI_ADDR: http://meilisearch:7700 BROWSER_WEB_URL: http://chrome:9222 - NEXTAUTH_SECRET: lB5mx3t9mdKclELtt+cs2pVBefB+8vD4dKuzhvUP+JzR9bL1 - MEILI_MASTER_KEY: Cvu7m/RIGYQPiYcIrxacHFhbfLKfKq3wwSAWJPKVWQEauiIX + # NEXTAUTH_SECRET provided via secrets/karakeep.env + # MEILI_MASTER_KEY provided via secrets/meilisearch.env NEXTAUTH_URL: https://karakeep.${DOMAIN} DISABLE_SIGNUPS: TRUE CRAWLER_VIDEO_DOWNLOAD: TRUE @@ -872,9 +882,10 @@ services: restart: unless-stopped env_file: - .env + - secrets/meilisearch.env environment: MEILI_NO_ANALYTICS: "true" - MEILI_MASTER_KEY: Cvu7m/RIGYQPiYcIrxacHFhbfLKfKq3wwSAWJPKVWQEauiIX + # MEILI_MASTER_KEY provided via secrets/meilisearch.env volumes: - meilisearch:/meili_data networks: @@ -890,6 +901,7 @@ services: - authentik_secret_key - postgres_general_username - postgres_general_password + - authentik_email_password environment: AUTHENTIK_POSTGRESQL__HOST: postgres AUTHENTIK_POSTGRESQL__NAME: authentik @@ -899,7 +911,7 @@ services: AUTHENTIK_EMAIL__HOST: smtp.protonmail.ch AUTHENTIK_EMAIL__PORT: 587 AUTHENTIK_EMAIL__USERNAME: lukas@kucharczyk.xyz - AUTHENTIK_EMAIL__PASSWORD: CQHMWAUWQG5FBJ2V + AUTHENTIK_EMAIL__PASSWORD: file:///run/secrets/authentik_email_password AUTHENTIK_EMAIL__USE_TLS: true AUTHENTIK_EMAIL__USE_SSL: false AUTHENTIK_EMAIL__TIMEOUT: 60 diff --git a/maloja.env b/maloja.env index 23d38d1..cd80706 100644 --- a/maloja.env +++ b/maloja.env @@ -1,2 +1,2 @@ MALOJA_DATA_DIRECTORY=/data -MALOJA_FORCE_PASSWORD=kralovna \ No newline at end of file +# MALOJA_FORCE_PASSWORD provided via secrets/maloja.env \ No newline at end of file diff --git a/mediawiki.env b/mediawiki.env deleted file mode 100644 index 972c746..0000000 --- a/mediawiki.env +++ /dev/null @@ -1,3 +0,0 @@ -MYSQL_DATABASE=mediawiki -MYSQL_USER=mediawiki -MYSQL_PASSWORD=41eebea0e3ef17dc68064e004e03dafeddd996bf513021b5cf7daf5a0c4d2b32 \ No newline at end of file diff --git a/penpot.yml b/penpot.yml index fbc23d0..fab425b 100644 --- a/penpot.yml +++ b/penpot.yml @@ -56,11 +56,13 @@ services: restart: always stop_signal: SIGINT + env_file: + - secrets/penpot.env environment: - POSTGRES_INITDB_ARGS=--data-checksums - POSTGRES_DB=penpot - POSTGRES_USER=penpot - - POSTGRES_PASSWORD=penpot + # POSTGRES_PASSWORD provided via secrets/penpot.env volumes: - penpot_postgres_data:/var/lib/postgresql/data diff --git a/photoprism.env b/photoprism.env index f2def08..670f092 100644 --- a/photoprism.env +++ b/photoprism.env @@ -1,7 +1,7 @@ -PHOTOPRISM_ADMIN_PASSWORD=kRalovna12514265! +# PHOTOPRISM_ADMIN_PASSWORD provided via secrets/photoprism.env PHOTOPRISM_DATABASE_DRIVER=mysql PHOTOPRISM_DATABASE_NAME=photoprism -PHOTOPRISM_DATABASE_PASSWORD=TWB64mcPZ^TSdo +# PHOTOPRISM_DATABASE_PASSWORD provided via secrets/photoprism.env PHOTOPRISM_DATABASE_SERVER=mariadb PHOTOPRISM_DATABASE_USER=photoprism PHOTOPRISM_IMPORT_PATH=/photoprism/import diff --git a/rtorrent.env b/rtorrent.env deleted file mode 100644 index 95dcf8a..0000000 --- a/rtorrent.env +++ /dev/null @@ -1,6 +0,0 @@ -VPN_ENABLED=no -ENABLE_WEBUI_AUTH=no -ENABLE_RPC2=yes -ENABLE_RPC2_AUTH=yes -RPC2_USER=lukas -RPC2_PASS=5zpxni8N@DYCaZL diff --git a/secrets/authentik_email_password b/secrets/authentik_email_password new file mode 100644 index 0000000..0ce8573 Binary files /dev/null and b/secrets/authentik_email_password differ diff --git a/secrets/baserow.env b/secrets/baserow.env new file mode 100644 index 0000000..e79d6b3 Binary files /dev/null and b/secrets/baserow.env differ diff --git a/secrets/jelu.env b/secrets/jelu.env index f3dcbc3..8ccab98 100644 Binary files a/secrets/jelu.env and b/secrets/jelu.env differ diff --git a/secrets/karakeep.env b/secrets/karakeep.env new file mode 100644 index 0000000..1a522bf Binary files /dev/null and b/secrets/karakeep.env differ diff --git a/secrets/komf.env b/secrets/komf.env new file mode 100644 index 0000000..7f50583 Binary files /dev/null and b/secrets/komf.env differ diff --git a/secrets/maloja.env b/secrets/maloja.env new file mode 100644 index 0000000..c8e02e8 Binary files /dev/null and b/secrets/maloja.env differ diff --git a/secrets/mealie.env b/secrets/mealie.env new file mode 100644 index 0000000..a58e71a Binary files /dev/null and b/secrets/mealie.env differ diff --git a/secrets/mediawiki.env b/secrets/mediawiki.env new file mode 100644 index 0000000..34b1e2d Binary files /dev/null and b/secrets/mediawiki.env differ diff --git a/secrets/meilisearch.env b/secrets/meilisearch.env new file mode 100644 index 0000000..56e510e Binary files /dev/null and b/secrets/meilisearch.env differ diff --git a/secrets/navidrome.env b/secrets/navidrome.env new file mode 100644 index 0000000..832d222 Binary files /dev/null and b/secrets/navidrome.env differ diff --git a/secrets/openldap.env b/secrets/openldap.env new file mode 100644 index 0000000..41ba573 Binary files /dev/null and b/secrets/openldap.env differ diff --git a/secrets/penpot.env b/secrets/penpot.env new file mode 100644 index 0000000..ec91210 Binary files /dev/null and b/secrets/penpot.env differ diff --git a/secrets/photoprism.env b/secrets/photoprism.env new file mode 100644 index 0000000..2315899 Binary files /dev/null and b/secrets/photoprism.env differ diff --git a/secrets/rtorrent.env b/secrets/rtorrent.env new file mode 100644 index 0000000..ab47d73 Binary files /dev/null and b/secrets/rtorrent.env differ diff --git a/secrets/snibox.env b/secrets/snibox.env new file mode 100644 index 0000000..62b8395 Binary files /dev/null and b/secrets/snibox.env differ diff --git a/secrets/valheim.env b/secrets/valheim.env new file mode 100644 index 0000000..968b2f0 Binary files /dev/null and b/secrets/valheim.env differ diff --git a/secrets/vaultwarden.env b/secrets/vaultwarden.env index 99ad039..a176f68 100644 Binary files a/secrets/vaultwarden.env and b/secrets/vaultwarden.env differ diff --git a/services/jelu.yml b/services/jelu.yml index cd1fffa..24ff19d 100644 --- a/services/jelu.yml +++ b/services/jelu.yml @@ -19,7 +19,7 @@ services: environment: SERVER_PORT: 80 SPRING_DATASOURCE_USERNAME: lukas - SPRING_DATASOURCE_PASSWORD: Q^k5i2^hN!wmEr6JLkYP9ME + # SPRING_DATASOURCE_PASSWORD provided via secrets/jelu.env JELU_CORS_ALLOWED-ORIGINS: https://jelu.${DOMAIN} restart: unless-stopped diff --git a/services/komga.yml b/services/komga.yml index dbebc4e..604e56c 100644 --- a/services/komga.yml +++ b/services/komga.yml @@ -45,7 +45,7 @@ configs: kavita: baseUri: "http://localhost:5000" #or env:KOMF_KAVITA_BASE_URI - apiKey: "16707507-d05d-4696-b126-c3976ae14ffb" #or env:KOMF_KAVITA_API_KEY + apiKey: # set via env:KOMF_KAVITA_API_KEY (secrets/komf.env) eventListener: enabled: false # if disabled will not connect to kavita and won't pick up newly added entries metadataLibraryFilter: [ ] # listen to all events if empty @@ -194,12 +194,14 @@ services: user: 1000:100 ports: - "8085:8085" + env_file: + - ../secrets/komf.env environment: - KOMF_KOMGA_BASE_URI=http://komga:25600 - KOMF_KOMGA_USER=lukas@kucharczyk.xyz - - KOMF_KOMGA_PASSWORD=kRalovna12514265! + # KOMF_KOMGA_PASSWORD provided via secrets/komf.env - KOMF_KAVITA_BASE_URI=http://kavita:${KAVITA_INTERNAL_PORT} - - KOMF_KAVITA_API_KEY=c8023836-7aab-46ed-9409-c24b950002d4 + # KOMF_KAVITA_API_KEY provided via secrets/komf.env - KOMF_LOG_LEVEL=INFO - JAVA_TOOL_OPTIONS=-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=compact -XX:ShenandoahGuaranteedGCInterval=3600000 -XX:TrimNativeHeapInterval=3600000 configs: diff --git a/services/openldap.yml b/services/openldap.yml index 9bb665d..a5519f7 100644 --- a/services/openldap.yml +++ b/services/openldap.yml @@ -12,13 +12,15 @@ services: volumes: - "${DOCKER_STORAGE_PATH}/openldap/config:/etc/ldap/slapd.d" - "${DOCKER_STORAGE_PATH}/openldap/data:/var/lib/ldap" + env_file: + - ../secrets/openldap.env environment: - LDAP_ORGANISATION=Homelab - LDAP_DOMAIN=${DOMAIN} - - LDAP_ADMIN_PASSWORD=kral + # LDAP_ADMIN_PASSWORD provided via secrets/openldap.env - LDAP_OPENLDAP_UID=${PUID} - LDAP_OPENLDAP_GID=${PGID} - LDAP_READONLY_USER=true - LDAP_READONLY_USER_USERNAME=readonly - - LDAP_READONLY_USER_PASSWORD=readonly + # LDAP_READONLY_USER_PASSWORD provided via secrets/openldap.env restart: unless-stopped diff --git a/snibox.env b/snibox.env deleted file mode 100644 index 5f0d1e1..0000000 --- a/snibox.env +++ /dev/null @@ -1,2 +0,0 @@ -SECRET_KEY_BASE=sMHYqzrgJQgPynv6ZDG7M8ZpF -FORCE_SSL=false \ No newline at end of file diff --git a/valheim.env b/valheim.env index e341220..dc50763 100644 --- a/valheim.env +++ b/valheim.env @@ -1,4 +1,4 @@ SERVER_NAME=LukasJirkaDominik WORLD_NAME=Mujnovyserver -SERVER_PASS=heslo +# SERVER_PASS provided via secrets/valheim.env VALHEIM_PLUS=true \ No newline at end of file