Every 401 looks identical
A401 returns the same body regardless of cause. Missing header, malformed
credential, unknown prefix, wrong secret, revoked key, expired key, API key
presented in a cookie — all the same response. A test compares them byte for
byte.
That is deliberate. Distinguishing them tells an attacker which part of a
forged credential to fix next, which turns the endpoint into an oracle. The
reason is classified internally for metrics and logs — where it is useful to an
operator and not reachable by the person attacking you.
The consequence for you is that a 401 must be diagnosed from your side. Work
through this list in order.
1. Is the credential in the right place?
Authorization: Bearer twk_…. Not X-API-Key, not a query parameter, not a
cookie.
The most common mechanical mistakes: an unset shell variable expanding to
Bearer with nothing after it, a copied secret carrying a trailing newline,
and a truncated paste. The credential is a fixed shape —
twk_ then 12 characters, then an underscore, then 52 characters, all base32
(A–Z and 2–7). Anything else fails at the parser before a lookup happens.
2. Has the key expired?
Expiry is mandatory — there is no “never expires” key. The default is 90 days from creation and the maximum is 365, so a key issued at the start of a project stops working partway through it, by design. A credential nobody rotates is the problem this prevents. List your keys through a browser session to see expiry dates:3. Has the key been revoked?
Revocation takes effect on the next request, and revoked rows are never deleted, so a revoked key appears in the listing with its revocation recorded. If someone rotated credentials and one deployment was missed, this is what it looks like. There is no un-revoking. Issue a new key.4. Are you sending an API key as a cookie?
A credential beginningtwk_ presented as the trustivan_session cookie is
refused outright, even when it is a genuine, valid, unexpired key.
This surprises people, so it is worth stating why rather than just that. A
cookie is ambient: the browser attaches it to any request to this origin,
including requests a hostile page caused. An API key deliberately carries no
CSRF protection precisely because it is not ambient. Honouring one from a
cookie would create a credential that is both ambient and unprotected — worse
than either mechanism on its own.
In practice this shows up when a client library is configured with a cookie jar
and something writes the key into it, or when a test harness sets both. Send it
in the Authorization header only.
5. Are you using a refresh token at the API?
Only access tokens are accepted at API endpoints. A refresh token presented to/api/v1/... is rejected, so it cannot extend a session past the
access-token lifetime or survive a revocation. Access tokens last 15 minutes by
default; use /api/v1/auth/refresh to obtain a new one. The dashboard and the
NHI Security console do this for you: a request that meets an expired session
renews it once and is repeated, and only a refused renewal sends you to sign in.
6. Are you being rate limited on the auth endpoints?
Sign-in, sign-up and refresh are limited to 10 requests per minute per IP address. A429 here rather than a 401 usually means a script retrying a
failed login in a tight loop, or several developers behind one egress address.
Wait a minute; do not retry harder.
Authenticated requests have a separate, much more generous per-credential
limit, counted against the credential rather than the organisation — so one
runaway job cannot lock out colleagues.
Missing X-CSRF-Token on a cookie session
Symptom. A browser-session request with POST, PATCH, PUT or DELETE
is refused, while GET on the same resource works.
Cookie sessions require a double-submit CSRF token on unsafe methods: the value
of the trustivan_csrf cookie, echoed in an X-CSRF-Token header.
HttpOnly and is not. A hostile page can
cause your browser to send the session cookie but cannot read the CSRF cookie
to construct the matching header.
Bearer requests never need this header, and adding one is harmless but
pointless. If you are writing a machine client and reaching for a CSRF token,
you are probably using a session where an API key would be simpler.
A 403, and how to read it
A403 means your credential is valid and belongs to this tenant, and lacks a
permission. Unlike a 401, it tells you exactly what is missing:
required_scopeis the exact scope name to add.principalisapi_keyoruser, which tells you which credential was actually used. If you expectedapi_keyand seeuser, a cookie jar is authenticating you instead of your key.
The scope you want may not be grantable at all
Three permissions can never be attached to a machine credential:
Requesting one at creation is a
400 naming the scope, not a silently narrowed
key — a key that is quietly less capable than you asked for surfaces as a 403
in a pipeline weeks later.
These actions require a human session. Ask the deployment for the authoritative
grantable list rather than trusting a copy:
A 404 that is really “not your tenant”
Symptom. AGET on an ID you are certain exists returns 404.
“Does not exist” and “exists, but is not yours” return the same status and
the same body, and a test asserts they match exactly.
A 403 would confirm the resource exists and merely belongs to someone else,
which turns a UUID guess into reconnaissance about who else is a customer. So
the ambiguity is the feature, and it applies everywhere — including bulk
operations, which report not_found for another tenant’s finding.
In practice, this usually means one of:
- The ID came from a different environment. Staging IDs do not resolve in production.
- The ID came from a different organisation. A request acts in the
organisation selected in the organisation menu (or with
PUT /api/v1/me/active-org), and an API key in the organisation that issued it — see Tenant isolation. - The resource was deleted.
- The ID is simply wrong — a truncated copy, or an asset ID used where a finding ID was expected.
Where to go next
- Authentication — the credential model in full.
- Credential handling — storage, expiry and revocation.
- Errors — the shape of every error response.

