n8n Cloud API Key — Playwright Secret Broker Playbook

Issue: #2802 (part of #2620) · Design: docs/adr/003-sops-secret-access-broker.md

This is the UI-only rotation flow for N8N_CLOUD_API_KEY. n8n Cloud does not expose an endpoint for creating a new API key without an authenticated browser session — the API-key management screen (Settings → n8n API) is UI-only — so a real login credential has to reach a live browser. It reuses the exact Playwright-broker carve-out proven for the GitLab admin PAT (gitlab-token-playwright-broker.md); only the site-specific automation script differs.

No API path exists. Unlike GitLab (where an unexpired admin PAT can still mint a new PAT via API), n8n Cloud has no create-API-key endpoint at all. This Playwright flow is the only rotation path. The prior “Option A: Playwright MCP” reference in issue #2802’s body pointed at a playbook that never existed — this file replaces it.

Ownership (do not skip — role matrix)

Authoring the JS / this playbook is dev-coder work. Running an N8N_CLOUD_API_KEY rotation is a secret-manager (SOPS write) operation — see the role matrix in CLAUDE.md. A dev-coder session must not run this flow against real credentials; it delegates to secret-manager.

Components

PiecePathRole
Broker wrapperscripts/secret-broker/Invoke-PlaywrightWithSecret.ps1Lints the JS, delivers the login secret only as a child-process env var, scrubs stdout + trace
Automation JSscripts/rotate/n8n-cloud-api-key.jsLogs in, creates the API key, writes the new value to TOKEN_OUT_FILE, prints only non-secret confirmation
Broker corescripts/lib/sops-common.psm1Invoke-WithSopsSecret, Test-PlaywrightSecretUsage, Protect-SecretInText, Test-SopsSecretRedactionDecrypt-in-process, lint, redact-all-encodings

The four carve-out rules (ADR 003 §Playwright carve-out)

  1. Value delivery — the login secret is set only as an env var in the child Node process (never a CLI arg → no ps/Task Manager exposure). The JS reads process.env.N8N_CLOUD_PASS by name only.
  2. Script lint before executionTest-PlaywrightSecretUsage refuses to run a JS where the env-var name appears anywhere except as the argument to a .fill() on a password locator. scripts/rotate/n8n-cloud-api-key.js references process.env.N8N_CLOUD_PASS exactly once, in the password .fill().
  3. Capture surfaces — the JS starts tracing/screenshots only after the post-login redirect; the login step is never captured. The wrapper additionally scrubs the whole TraceDir.
  4. Output filtering — the broker holds the plaintext, so it (and only it) redacts the login password and the newly created key (raw + URL-encoded + base64) out of stdout/stderr and every trace/artifact file before returning pass/fail. The key value is left only in TOKEN_OUT_FILE for the downstream sops-set.ps1 write.

Prerequisites (on the host with the age key)

  • The n8n Cloud login email + password already exist as SOPS keys in secrets/monitoring.env.sops: N8N_CLOUD_LOGIN_EMAIL and N8N_CLOUD_LOGIN_PASSWORD (verified for #2802). No secret-manager add is needed. If they are ever missing, that is a secret-manager add — reference the key NAME only, never paste the value; see docs/playbooks/secret-manager-request.md.
  • Node + Playwright + Chromium installed (same as scripts/rotate/*.js flows).
  • SOPS_AGE_KEY_FILE pointing at the developer age key.

Run (secret-manager session)

$env:N8N_CLOUD_URL   = "https://p24.app.n8n.cloud"          # or N8N_CLOUD_BASE_URL from SOPS
$env:N8N_CLOUD_EMAIL = "radieu@gmail.com"                   # not secret (N8N_CLOUD_LOGIN_EMAIL)
$env:TOKEN_LABEL     = "p24-infra-2026-08-11"
$env:TOKEN_OUT_FILE  = Join-Path $env:TEMP "new-n8n-cloud-key.txt"
$env:TRACE_DIR       = Join-Path $env:TEMP "n8n-cloud-key-trace"
 
.\scripts\secret-broker\Invoke-PlaywrightWithSecret.ps1 `
  -SopsFile     secrets\monitoring.env.sops `
  -SopsKey      N8N_CLOUD_LOGIN_PASSWORD `
  -EnvVarName   N8N_CLOUD_PASS `
  -ScriptPath   scripts\rotate\n8n-cloud-api-key.js `
  -TraceDir     $env:TRACE_DIR `
  -TokenOutFile $env:TOKEN_OUT_FILE

The wrapper prints only redacted output + [broker] automation exit code: N. On success $env:TOKEN_OUT_FILE holds the new key value (mode 0600). Do not cat/Read that file — pipe it straight into the SOPS write:

# Write the new key INTO SOPS via the enforced write path — value never printed.
$env:NEW_VALUE = Get-Content $env:TOKEN_OUT_FILE -Raw
.\scripts\sops-set.ps1 -SopsFile secrets\monitoring.env.sops -Key N8N_CLOUD_API_KEY
# N8N_CLOUD_API_KEY also lives in secrets/n8n-bms4.env.sops — rotate BOTH
# atomically with the same value (see issue #2802).
.\scripts\sops-set.ps1 -SopsFile secrets\n8n-bms4.env.sops -Key N8N_CLOUD_API_KEY
$env:NEW_VALUE = ""
Remove-Item $env:TOKEN_OUT_FILE -Force

Then complete the distribution chain: update the GH Secret N8N_CLOUD_API_KEY and let secrets-sync.yml propagate the SOPS change to the live envs, and delete the old key in the n8n Cloud UI once the new one is confirmed working.

Verify the new key

# Non-secret check only — status code, never the body.
$env:KEY = Get-Content $env:TOKEN_OUT_FILE -Raw   # or re-decrypt via sops-invoke.ps1
curl -s -o /dev/null -w "%{http_code}" `
  -H "X-N8N-API-KEY: $env:KEY" `
  "https://p24.app.n8n.cloud/api/v1/workflows"
$env:KEY = ""
# 200 = key valid; 401 = still stale.

After running — compliance + audit

  • Register rotation_type='playwright' for N8N_CLOUD_API_KEY in Supabase dev_r_services (consistent with playwright-rotation-template.md). This is a data change for the running secret-manager session, not a code change in this PR.
  • Log to infra_operations (op_type=credential_rotation, resource=N8N_CLOUD_API_KEY).

If the lint fails

Test-PlaywrightSecretUsage reports total/allowed. total > allowed means process.env.N8N_CLOUD_PASS is referenced somewhere other than a .fill() on a password locator. Fix the JS so the env var is only ever .fill()-ed into the password field — keep the reference on the locator statement itself (page.locator('input[type="password"]...').first().fill(process.env.N8N_CLOUD_PASS)).

If n8n’s API-settings UI changed

n8n-cloud-api-key.js targets n8n’s current selectors (/settings/api, the “Create an API key” button, and the readonly created-key field). n8n reworks this UI periodically. Run once with HEADLESS=0 to watch, adjust the create-button / label / created-key selectors, and re-lint before a real run.