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_rotated is the initial setup date and rotation has never occurred since) are overdue. Rotate as soon as operationally feasible.
  • Every rotation must update four locations: .env on the relevant VPS server(s), the corresponding GitHub Secret, .env.local on the dev workstation, and dev_r_services.last_rotated / next_due in Supabase.
  • Append a row to docs/secrets-rotation-log.md for 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

ServiceWhat to rotateFrequencyKey / identifier
grafanaGRAFANA_ADMIN_PASSWORD180 daysGRAFANA_ADMIN_PASSWORD in GH Secrets
vps-i1SSH key pair (root + claude-admin)365 daysVPS_ROOT_SSH_KEY, VPS_SSH_PRIVATE_KEY in GH Secrets
vps-h1Root SSH key + root console password365 daysVPS_ROOT_SSH_KEY (h1), HOSTINGER_ROOT_PASSWORD in GH Secrets
n8nn8n admin account password (vps-h1)180 daysStored in .env.local as N8N_ADMIN_PASSWORD
n8n-cloudn8n Cloud account password180 daysStored in .env.local as N8N_CLOUD_PASSWORD
traccar-serverAdmin UI password180 daysStored in .env.local as TRACCAR_ADMIN_PASSWORD
openclawAdmin password / API key90 daysStored in .env.local
cloudflare-dnsCloudflare account password365 daysStored in .env.local
cloudflare-dnsAPI tokens180 daysCLOUDFLARE_TOKEN_ZINTEGROWANA, CF_API_TOKEN in GH Secrets
githubAccount password + 2FA recovery codes365 daysStored in password manager
githubPATs (GH_TOKEN, GH_PAT)90 daysGH Secrets + .env.local
supabaseservice_role key90 daysSUPABASE_SERVICE_KEY in GH Secrets
supabasepostgres superuser password90 daysSUPABASE_DB_PASSWORD in GH Secrets + .env.local — last rotated 2026-06-13, next due 2026-09-13
vercelAPI token90 daysVERCEL_TOKEN in GH Secrets
wahaWAHA_API_KEY180 daysWAHA_API_KEY in GH Secrets + vps-h1 .env
mailgun-euSMTP credentials365 daysSMTP_USER, SMTP_PASSWORD in GH Secrets
wasabi-monitoringS3 access key pair (ecotrans-monitoring bucket, eu-central-1)180 daysWASABI_ACCESS_KEY, WASABI_SECRET_KEY in GH Secrets + monitoring/.env
wasabi-p24-infraS3 access key pair (p24-infra bucket, eu-central-2, IAM user p24-infra)90 daysP24_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.

CredentialKeysReason
TrelloTRELLO_API_KEY, TRELLO_TOKENRead/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.

ScopeBucketRegionKeysRotation frequency
Monitoring bucketecotrans-monitoringeu-central-1WASABI_ACCESS_KEY, WASABI_SECRET_KEY180 days
p24-infra (IAM user p24-infra)p24-infraeu-central-2P24_INFRA_WASABI_ACCESS_KEY, P24_INFRA_WASABI_SECRET_KEY90 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:

  1. Opens a GitHub Issue with label human-action
  2. Lists the credential names and days overdue
  3. The issue remains open until the rotation is performed and next_due is 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

  1. 90-day credentials overdue (SUPABASE_SERVICE_KEY, VERCEL_TOKEN, GitHub PATs, OpenClaw) — highest risk if compromised
  2. 180-day credentials overdue (Grafana password, Wasabi keys, Cloudflare API tokens, WAHA, n8n passwords)
  3. 365-day credentials overdue (SSH keys, account passwords, Mailgun)
  4. 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-infra

SUPABASE_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 calls
  • HSTGR_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.local
  • P24_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>