Playbook: N8N_CLOUD_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 Cloud (https://app.n8n.cloud) Secret: N8N_CLOUD_API_KEY Rotation frequency: 90 days (or immediately on suspected exposure) Last rotated: 2026-06-27 Next due: 2026-09-27


What uses this credential

ConsumerHowEffect if missing
monitoring/exporters/Polls n8n Cloud workflow execution statsWorkflow health metrics missing from Grafana
credential-exporterChecks key age for rotation alertsRotation alert suppressed
Scripts triggering n8n Cloud webhooksAPI auth headerScript calls to n8n Cloud fail with 401

Where stored

LocationKey nameHow to update
secrets/monitoring.env.sopsN8N_CLOUD_API_KEYSOPS write pattern (see below)
secrets/n8n-bms4.env.sopsN8N_CLOUD_API_KEYSame SOPS write pattern
vps-i1 deployed env/opt/p24-infra/monitoring/.envAuto-synced by secrets-sync.yml on merge to dev/main
bms-4 deployed env/opt/p24-infra/bms-4/.envAuto-synced by secrets-sync.yml on merge to dev/main

Note: Key exists in both SOPS files because both vps-i1 (monitoring exporters) and bms-4 (n8n workflows) may reference n8n Cloud status.


Login credentials

  • Email: secrets/monitoring.env.sops key N8N_CLOUD_LOGIN_EMAIL (radieu@gmail.com)
  • Password: secrets/monitoring.env.sops key N8N_CLOUD_LOGIN_PASSWORD — reference key name only, never display value
  • Added 2026-08-05 (#2641) — previously this section named no concrete SOPS key, and a separate doc (docs/password-rotation-procedures.md) claimed a different, wrong location (.env.local, N8N_CLOUD_PASSWORD). Both blocked the Option A automation below until fixed.

Automation status

Playwright-automatable — Claude can rotate this without human involvement. n8n Cloud provides an API Keys section under Account Settings. Spawn a Playwright agent with the prompt below.


Rotation steps

Option A — Playwright agent (preferred)

Spawn a background agent with this prompt:

You are rotating N8N_CLOUD_API_KEY for the n8n Cloud account at https://app.n8n.cloud.

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

1. Read the n8n Cloud login email/password from SOPS silently into $env:N8N_CLOUD_EMAIL / $env:N8N_CLOUD_PASS
   (keys: N8N_CLOUD_LOGIN_EMAIL / N8N_CLOUD_LOGIN_PASSWORD in secrets/monitoring.env.sops)
   $env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
   Use Select-String on the SOPS output — never print the value.

2. Use Playwright to navigate to https://app.n8n.cloud
   - Login with email radieu@gmail.com and the password from step 1
   - Click the user avatar / account menu (top right)
   - Go to Settings → API (or Account → API Keys)
   - Find the existing key(s) named "p24-infra" or similar
   - Delete the old key
   - Click "Create API Key"
   - Set label: p24-infra-<YYYY-MM-DD>
   - Copy the new key value into $env:NEW_KEY (never print it)

3. Update secrets/monitoring.env.sops:
   $env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
   Follow the SOPS write pattern: decrypt → update N8N_CLOUD_API_KEY line → re-encrypt → canary.
   Temp file: secrets/monitoring-edit.env.sops
   Use [System.IO.File]::WriteAllText with UTF8Encoding($false).

4. Update secrets/n8n-bms4.env.sops with the same new key value:
   Same SOPS write pattern; temp file: secrets/n8n-bms4-edit.env.sops

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

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

7. Append to docs/secrets-rotation-log.md:
   | <YYYY-MM-DD> | N8N_CLOUD_API_KEY | scheduled 90d rotation | AI-agent | SOPS (monitoring + n8n-bms4) |

Option B — Manual (fallback)

Step 1 — Log into n8n Cloud
  URL: https://app.n8n.cloud
  Email: radieu@gmail.com
  Password: read from secrets/monitoring.env.sops (key name in SOPS — never print value)

Step 2 — Delete old API key
  → Account menu (top-right avatar) → Settings → API
  → Find key named p24-infra or similar
  → Click delete → Confirm

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

Step 4 — Update secrets/monitoring.env.sops
  $env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
  $plain = sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops
  $tempPath = "$PWD\secrets\monitoring-edit.env.sops"
  $env:NEW_KEY = "<paste-from-step-3>"
  $updated = $plain -replace "^N8N_CLOUD_API_KEY=.*", "N8N_CLOUD_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\monitoring.env.sops",
    ($enc -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
  sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops | Out-Null
  if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }
  [System.IO.File]::Delete($tempPath)

Step 5 — Update secrets/n8n-bms4.env.sops (same pattern)
  $plain = sops --decrypt --input-type dotenv --output-type dotenv secrets\n8n-bms4.env.sops
  $tempPath = "$PWD\secrets\n8n-bms4-edit.env.sops"
  $updated = $plain -replace "^N8N_CLOUD_API_KEY=.*", "N8N_CLOUD_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))
  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)
  $env:NEW_KEY = ""

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

Step 7 — Append to docs/secrets-rotation-log.md
  | <YYYY-MM-DD> | N8N_CLOUD_API_KEY | scheduled 90d rotation | radieu | SOPS (monitoring + n8n-bms4) |

Verification

# 1. Confirm SOPS updated in both files
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
 
$m = sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops |
    Select-String "^N8N_CLOUD_API_KEY="
$n = sops --decrypt --input-type dotenv --output-type dotenv secrets\n8n-bms4.env.sops |
    Select-String "^N8N_CLOUD_API_KEY="
Write-Host "monitoring.env.sops N8N_CLOUD_API_KEY length: $($m.ToString().Split('=',2)[1].Length)"
Write-Host "n8n-bms4.env.sops   N8N_CLOUD_API_KEY length: $($n.ToString().Split('=',2)[1].Length)"
 
# 2. Confirm both values match (compare hashes — never display values)
$mv = $m.ToString().Split('=',2)[1]
$nv = $n.ToString().Split('=',2)[1]
Write-Host "Values match: $($mv -eq $nv)"   # Must be True

Escalation

SymptomAction
Can’t find API Keys section in n8n Cloud UIn8n Cloud UI changes frequently — check under Account → Settings → Advanced
Login failsTry password reset at https://app.n8n.cloud/forgot-password (radieu@gmail.com)
API returns 403 instead of 401Account may have IP restrictions; try from a known-good IP
Both SOPS files have different values after rotationRe-run step 4-5 above to sync both files
secrets-sync.yml not yet triggeredServices on vps-i1 and bms-4 still use old key until merge + container restart

Prevention

  • The credential-rotation.yml GH Actions workflow (Monday 06:00 UTC) opens a human-action issue when next_due passes.
  • n8n Cloud does not send expiry warnings — only the GH Actions check triggers rotation.
  • Keep both SOPS files (monitoring.env.sops + n8n-bms4.env.sops) in sync — both must be updated in the same commit.
  • 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="N8N_CLOUD_API_KEY",
    result="success",  # "success" | "failed" | "skipped"
    detail="Scheduled rotation — n8n Cloud API key regenerated and SOPS updated",
    env="vps-i1",
    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', 'N8N_CLOUD_API_KEY', 'success', 'Scheduled rotation — n8n Cloud API key regenerated and SOPS updated', 'vps-i1')
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''