docs/security/CLI_TOKEN.md
OmniRoute CLI commands authenticate against the local management API using a
HMAC-SHA256(machine-id, salt) token sent via the x-omniroute-cli-token
request header.
This allows CLI subcommands (omniroute status, omniroute providers, etc.)
to call management endpoints without requiring the user to supply a JWT or
password on every invocation.
getMachineTokenSync() reads the hardware machine ID via node-machine-id
(falls back to an empty string on failure, disabling CLI auth).HMAC-SHA256(machine_id, salt) and returns the full 64-char
hex digest — a deterministic, non-reversible token tied to this machine.x-omniroute-cli-token only when the resolved
destination is an explicit loopback URL (localhost, 127.0.0.0/8, or
loopback IPv6). Requests carrying the token use redirect: error, so a local
redirect cannot forward it to another origin. Remote contexts use scoped
access tokens instead. If derivation is unavailable, the CLI omits the header
and omniroute doctor reports the failure instead of treating an empty token
as valid.src/server/authz/policies/management.ts) recomputes the
expected token with the same salt and compares via timingSafeEqual to
prevent timing-based extraction.| Property | Detail |
|---|---|
| Loopback-only | Accepted only when the server's trusted peer-locality stamp (derived from the real TCP peer address) says loopback. The client-controlled Host header is never trusted for locality. |
| Constant-time compare | crypto.timingSafeEqual prevents timing attacks. |
| Non-reversible | HMAC output cannot recover the machine-id. |
No always-protected bypass | isAlwaysProtectedPath() is evaluated before the CLI token check. /api/shutdown and /api/settings/database always require JWT. |
| Non-exportable | Token is never written to disk or logged. |
Set OMNIROUTE_CLI_SALT to rotate the derived token without code changes.
After rotation, all CLI processes on this machine will use the new token
automatically. Useful after a process-list leak that may have exposed the
previous derived value.
# Persistent rotation (add to shell profile)
export OMNIROUTE_CLI_SALT="my-secret-salt-2026"
# Verify new token is in use
omniroute status
Default salt: omniroute-cli-auth-v1
Before the HMAC format above, the CLI derived its token as
SHA-256(machineId + salt).hex[0..32] (a 32-char prefix) in
bin/cli/utils/cliToken.mjs (getLegacyCliTokenSync in src/lib/machineToken.ts).
For backwards compatibility the server accepts both formats: the verifier builds
expectedTokens = [getMachineTokenSync(), getLegacyCliTokenSync()] and compares the
incoming header against each with timingSafeEqual
(src/server/authz/policies/management.ts and src/lib/middleware/cliTokenAuth.ts).
So a token is valid if it matches either the 64-char HMAC digest or the 32-char
legacy SHA-256 prefix.
Opt-out: set OMNIROUTE_DISABLE_CLI_TOKEN=true (env or .env) to disable the CLI
token mechanism entirely; all access then requires an explicit API key. On multi-user
hosts this is recommended, since machine-id is per-device (not per-user) and another
user on the same host could compute the same token.
| File | Purpose |
|---|---|
src/lib/machineToken.ts | Token derivation (getMachineTokenSync) |
src/server/authz/headers.ts | CLI_TOKEN_HEADER constant |
src/server/authz/policies/management.ts | Server-side verification |
src/server/authz/routeGuard.ts | Loopback host check (isLoopbackHost) |
docs/security/ROUTE_GUARD_TIERS.md — route protection tiersdocs/architecture/AUTHZ_GUIDE.md — full authorization pipeline