Skip to main content

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 FULL name:key string 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 /data routes (verified: valid commissioned admin credentials → 401). ign keeps 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:

ResponseMeaningWhat to fix
401token not recognizedthe header must be the full name:key string — no name: prefix or wrong key → 401; Basic never works on /data
403token recognized but under-permittedthree-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:

StepSourceApplies to
1IGNITION_TOKEN_<PROFILE_UP>EVERY auth shape — the universal explicit override
2the profile's own token_env varTokenEnv profiles
3bare IGNITION_TOKENONLY a profile whose auth equals the default — i.e. no auth key in the TOML
4keyring entry ignition-cli / profile:<name>every profile (unconditional — the lookup is already profile-named)
5the profile's own user_env/password_envBasic profiles
6bare IGNITION_USER + IGNITION_PASSWORDa 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:

  1. 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/Roles mid-tree logs a WARN and is ignored).
  2. 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 just Authenticated gets 403 (verified live, then fixed by adding the level to the permission lists). ign doctor reads the security-properties config and shows you the actual wiring; when even that read 403s, the wiring is your culprit.
  3. "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:

StepWhat it does
loginThe native OIDC dance (session cookie + CSRF) — the same machinery as ign rig trial reset, the only credential path that can mint the first key
keyMints an Administrator-level API key named ign-cli with "Require secure connections" off — idempotent by name over the ignition/api-token resource list
permissionsAdds the key's level root to the gateway's read/write permission lists (merge-only, never removes)
probegateway-info with the fresh name:key — the live end-to-end proof
persistStores 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.