Plan: n8n Supabase Vault as runtime secret store for Supabase-coupled workflows
Issue: #4032 Type: Code-change-design (plan only) Status: Draft — iteration 1 Last updated: 2026-07-12 Author: Claude Opus 4.7 (worker session on vps-h1) Related: #1713 (ATRAX token handling), #4021 (ATRAX rotation incident 2026-07-12), #3941 / #3942 (fleet-update failure alerts), #2735 (per-consumer Postgres roles), #3826 (Pinbox24 CF Worker broker — orthogonal)
0. Summary — recommendation
Adopt Model B, narrowly: use vault.decrypted_secrets as the runtime source of truth only for the 3 ATRAX-cluster workflows already on Supabase, using a dedicated Postgres role (svc_n8n_vault_atrax) with row-level grants restricted to the ATRAX secret rows. The bootstrap credential is a new dedicated Postgres connection string added to secrets/n8n-bms4.env.sops; existing SUPABASE_SERVICE_ROLE_KEY is not reused for this purpose (least-privilege — the vault reader must not have RLS-bypass capability). After migration is verified end-to-end on all 3 workflows, ATRAX_* env-var keys are removed from both SOPS files that carry them today (secrets/n8n-bms4.env.sops and secrets/monitoring.env.sops — the issue body’s “SOPS n8n-bms4 only” premise is incorrect; §2.1 documents the actual state).
Rotation collapses from a 4-step multi-file SOPS+CI+deploy cycle to a single UPDATE vault.secrets SET secret=... WHERE name='ATRAX_PASSWORD' executed by the secret-manager role — no PR, no secrets-sync.yml run, no n8n container restart. Cache TTL on the n8n side is bounded (per §4.4) so rotation propagates within one workflow run.
Secondary candidates (Telegram Inspection Bot, p24-oauth-token-refresh) are named in §7 but deferred — do not migrate in the same PR. This plan ships ATRAX only; the secondaries follow after the ATRAX cutover produces a full rotation-cycle data point.
1. Why this architecture (recap of Model A vs B)
The issue records the decision. Restated compactly so the constraints stay visible to reviewers:
- Model A (SOPS remains sole runtime source): rotation requires SOPS edit → PR →
secrets-sync.yml→ container restart. This is what leftatrax, kravag-scheduled-fleet-updatesfiring 3942 for ~7+ hours during the 2026-07-12 rotation window — the workflow can’t consume a new value until every step of that chain completes. - Model B (Supabase Vault as runtime source): rotation is one
UPDATE. The trade-off is that Supabase becomes a hard runtime dependency for the reading workflow.
The chosen scope makes Model B’s trade-off free: the ATRAX workflows already write their results into Supabase. If Supabase is unavailable they are dead regardless. Adding Vault reads on the same connection introduces zero new failure mode.
Model B is not adopted for non-Supabase-coupled workflows — that would add a new Supabase runtime dependency to workflows that don’t have one today. The W4 ingestion workflow (MongoDB-coupled, path via #3826) is the canonical excluded case.
2. Current state — facts on the ground
2.1 SOPS ATRAX inventory (issue-body claim corrected)
The issue body says ATRAX_* “lives only in SOPS (secrets/n8n-bms4.env.sops)”. That is incorrect. Both SOPS files below carry the full 7-key ATRAX block:
| SOPS file | ATRAX keys present | Deployed to | Consumer |
|---|---|---|---|
secrets/n8n-bms4.env.sops | ATRAX_USERNAME, ATRAX_PASSWORD, ATRAX_CLIENT_ID, ATRAX_CLIENT_SECRET, ATRAX_AUTH_STRING, ATRAX_SCOPE, ATRAX_WEBAPP_INSTANCE_ID | bms-4 /opt/p24-infra/bms-4/.env | n8n container (docker-compose) |
secrets/monitoring.env.sops | same 7 keys | vps-i1 /opt/p24-infra/monitoring/.env | (grep confirms no ATRAX exporter consumes them — see §2.2) |
secrets/vps-h1.env.sops | none | vps-h1 | n/a — issue-body claim of a 3rd file was wrong |
Verification method: grep -l '^ATRAX_' secrets/*.env.sops — SOPS-encrypted dotenv keeps key names in cleartext. Do not decrypt.
Migration must therefore remove from both files, not one. If the removal PR touches only n8n-bms4.env.sops, the monitoring copy remains — nothing consumes it, but it drifts and shows up in future audits.
2.2 monitoring.env.sops ATRAX keys — why they exist, whether they can go
The keys were copied into monitoring.env.sops during the 2026-07-08 credential-sync episode referenced in the rotation log for #3178. No exporter or monitoring service currently reads them (verified by grepping monitoring/exporters/* and monitoring/docker-compose.yml for ATRAX_). They are dead copies and can be removed together with the migration — no additional consumer analysis needed.
2.3 Workflows that read ATRAX today (from issue-body live inventory)
| Workflow | ID | Env-var keys read via $env |
|---|---|---|
atrax, kravag-scheduled-fleet-updates | CCx9UMdphmGficDX | ATRAX_USERNAME, ATRAX_PASSWORD, ATRAX_CLIENT_ID, ATRAX_CLIENT_SECRET |
atrax-drivers-daily-snapshot | akgnvueDkDPbLJ49 | ATRAX_USERNAME, ATRAX_PASSWORD |
atrax-ecodriving-daily | zdzyIQ8UBsDAF0wI | ATRAX_USERNAME, ATRAX_PASSWORD |
fleet-update-v2-batch and atrax-daily-stats-collector are Supabase-coupled but sourced their token upstream — they do not read $env for ATRAX creds and are not in scope for the Vault rewrite.
The two remaining keys (ATRAX_AUTH_STRING, ATRAX_SCOPE, ATRAX_WEBAPP_INSTANCE_ID) are not read by these workflows per the enumeration. Removal §5.4 handles them as dead keys, not as migration targets.
2.4 Prior art — Vault runtime lookup exists
audit-engine/vault.py already reads vault.decrypted_secrets in production (db.get_client().schema("vault").table("decrypted_secrets").select("*").eq("name", …)). This confirms:
- The Vault schema is exposed to the service_role over PostgREST.
- Read latency is acceptable for a per-request lookup (audit-engine caches with
functools.lru_cache, so runtime perf on n8n side needs its own caching — see §4.4). - No new Supabase feature enablement is needed on the project side.
2.5 Prior art — n8n credential sync pipeline
bms-4/sync-n8n-credentials.py already reconciles n8n credentials with values in /opt/p24-infra/bms-4/.env on a scheduled basis. This plan intentionally does NOT touch that pipeline — the Vault path bypasses n8n credentials entirely by reading from a Postgres node inside the workflow. The sync-n8n-credentials.py change scope is: remove ATRAX_* from its credential map so it doesn’t try to reconcile keys that no longer exist in .env (§5.3).
2.6 Prior art — SOPS key removal pattern
Confirmed pattern from PR #2162 (WAHA decommission, 2026-06-30) and commit 27cd9cc (Twilio/OneSignal/Jabber removal):
- Remove the key line from the plaintext
.envinside SOPS via SOPS edit. - Canary-decrypt to verify integrity.
- PR to
main—secrets-sync.ymldeploys on merge. - Post-merge health check that the target container no longer sees the env var.
Reused verbatim in §5.4.
3. Target design — components
3.1 Least-privilege Postgres role
Create a dedicated Postgres role in the Supabase project that can read only the ATRAX secret rows in vault.decrypted_secrets. service_role is not reused for this — the whole point of the design is that this role can decrypt the ATRAX secrets and nothing else.
-- Migration YYYYMMDDHHMMSS_n8n_vault_atrax_role.sql
CREATE ROLE svc_n8n_vault_atrax LOGIN PASSWORD :'pw';
-- Access to the decrypted-secrets view is required (Vault ships with a security-definer view).
-- Grant SELECT on the view itself, then rely on row-level policy for row scoping.
GRANT USAGE ON SCHEMA vault TO svc_n8n_vault_atrax;
GRANT SELECT ON vault.decrypted_secrets TO svc_n8n_vault_atrax;
-- Row-level scope: only ATRAX_* rows. Use a policy on the view's underlying table,
-- because vault.decrypted_secrets is a view — RLS on the view is inherited from vault.secrets.
ALTER TABLE vault.secrets ENABLE ROW LEVEL SECURITY; -- idempotent; already enabled by pgsodium default
CREATE POLICY svc_n8n_vault_atrax_read
ON vault.secrets FOR SELECT
TO svc_n8n_vault_atrax
USING (name LIKE 'ATRAX\_%' ESCAPE '\');Confirm the policy on a test row (ATRAX_USERNAME) before inserting production values. The policy uses a LIKE 'ATRAX\_%' predicate — future ATRAX keys are covered automatically; secondary migrations (Telegram bot, LinkedIn — §7) will need their own policy row, not a widened predicate.
Why not service_role: the whole design point is that a Vault reader with service_role bypasses RLS everywhere and has no less privilege than the current SUPABASE_SERVICE_ROLE_KEY in SOPS. That would be a rename, not a security improvement.
Why a policy on vault.secrets rather than vault.decrypted_secrets: the view is SECURITY DEFINER in pgsodium’s default install and does not honour policies on itself; policies must be on the underlying table. Verify the pgsodium extension version on the project first (§6.2 §pre-flight checklist item 1) — if the view semantics differ from what this plan assumes, the policy target may need to move to the view directly and the security-definer bit adjusted.
3.2 Vault secret rows
Insert (not upsert — the initial value comes from human rotation input, not from the current SOPS values which are stale per #4021):
-- After the human-provided fresh credentials are pasted into a session-only variable,
-- the secret-manager role inserts:
SELECT vault.create_secret('<new_value>', 'ATRAX_USERNAME', 'ATRAX portal login for the n8n atrax-cluster workflows');
SELECT vault.create_secret('<new_value>', 'ATRAX_PASSWORD', 'ATRAX portal password (Tier 3 manual rotation)');
SELECT vault.create_secret('<new_value>', 'ATRAX_CLIENT_ID', 'ATRAX OAuth client id');
SELECT vault.create_secret('<new_value>', 'ATRAX_CLIENT_SECRET', 'ATRAX OAuth client secret');These 4 keys match the actual $env reads in §2.3. The 3 dead keys (ATRAX_AUTH_STRING, ATRAX_SCOPE, ATRAX_WEBAPP_INSTANCE_ID) are not inserted into Vault — they are deleted from SOPS in §5.4 as unused.
Rotation, going forward:
UPDATE vault.secrets SET secret = '<new_value>', updated_at = NOW()
WHERE name = 'ATRAX_PASSWORD';Executed by the secret-manager role (not svc_n8n_vault_atrax — it has SELECT only). A ~one-line docs/playbooks/n8n-vault-secret-rotation.md documents this in §5.5.
3.3 Bootstrap credential
n8n needs to authenticate to Postgres before it can read Vault. That connection string stays in SOPS. Add one new key to secrets/n8n-bms4.env.sops:
ATRAX_VAULT_PG_CONN=postgresql://svc_n8n_vault_atrax:<pw>@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=require
Port 6543 is the pooler (session-mode); use 5432 for direct connection if pooler compatibility with pgsodium is confirmed problematic (§6 open questions). No new SOPS file needed — key is added to the file that already carries n8n bms-4 secrets.
Deliberately not: reusing an existing SUPABASE_SERVICE_ROLE_KEY or SUPABASE_DB_URL from n8n-bms4.env.sops. Those are broader-scope credentials; this is the whole point of a dedicated role.
3.4 n8n node — the Vault read
Each ATRAX workflow gets a single Postgres node inserted immediately before the get_token node. Node config:
- Connection: new n8n credential
atrax-vault-pgpopulated fromATRAX_VAULT_PG_CONNbysync-n8n-credentials.py(see §5.3 for the sync change). - Query:
SELECT name, decrypted_secret FROM vault.decrypted_secrets WHERE name IN ('ATRAX_USERNAME','ATRAX_PASSWORD','ATRAX_CLIENT_ID','ATRAX_CLIENT_SECRET'); - Post-processing: a
Setnode (orFunctionnode) reshapes the 4-row result into{atrax_username, atrax_password, atrax_client_id, atrax_client_secret}for downstreamget_token. - Downstream nodes switch from
{{ $env.ATRAX_USERNAME }}to{{ $node["Read ATRAX Vault"].json.atrax_username }}(and so on for the other 3).
For workflows that only need 2 keys (atrax-drivers-daily-snapshot, atrax-ecodriving-daily), the WHERE name IN (…) list is trimmed to just those 2 — no need for the other 2 to hit RLS.
3.5 What does NOT change
sync-n8n-supabase-creds.py(script that reconciles thesupabase-service-role-keyn8n credential) — unchanged; the bootstrap key is a new n8n credential (atrax-vault-pg), not the service-role key.- W4 direct API ingestion workflow — excluded per issue-body decision; not Supabase-coupled.
- Telegram bot, LinkedIn refresh workflows — deferred (see §7).
- audit-engine
vault.py— unchanged; different Vault reader, different code path.
4. Sequence of change — files touched
Grouped by phase. Each phase corresponds to one PR unless flagged.
4.1 Phase 1 — DB role + Vault seed (secret-manager role work)
PR: none from this dev-issue worker. Delegated to secret-manager per role-boundary matrix (creating DB role + inserting Vault rows requires DB DDL + secret values). This dev-issue plan produces the SQL text and the delegation checklist; the actual DDL is applied by the secret-manager session.
Files produced by this phase (in this repo):
supabase-migrations/YYYYMMDDHHMMSS_n8n_vault_atrax_role.sql— CREATE ROLE + GRANT + POLICY as in §3.1 (password bind param, not literal). Applied viamcp__claude_ai_Supabase__apply_migration.docs/playbooks/n8n-vault-secret-rotation.md— the rotation ops doc (§5.5).
No change to n8n yet; no removal from SOPS yet.
4.2 Phase 2 — n8n workflow rewrite (bms-4 side)
PR: yes — small, reversible, ships alongside phase 1’s DB role.
Files touched:
- n8n workflow JSON export (if the workflows are backed up in the repo — verify at implementation time; the
n8n-workflows-exportmentioned indocs/audits/2026-07-02-p24-infra-information-summary.mdmay or may not commit exported JSON to git). If the JSON files are versioned, they are edited in this PR. If they are not, the workflow changes are made through the n8n UI on bms-4 and captured in the ops doc as a manual step. bms-4/sync-n8n-credentials.py— addatrax-vault-pgcredential definition (readsATRAX_VAULT_PG_CONNfrom.env, POSTs the connection string to n8n’s credential API).docs/playbooks/atrax-credential-rotation.md— update to reference the Vault path as primary; keep the SOPS+PR path as the fallback for the 24-h period after cutover.
Deployment order matters: the DB role and Vault rows must exist before the workflow edit is merged, or the modified workflows will fail on first run. Phase 1 lands first (secret-manager PR), Phase 2 second (this dev-issue PR).
4.3 Phase 3 — SOPS removal (secret-manager role work)
PR: delegated to secret-manager. Not part of this dev-issue’s PR.
Files touched:
secrets/n8n-bms4.env.sops— removeATRAX_USERNAME,ATRAX_PASSWORD,ATRAX_CLIENT_ID,ATRAX_CLIENT_SECRET. Also remove the 3 dead keys (ATRAX_AUTH_STRING,ATRAX_SCOPE,ATRAX_WEBAPP_INSTANCE_ID) per §2.3.secrets/monitoring.env.sops— remove the same 7 keys (per §2.1 correction to the issue body).
Gate: 3 successful production runs of each ATRAX workflow after Phase 2 lands. See §5.4 for the runbook.
4.4 n8n cache TTL for Vault reads
n8n does not have a first-class read-through cache for Postgres queries — every workflow run issues its own SELECT. That is fine for these workflows (2/day + 1 scheduled trigger), and matches the audit-engine pattern (per-request lookup, warm within the process). No cache layer is introduced.
If future workflows execute at a higher rate and query cost becomes measurable, the Set node output could be reused across the same execution via $node["Read ATRAX Vault"].json, and cache-across-executions would need a separate design — out of scope here.
Rotation propagation window: worst case = the interval to the next scheduled workflow run (currently hourly for atrax, kravag-scheduled-fleet-updates per its cron; verify at implementation time). This is bounded and acceptable — the ATRAX portal itself does not honour more than one credential simultaneously, so any window shorter than “next workflow run” is moot.
5. Runbook — how the migration is actually executed
Written for the two roles that execute it: secret-manager (Phases 1, 3) and dev-coder (Phase 2, this PR).
5.1 Pre-flight (dev-coder — do this first)
- Confirm the pgsodium version on the Supabase project supports
vault.create_secret()andvault.decrypted_secrets(audit-engine already reads from it; if it works there, it works here). One-off query:SELECT extname, extversion FROM pg_extension WHERE extname = 'pgsodium'; - Confirm the current n8n container on bms-4 can reach
*.pooler.supabase.com:6543(already true — same host is used byfleet-update-v2-batch’s existing Supabase credential). - Confirm no other consumer reads
ATRAX_*frommonitoring.env.sops— grepmonitoring/once more at implementation time.
5.2 Phase 1 — DB role (secret-manager)
- Apply migration
YYYYMMDDHHMMSS_n8n_vault_atrax_role.sql. The password is provided as a bind param and stored only in the secret-manager’s session. INSERTthe 4 Vault rows (§3.2) using freshly-rotated ATRAX credentials — this Vault insert is the credential rotation for #1713 / #4021 that has been pending, done in one step.- Add
ATRAX_VAULT_PG_CONNtosecrets/n8n-bms4.env.sopsvia SOPS edit (Windows CRLF/BOM guard per CLAUDE.md). - Merge secret-manager PR →
secrets-sync.ymldeploys the new key to bms-4.env. - Verify from bms-4 shell:
psql "$ATRAX_VAULT_PG_CONN" -c "SELECT name FROM vault.decrypted_secrets WHERE name LIKE 'ATRAX\_%'"returns 4 rows.
5.3 Phase 2 — n8n workflows (this PR’s dev-coder work)
- Update
bms-4/sync-n8n-credentials.py: add anatrax-vault-pgentry that readsATRAX_VAULT_PG_CONNfrom.envand POSTs apostgrescredential to n8n. - Run
sync-n8n-credentials.pyon bms-4 (or wait for its scheduled run) — the credential appears in n8n’s UI. - Edit each of the 3 workflows (via n8n UI unless JSON is versioned in-repo): insert
Read ATRAX VaultPostgres node +Setreshape node beforeget_token. Repoint downstream expressions from$env.ATRAX_*to the node output. - Test each workflow off-schedule via its webhook trigger. Assert:
- Postgres node returns 4 (or 2) rows.
get_tokennode returns a 200 with a bearer token.- Downstream Supabase write succeeds (i.e. the migration path does not regress functionality).
- Update
docs/playbooks/atrax-credential-rotation.md— rotation is now a VaultUPDATE; the SOPS multi-file dance is legacy. - Merge this PR.
5.4 Phase 3 — SOPS cleanup (secret-manager, T + 3 successful runs)
Gate: 3 successful production runs of each of the 3 workflows on the Vault path (≈ 3 hours for atrax, kravag-scheduled-fleet-updates; ≈ 3 days for the daily ones — start the count from the last one). No fallback SQL touches n8n.
- Confirm no other repo file references
ATRAX_USERNAME,ATRAX_PASSWORD,ATRAX_CLIENT_ID,ATRAX_CLIENT_SECRET,ATRAX_AUTH_STRING,ATRAX_SCOPE,ATRAX_WEBAPP_INSTANCE_ID:grep -rn 'ATRAX_' --exclude-dir=.git --exclude='*.env.sops' . - SOPS edit
secrets/n8n-bms4.env.sops— remove all 7 keys. - SOPS edit
secrets/monitoring.env.sops— remove same 7 keys. - Canary-decrypt each file before
git add. - PR to main →
secrets-sync.ymldeploys → bms-4/opt/p24-infra/bms-4/.envand vps-i1/opt/p24-infra/monitoring/.envlose those keys on next container restart. - Health check: SSH bms-4,
grep '^ATRAX_' /opt/p24-infra/bms-4/.envreturns empty;docker exec p24-n8n printenv | grep '^ATRAX_'returns empty after next n8n restart.
5.5 New playbook — docs/playbooks/n8n-vault-secret-rotation.md
Short doc (≈ 60 lines). Sections:
- When to use — a Supabase-coupled n8n workflow reads a secret that needs rotating.
- Rotation flow — one-line
UPDATE vault.secretsviamcp__claude_ai_Supabase__execute_sql(or REST if MCP unavailable), issued by the secret-manager role. - Propagation window — next workflow run picks up the new value; no restart, no PR.
- Verifying — trigger the workflow off-schedule via its webhook and assert token acquisition succeeds.
- When NOT to use — non-Supabase-coupled workflows; those stay on SOPS.
- Fallback — if Vault is unavailable, temporarily re-add the key to SOPS + secrets-sync (this is the escape hatch; the 24-h post-cutover window keeps the SOPS-based path documented).
6. Risks & unknowns
6.1 Correctness risks
| Risk | Mitigation |
|---|---|
pgsodium view semantics differ across extension versions — vault.decrypted_secrets may or may not honour RLS on the underlying table depending on version. | §5.1 pre-flight query; if the policy target must change, adjust §3.1 before Phase 1. Failure mode is loud (SELECT returns 0 rows or the wrong rows) — caught in §5.2 step 5 before any workflow edit lands. |
| n8n Postgres node connection pooling with pgsodium — Supavisor session mode is safest; transaction mode may reset the session state pgsodium relies on. | Use pooler port 6543 (session mode). If the SELECT is unstable, fall back to direct port 5432. Both are on the same host; no SOPS churn to switch. |
| Rotation-propagation window is longer than “container restart” (worst case = 1 hour for the hourly workflow). | Accepted trade-off — the ATRAX portal itself does not honour concurrent credentials, so a shorter window has no operational value. Documented in §5.5. |
6.2 Operational risks
| Risk | Mitigation |
|---|---|
| Someone rotates the SOPS ATRAX keys after cutover instead of the Vault row, and can’t understand why the workflow keeps using the old value. | Update docs/playbooks/atrax-credential-rotation.md in Phase 2 (before SOPS removal in Phase 3) to make Vault-first primary. After Phase 3, the SOPS keys are gone entirely — no dual-source ambiguity. |
| The Vault path is unavailable (Supabase incident) at the same moment the ATRAX portal rotates the credential. | Restore path is: SOPS edit + secrets-sync + container restart, i.e. what we have today. The escape hatch is documented in §5.5. |
A future ATRAX-adjacent secret (e.g. ATRAX_NEW_KEY) is added to Vault without going through this plan and lands in a wider policy. | The RLS predicate is name LIKE 'ATRAX\_%' (§3.1) — any new ATRAX_* key is automatically covered by the same role. This is deliberate: same trust boundary, same 3 workflows. For secondary migrations (non-ATRAX), Phase 7 requires a new role + policy row. |
6.3 Open questions (call out before Phase 1)
- Are the ATRAX workflow JSON files versioned in-repo? If yes, Phase 2 also modifies those files; if no, Phase 2 is a manual n8n UI change captured in the ops doc. Verify at implementation time — grep repo for
CCx9UMdphmGficDX. - Does bms-4’s n8n image include the Postgres node? It’s a first-party n8n node — should be present in every stock build — but confirm before merging by checking that
sync-n8n-credentials.pycan create apostgrescredential. - Is there an existing dev_r_services row for the n8n Vault path that overlaps? Grep confirms no; the plan does not introduce a
dev_r_servicesrow for the Vault lookup itself because it is not a service — it is a Postgres role. The individual secrets (ATRAX_*) already have theirdev_r_servicescredential rows perdocs/infrastructure-standard.md; those rows are updated post-migration to reflect the new source of truth (see §6.4).
6.4 Compliance updates
dev_r_services: for each ofATRAX_USERNAME,ATRAX_PASSWORD,ATRAX_CLIENT_ID,ATRAX_CLIENT_SECRET, update thelocation/notescolumn to referencevault.decrypted_secretsinstead ofsecrets/n8n-bms4.env.sops.rotation_frequnchanged (Tier 3 manual, still bounded by ATRAX portal policy).- Audit-log entry: recording who / when / which key was migrated (per issue-body Migration Plan step 6). This is a single
INSERTinto the audit log, done as part of the secret-manager Phase 1 PR. - No
dev_r_ai_systemschange — this migration does not introduce or modify an AI system. - EU AI Act traceability — no impact; the workflows in scope are automation, not AI-decisioning systems.
7. Deferred — secondary candidates
Named in the issue body’s “Secondary candidates” table. Explicitly out of scope for this PR:
| Workflow | ID | Secret keys | Reason for deferral |
|---|---|---|---|
| Telegram Inspection Bot | fJRm8PGfdVC0sFIJ | ET_SERWIS_PROD_BOT_TOKEN | Same pattern applies but rotation is orthogonal (Telegram BotFather). Migrate after the ATRAX cutover produces a full rotation-cycle data point. |
| p24-oauth-token-refresh | C1o4xIkV9jMv9vhr | LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET | BrandPilot Supabase project, not the main p24-infra Supabase — separate role, separate migration. Deferred with an explicit follow-up issue after ATRAX proves out. |
| resource-incident-analysis | DL0Q9CUdn38vkiqx | SUPABASE_SERVICE_KEY | Chicken-and-egg — the secret IS the Supabase key. Do not migrate; keep in SOPS as the bootstrap. This is a permanent exclusion, not a deferral. |
Each secondary migration is a separate design issue, not a follow-up PR on this one.
8. Success criteria
- The 3 ATRAX workflows execute cleanly on the Vault path for 3 consecutive scheduled runs (no fallback SQL to
$env). -
grep '^ATRAX_' secrets/*.env.sopsreturns 0 lines. -
docker exec p24-n8n printenv | grep '^ATRAX_'on bms-4 returns 0 lines after the post-Phase-3 container restart. -
docs/playbooks/n8n-vault-secret-rotation.mdexists and covers the Vault-first rotation flow. -
docs/playbooks/atrax-credential-rotation.mdis updated to point at the new playbook as primary. -
dev_r_servicesrows for the 4 migrated ATRAX keys reference Vault as source of truth. - Rotation drill: a secret-manager
UPDATE vault.secrets SET secret = '<test_val>' WHERE name = 'ATRAX_USERNAME'propagates to the next workflow run without any container action. (Value reverted after the drill.)
9. Files this PR (the plan-only PR) changes
Only this document is added. No code, no SOPS edit, no schema change is executed by this dev-issue worker. The plan is the deliverable; implementation is queued via follow-up issues (Phase 1 → secret-manager; Phase 2 → dev-coder; Phase 3 → secret-manager).
files_to_change (this PR): docs/plans/plan-4032-n8n-supabase-vault-runtime-secrets.md
File-overlap signature: docs/plans/plan-4032-n8n-supabase-vault-runtime-secrets.md
Plan-only deliverable. Implementation issues to spawn after review.