Gateway authentication (8.3)
Machine auth against Ignition 8.3 /data REST routes is token-only:
- Send the header
X-Ignition-API-Token: <name>:<key>— the FULLname:keystring the gateway UI shows when you create the key (Platform → Security → API Keys). Key-only headers are rejected (401). - Basic auth does not work on 8.3
/dataroutes (verified: valid commissioned admin credentials → 401).ignkeeps the basic-credential profile arm for future/legacy surfaces, warns loudly on stderr each time it is used against the gateway, and never silently retries — create an API token instead. - Token-authenticated mutations need no CSRF machinery (CSRF applies only to cookie/session auth).
Creating a working token: gateway UI → Platform → Security → API Keys →
Create (Basic Token) → pick a security level → uncheck "Require secure
connections" for http:// gateways → copy the full name:key string.
The two rejection codes carry distinct diagnoses:
| Response | Meaning | What to fix |
|---|---|---|
| 401 | token not recognized | the header must be the full name:key string — no name: prefix or wrong key → 401; Basic never works on /data |
| 403 | token recognized but under-permitted | three-part setup: (1) token holds an adequate security level, (2) gateway read/write permissions include that level, (3) "Require secure connections" unchecked for http — ign doctor diagnoses all three |
Credential resolution order
Which env var or store answers for a profile depends on that profile's
declared auth shape — the generic, profile-less env vars no longer win
for every profile:
| Step | Source | Applies to |
|---|---|---|
| 1 | IGNITION_TOKEN_<PROFILE_UP> | EVERY auth shape — the universal explicit override |
| 2 | the profile's own token_env var | TokenEnv profiles |
| 3 | bare IGNITION_TOKEN | ONLY a profile whose auth equals the default — i.e. no auth key in the TOML |
| 4 | keyring entry ignition-cli / profile:<name> | every profile (unconditional — the lookup is already profile-named) |
| 5 | the profile's own user_env/password_env | Basic profiles |
| 6 | bare IGNITION_USER + IGNITION_PASSWORD | a default-auth profile, or a Basic profile whose own named vars are unset |
The generic IGNITION_TOKEN and IGNITION_USER/IGNITION_PASSWORD vars
(steps 3 and 6) apply ONLY to a profile with no auth block. A profile
that declares auth = { keyring = ... } or auth = { token_env = ... }
/ auth = { user_env = ..., password_env = ... } authenticates as
itself — it never falls through to an unrelated shell credential meant
for a different gateway.
IGNITION_TOKEN_<PROFILE_UP> (step 1) is the universal escape hatch: it
still overrides every auth shape, including keyring profiles. The
profile name maps to the var name by uppercasing and turning every
non-alphanumeric character into _ — profile my-rig reads
IGNITION_TOKEN_MY_RIG.
This matters most right after ign adopt, which rewrites the adopted
profile to auth = { keyring = ... }. Before this order existed, a
leftover IGNITION_TOKEN for some OTHER gateway in the developer's
shell would shadow the freshly-stored keyring credential, and
ign --profile <name> status would answer auth_rejected (exit 5)
immediately after a successful adopt. Now a keyring profile with an
empty keyring entry and a foreign IGNITION_TOKEN in the shell refuses
loudly with secret_unavailable (exit 3) instead of silently sending
the wrong token — a diagnosable refusal beats a misleading rejection.
Token-setup troubleshooting (the three-part failure, ign doctor's bread and butter)
A 403 means the gateway RECOGNIZED your token but its security level doesn't satisfy the gateway's permissions. The setup has exactly three parts — all three must hold:
- The token holds an adequate security level. When creating the
key (Platform → Security → API Keys), pick a level the gateway's
permissions already accept — only leaf paths count (granting
Authenticated/Rolesmid-tree logs a WARN and is ignored). - The gateway's read/write permissions include that level. Under
8.3 defaults, Gateway Read/Write Permissions only include
Authenticated/Roles/Administrator— a token granted justAuthenticatedgets 403 (verified live, then fixed by adding the level to the permission lists).ign doctorreads thesecurity-propertiesconfig and shows you the actual wiring; when even that read 403s, the wiring is your culprit. - "Require secure connections" is unchecked when the gateway URL
is
http://(the create dialog CHECKS it by default — the #1 http-rig trap; uncheck it).
ign doctor walks the whole chain — URL/TCP reachability, liveness
(no credential involved, so down-ness is never confused with bad
auth), commissioning, 401-vs-403, the permissions wiring, write
permission, WebDev-route presence, and Docker presence — and exits 0
whenever the diagnosis completes so agents can parse checks[] from a
broken setup without a nonzero exit getting in the way.
Gateway adoption (ign adopt)
Doctor diagnoses the three-part token setup; adopt fixes it. One idempotent, profile-scoped verb walks a commissioned-but-unadopted gateway to a fully working profile:
export IGNITION_PASSWORD='<gateway admin password>'
ign adopt --profile se-slot5-backend --project Flux --testing \
--checkout ~/gateways/slot5 --bake ~/rigs/slot5/restore.gwbk
Five bootstrap steps, each reported as a doctor-shaped row, each skippable when already satisfied — re-running on an adopted gateway is an all-skip no-op:
| Step | What it does |
|---|---|
login | The native OIDC dance (session cookie + CSRF) — the same machinery as ign rig trial reset, the only credential path that can mint the first key |
key | Mints an Administrator-level API key named ign-cli with "Require secure connections" off — idempotent by name over the ignition/api-token resource list |
permissions | Adds the key's level root to the gateway's read/write permission lists (merge-only, never removes) |
probe | gateway-info with the fresh name:key — the live end-to-end proof |
persist | Stores the credential in the OS keyring and rewrites the profile to auth = { keyring = … }; on headless hosts falls back to IGNITION_TOKEN_<PROFILE> and prints the token exactly once |
Composition flags ride the bootstrap after the key is proven working:
--project NAME deploys the CLI's WebDev routes (scriptExec on, the
existing secret lifecycle), --project … --testing additionally ships
the embedded Jython test framework + testing/run/testing/tags
routes and asserts the suite is live before reporting green
(discover ≥1 module + a passing smoke run — the empty-suite trap;
a freshly deployed route's first-touch lazy-compile 500 is retried
once), --checkout DIR lands every enabled project at
DIR/<project> (scripts decoded, existing targets skip), and
--bake FILE saves a roaming gwbk after everything landed — a
gateway reset restored from it keeps the key, the wiring, and the
routes (live-proven on 8.3.6).
Flags: --user NAME (default admin), --key-name NAME (default
ign-cli), --level PATH (default Authenticated/Roles/Administrator).
The password comes from IGNITION_PASSWORD env-only (missing → exit
3, rejected → exit 5). Unlike doctor, adopt mutates: a failed
step is a hard error through the normal exit-code taxonomy, never a
skip-ahead table. A same-name key that is disabled or
secure-connections-only is a hard error (its plaintext is
unrecoverable — delete it and re-run).
--testing only smoke-checks the bundle it deploys (discover ≥1
module + a green testing.__tests__ sentinel run). Running the suite
itself is ign testing run --project NAME — the
standalone verb over the same testing/run route, with --discover,
--module/--package selection, --format junit|text, and the
verdict in the exit code (0 green, 6 red with the full results still
in the envelope).
Wire shapes, pitfalls, and the fresh-gateway verification log live in
.planning/research/ADOPT-RESEARCH.md.
Live-gateway verification (opt-in)
An #[ignore]-gated suite (crates/ignition-core/tests/live_gateway.rs)
runs read-only checks against a real commissioned gateway when
IGNITION_LIVE_URL (and optionally IGNITION_LIVE_TOKEN) are set:
cargo test -p ignition-core --test live_gateway -- --ignored
With no envs set the suite skips cleanly (green no-op). The file header carries the one-command Docker rig recipe for reproducing a test gateway.
A second #[ignore]-gated suite
(crates/ignition-core/tests/live_rig_module_injection.rs) spins a real
stock Ignition container and is the only proof that a declared module is
mounted, loaded, undeclared and finally uninstalled from the gateway.
It needs Docker in Linux-container mode and network egress to GitHub, and it
is destructive by design — the uninstall it performs cannot be undone within
a run:
IGNITION_LIVE_RIG_MODULES=1 \
cargo test -p ignition-core --test live_rig_module_injection -- --ignored --nocapture
# and again on a second pinned 8.3.x, because the uninstall contract was
# probed live on 8.3.3 only
IGNITION_LIVE_RIG_MODULES=1 IGNITION_LIVE_RIG_IMAGE_TAG=8.3.9 \
cargo test -p ignition-core --test live_rig_module_injection -- --ignored --nocapture
With IGNITION_LIVE_RIG_MODULES unset the suite skips cleanly, so CI and any
machine without a Docker daemon are unaffected. IGNITION_LIVE_RIG_IMAGE_TAG
defaults to 8.3.3; unset reproduces the default run exactly.