Design — Kravag/ATRAX n8n fleet-update runtime session, not a replayed cookie
Issue: #4875 · Status: Design (proposal — no implementation) · Role: dev-coder (proposal); live n8n changes are sys-admin/infra-task
Related: #4691 (scrub finding) · #4694 (static-key → credential migration — non-goal here) · #4699 (accept-vs-fix for body-plaintext creds) · #1713 (ATRAX_PASSWORD stale) · #4059 (re-enable guard) · #4032 (Vault-for-static-secret — distinct problem)
TL;DR — The premise “build a runtime login step to obtain a fresh session cookie” rests on a misdiagnosis. These 4 workflows already perform a runtime login — an OAuth2 password-grant POST to
https://tronik.atrax4.com/oauth/token(theget_tokennode) that returns a fresh Bearer token every scheduled run. Every downstream API node already authenticates with that runtime token (Authorization: Bearer {{ … }}). TheCookie:header sitting alongside the Bearer on those same nodes is a redundant browser-capture artifact — anember_simple_auth-sessionvalue that is a client-side serialization of the very token the API already receives in theAuthorizationheader. No node authenticates on the cookie alone. The correct fix is therefore to delete the cookie header (Option A), not to build a cookie-refresh flow. That eliminates the staleness class entirely — you cannot have a stale cookie if there is no cookie.
1. Survey — what the login flow actually is
All four workflows share the same node shapes and target the ATRAX telematics portal
(tronik.atrax4.com — a white-labelled Ember.js SPA; “Kravag” is the fleet/customer, ATRAX is the
platform). Extracted from the committed backups (structure only, no values):
The login node already exists — get_token:
| Field | Value |
|---|---|
| URL | https://tronik.atrax4.com/oauth/token |
| Method | POST |
| Body (form) | grant_type=password, username, password, scope, client_id, client_secret |
| Returns | JSON containing access_token (OAuth2 Bearer) |
This is a standard OAuth2 Resource-Owner Password-Credentials grant. It runs at the start of every scheduled execution and yields a token valid for that run.
Every ATRAX API node consumes that token at runtime, via an n8n expression — not a pasted literal:
Authorization: =Bearer {{ $json.value }} # dev / kravag / copy
Authorization: =Bearer {{ $('get_token').item.json.access_token }} # fleet-update-v2-batch
The Cookie: header is redundant. Auth-vs-cookie map across all four workflows — the load-bearing
observation is the last column: there is no row with a cookie and no Bearer.
| Node class | Host | Auth header | Cookie header | Cookie is the sole credential? |
|---|---|---|---|---|
ATRAX API calls (et-auta-na-placu, rt_vehicles*, exploatacja*, externall_vehicles*, HTTP Request1/autoreportorder, fetch_*) | tronik.atrax4.com / api.atrax4.com | Bearer {{ get_token… }} (runtime) | present | No |
get_token (the login endpoint itself) | tronik.atrax4.com/oauth/token | grant body | sometimes present | No — a login endpoint cannot need a pre-existing session |
| Supabase nodes | …supabase.co | static service-role key (#4694 scope) | absent | n/a — non-goal |
Two facts prove the cookie is capture ballast, not a credential:
get_tokenitself carries a cookie in some copies. You do not need a valid session to log in; a session cookie on the login request is definitionally inert. It is there because the whole node — including the fullsec-ch-ua,user-agent,priority,pragmabrowser-header set — was pasted from a Chrome DevTools “Copy as fetch”. The cookie rode along with the rest of the capture.ember_simple_auth-sessionis a client-side store. Ember Simple Auth’s OAuth2 password authenticator keeps the session (which contains theaccess_token) in a browser cookie so the SPA can rehydrate after reload. The SPA then sendsAuthorization: Bearer <access_token>on API calls. Server-side, the ATRAX API authorizes on that Bearer header — the same tokenget_tokenalready fetches. The cookie is a second, staler copy of the credential the request is already sending.
Conclusion: the workflows do not lack a runtime login. They have one, and it works (when
ATRAX_PASSWORD is current — see §4/#1713). What they carry in addition is a dead cookie.
2. Proposed approach
Option A — Delete the Cookie: header (recommended)
Remove the cookie header parameter from every ATRAX HTTP node in all four workflows. Nothing else
changes: the existing get_token → Bearer {{ … }} chain already supplies fresh auth per run.
- Staleness: eliminated by construction. No cookie ⇒ no cookie expiry. The only remaining
time-limited artifact is
ATRAX_PASSWORD, which is a static rotated secret already owned by #1713 — a different problem with a different fix, exactly as the issue notes. - New credentials: none. No new secret, no new sub-workflow, no new node.
- Blast radius: minimal. Only the ATRAX nodes lose a header they were already ignoring server-side.
- Companion cleanup, not part of this fix: the exported backups self-heal — the next
n8n-backup.yml/n8n-workflow-snapshot.ymlrun re-exports the (now cookie-free) live workflows and the scrub gate reportsclean. Seedocs/playbooks/n8n/n8n-backup-secret-scrubbing.md.
Mandatory one-time verification before bulk removal (this is the single assumption Option A rests
on — that the API accepts Bearer-only). During implementation, on the atrax-fleet-updates-dev
workflow only, issue one authenticated ATRAX API call (e.g. et-auta-na-placu) with the cookie header
removed but the runtime Bearer intact, and confirm HTTP 200 + expected payload. If green → roll the
removal across the other three. This verification needs a current ATRAX_PASSWORD (see §4), so it
is naturally sequenced after the #1713 rotation and before the #4059 re-enable.
Option B — Contingency: reconstruct the ember cookie from the token (only if A’s verification fails)
If — against the analysis above — the verification shows the ATRAX API genuinely rejects a Bearer-only
request and requires the ember_simple_auth-session cookie, do not paste a fresh browser cookie
(that just re-creates today’s staleness). Instead build it deterministically from get_token’s output:
- A
Codenode afterget_tokenserializes the ember session object ({"authenticated":{"authenticator":"authenticator:oauth2-password-grant","access_token": <token>, "token_type":"Bearer", …}}), URL-encodes it, and emits it as a per-execution value. - Downstream nodes set
Cookie: ember_simple_auth-session={{ $('build_cookie').item.json.cookie }}— a runtime expression, never a persisted literal, never committed.
This keeps everything inside n8n, needs no browser automation, and — like Option A — refreshes every run from the same OAuth token, so it is also staleness-free.
Option C — Headless-browser login (rejected)
Driving a real Ember login with Playwright to capture the server Set-Cookie is possible but is
strictly worse: it needs a Playwright-weight runner, is brittle against portal UI changes, and produces
nothing the OAuth grant does not already give us. Recorded here only to show it was considered.
Recommendation: Option A, with Option B held as a documented fallback gated on the §2 verification.
3. Session lifetime, expiry, and login-failure handling
- Token TTL ≫ run duration.
get_tokenruns at the start of each scheduled execution and the run completes in minutes; the OAuth token comfortably outlives a single run, so intra-run expiry is not a concern for the per-run workflows.fleet-update-v2-batchalready references$('get_token').item.json.access_tokenper node, so even its batched nodes read one consistent token. If a future long batch ever risks crossing token TTL, re-invokeget_tokenper batch iteration rather than caching — cheap and stateless. - Login failure mid-run = existing behaviour, keep it. A failed
get_tokenalready surfaces via theerror_flowDiscord alert (P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL) — this is the standard repo error path and is how #1713’sinvalid_grantspam was first noticed. No new alerting is needed; Option A removes a header and does not touch the error path. - Retry policy (recommendation, not a change of scope): set
get_tokentoretryOnFailwith a short backoff for transient failures (network / 5xx) only. Do not auto-retry a400 invalid_grant— that is a stale-credential signal (#1713) that retrying cannot fix and only amplifies the alert spam that got these two workflows disabled in the first place.
4. Credentials the login step needs, and where they live
The login (get_token) needs: username, ATRAX_PASSWORD, scope, client_id, client_secret.
- SOPS home:
secrets/n8n-bms4.env.sops(per CLAUDE.md §SOPS file map — n8n/bms-4 secrets). The canonical key isATRAX_PASSWORD; its staleness is tracked by #1713 (rotation is a secret-manager action, never dev-issue). - Option A adds no new credential. Option B adds none either (it reuses the same token). Option C
would reuse the same username/
ATRAX_PASSWORD, still no new secret. - ⚠️ Separately-tracked exposure, flagged by key NAME only, explicitly out of scope here: in the
current live/committed workflows the
get_tokenrequest body carries the ATRAX login credentials (ATRAX_PASSWORD,client_secret,username) as plaintext literals, not{{ $env.… }}expressions. That is the “request-body plaintext Atrax credential set” already recorded indocs/priorities.mdand owned by #4699 (accept-vs-fix decision) and #4691/#4694 (credential migration). Migrating those body literals to n8n env/credential references is the right next cleanup and pairs naturally with this work, but it is a **non-goal for 4875 (see §5) — this issue removes a redundant cookie; it does not rotate or migrate the login credential.
5. Non-goals (explicit)
- No implementation in this issue. Proposal only, to be reviewed via
/review-planbefore any n8n/workflow change is made. - **Do not touch the static-key nodes covered by 4694 — the Supabase service-role-key nodes carry no cookie and are outside this scope.
- Do not rotate or migrate
ATRAX_PASSWORDor theget_tokenbody credentials here — that is #1713 (rotation) and 4694 (body-literal migration), both credential work for secret-manager. - Do not re-enable the disabled workflows here —
atrax_kravag-scheduled-fleet-updates(CCx9UMdphmGficDX) andfleet-update-v2-batch(AJ1px9uHIfbsriof) stay disabled until the #1713 rotation lands; #4059 is the re-enable guard. This design should land before re-enable so the cookie is gone before the workflows fire again.
6. Ownership / who implements
This is a design (dev-coder). Implementation edits the live n8n workflows on bms-4, which is a
sys-admin/infra-task operation (n8n API access), following
docs/playbooks/n8n/n8n-backup-secret-scrubbing.md §“Responding to a finding”. The committed JSON backups
are a structure backup and self-heal on the next nightly export — they are not edited by hand. The
authoritative list of exact sites to change is the live scrub report / #4699 categorized list; the
committed-backup snapshot may lag the live workflows, so implementation counts sites from the live
instance, not from git.
Design authored by dev-coder worker for #4875. No values displayed; credential references are by key name only.