diff --git a/.gitignore b/.gitignore index c4ea55a..5df20e1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ coverage deploy/.last-image creds/ +!creds/secrets.enc.env k3s.yaml # Local-only documentation (now tracked in docs/) diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..f31ab33 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,7 @@ +creation_rules: + - paths: + - creds/secrets.enc.env + key_groups: + - age: + - age1hkehkc2rryjl975c2mg5cghmjr54n4wjshncl292h2eg5l394fhs4uydrh + encrypted_regex: '^(AUTH_SECRET|DATABASE_URL|DB_.*|APP_URL|SMTP_.*|DKIM_.*|AUTO_APPROVE_LISTINGS|OPENAI_.*|H(ETZNER|CLOUD)_TOKEN|JOKER_DYNDNS_.*|REGISTRY_.*|NETDATA_.*|ADMIN_.*)$' diff --git a/creds/secrets.enc.env b/creds/secrets.enc.env new file mode 100644 index 0000000..21fee45 --- /dev/null +++ b/creds/secrets.enc.env @@ -0,0 +1,44 @@ +# Encrypted with sops (age). To edit: sops creds/secrets.enc.env +AUTH_SECRET=ENC[AES256_GCM,data:DQl4MEDrsUe76/NYdI03BZb31/NTTA0nBTUALhsULvT1EvMRRiR1,iv:rXjuUm3Z5OFMa0Jc/h8BSF1DWxddrtH2DyvPwX8KZ3c=,tag:eL7NZj0WE44y9AxVoCXrFA==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:87ysYMPY0lI8LrL46dHPjcZOYo4IlP/aQ4h+psSXS4EKzUiBJuExtF8f+kAZNJBc4Xj+B82Q34CfbbsBC8NdQoeebqvXF6/0IluvmpCBhpN2MKWqwlx/mQZTENZA3J0QiAP+BapeZXuXLc//,iv:n4W+hyWf1oH8IahJJs1XQ3d3i5hJjLXaNE+b4Z66Zbw=,tag:JYsBd5d0APkikYYg8Ea/Fg==,type:str] +DB_HOST=ENC[AES256_GCM,data:qlmMVJEkDwfiRYGFUdP/,iv:syUEM2Cx64jE7IW8zRSqfe9uAL+JEMl9Tu5nw12kdWI=,tag:VoAJgXgGy8OXksUvn/SpUQ==,type:str] +DB_PORT=ENC[AES256_GCM,data:vLjEIHpK,iv:ym9R0E/S5WKLKF+0thz9tmwI4pYa7skrY4sDpY42y0E=,tag:SK8gDGrnx8/YAPi8qB7KeQ==,type:str] +DB_USER=ENC[AES256_GCM,data:KIlxtjxjx0EIDxXxHKY=,iv:dlhX4pIe6AE8fzT+TVB3hg9gJ46sq85Qp8f4pkxO9n8=,tag:WCMaOPpnI8SA8WMRDA392w==,type:str] +DB_PASS=ENC[AES256_GCM,data:CJEh2+yQrPptlTnthe2UWsc/baNXBIAC0lpxtm8DI47gTg==,iv:UPrDDlMbWaiAiz5QcjRqXRIiVohMRF7wtmi5ioONcLE=,tag:jLSfXcefy8+cll8bVclkSQ==,type:str] +APP_URL=ENC[AES256_GCM,data:1vZsUQ/d4MTbBM+juPJeVwseAVz4O2M=,iv:KrsOr3w1i3j3R19KAowFxFbdSm1B9IikqRoqU9tmQus=,tag:BwR3v/2R7bAFxHnB5waFIA==,type:str] +SMTP_HOST=ENC[AES256_GCM,data:H+47VDofg1EEBo6q1UEleQ==,iv:KlAxFj/bKOMikubGbqPYkKPZt18oIdXxrLjEVnbxhN8=,tag:LpC930G7lsjt9YuSEFxW8A==,type:str] +SMTP_PORT=ENC[AES256_GCM,data:F6fiIz4=,iv:Pelfn6ciIGvqF4nMPX5WrsWqkLtAxPBmBOSBvn4RUmo=,tag:UWBGXnDAYf5zCQJagiSE9Q==,type:str] +SMTP_USER=ENC[AES256_GCM,data:CnbiHSRC0w/TOwwUwto=,iv:s8tJxs3X3BaFf5LrR+hHVvRIbDEfNI6ZszdTi+lg3PQ=,tag:DAbkk3szBzmhUrNTPZdA6A==,type:str] +SMTP_PASS=ENC[AES256_GCM,data:GmmDQ2mROFz9zpGpXgtwlUvU2dmWg9KHsw==,iv:pl7O1QCjkaaqZ0sGS6kIBUKJ5glE8FTyA1vCYz0BfoU=,tag:/eE1ZWrLlKsa043VY3p3DQ==,type:str] +SMTP_FROM=ENC[AES256_GCM,data:XY9BFPRmpO5KblsSMB2680DNx/bvUndfXA==,iv:yCUsaOMu1MrHtfR9AwfuIscyiEqsl0OYvO8UiKO6IYk=,tag:MpEIu2UJXEjE+9n1MVaHRQ==,type:str] +SMTP_TLS=ENC[AES256_GCM,data:sBPcDhWs,iv:lZRjMBtrtAinC1blRFE0RH5jzyEcdLf+UOgEHuKitEk=,tag:R3Q4+ri3m5LB+xzohsYJHA==,type:str] +SMTP_SSL=ENC[AES256_GCM,data:dYAo31bHTw==,iv:OG0c0HYzewLwlsgPlyn0nz+vwdeuSCjJ5ifdE9bfQvI=,tag:EJ5CX6+weXcrcjdrr1uicg==,type:str] +SMTP_REJECT_UNAUTHORIZED=ENC[AES256_GCM,data:HhBgopyX,iv:7W9TYVN1aEH5QWK1NXGE9mw9gCd7yagg/LFVNHSdMV4=,tag:rcWGdoXH2wXtqWaImLEfHA==,type:str] +DKIM_SELECTOR=ENC[AES256_GCM,data:taqEDa6Ah1G6yQ==,iv:n4KfE3lFBIyndBFkgvoBvzAlqP2HV4RIyL8c7lLoQSU=,tag:nos80rdqlj7YQshANBoTvw==,type:str] +DKIM_DOMAIN=ENC[AES256_GCM,data:ZQkoRT8bQ7/Kdt0O/M4kRB8=,iv:wSbk2ALpbcxRT8neFrcQBEj8ja54ZtWeKTfllm8Pr5s=,tag:o5ZvS1enT31pV4+EM8i5LA==,type:str] +DKIM_PRIVATE_KEY_PATH=ENC[AES256_GCM,data:8hbyhw/+I1VWGzoDNIKMY3avRvSmt17EmHiwv7WpeJZzalEXqTmDB+bpkVEQ,iv:/7bUU6boXo1HPccvNBCDnLyql0hGaMXmGIeQEqpFnwM=,tag:nTE+dR0JB/Y1ri5h/FaC3A==,type:str] +AUTO_APPROVE_LISTINGS=ENC[AES256_GCM,data:bAHiVYhjXw==,iv:/jKFQ5L+meSNF6tn54Dc8dpK1I0+zG/o/F0QqMInmwU=,tag:9f7mb2QN17AoualTjkLeSQ==,type:str] +OPENAI_API_KEY=ENC[AES256_GCM,data:+Avly5xD/jgkeiiHg7WNVH/OyGRf9L+iKDh/SvD7LrFuJlltORwzY253l29vD6cucaYfFanTWwDaV6TKuatNiRXxzy7aSoOfs0y4qonXofIRb3BH2/DarGWxQtRi5LRrXFIpCRQfN7x+XGbeF8ZZBW/DHFYywH0E2fU9li/iafXVOKBRcV4WYTIA3sJJuObHFjxgRYMB2SWtw8noZvkK+pzQi6lCKA==,iv:WTbvAzkjoDLAuDIdbFjt9BU5+2pGDv5Ksc4xru0Wx20=,tag:b8ddMgRoq+kMlS994yPx9A==,type:str] +OPENAI_TRANSLATIONS_KEY=ENC[AES256_GCM,data:QGIhIhzn775yXZq9OYKTwGwcLnQAIJn8TM95TRFb5jWY/gTevspeYLDVrrGDKv/WHX8aTg4r4z9t6cbgbSX677fSlrNy2c2YEb5uCLGh0QUK2LoPt9R9yF/iyi5CU2obRlqamtF6knUGG5mdOCewKx/k+updDLmOe+CPD91H6nWbE9+1Pmbdf22bM+rOt9t4+MS6UP0UbIYU0sBEtfPTvN68260fUA==,iv:eYzIudN/i0A38X1up+Jnxv7qE5aiUEu0WQHEtQ4DvlA=,tag:qM7noB1GvXt2F69aVd0KVQ==,type:str] +HETZNER_API_TOKEN="hoRULGviS8G3OGaJ68josx00M53efhuntVM5Rfft1AOvUR0ZQTXlO6yivhGqBM5o" +HCLOUD_TOKEN=ENC[AES256_GCM,data:0qm+oR2daCd9e0qc+2wApobRod0RsUeA2QL1kQarQW77FvPAgQ7jBkML39kBP3xsYsrdmiZuIdg63Q3xeBQ9mVq2,iv:JUjR7vGyX00TOlUCOD8M9jiB6iio4IiX73sLzW/CADA=,tag:9p0kgDD7ul8s5T0cL7vsfA==,type:str] +HETZNER_TOKEN=ENC[AES256_GCM,data:e95QAx49/qkoBnAmyup7NBs/Xm/fkWutMIWzlAv4P1ybSJNoCyC4ZibpNTCtYtK/3Hux5ItdOBm/jhssjlX363cI,iv:4UzDqJd0mYP3IJgeMn9xz9UjsVyODZ44CDAz5PPaXbk=,tag:ZEAqueoXY4kBfe+89nA+Kg==,type:str] +JOKER_DYNDNS_USERNAME=ENC[AES256_GCM,data:FBAh4B+vxosvxfHbrZukSNea,iv:4FexfnCieAaZ7iUSvaiATr5THpqoHaebX/QigA9Wl58=,tag:dY6lQnHTJWOZa7WhHwPELA==,type:str] +JOKER_DYNDNS_PASSWORD=ENC[AES256_GCM,data:1neBKFvQhFGfOgdPI6EHZKKI,iv:fC1J7QjAoQMUKxx6lMO32n6zjQ5tQa/icBDn19Lih2k=,tag:W110zqgwbsrVbkZzav/F0Q==,type:str] +REGISTRY_USERNAME=ENC[AES256_GCM,data:Mt1mk15DPH8=,iv:Hxg4fRLRZ2hKRACTIIUMSODRBOgRv5Y6KfZ8477cwc0=,tag:Ufq2jYQjYzjiB88PF0gpBA==,type:str] +REGISTRY_PASSWORD=ENC[AES256_GCM,data:4l2oMR65+X+1,iv:cfh6Esayb6zJEqKrtfY0LAncIrPi7lj/ckcbMgTMFys=,tag:vhsiagOyGVjN7+GNPwZ8LA==,type:str] +NETDATA_USER=ENC[AES256_GCM,data:6laH3H7JKgpU,iv:PCI8HL6S68AaBRg11YlNbK9R12G1ROMqRIpXKIlsBDQ=,tag:qL1uWrtWz5+c6f6Xbv4Low==,type:str] +NETDATA_PASS=ENC[AES256_GCM,data:CZxY7WeoHuUNmECOiL6XLVeTt/k5PAHFyV4dVBrF,iv:Zp00OVvA5hI8c9AD4pvDB1s99yw9iOlgBV4K4nbFFVg=,tag:Y077Bp8qk/SoEOajOdvMLg==,type:str] +NETDATA_HOST_NODE1=ENC[AES256_GCM,data:QEHB348q+BQubmvIJkH/8mie2fjb0mE=,iv:ZUNZA64cuflqQq+JS/e0wByTzny0eE/4rvaxzNN1JFw=,tag:YfmJWJCd1MQW2Q2bWferLg==,type:str] +NETDATA_HOST_DB1=ENC[AES256_GCM,data:B8mSqI+rusKxazFcziGabYMM1i1q,iv:1hDrheKjdo7dMF1m7qOWQ9llye2glEQm79LONauLiYk=,tag:H1BVhIKfymLBzGS6CZEp3Q==,type:str] +NETDATA_PG_USER=ENC[AES256_GCM,data:n0G+MZWEWQK9,iv:11id08TbMpc5NUqtKsoXiIFfM5GJCFStzJ5aY1AGplc=,tag:gXWcgEXko4P7qMwVLGnF1Q==,type:str] +NETDATA_PG_PASS=ENC[AES256_GCM,data:ylpbhrcFFKC5qiyt+RRPZhCWt098tPhGWxabgsO31tR7+g==,iv:O4b5DsNfCpqlhZ3GmXMRzp2/fUpMtLnhSqmoaD4+Q04=,tag:TG7ge49hjzkWcBO93hoXuA==,type:str] +NETDATA_PG_ROLE=ENC[AES256_GCM,data:6/ESugRIiDgBM2e8,iv:kXJOKx7fbO4k9zV2APTXCxOrGJ/93dCkmVA33v9fzrY=,tag:HwEWzVagm6cDnPychaekaw==,type:str] +ADMIN_EMAIL=ENC[AES256_GCM,data:AuhvA1iUOnMcE7el4TSE7cbLAg==,iv:eWNxWDA978OWQO10/ywc8CpKuUhC0uTqObmD5Tefgtc=,tag:pI4SXW1ZrZPthD2qRAPrIw==,type:str] +ADMIN_INITIAL_PASSWORD=ENC[AES256_GCM,data:WiJm5VO57/qLu8fK0rZqFDk=,iv:2SwLMvMUjwkEDZo8VRtPHfs+cabz2L+9ZQAaWyn1o9c=,tag:+TunF3tuLxYvMeJsYNQuCQ==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNlZ6dWs5VnJXa0hrekdp\nYlpzYll6NUFnTnV0Sk0zbnB2WEw5alVwT2swCm82NmlUbjdabEJ2d1pHTGhYRG1m\nZmIyUHFQcVhvZHNzRTJuZVNqYThaWGMKLS0tIGtHTEhzQnh2SFVxdlBvRlpMblVW\nOWdVY0ErK2pVSzdtckc4Y0lPRTdrdmsKv5M0ojCoW5SQhnjXY116SmjvyCtSnehg\nQqtL6jElOv4MeLASHwYLYzznU6dxkZK3OKvcLh6mu+41Pnbl8u26yw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1hkehkc2rryjl975c2mg5cghmjr54n4wjshncl292h2eg5l394fhs4uydrh +sops_encrypted_regex=^(AUTH_SECRET|DATABASE_URL|DB_.*|APP_URL|SMTP_.*|DKIM_.*|AUTO_APPROVE_LISTINGS|OPENAI_.*|H(ETZNER|CLOUD)_TOKEN|JOKER_DYNDNS_.*|REGISTRY_.*|NETDATA_.*|ADMIN_.*)$ +sops_lastmodified=2025-12-11T11:36:56Z +sops_mac=ENC[AES256_GCM,data:bnDwxj/t2X+vkq1nd2Bej23GBn3hALXW6PAp4FyoAlvwajztp9U2eyF7voLQDeX1kurVBuACPExzzMnerEXOebF9l5SGcIYfvtVj9kk4I0WRCbVBt/QKgEtqYJ3l1TXrDe8ZPTj6O2rK6WW36RDExFDu3tzzvVEaHErZMjAhD1U=,iv:hBwrHOvabZEqeWHSFGvk5sHYbJiF6/3wY1JXgaevB9w=,tag:2fZQ9AQoHQJ+zte2yLpi5g==,type:str] +sops_version=3.11.0 diff --git a/docs/secrets.md b/docs/secrets.md new file mode 100644 index 0000000..09f09d3 --- /dev/null +++ b/docs/secrets.md @@ -0,0 +1,32 @@ +# Secrets workflow (sops + age) + +## Files +- `creds/age-key.txt`: age private key (keep out of git; store in a password manager). Public key is in the header. +- `creds/secrets.enc.env`: encrypted dotenv managed by sops/age (committable). +- `creds/secrets.env`: decrypted dotenv (git-ignored) produced when loading secrets; not committed. +- Legacy plaintext secrets moved to `creds/deprecated/` for reference. + +## Editing secrets +```bash +# Ensure sops+age binaries are available +sops creds/secrets.enc.env +``` +Sops will decrypt, open in $EDITOR, and re-encrypt on save. The age recipient is configured in `.sops.yaml`. + +## Loading secrets locally +```bash +source scripts/load-secrets.sh +``` +This decrypts `creds/secrets.enc.env` to `creds/secrets.env` if needed (requires sops) and exports all variables. + +## Adding developers +- Share `creds/age-key.txt` securely (password manager). They need the age secret key to decrypt. +- No change to `.sops.yaml` is needed unless you rotate keys. + +## Deploys/CI +- `deploy/deploy.sh` sources `scripts/load-secrets.sh`, so providing `creds/secrets.enc.env` + age key is enough for secret env injection. + +## Rotating keys +- Generate a new age key: `age-keygen -o creds/age-key.txt` (keep old backup if you need to reencrypt). +- Update `.sops.yaml` recipient to the new public key. +- Re-encrypt: `SOPS_AGE_KEY_FILE=creds/age-key.txt sops --encrypt --in-place creds/secrets.enc.env`. diff --git a/scripts/load-secrets.sh b/scripts/load-secrets.sh index 8f8d4c2..87276b7 100644 --- a/scripts/load-secrets.sh +++ b/scripts/load-secrets.sh @@ -6,11 +6,24 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SECRETS_FILE="${SECRETS_FILE:-$ROOT_DIR/creds/secrets.env}" +ENCRYPTED_FILE="${ENCRYPTED_FILE:-$ROOT_DIR/creds/secrets.enc.env}" -if [[ ! -f "$SECRETS_FILE" ]]; then - echo "secrets file not found: $SECRETS_FILE (skipping)" >&2 - return 0 2>/dev/null || exit 0 -fi +ensure_decrypted() { + if [[ -f "$SECRETS_FILE" ]]; then + return 0 + fi + if [[ -f "$ENCRYPTED_FILE" ]]; then + if command -v sops >/dev/null 2>&1; then + echo "Decrypting $ENCRYPTED_FILE -> $SECRETS_FILE" + sops -d "$ENCRYPTED_FILE" >"$SECRETS_FILE" + else + echo "sops not found and $SECRETS_FILE is missing. Install sops or set SECRETS_FILE." >&2 + return 1 + fi + fi +} + +ensure_decrypted || exit 0 echo "Loading secrets from $SECRETS_FILE" set -a