Password & Credential Rotation — Master Runbook
Workbook last reviewed: 2026-05-15
Overview
All credentials are tracked in the dev_r_services Supabase table (project mwkqmgadqnkkihjdeqsi). The fields rotation_freq, last_rotated, and next_due drive the daily audit check (infra_docs_check). When next_due < today, the audit engine files a GitHub Issue.
Policy
- Bootstrap credentials (entries where
last_rotatedis the initial setup date and rotation has never occurred since) are overdue. Rotate as soon as operationally feasible. - Every rotation must update four locations:
.envon the relevant VPS server(s), the corresponding GitHub Secret,.env.localon the dev workstation, anddev_r_services.last_rotated/next_duein Supabase. - Append a row to
docs/secrets-rotation-log.mdfor every rotation. - Never log secret values — log key names and dates only.
Rotation record update (run after every rotation)
UPDATE dev_r_services
SET last_rotated = '2026-MM-DD',
next_due = '2026-MM-DD' -- last_rotated + rotation_freq
WHERE service_name = '<KEY_NAME>';Rotation Schedule Summary
| Service | What to rotate | Frequency | Key / identifier |
|---|---|---|---|
| grafana | GRAFANA_ADMIN_PASSWORD | 180 days | GRAFANA_ADMIN_PASSWORD in GH Secrets |
| vps-i1 | SSH key pair (root + claude-admin) | 365 days | VPS_ROOT_SSH_KEY, VPS_SSH_PRIVATE_KEY in GH Secrets |
| vps-h1 | Root SSH key + root console password | 365 days | VPS_ROOT_SSH_KEY (h1), HOSTINGER_ROOT_PASSWORD in GH Secrets |
| n8n | n8n admin account password (vps-h1) | 180 days | Stored in .env.local as N8N_ADMIN_PASSWORD |
| n8n-cloud | n8n Cloud account password | 180 days | Stored in .env.local as N8N_CLOUD_PASSWORD |
| traccar-server | Admin UI password | 180 days | Stored in .env.local as TRACCAR_ADMIN_PASSWORD |
| openclaw | Admin password / API key | 90 days | Stored in .env.local |
| cloudflare-dns | Cloudflare account password | 365 days | Stored in .env.local |
| cloudflare-dns | API tokens | 180 days | CLOUDFLARE_TOKEN_ZINTEGROWANA, CF_API_TOKEN in GH Secrets |
| github | Account password + 2FA recovery codes | 365 days | Stored in password manager |
| github | PATs (GH_TOKEN, GH_PAT) | 90 days | GH Secrets + .env.local |
| supabase | service_role key | 90 days | SUPABASE_SERVICE_KEY in GH Secrets |
| supabase | postgres superuser password | 90 days | SUPABASE_DB_PASSWORD in GH Secrets + .env.local — last rotated 2026-06-13, next due 2026-09-13 |
| vercel | API token | 90 days | VERCEL_TOKEN in GH Secrets |
| waha | WAHA_API_KEY | 180 days | WAHA_API_KEY in GH Secrets + vps-h1 .env |
| mailgun-eu | SMTP credentials | 365 days | SMTP_USER, SMTP_PASSWORD in GH Secrets |
| wasabi-monitoring | S3 access key pair (ecotrans-monitoring bucket, eu-central-1) | 180 days | WASABI_ACCESS_KEY, WASABI_SECRET_KEY in GH Secrets + monitoring/.env |
| wasabi-p24-infra | S3 access key pair (p24-infra bucket, eu-central-2, IAM user p24-infra) | 90 days | P24_INFRA_WASABI_ACCESS_KEY, P24_INFRA_WASABI_SECRET_KEY in GH Secrets + monitoring/.env — next due 2026-09-12 |
Credentials excluded from rotation
These credentials are intentionally not rotated. They are tracked in dev_r_services with rotation_freq = NULL and will not trigger audit alerts.
| Credential | Keys | Reason |
|---|---|---|
| Trello | TRELLO_API_KEY, TRELLO_TOKEN | Read/board-access only; no write access to sensitive data; rotation overhead exceeds security benefit |
Rotation Procedures
GRAFANA_ADMIN_PASSWORD
Used for: Grafana admin login, and Caddy basic_auth for the Prometheus and Alertmanager public URLs.
# 1. Generate new password
openssl rand -base64 32
# 2. Update monitoring/.env on vps-i1
# SSH in and edit:
# GF_SECURITY_ADMIN_PASSWORD=<new>
# 3. Generate new bcrypt hash for Caddyfile basic_auth
docker run --rm caddy:2.8-alpine caddy hash-password --plaintext "<new>"
# Replace the hash in monitoring/Caddyfile
# Commit and push, then git pull on vps-i1
# 4. Restart affected services
cd /opt/p24-infra/monitoring
docker compose restart grafana
docker compose restart caddy
# 5. Verify login at https://grafana.vps-i1.infra.zintegrowana.online
# 6. Update GH Secret: GRAFANA_ADMIN_PASSWORD
# 7. Update .env.local on dev workstation
# 8. Update dev_r_services
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+180d>'
WHERE service_name = 'GRAFANA_ADMIN_PASSWORD';Log entry in docs/secrets-rotation-log.md:
| <date> | GRAFANA_ADMIN_PASSWORD | scheduled | radieu | yes |
vps-i1 SSH Key Rotation (root + claude-admin)
Two key pairs are in use on vps-i1: the root key (stored locally at C:\Users\konar\.ssh\id_ed25519) and the claude-admin key (stored in GH Secret VPS_SSH_PRIVATE_KEY).
# --- Root key rotation ---
# 1. Generate new key pair on dev workstation
ssh-keygen -t ed25519 -f C:\Users\konar\.ssh\id_ed25519_new -C "root-vps-i1-<date>"
# 2. Add new public key to vps-i1 (while old key still works)
# Using Python paramiko with old key:
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect("217.154.82.162", port=22, username="root",
key_filename=r"C:\Users\konar\.ssh\id_ed25519")
pub = open(r"C:\Users\konar\.ssh\id_ed25519_new.pub").read().strip()
client.exec_command(f'echo "{pub}" >> /root/.ssh/authorized_keys')
# 3. Verify new key works
client.connect("217.154.82.162", port=22, username="root",
key_filename=r"C:\Users\konar\.ssh\id_ed25519_new")
# 4. Remove old public key from /root/.ssh/authorized_keys on vps-i1
# 5. Replace old key files on dev workstation
# Rename id_ed25519_new -> id_ed25519
# Rename id_ed25519_new.pub -> id_ed25519.pub
# 6. Update GH Secret VPS_ROOT_SSH_KEY with base64-encoded new private key
$key = [Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\Users\konar\.ssh\id_ed25519"))
# Paste $key into GH Secret VPS_ROOT_SSH_KEY
# --- claude-admin key rotation (similar procedure) ---
# Generate new key, add to /home/claude-admin/.ssh/authorized_keys,
# verify, remove old, update GH Secret VPS_SSH_PRIVATE_KEY
# 7. Update dev_r_services
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+365d>'
WHERE service_name IN ('SSH_KEY_ROOT', 'SSH_KEY_CLAUDE_ADMIN');vps-h1 SSH Key Rotation + Root Password
# --- Root SSH key rotation ---
# Same procedure as vps-i1 but target IP is 72.60.32.61
# Public key lives in /root/.ssh/authorized_keys on vps-h1
# --- Root console password rotation ---
# 1. SSH into vps-h1 as root
# 2. Set new password:
passwd root
# 3. Update .env.local: HOSTINGER_ROOT_PASSWORD=<new>
# 4. Update GH Secret: HOSTINGER_ROOT_PASSWORD
# 5. Update dev_r_services
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+365d>'
WHERE service_name IN ('SSH_KEY_ROOT_H1', 'VPS1_HOSTINGER_ROOT_PASSWORD');n8n Admin Password (vps-h1 self-hosted instance)
1. Open https://n8n.vps-h1.infra.zintegrowana.online in a browser
2. Log in as admin
3. Click top-right avatar → Settings → Personal → Change password
4. Enter current password and the new password (use: openssl rand -base64 24)
5. Save
6. Update .env.local on dev workstation: N8N_ADMIN_PASSWORD=<new>
7. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+180d>'
WHERE service_name = 'N8N_ADMIN_PASSWORD';
n8n Cloud Account Password
1. Open https://app.n8n.cloud in a browser
2. Log in → Account → Security → Change password
3. Enter current password and new password (use: openssl rand -base64 24)
4. Save
5. Update .env.local on dev workstation: N8N_CLOUD_PASSWORD=<new>
6. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+180d>'
WHERE service_name = 'N8N_CLOUD_PASSWORD';
Traccar Admin Password
1. Open https://traccar.vps-i1.infra.zintegrowana.online in a browser
2. Log in as admin
3. Click top-right → Account → Change password
4. Enter current password and new password (use: openssl rand -base64 24)
5. Save
6. Update .env.local on dev workstation: TRACCAR_ADMIN_PASSWORD=<new>
7. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+180d>'
WHERE service_name = 'TRACCAR_ADMIN_PASSWORD';
OpenClaw Admin Password / API Key
1. Open https://openclaw.vps-i1.infra.zintegrowana.online in a browser
2. Authenticate and navigate to admin settings
3. Rotate the API key or change the admin password — exact path depends on OpenClaw version
4. Update any dependent services or n8n credentials that use the OpenClaw API key
5. Update .env.local on dev workstation
6. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name LIKE '%OPENCLAW%';
Cloudflare Account Password
1. Log into Cloudflare at https://dash.cloudflare.com
2. Profile → Security → Change password
3. Follow the password reset flow
4. Update .env.local on dev workstation
5. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+365d>'
WHERE service_name = 'CLOUDFLARE_ACCOUNT_PASSWORD';
Cloudflare API Tokens
Token ID: 64a1ed3ca2dcfa0bef3c12c0580ba2d6 (CLOUDFLARE_TOKEN_ZINTEGROWANA, DNS-edit scope)
Rotation method: Global API Key (CF_GLOBAL_API_KEY in .env.local) +
email radieu@gmail.com via X-Auth-Key header. The DNS-scoped token cannot
roll itself (requires User:API Tokens:Edit), but the Global API Key can roll any token.
# Step 1 — verify current token is still active
$t = (gc d:\code_2026\p24-infra\.env.local | sls "^CLOUDFLARE_TOKEN_ZINTEGROWANA=(.+)").Matches[0].Groups[1].Value
Invoke-RestMethod "https://api.cloudflare.com/client/v4/user/tokens/verify" `
-Headers @{ Authorization = "Bearer $t" }
# Step 2 — roll using Global API Key
$gk = (gc d:\code_2026\p24-infra\.env.local | sls "^CF_GLOBAL_API_KEY=(.+)").Matches[0].Groups[1].Value
$h = @{ "X-Auth-Email" = "radieu@gmail.com"; "X-Auth-Key" = $gk; "Content-Type" = "application/json" }
$roll = Invoke-RestMethod "https://api.cloudflare.com/client/v4/user/tokens/64a1ed3ca2dcfa0bef3c12c0580ba2d6/value" -Headers $h -Method PUT -Body "{}"
$newToken = $roll.result
# Step 3 — update all consumers
(gc d:\code_2026\p24-infra\.env.local) -replace "^CLOUDFLARE_TOKEN_ZINTEGROWANA=.*", "CLOUDFLARE_TOKEN_ZINTEGROWANA=$newToken" |
sc d:\code_2026\p24-infra\.env.local -Encoding utf8
gh secret set CLOUDFLARE_TOKEN_ZINTEGROWANA --body $newToken --repo radieu/p24-infra
# Step 4 — verify and test DNS manager
Invoke-RestMethod "https://api.cloudflare.com/client/v4/user/tokens/verify" -Headers @{ Authorization = "Bearer $newToken" }
ssh root@217.154.82.162 "python3 /opt/p24-infra/scripts/dns-manager.py list"
# Step 5 — update dev_r_services
# UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+180d>'
# WHERE service_name = 'CLOUDFLARE_TOKEN_ZINTEGROWANA';GitHub Account Password + 2FA
1. Log into GitHub → Settings → Password and authentication → Change password
2. Rotate 2FA recovery codes: Settings → Password and authentication → Two-factor methods → View recovery codes → Regenerate
3. Store new recovery codes in password manager
4. Update .env.local: GITHUB_PASSWORD=<new>
5. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+365d>'
WHERE service_name = 'GITHUB_ACCOUNT';
GitHub PATs (GH_TOKEN, GH_PAT)
1. Log into GitHub → Settings → Developer settings → Personal access tokens
2. Locate GH_TOKEN and/or GH_PAT
3. Click the token → Regenerate → confirm scopes → copy new value
4. Update GH Secret GH_TOKEN (or GH_PAT):
gh secret set GH_TOKEN --repos radieu/p24-infra,radieu/et-operational-platform
5. Update .env.local on dev workstation
6. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name IN ('GH_TOKEN', 'GH_PAT');
SUPABASE_SERVICE_KEY
# The service_role key is managed by Supabase. It can be rolled in the project dashboard.
# Note: rolling the key immediately invalidates the old one — update all consumers first.
# 1. Identify all locations using SUPABASE_SERVICE_KEY:
# - monitoring/.env on vps-i1 (vercel-exporter, possibly others)
# - GH Secret SUPABASE_SERVICE_KEY
# - .env.local on dev workstation
# - Any other VPS .env files
# 2. In Supabase dashboard → Project Settings → API → Service role key → Roll
# Copy the new key immediately
# 3. Update all locations identified in step 1
# 4. Restart affected exporters on vps-i1:
cd /opt/p24-infra/monitoring
docker compose restart vercel-exporter
# 5. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name = 'SUPABASE_SERVICE_KEY';VERCEL_TOKEN
# 1. Log into Vercel → Account settings → Tokens → Create new token
# Set expiration to at least 90 days out
# Scope: Full account
# 2. Delete the old token from Vercel once the new one is in all locations
# 3. Update all consumers:
# - monitoring/.env on vps-i1 (cost-exporter, vercel-exporter)
# - GH Secret VERCEL_TOKEN
# - .env.local on dev workstation
# 4. Restart affected exporters:
cd /opt/p24-infra/monitoring
docker compose restart cost-exporter vercel-exporter
# 5. Verify metrics:
curl -s http://localhost:9202/metrics | grep 'vercel_'
curl -s http://localhost:9210/metrics | grep 'vercel_'
# 6. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name = 'VERCEL_TOKEN';WAHA_API_KEY
# WAHA does not have a built-in "rotate key" flow.
# Generate a new key, update the config, and restart.
# 1. Generate new key:
openssl rand -hex 32
# 2. Update /root/.env on vps-h1: WAHA_API_KEY=<new>
# 3. Restart WAHA container:
cd /root && docker compose restart waha
# 4. Update GH Secret WAHA_API_KEY
# 5. Update .env.local on dev workstation
# 6. Update any n8n credentials or webhook configs that include the key
# in the X-Api-Key header — update them in n8n Credential Manager
# 7. Verify WAHA health:
curl -s https://waha2.vps-h1.infra.zintegrowana.online/api/health \
-H "X-Api-Key: <new>"
# 8. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+180d>'
WHERE service_name = 'WAHA_API_KEY';Mailgun SMTP Credentials (SMTP_USER / SMTP_PASSWORD)
1. Log into Mailgun EU at https://app.eu.mailgun.com
2. Sending → Domain settings → SMTP credentials
3. For the relevant SMTP user: Reset password → copy new password
4. Update monitoring/.env on vps-i1:
SMTP_USER=<user>
SMTP_PASSWORD=<new>
5. Reload Alertmanager:
cd /opt/p24-infra/monitoring
docker compose restart alertmanager
# or hot reload:
curl -X POST http://localhost:9093/-/reload
6. Send a test alert to confirm delivery
7. Update GH Secrets: SMTP_USER, SMTP_PASSWORD
8. Update .env.local on dev workstation
9. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+365d>'
WHERE service_name IN ('SMTP_USER', 'SMTP_PASSWORD');
Wasabi S3 Access Keys (monitoring + p24-infra scope)
Two separate IAM users and key pairs are in use. Rotate them independently.
| Scope | Bucket | Region | Keys | Rotation frequency |
|---|---|---|---|---|
| Monitoring bucket | ecotrans-monitoring | eu-central-1 | WASABI_ACCESS_KEY, WASABI_SECRET_KEY | 180 days |
p24-infra (IAM user p24-infra) | p24-infra | eu-central-2 | P24_INFRA_WASABI_ACCESS_KEY, P24_INFRA_WASABI_SECRET_KEY | 90 days |
Lesson learned (2026-06-12): Never delete a Wasabi key from the console without first updating all consumers. The backup-exporter accumulated 65+ errors because the previous key was deleted without updating .env. Always create the new key and update all consumers before deleting the old key.
To rotate P24_INFRA_WASABI_ACCESS_KEY / P24_INFRA_WASABI_SECRET_KEY (p24-infra IAM user):
# NOTE: Use the Wasabi console (not the Windows workstation CLI) for key creation
# if SSL issues occur on Windows with Wasabi's endpoint.
# Alternative: SSH into vps-i1 and use the IAM API from there.
# 1. Create new key in Wasabi console → IAM → Users → p24-infra → Security credentials → Create access key
# (Do NOT delete the old key yet)
# 2. Update monitoring/.env on vps-i1:
# P24_INFRA_WASABI_ACCESS_KEY=<new_access_key>
# P24_INFRA_WASABI_SECRET_KEY=<new_secret_key>
# 3. Restart backup-exporter:
cd /opt/p24-infra/monitoring
docker compose restart backup-exporter
docker compose logs --tail=20 backup-exporter
# Confirm it reads backup-status.prom successfully
# 4. Update GH Secrets:
gh secret set P24_INFRA_WASABI_ACCESS_KEY -b "<new>" -R radieu/p24-infra
gh secret set P24_INFRA_WASABI_SECRET_KEY -b "<new>" -R radieu/p24-infra
# 5. Update .env.local on dev workstation
# 6. Only now delete the old key from Wasabi console
# 7. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '2026-MM-DD', next_due = '2026-MM-DD' -- +90d
WHERE service_name = 'P24_INFRA_WASABI_ACCESS_KEY';To rotate WASABI_ACCESS_KEY / WASABI_SECRET_KEY (monitoring bucket, eu-central-1):
# 1. Log into Wasabi console → Access Keys → Create new key pair
# (Do NOT delete the old key yet)
# 2. Update monitoring/.env on vps-i1 with new key pair:
# WASABI_ACCESS_KEY=<new>
# WASABI_SECRET_KEY=<new>
# 3. Restart affected services:
cd /opt/p24-infra/monitoring
docker compose restart thanos-sidecar cost-exporter
# Wait ~30s, then check Thanos sidecar is uploading blocks normally:
docker compose logs --tail=20 thanos-sidecar
# 4. Verify S3 connectivity:
docker run --rm \
-v /opt/p24-infra/monitoring/thanos/s3.yml:/s3.yml:ro \
quay.io/thanos/thanos:latest \
tools bucket ls --objstore.config-file /s3.yml
# 5. If verified, delete the old key from Wasabi console
# 6. Update GH Secrets:
gh secret set WASABI_ACCESS_KEY -b "<new>" -R radieu/p24-infra
gh secret set WASABI_SECRET_KEY -b "<new>" -R radieu/p24-infra
# 7. Update .env.local on dev workstation
# 8. Update dev_r_services:
UPDATE dev_r_services
SET last_rotated = '2026-MM-DD', next_due = '2026-MM-DD' -- +180d
WHERE service_name = 'WASABI_ACCESS_KEY';Automation and Alerting
The infra_docs_check GitHub Actions workflow runs daily and queries dev_r_services for any rows where next_due < CURRENT_DATE. When it finds overdue rotations, it:
- Opens a GitHub Issue with label
human-action - Lists the credential names and days overdue
- The issue remains open until the rotation is performed and
next_dueis updated in Supabase
There is no automated credential rotation — all rotations are manual. The automation only surfaces what is overdue.
Priority order for overdue rotations
- 90-day credentials overdue (SUPABASE_SERVICE_KEY, VERCEL_TOKEN, GitHub PATs, OpenClaw) — highest risk if compromised
- 180-day credentials overdue (Grafana password, Wasabi keys, Cloudflare API tokens, WAHA, n8n passwords)
- 365-day credentials overdue (SSH keys, account passwords, Mailgun)
- Bootstrap entries (never rotated since initial setup) — treat as overdue regardless of
rotation_freq
After Any Rotation — Checklist
[ ] New value in .env on relevant VPS server(s)
[ ] New value in GitHub Secrets (gh secret set <NAME> -R radieu/p24-infra)
[ ] New value in .env.local on dev workstation
[ ] Affected service restarted and verified working
[ ] last_rotated and next_due updated in dev_r_services
[ ] Row appended to docs/secrets-rotation-log.md
[ ] Old credential deleted/revoked from the issuing service
SUPABASE_GRAFANA_PASSWORD
Password for the grafana_readonly Supabase role used by Grafana’s direct PostgreSQL datasource.
Locations: GH Secret SUPABASE_GRAFANA_PASSWORD · monitoring/.env on vps-i1 · .env.local
-- 1. Rotate at Supabase (dashboard SQL editor or psql)
ALTER ROLE grafana_readonly WITH PASSWORD '<new>';
-- 5. Update dev_r_services after rotation
UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+180d>'
WHERE service_name = 'SUPABASE_GRAFANA_PASSWORD';# 2. Update monitoring/.env on vps-i1, then restart Grafana
ssh root@217.154.82.162 "cd /opt/p24-infra/monitoring && docker compose restart grafana"
# 3. Verify Grafana PostgreSQL datasource is green
# 4. Update GH Secret + .env.local
gh secret set SUPABASE_GRAFANA_PASSWORD -b "<new>" -R radieu/p24-infraSUPABASE_DB_PASSWORD
Direct PostgreSQL password for the postgres superuser on Supabase project mwkqmgadqnkkihjdeqsi. Used for manual pg_dump, emergency psql access, and the db-maintenance.py GitHub Actions job.
Locations: .env.local (local workstation) · GitHub Secret SUPABASE_DB_PASSWORD
NOT stored in vps-i1 monitoring/.env — Grafana uses the separate grafana_readonly role (SUPABASE_GRAFANA_PASSWORD).
API method: PATCH (not PUT or POST — those return 404):
# Read token from .env.local
$token = (gc d:\code_2026\p24-infra\.env.local | sls "^SUPABASE_ACCESS_TOKEN=(.+)").Matches[0].Groups[1].Value
$ref = "mwkqmgadqnkkihjdeqsi"
$newPw = -join ((33..126) | Get-Random -Count 32 | % { [char]$_ }) # or use openssl rand -base64 24
# Rotate via management API
Invoke-RestMethod "https://api.supabase.com/v1/projects/$ref/database/password" `
-Method PATCH `
-Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
-Body (ConvertTo-Json @{ password = $newPw })
# Update GH Secret
gh secret set SUPABASE_DB_PASSWORD -b $newPw -R radieu/p24-infra
# Update .env.local
(gc d:\code_2026\p24-infra\.env.local) -replace "^SUPABASE_DB_PASSWORD=.*", "SUPABASE_DB_PASSWORD=$newPw" |
sc d:\code_2026\p24-infra\.env.local -Encoding utf8-- Update dev_r_services after rotation
UPDATE dev_r_services
SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name = 'SUPABASE_DB_PASSWORD';Log entry in docs/secrets-rotation-log.md:
| <date> | SUPABASE_DB_PASSWORD | scheduled | radieu | yes |
SUPABASE_ACCESS_TOKEN
Personal access token for the Supabase management API (project creation, migrations, edge functions).
Locations: .env.local only (not in CI)
1. Supabase dashboard → Account → Access tokens → Generate new token
2. Delete old token from the dashboard
3. Update .env.local: SUPABASE_ACCESS_TOKEN=<new>
4. Update dev_r_services:
UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name = 'SUPABASE_ACCESS_TOKEN';
ANTHROPIC_API_KEY
Anthropic Claude API key. Used as fallback in audit-engine when claude-proxy is unavailable. VPS nodes authenticate via Claude Max OAuth — this key is a backup only.
Locations: GH Secret ANTHROPIC_API_KEY · .env.local
1. Anthropic console → API keys → Create new key
2. Update GH Secret ANTHROPIC_API_KEY
3. Update .env.local
4. Delete old key from Anthropic console
5. Update dev_r_services:
UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name = 'ANTHROPIC_API_KEY';
OPENAI_MONITORING_TOKEN
OpenAI API key used for monitoring/cost tracking queries.
Locations: .env.local only
1. OpenAI platform → API keys → Create new secret key
2. Update .env.local: OPENAI_MONITORING_TOKEN=<new>
3. Revoke old key from OpenAI platform
4. Update dev_r_services:
UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name = 'OPENAI_MONITORING_TOKEN';
SENTRY_AUTH_TOKEN
Sentry auth token for error tracking integration in et-operational-platform deploys.
Locations: GH Secret SENTRY_AUTH_TOKEN · .env.local
1. Sentry → User settings → Auth tokens → Generate new token
2. Update GH Secret SENTRY_AUTH_TOKEN
3. Update .env.local
4. Revoke old token from Sentry
5. Update dev_r_services:
UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+90d>'
WHERE service_name = 'SENTRY_AUTH_TOKEN';
EMAIL_SENDER_API_KEY
Cloudflare email worker API key. Used by the monitoring stack (Alertmanager) on vps-i1 to send alert emails via Cloudflare Email Workers (replaced Mailgun 2026-05-16).
Locations: monitoring/.env on vps-i1 · .env.local
# 1. Cloudflare dashboard → Workers & Pages → your email worker → Settings → API key → Rotate
# 2. Update monitoring/.env on vps-i1:
ssh root@217.154.82.162 "sed -i 's/EMAIL_SENDER_API_KEY=.*/EMAIL_SENDER_API_KEY=<new>/' /opt/p24-infra/monitoring/.env"
# 3. Restart Alertmanager and verify alert delivery
ssh root@217.154.82.162 "cd /opt/p24-infra/monitoring && docker compose restart alertmanager"
# 4. Update .env.local
# 5. Update GH Secret if added to CI
gh secret set EMAIL_SENDER_API_KEY -b "<new>" -R radieu/p24-infra
# 6. Update dev_r_services:
# UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+180d>'
# WHERE service_name = 'EMAIL_SENDER_API_KEY';HSTGR_N8N_API_KEY + HSTGR_N8N_MCP_TOKEN
Two n8n tokens for the self-hosted Hostinger instance:
HSTGR_N8N_API_KEY— REST API key for workflow management callsHSTGR_N8N_MCP_TOKEN— MCP server token for Claude ↔ n8n integration
Locations: .env.local only
HSTGR_N8N_API_KEY:
1. n8n.vps-h1.infra.zintegrowana.online → Settings → n8n API → Create new key
2. Update .env.local: HSTGR_N8N_API_KEY=<new>
3. Delete old key from n8n settings
HSTGR_N8N_MCP_TOKEN:
1. Regenerate from n8n MCP server config or restart the MCP service with a new token
2. Update .env.local: HSTGR_N8N_MCP_TOKEN=<new>
After both:
UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+180d>'
WHERE service_name IN ('HSTGR_N8N_API_KEY', 'HSTGR_N8N_MCP_TOKEN');
DISCORD_WEBHOOK_URL + P24_DISCORD_SCRIPTS_ERRORS_WEBHOOK_URL
Discord incoming webhooks for alert and script-error notifications. Webhooks do not expire and are not rotated on a schedule — rotate only if a URL is exposed publicly.
Locations:
DISCORD_WEBHOOK_URL: GH Secret ·.env.localP24_DISCORD_SCRIPTS_ERRORS_WEBHOOK_URL: GH Secret ·.env.local
If rotation is needed:
1. Discord server → Integrations → Webhooks → locate webhook → Regenerate URL
2. Update GH Secret
3. Update .env.local
4. Update any VPS .env files that reference the webhook URL
No next_due tracking — rotation is event-driven, not scheduled.
NEXT_PUBLIC_SUPABASE_ANON_KEY
Supabase public anonymous key. Safe to expose in client-side code by design. No rotation needed unless the Supabase project is recreated.
Locations: Vercel env (et-operational-platform) · .env.local
Rotation: only if Supabase project is reset. Update Vercel env and .env.local.
MYSQL_PASSWORD
MySQL root password for the Traccar database on vps-i1 (traccar-db container).
Locations: /root/traccar/.env on vps-i1 · .env.local
# 1. Generate new password
openssl rand -base64 24
# 2. Update the password in MySQL (while container is running):
ssh root@217.154.82.162 "docker exec traccar-db mysql -uroot -p'<old>' -e \"ALTER USER 'root'@'%' IDENTIFIED BY '<new>';\""
# 3. Update /root/traccar/.env on vps-i1:
# MYSQL_ROOT_PASSWORD=<new>
# MYSQL_PASSWORD=<new> (if separate app user)
# 4. Restart Traccar:
ssh root@217.154.82.162 "cd /root/traccar && docker compose restart"
# 5. Update .env.local on dev workstation
# 6. Update dev_r_services:
# UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+365d>'
# WHERE service_name = 'MYSQL_PASSWORD';CLAUDE_MAX_OAUTH
Claude Max OAuth credentials stored on VPS nodes at /home/claude-runner/.claude/.credentials.json. Access token auto-refreshes every 8–12h — no scheduled rotation needed. Rotate only on account compromise or if refresh token expires.
Locations: /home/claude-runner/.claude/.credentials.json on vps-i1 and vps-h1
Re-auth procedure (run locally when Discord alerts fire about auth failure):
python d:\tmp\reauth-hstgr.py # for vps-h1
# Then copy .credentials.json to the VPS manually
See CLAUDE.md “Claude Code Auth on VPSes” for full details. No next_due tracking.
ATRAX_AUTH_STRING
Authentication string for the Atrax GPS/fleet data integration.
Locations: .env.local · n8n env var (vps-h1)
1. Obtain new auth string from Atrax provider portal
2. Update .env.local: ATRAX_AUTH_STRING=<new>
3. Update n8n credential in n8n Credential Manager on vps-h1
4. Test the Atrax integration workflow in n8n
5. Update dev_r_services:
UPDATE dev_r_services SET last_rotated = '<date>', next_due = '<date+365d>'
WHERE service_name = 'ATRAX_AUTH_STRING';
TRELLO_KEYS
Trello API key + token (TRELLO_API_KEY, TRELLO_TOKEN) used for board management automation.
Locations: .env.local only
Excluded from scheduled rotation — read/board-access only; rotation overhead exceeds security benefit. Rotate only if credentials are suspected compromised.
If rotation is needed:
1. https://trello.com/app-key → regenerate API key
2. Generate new token for the key
3. Update .env.local: TRELLO_API_KEY=<new>, TRELLO_TOKEN=<new>