Files
homelab-optimized/docs/runbooks/credential-rotation.md
Gitea Mirror Bot 6b5bdf7b8d
Some checks failed
Documentation / Build Docusaurus (push) Failing after 17m30s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-04-04 03:48:45 UTC
2026-04-04 03:48:45 +00:00

662 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Credential Rotation Runbook
## Overview
Step-by-step rotation procedures for all credentials exposed in the
`homelab-optimized` public mirror (audited 2026-02-20). Work through each
section in priority order. After updating secrets in compose files, commit
and push — GitOps will redeploy automatically.
> **Note:** Almost all of these stem from the same root cause — secrets were
> hard-coded in compose files, then those files were committed to git, then
> `generate_service_docs.py` and wiki-upload scripts duplicated those secrets
> into documentation, creating 35× copies of every secret across the repo.
> See the "Going Forward" section for how to prevent this.
## Prerequisites
- [ ] SSH / Tailscale access to Atlantis, Calypso, Homelab VM, Seattle VM, matrix-ubuntu-vm
- [ ] Gitea admin access (`git.vish.gg`)
- [ ] Authentik admin access
- [ ] Google account access (Gmail app passwords)
- [ ] Cloudflare dashboard access
- [ ] OpenAI platform access
- [ ] Write access to this repository
## Metadata
- **Estimated Time**: 46 hours
- **Risk Level**: Medium (service restarts required for most items)
- **Requires Downtime**: Brief per-service restart only
- **Reversible**: Yes (old values can be restored if something breaks)
- **Last Updated**: 2026-02-20
---
## Priority 1 — Rotate Immediately (Externally Usable Tokens)
### 1. Gitea API Tokens
Two tokens hard-coded across scripts and docs.
#### 1a. Wiki/scripts token (`77e3ddaf...`)
**Files to update:**
- `scripts/cleanup-gitea-wiki.sh`
- `scripts/upload-all-docs-to-gitea-wiki.sh`
- `scripts/upload-to-gitea-wiki.sh`
- `scripts/create-clean-organized-wiki.sh`
- `scripts/upload-organized-wiki.sh`
- `docs/admin/DOCUMENTATION_MAINTENANCE_GUIDE.md`
```bash
# 1. Go to https://git.vish.gg/user/settings/applications
# 2. Revoke the token starting 77e3ddaf
# 3. Generate new token, name: homelab-wiki, scope: repo
# 4. Replace in all files:
NEW_TOKEN=REDACTED_TOKEN
for f in scripts/cleanup-gitea-wiki.sh \
scripts/upload-all-docs-to-gitea-wiki.sh \
scripts/upload-to-gitea-wiki.sh \
scripts/create-clean-organized-wiki.sh \
scripts/upload-organized-wiki.sh \
docs/admin/DOCUMENTATION_MAINTENANCE_GUIDE.md; do
sed -i "s/REDACTED_GITEA_TOKEN/$NEW_TOKEN/g" "$f"
done
```
#### 1b. Retro-site clone token (`52fa6ccb...`)
**File:** `Calypso/retro-site.yaml` and `hosts/synology/calypso/retro-site.yaml`
```bash
# 1. Go to https://git.vish.gg/user/settings/applications
# 2. Revoke the token starting 52fa6ccb
# 3. Generate new token, name: retro-site-deploy, scope: repo:read
# 4. Update the git clone URL in both compose files
# Consider switching to a deploy key for least-privilege access
```
---
### 2. Cloudflare API Token (`FGXlHM7doB8Z...`)
Appears in 13 files including active dynamic DNS updaters on multiple hosts.
**Files to update (active deployments):**
- `hosts/synology/atlantis/dynamicdnsupdater.yaml`
- `hosts/physical/guava/portainer_yaml/dynamic_dns.yaml`
- `hosts/physical/concord-nuc/dyndns_updater.yaml`
- Various Calypso/homelab-vm DDNS configs
**Files to sanitize (docs):**
- `docs/infrastructure/cloudflare-dns.md`
- `docs/infrastructure/npm-migration-jan2026.md`
- Any `docs/services/individual/ddns-*.md` files
```bash
# 1. Go to https://dash.cloudflare.com/profile/api-tokens
# 2. Find the token (FGXlHM7doB8Z...) and click Revoke
# 3. Create a new token: use "Edit zone DNS" template, scope to your zone only
# 4. Replace in all compose files above
# 5. Replace hardcoded value in docs with: YOUR_CLOUDFLARE_API_TOKEN
# Verify DDNS containers restart and can still update DNS:
docker logs cloudflare-ddns --tail 20
```
---
### 3. OpenAI API Key (`sk-proj-C_IYp6io...`)
**Files to update:**
- `hosts/vms/homelab-vm/hoarder.yaml`
- `docs/services/individual/web.md` (replace with placeholder)
```bash
# 1. Go to https://platform.openai.com/api-keys
# 2. Delete the exposed key
# 3. Create a new key, set a usage limit
# 4. Update OPENAI_API_KEY in hoarder.yaml
# 5. Replace value in docs with: YOUR_OPENAI_API_KEY
```
---
## Priority 2 — OAuth / SSO Secrets
### 4. Grafana ↔ Authentik OAuth Secret
**Files to update:**
- `hosts/vms/homelab-vm/monitoring.yaml`
- `hosts/synology/atlantis/grafana.yml`
- `docs/infrastructure/authentik-sso.md` (replace with placeholder)
- `docs/services/individual/grafana-oauth.md` (replace with placeholder)
```bash
# 1. Log into Authentik admin: https://auth.vish.gg/if/admin/
# 2. Applications → Providers → find Grafana OAuth2 provider
# 3. Edit → regenerate Client Secret → copy both Client ID and Secret
# 4. Update in both compose files:
# GF_AUTH_GENERIC_OAUTH_CLIENT_ID: NEW_ID
# GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: NEW_SECRET
# 5. Commit and push — both Grafana stacks restart automatically
# Verify SSO works after restart:
curl -I https://gf.vish.gg
```
---
### 5. Seafile ↔ Authentik OAuth Secret
**Files to update:**
- `hosts/synology/calypso/seafile-oauth-config.py`
- `docs/services/individual/seafile-oauth.md` (replace with placeholder)
```bash
# 1. Log into Authentik admin
# 2. Applications → Providers → find Seafile OAuth2 provider
# 3. Regenerate client secret
# 4. Update OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET in seafile-oauth-config.py
# 5. Re-run the config script on the Seafile server to apply
```
---
### 6. Authentik Secret Key (`RpRexcYo5HAz...`)
**Critical** — this key encrypts all Authentik data (tokens, sessions, stored credentials).
**File:** `hosts/synology/calypso/authentik/docker-compose.yaml`
```bash
# 1. Generate a new secret:
python3 -c "import secrets; print(secrets.token_urlsafe(50))"
# 2. Update AUTHENTIK_SECRET_KEY in docker-compose.yaml
# 3. Commit and push — Authentik will restart
# WARNING: All active Authentik sessions will be invalidated.
# Users will need to log back in. SSO-protected services
# may temporarily show login errors while Authentik restarts.
# Verify Authentik is healthy after restart:
docker logs authentik_server --tail 30
```
---
## Priority 3 — Application Secrets (Require Service Restart)
### 7. Gmail App Passwords
Five distinct app passwords were found across the repo. Revoke all of them
in Google Account → Security → App passwords, then create new per-service ones.
| Password | Used For | Active Files |
|----------|----------|-------------|
| (see Vaultwarden) | Mastodon, Joplin, Authentik SMTP | `matrix-ubuntu-vm/mastodon/.env.production.template`, `atlantis/joplin.yml`, `calypso/authentik/docker-compose.yaml` |
| (see Vaultwarden) | Vaultwarden SMTP | `atlantis/vaultwarden.yaml` |
| (see Vaultwarden) | Documenso SMTP | `atlantis/documenso/documenso.yaml` |
| (see Vaultwarden) | Reactive Resume v4 (archived) | `archive/reactive_resume_v4_archived/docker-compose.yml` |
| (see Vaultwarden) | Reactive Resume v5 (active) | `calypso/reactive_resume_v5/docker-compose.yml` |
**Best practice:** Create one app password per service, named clearly (e.g.,
`homelab-joplin`, `homelab-mastodon`). Update each file's `SMTP_PASS` /
`SMTP_PASSWORD` / `MAILER_AUTH_PASSWORD` / `smtp_password` field.
---
### 8. Matrix Synapse Secrets
Three secrets in `homeserver.yaml`, plus the TURN shared secret.
**File:** `hosts/synology/atlantis/matrix_synapse_docs/homeserver.yaml`
```bash
# Generate fresh values for each:
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
# Fields to rotate:
# registration_shared_secret
# macaroon_secret_key
# form_secret
# turn_shared_secret
# After updating homeserver.yaml, restart Synapse:
docker restart synapse # or via Portainer
# Also update coturn config on the server directly:
ssh atlantis
nano /path/to/turnserver.conf
# Update: static-auth-secret=NEW_TURN_SECRET
systemctl restart coturn
# Update instructions.txt — replace old values with REDACTED
```
---
### 9. Mastodon `SECRET_KEY_BASE` + `OTP_SECRET`
**File:** `hosts/synology/atlantis/mastodon.yml`
**Also in:** `docs/services/individual/mastodon.md` (replace with placeholder)
```bash
# Generate new values:
openssl rand -hex 64 # for SECRET_KEY_BASE
openssl rand -hex 64 # for OTP_SECRET
# Update both in mastodon.yml
# Commit and push — GitOps restarts Mastodon
# WARNING: All active user sessions are invalidated. Users must log back in.
# Verify Mastodon web is accessible:
curl -I https://your-mastodon-domain/
docker logs mastodon_web --tail 20
```
---
### 10. Documenso Secrets (3 keys)
**Files:**
- `hosts/synology/atlantis/documenso/documenso.yaml`
- `hosts/synology/atlantis/documenso/Secrets.txt` (will be removed by sanitizer)
- `docs/services/individual/documenso.md` (replace with placeholder)
```bash
# Generate new values:
python3 -c "import secrets; print(secrets.token_urlsafe(32))" # NEXTAUTH_SECRET
python3 -c "import secrets; print(secrets.token_urlsafe(32))" # NEXT_PRIVATE_ENCRYPTION_KEY
python3 -c "import secrets; print(secrets.token_urlsafe(32))" # NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY
# Update all three in documenso.yaml
# NOTE: Rotating encryption keys will invalidate signed documents.
# Confirm this is acceptable before rotating.
```
---
### 11. Paperless-NGX API Token
**Files:**
- `hosts/synology/calypso/paperless/paperless-ai.yml`
- `hosts/synology/calypso/paperless/README.md` (replace with placeholder)
- `docs/services/paperless.md` (replace with placeholder)
```bash
# 1. Log into Paperless web UI
# 2. Admin → Auth Token → delete existing, generate new
# 3. Update PAPERLESS_API_TOKEN in paperless-ai.yml
# 4. Commit and push
```
---
### 12. Immich JWT Secret (Both NAS)
**Files:**
- `hosts/synology/atlantis/immich/stack.env` (will be removed by sanitizer)
- `hosts/synology/calypso/immich/stack.env` (will be removed by sanitizer)
Since these files are removed by the sanitizer, ensure they are in `.gitignore`
or managed via Portainer env variables going forward.
```bash
# Generate new secret:
openssl rand -base64 96
# Update JWT_SECRET in both stack.env files locally,
# then apply via Portainer (not committed to git).
# WARNING: All active Immich sessions invalidated.
```
---
### 13. Revolt/Stoatchat — LiveKit API Secret + VAPID Private Key
**Files:**
- `hosts/vms/seattle/stoatchat/livekit.yml`
- `hosts/vms/seattle/stoatchat/Revolt.overrides.toml`
- `hosts/vms/homelab-vm/stoatchat.yaml`
- `docs/services/stoatchat/Revolt.overrides.toml` (replace with placeholder)
- `hosts/vms/seattle/stoatchat/DEPLOYMENT_SUMMARY.md` (replace with placeholder)
```bash
# Generate new LiveKit API key/secret pair:
# Use the LiveKit CLI or generate random strings:
python3 -c "import secrets; print(secrets.token_urlsafe(24))" # API key
python3 -c "import secrets; print(secrets.token_urlsafe(32))" # API secret
# Generate new VAPID key pair:
npx web-push generate-vapid-keys
# or: python3 -c "from py_vapid import Vapid; v=Vapid(); v.generate_keys(); print(v.private_key)"
# Update in livekit.yml and Revolt.overrides.toml
# Restart LiveKit and Revolt services
```
---
### 14. Jitsi Internal Auth Passwords (6 passwords)
**File:** `hosts/synology/atlantis/jitsi/jitsi.yml`
**Also in:** `hosts/synology/atlantis/jitsi/.env` (will be removed by sanitizer)
```bash
# Generate new passwords for each variable:
for var in JICOFO_COMPONENT_SECRET JICOFO_AUTH_PASSWORD JVB_AUTH_PASSWORD \
JIGASI_XMPP_PASSWORD JIBRI_RECORDER_PASSWORD JIBRI_XMPP_PASSWORD; do
echo "$var=$(openssl rand -hex 10)"
done
# Update all 6 in jitsi.yml
# Restart the entire Jitsi stack — all components must use the same passwords
docker compose -f jitsi.yml down && docker compose -f jitsi.yml up -d
```
---
### 15. SNMP v3 Auth + Priv Passwords
Used for NAS monitoring — same credentials across 6 files.
**Files to update:**
- `hosts/synology/setillo/prometheus/snmp.yml`
- `hosts/synology/atlantis/grafana_prometheus/snmp.yml`
- `hosts/synology/atlantis/grafana_prometheus/snmp_mariushosting.yml`
- `hosts/synology/calypso/grafana_prometheus/snmp.yml`
- `hosts/vms/homelab-vm/monitoring.yaml`
```bash
# 1. Log into each Synology NAS DSM
# 2. Go to Control Panel → Terminal & SNMP → SNMP tab
# 3. Update SNMPv3 auth password and privacy password to new values
# 4. Update the same values in all 5 config files above
# 5. The archive file (deprecated-monitoring-stacks) can just be left for
# the sanitizer to redact.
```
---
### 16. Invidious `hmac_key`
**Files:**
- `hosts/physical/concord-nuc/invidious/invidious.yaml`
- `hosts/physical/concord-nuc/invidious/invidious_old/invidious.yaml`
- `hosts/synology/atlantis/invidious.yml`
```bash
# Generate new hmac_key:
python3 -c "import secrets; print(secrets.token_hex(16))"
# Update hmac_key in each active invidious.yaml
# Restart Invidious containers
```
---
### 17. Open WebUI Secret Keys
**Files:**
- `hosts/vms/contabo-vm/ollama/docker-compose.yml`
- `hosts/synology/atlantis/ollama/docker-compose.yml`
- `hosts/synology/atlantis/ollama/64_bit_key.txt` (will be removed by sanitizer)
```bash
# Generate new key:
openssl rand -hex 32
# Update WEBUI_SECRET_KEY in both compose files
# Restart Open WebUI containers — active sessions invalidated
```
---
### 18. Portainer Edge Key
**File:** `hosts/vms/homelab-vm/portainer_agent.yaml`
```bash
# 1. Log into Portainer at https://192.168.0.200:9443
# 2. Go to Settings → Edge Compute → Edge Agents
# 3. Find the homelab-vm agent and regenerate its edge key
# 4. Update EDGE_KEY in portainer_agent.yaml with the new base64 value
# 5. Restart the Portainer edge agent container
```
---
### 19. OpenProject Secret Key
**File:** `hosts/vms/homelab-vm/openproject.yml`
**Also in:** `docs/services/individual/openproject.md` (replace with placeholder)
```bash
openssl rand -hex 64
# Update OPENPROJECT_SECRET_KEY_BASE in openproject.yml
# Restart OpenProject — sessions invalidated
```
---
### 20. RomM Auth Secret Key
**File:** `hosts/vms/homelab-vm/romm/romm.yaml`
**Also:** `hosts/vms/homelab-vm/romm/secret_key.yaml` (will be removed by sanitizer)
```bash
openssl rand -hex 32
# Update ROMM_AUTH_SECRET_KEY in romm.yaml
# Restart RomM — sessions invalidated
```
---
### 21. Hoarder NEXTAUTH Secret
**File:** `hosts/vms/homelab-vm/hoarder.yaml`
**Also in:** `docs/services/individual/web.md` (replace with placeholder)
```bash
openssl rand -base64 36
# Update NEXTAUTH_SECRET in hoarder.yaml
# Restart Hoarder — sessions invalidated
```
---
## Priority 4 — Shared / Weak Passwords
### 22. `REDACTED_PASSWORD123!` — Used Across 5+ Services
This password is the same for all of the following. Change each to a
**unique** strong password:
| Service | File | Variable |
|---------|------|----------|
| NetBox | `hosts/synology/atlantis/netbox.yml` | `SUPERUSER_PASSWORD` |
| Paperless admin | `hosts/synology/calypso/paperless/docker-compose.yml` | `PAPERLESS_ADMIN_PASSWORD` |
| Seafile admin | `hosts/synology/calypso/seafile-server.yaml` | `INIT_SEAFILE_ADMIN_PASSWORD` |
| Seafile admin (new) | `hosts/synology/calypso/seafile-new.yaml` | `INIT_SEAFILE_ADMIN_PASSWORD` |
| PhotoPrism | `hosts/physical/anubis/photoprism.yml` | `PHOTOPRISM_ADMIN_PASSWORD` |
| Hemmelig | `hosts/vms/bulgaria-vm/hemmelig.yml` | `SECRET_JWT_SECRET` |
| Vaultwarden admin | `hosts/synology/atlantis/bitwarden/bitwarden_token.txt` | (source password) |
For each: generate `openssl rand -base64 18`, update in the compose file,
restart the container, then log in to verify.
---
### 23. `REDACTED_PASSWORD` — Used Across 3 Services
| Service | File | Variable |
|---------|------|----------|
| Gotify | `hosts/vms/homelab-vm/gotify.yml` | `GOTIFY_DEFAULTUSER_PASS` |
| Pi-hole | `hosts/synology/atlantis/pihole.yml` | `WEBPASSWORD` |
| Stirling PDF | `hosts/synology/atlantis/stirlingpdf.yml` | `SECURITY_INITIAL_LOGIN_PASSWORD` |
---
### 24. `mastodon_pass_2026` — Live PostgreSQL Password
**Files:**
- `hosts/vms/matrix-ubuntu-vm/mastodon/.env.production.template`
- `hosts/vms/matrix-ubuntu-vm/docs/SETUP.md`
```bash
# On the matrix-ubuntu-vm server:
ssh YOUR_WAN_IP
sudo -u postgres psql
ALTER USER mastodon WITH PASSWORD 'REDACTED_PASSWORD';
\q
# Update the password in .env.production.template and Mastodon's running config
# Restart Mastodon services
```
---
### 25. Watchtower API Token (`REDACTED_WATCHTOWER_TOKEN`)
| File |
|------|
| `hosts/synology/atlantis/watchtower.yml` |
| `hosts/synology/calypso/prometheus.yml` |
```bash
# Generate a proper random token:
openssl rand -hex 20
# Update WATCHTOWER_HTTP_API_TOKEN in both files
# Update any scripts that call the Watchtower API
```
---
### 26. `test:test` SSH Credentials on `YOUR_WAN_IP`
The matrix-ubuntu-vm CREDENTIALS.md shows a `test` user with password `test`.
```bash
# SSH to the server and remove or secure the test account:
ssh YOUR_WAN_IP
passwd test # change to a strong password
# or: userdel -r test # remove entirely if unused
```
---
## Priority 5 — Network Infrastructure
### 27. Management Switch Password Hashes
**File:** `mgmtswitch.conf` (will be removed from public mirror by sanitizer)
The SHA-512 hashes for `root`, `vish`, and `vkhemraj` switch accounts are
crackable offline. Rotate the switch passwords:
```bash
# SSH to the management switch
ssh admin@10.0.0.15
# Change passwords for all local accounts:
enable
configure terminal
username root secret NEW_PASSWORD
username vish secret NEW_PASSWORD
username vkhemraj secret NEW_PASSWORD
write memory
```
---
## Final Verification
After completing all rotations:
```bash
# 1. Commit and push all file changes
git add -A
git commit -m "chore(security): rotate all exposed credentials"
git push origin main
# 2. Wait for the mirror workflow to complete, then pull:
git -C /home/homelab/organized/repos/homelab-optimized pull
# 3. Verify none of the old secrets appear in the public mirror:
cd /home/homelab/organized/repos/homelab-optimized
grep -r "77e3ddaf\|52fa6ccb\|FGXlHM7d\|sk-proj-C_IYp6io\|ArP5XWdkwVyw\|bdtrpmpce\|toiunzuby" . 2>/dev/null
grep -r "244c619d\|RpRexcYo5\|mastodon_pass\|REDACTED_PASSWORD\|REDACTED_PASSWORD\|REDACTED_WATCHTOWER_TOKEN" . 2>/dev/null
grep -r "2e80b1b7d3a\|eca299ae59\|rxmr4tJoqfu\|ZjCofRlfm6\|QE5SudhZ99" . 2>/dev/null
# All should return no results
# 4. Verify GitOps deployments are healthy in Portainer:
# https://192.168.0.200:9443
```
---
## Going Forward — Preventing This Again
The root cause: secrets hard-coded in compose files that get committed to git.
**Rules:**
1. **Never hard-code secrets in compose files** — use Docker Secrets, or an
`.env` file excluded by `.gitignore` (Portainer can load env files from the
host at deploy time)
2. **Never put real values in documentation** — use `YOUR_API_KEY` placeholders
3. **Never create `Secrets.txt` or `CREDENTIALS.md` files in the repo** — use
a password manager (you already have Vaultwarden/Bitwarden)
4. **Run the sanitizer locally** before any commit that touches secrets:
```bash
# Test in a temp copy — see what the sanitizer would catch:
tmpdir=$(mktemp -d)
cp -r /path/to/homelab "$tmpdir/"
python3 "$tmpdir/homelab/.gitea/sanitize.py"
```
## Related Documentation
- [Security Hardening](../security/SERVER_HARDENING.md)
- [Repository Sanitization](../admin/REPOSITORY_SANITIZATION.md)
- [GitOps Deployment Guide](../admin/GITOPS_DEPLOYMENT_GUIDE.md)
## Portainer Git Credential Rotation
The saved Git credential **`portainer-homelab`** (credId: 1) is used by ~43 stacks to
pull compose files from `git.vish.gg`. When the Gitea token expires or is rotated,
all those stacks fail to redeploy.
```bash
# 1. Generate a new Gitea token at https://git.vish.gg/user/settings/applications
# Scope: read:repository
# 2. Test the token:
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token YOUR_NEW_TOKEN" \
"https://git.vish.gg/api/v1/repos/Vish/homelab"
# Should return 200
# 3. Update in Portainer:
curl -k -s -X PUT \
-H "X-API-Key: "REDACTED_API_KEY" \
-H "Content-Type: application/json" \
"https://192.168.0.200:9443/api/users/1/gitcredentials/1" \
-d '{"name":"portainer-homelab","username":"vish","password":"YOUR_NEW_TOKEN"}'
```
> Note: The API update may not immediately propagate to automated pulls.
> Pass credentials inline in redeploy calls to force use of the new token.
---
## Change Log
- 2026-02-27 — Incident: sanitization commit `037d766a` replaced credentials with
`REDACTED_PASSWORD` placeholders across 14 compose files. All affected containers
detected via Portainer API env scan and restored from `git show 037d766a^`. Added
Portainer Git credential rotation section above.
- 2026-02-20 — Initial creation (8 items)
- 2026-02-20 — Expanded after full private repo audit (27 items across 34 exposure categories)