Playbook: BMS4_N8N_API_KEY Rotation

Part of the n8n playbook index (#2641) — Credential rotation category. Consumed by scripts/check-p24-workflows-connection.sh (auth + retry audit, docs/n8n-workflow-inventory.md §Stale API key tracker).

Service: n8n (self-hosted, bms-4) Secret: BMS4_N8N_API_KEY Rotation frequency: 90 days (or immediately on suspected exposure) Last rotated: 2026-08-01 (exposure-triggered, #4918 — Playwright automation, scripts/rotate/n8n-bms4-api-key.js) Next due: 2026-10-30


What uses this credential

ConsumerHowEffect if missing
Monitoring stack (vps-i1)HTTP calls to n8n REST API for workflow triggersAutomated workflow triggers fail
credential-exporterChecks key age for rotation alertsRotation alert suppressed
Scripts in scripts/Direct n8n API callsScript failures
n8n MCP toolsHSTGR_N8N_MCP_TOKEN is separate; this key is for REST API authREST API calls 401

Where stored

LocationKey nameHow to update
secrets/n8n-bms4.env.sopsBMS4_N8N_API_KEYSOPS write pattern (see below)
GH Secret radieu/p24-infraHSTGR_N8N_API_KEYgh secret set
bms-4 deployed env/opt/p24-infra/bms-4/.envAuto-synced by secrets-sync.yml on merge to dev/main
bms-4 monitoring env/root/.p24-auth-monitor.envManual SSH update — used by p24-auth monitoring cron

Automation status

Playwright-automatable (Tier 2) — Claude can rotate this without human involvement, but requires Playwright MCP (browser session). The n8n Settings → API page allows creating and deleting API keys.

n8n 2.26.3 finding (2026-07-02): The REST API endpoints for API key management (/api/v1/user/api-key, /api/v1/users/{id}/api-keys, etc.) all return 404. The keys are stored as signed JWTs in the user_api_keys DB table — they cannot be replaced via direct DB UPDATE (signature would break). Rotation requires Playwright or manual UI login.

n8n admin password (2026-07-03 confirmed): bms4_n8n_db_password in SOPS is the PostgreSQL password — NOT the n8n web UI admin password. The n8n admin password was originally stored as BMS4_N8N_RADIEU_PASSWORD (renamed 2026-07-09 in #3287 from the shell-invalid key n8n-bms-radieu-password), but that key was superseded 2026-08-01 (#4918) after a self-service password reset created a new key, BMS4_N8N_ADMIN_PASSWORD, in secrets/n8n-bms4.env.sops — that is the current live key (confirmed via live login test, HTTP 200; BMS4_N8N_RADIEU_PASSWORD returns 401 as of 2026-08-11, #6088). scripts/rotate/n8n-bms4-api-key.ps1 already reads BMS4_N8N_ADMIN_PASSWORD, not BMS4_N8N_RADIEU_PASSWORD — this doc previously lagged that fix. To reset it in the DB: generate a bcrypt hash locally and UPDATE the "user" table in bms-4-n8n-postgres-1.

If Playwright MCP is unavailable, use Option B (manual) below.


Rotation steps

Option A — Playwright agent (preferred)

Spawn a background agent with this prompt (do NOT include the actual password value — the agent reads it from SOPS):

You are rotating BMS4_N8N_API_KEY for the self-hosted n8n instance on bms-4.

IMPORTANT: Never display any secret value in your response. Reference key names only.

1. Read the n8n admin password from SOPS:
   $env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
   Store the value of BMS4_N8N_ADMIN_PASSWORD from secrets/n8n-bms4.env.sops silently in $env:N8N_PASS
   (use Select-String pattern — never print $env:N8N_PASS)
   NOTE: bms4_n8n_db_password is the PostgreSQL password — do NOT use it for n8n UI login.

2. Use Playwright to navigate to https://n8n.bms-4.infra.zintegrowana.online
   - Login with email radieu@gmail.com and the password from step 1
   - Go to Settings → API (left sidebar)
   - If an existing key named "p24-infra-api" exists, click the trash icon to delete it
   - Click "Create an API Key"
   - Set label: p24-infra-api-<YYYY-MM-DD>
   - Copy the new key value into $env:NEW_KEY (never print it)
   - Close the dialog

3. Update secrets/n8n-bms4.env.sops with the new BMS4_N8N_API_KEY value
   Follow the SOPS write pattern from CLAUDE.md (decrypt → edit → re-encrypt → canary).
   Use [System.IO.File]::WriteAllText with UTF8Encoding($false).
   Temp file must be named secrets/n8n-bms4-edit.env.sops

4. Update GH Secret:
   $env:NEW_KEY | gh secret set BMS4_N8N_API_KEY --repo radieu/p24-infra
   $env:NEW_KEY = ""

5. Verify the new key works:
   $r = Invoke-RestMethod "https://n8n.bms-4.infra.zintegrowana.online/api/v1/workflows" `
     -Headers @{ "X-N8N-API-KEY" = $env:NEW_KEY }
   Write-Host "Workflows count: $($r.data.Count)"
   $env:NEW_KEY = ""

6. Commit and open a PR targeting main:
   git add secrets/n8n-bms4.env.sops
   git commit -m "chore: rotate BMS4_N8N_API_KEY (scheduled 90d)"
   Push and open PR via gh pr create.

7. Append to docs/secrets-rotation-log.md:
   | <YYYY-MM-DD HH:MM UTC> | — | BMS4_N8N_API_KEY | scheduled 90d rotation | AI-agent | SOPS + GH Secret |

Option B — Manual (fallback)

Step 1 — Log into n8n
  URL: https://n8n.bms-4.infra.zintegrowana.online
  Email: radieu@gmail.com
  Password: read bms4_n8n_db_password from secrets/n8n-bms4.env.sops (never print value)

Step 2 — Delete old API key
  → Settings (left sidebar) → API
  → Find key named p24-infra-api or similar
  → Click trash icon → Confirm deletion

Step 3 — Create new API key
  → Click "Create an API Key"
  → Label: p24-infra-api-<YYYY-MM-DD>
  → Copy value immediately (shown once only)

Step 4 — Update secrets/n8n-bms4.env.sops (PowerShell, Windows dev workstation)
  $env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
  $plain = sops --decrypt --input-type dotenv --output-type dotenv secrets\n8n-bms4.env.sops
  $tempPath = "$PWD\secrets\n8n-bms4-edit.env.sops"
  # Store new key silently — never print it
  $env:NEW_KEY = "<paste-from-step-3>"
  $updated = $plain -replace "^BMS4_N8N_API_KEY=.*", "BMS4_N8N_API_KEY=$env:NEW_KEY"
  [System.IO.File]::WriteAllText($tempPath, ($updated -join "`n") + "`n",
    [System.Text.UTF8Encoding]::new($false))
  $enc = sops --encrypt --input-type dotenv --output-type dotenv $tempPath
  [System.IO.File]::WriteAllText("$PWD\secrets\n8n-bms4.env.sops",
    ($enc -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
  # Canary — verify before commit
  sops --decrypt --input-type dotenv --output-type dotenv secrets\n8n-bms4.env.sops | Out-Null
  if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }
  [System.IO.File]::Delete($tempPath)

Step 5 — Update GH Secret
  $env:NEW_KEY | gh secret set BMS4_N8N_API_KEY --repo radieu/p24-infra
  $env:NEW_KEY = ""

Step 6 — Commit and push
  git add secrets/n8n-bms4.env.sops
  git commit -m "chore: rotate BMS4_N8N_API_KEY (scheduled 90d)"
  git push
  gh pr create --base main --title "chore: rotate BMS4_N8N_API_KEY"

Step 7 — Append to docs/secrets-rotation-log.md
  | <YYYY-MM-DD HH:MM UTC> | — | BMS4_N8N_API_KEY | scheduled 90d rotation | radieu | SOPS + GH Secret |

Verification

# 1. Confirm SOPS updated (key present, check length > 200 as proxy for valid key)
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$line = sops --decrypt --input-type dotenv --output-type dotenv secrets\n8n-bms4.env.sops |
    Select-String "^BMS4_N8N_API_KEY="
$val = $line.ToString().Split('=', 2)[1]
Write-Host "BMS4_N8N_API_KEY length: $($val.Length)"   # expected: >200 chars
 
# 2. Confirm key works against n8n API
$env:K = $val
$r = Invoke-RestMethod "https://n8n.bms-4.infra.zintegrowana.online/api/v1/workflows" `
    -Headers @{ "X-N8N-API-KEY" = $env:K }
Write-Host "Active workflows: $($r.data.Count)"
$env:K = ""; $env:SOPS_AGE_KEY_FILE = ""

Escalation

SymptomAction
n8n Settings → API page not visibleUpgrade n8n or check if API feature is enabled in instance settings
Login failsVerify bms4_n8n_db_password in SOPS is current; check n8n container logs on bms-4
API call returns 401 after rotationWait 30s (cache flush); if still failing, check SOPS was committed and secrets-sync.yml has run
secrets-sync.yml not yet mergedUse the new key directly; service will self-heal on merge + container restart
Key not accepted anywheren8n may need a container restart: ssh root@54.36.123.110 "cd /opt/p24-infra && docker compose restart n8n"

Prevention

  • The credential-rotation.yml GH Actions workflow (Monday 06:00 UTC) opens a human-action issue when next_due passes.
  • n8n does not emit expiry warnings for API keys — only the calendar/GH Actions check triggers rotation.
  • After rotation, update this playbook’s Last rotated and Next due dates.

Audit Log — Log to infra_operations

After this operation completes, log it to the infra_operations audit table.

Python (Linux server — bms-4, vps-i1, vps-h1, or similar):

import sys
sys.path.insert(0, '/opt/p24-infra')
from scripts.lib.log_op import log_op
 
log_op(
    actor="claude",  # "radieu" for manual human ops, "claude" for agent
    op_type="credential_rotation",
    resource="BMS4_N8N_API_KEY",
    result="success",  # "success" | "failed" | "skipped"
    detail="Scheduled rotation — n8n bms-4 API key regenerated via n8n UI and SOPS updated",
    env="bms-4",
    gh_issue=2730,
)

PowerShell (Windows dev machine):

$env:SUPABASE_URL = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_URL=").ToString().Split("=",2)[1].Trim()
$env:SUPABASE_SERVICE_KEY = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_SERVICE_KEY=").ToString().Split("=",2)[1].Trim()
python -c "
import os, sys
sys.path.insert(0, 'C:/code_2026/p24-infra')
from scripts.lib.log_op import log_op
log_op('claude', 'credential_rotation', 'BMS4_N8N_API_KEY', 'success', 'Scheduled rotation — n8n bms-4 API key regenerated via n8n UI and SOPS updated', 'bms-4')
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''