# 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 3–5× 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**: 4–6 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)
