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
| Piece | Path | Role |
|---|---|---|
| Broker wrapper | scripts/secret-broker/Invoke-PlaywrightWithSecret.ps1 | Lints the JS, delivers the login secret only as a child-process env var, scrubs stdout + trace |
| Automation JS | scripts/rotate/n8n-cloud-api-key.js | Logs in, creates the API key, writes the new value to TOKEN_OUT_FILE, prints only non-secret confirmation |
| Broker core | scripts/lib/sops-common.psm1 → Invoke-WithSopsSecret, Test-PlaywrightSecretUsage, Protect-SecretInText, Test-SopsSecretRedaction | Decrypt-in-process, lint, redact-all-encodings |
The four carve-out rules (ADR 003 §Playwright carve-out)
- 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 readsprocess.env.N8N_CLOUD_PASSby name only. - Script lint before execution —
Test-PlaywrightSecretUsagerefuses 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.jsreferencesprocess.env.N8N_CLOUD_PASSexactly once, in the password.fill(). - 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. - 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_FILEfor the downstreamsops-set.ps1write.
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_EMAILandN8N_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; seedocs/playbooks/secret-manager-request.md. - Node + Playwright + Chromium installed (same as
scripts/rotate/*.jsflows). SOPS_AGE_KEY_FILEpointing 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_FILEThe 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 -ForceThen 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'forN8N_CLOUD_API_KEYin Supabasedev_r_services(consistent withplaywright-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.