The broker ships with authentication off. JWT_ENABLED defaults to false, and
with it off the middleware stamps an empty identity into every request and calls the
handler: no header is read, nothing is rejected. Turning it on installs a real
per-request validator that verifies a signature, checks the temporal claims, and
compares the token’s roles against the level the route requires.
Everything on this page is one middleware and one route table. There is no per-queue authorization, no tenant binding, and no session state.
Turn it on
-
Decide the algorithm family. Symmetric (
HS256/HS384/HS512) needs one shared secret and is the default. Asymmetric (RS256/RS384/RS512/EdDSA) lets an identity provider hold the private key. -
Provide the matching key material:
JWT_SECRETfor HS, orJWT_PUBLIC_KEY(a PEM) orJWT_JWKS_URLfor RS/EdDSA. -
Set
JWT_ENABLED=trueand restart. Configuration is read once at boot; there is no reload. -
Read the boot log.
JWT auth ENABLEDcarries the resolved algorithm and the skip list. With a JWKS URL you also getJWKS pre-fetch OKand a key count.
JWT_ENABLED=true JWT_ALGORITHM=HS256 JWT_SECRET=<32+ random bytes> queenIf the algorithm and the key material disagree, the broker does not start. See boot-time validation below.
Algorithms and auto-detection
JWT_ALGORITHM (default HS256) does not name the algorithm the broker uses. It
names the set of algorithms an incoming token is allowed to declare in its header.
The broker then dispatches on the token’s own alg.
JWT_ALGORITHM |
Token alg values accepted |
|---|---|
auto |
HS256, HS384, HS512, RS256, RS384, RS512, EdDSA |
HS256 |
HS256 |
HS384 |
HS384 |
HS512 |
HS512 |
RS256 |
RS256, RS384, RS512 |
RS384 |
RS384 |
RS512 |
RS512 |
EdDSA |
EdDSA |
Two rows are worth reading twice. RS256 is the widest of the RSA settings (it
accepts RS384 and RS512 too), while RS384 and RS512 are exact. And auto
accepts both families at once, so with auto a deployment holding both an HS
secret and an RSA public key will verify a token signed with either. Pin the
algorithm explicitly unless you actually need the token to choose.
Anything outside the accepted set is rejected with 401 Unsupported token algorithm
before any key lookup happens.
Where the verification key comes from
The key is chosen per token, from the token’s alg:
- HS256/384/512:
JWT_SECRET, used as raw bytes. An empty secret is a500 HS secret not configured, not a401: the request was well-formed, the server is not. - RS256/384/512:
JWT_PUBLIC_KEYif it is set, parsed as an RSA PEM. A PEM that will not parse is a500 invalid RSA public key PEM. IfJWT_PUBLIC_KEYis empty, the JWKS cache is consulted bykid. - EdDSA: the same, with
JWT_PUBLIC_KEYparsed as an Ed25519 PEM (500 invalid Ed public key PEMon failure), otherwise the JWKS cache.
A static PEM always wins over JWKS. There is no fallback from a bad PEM to the JWKS endpoint. Fix the PEM or unset it.
A JWKS endpoint
Set JWT_JWKS_URL and leave JWT_PUBLIC_KEY empty. The broker pre-fetches the
document at boot and keeps the keys in a cache keyed by kid.
| Variable | Default | Effect |
|---|---|---|
JWT_JWKS_URL |
(empty) | The endpoint. Fetched with a plain GET, no redirects followed |
JWT_JWKS_REFRESH_INTERVAL |
3600 |
Seconds between background refreshes |
JWT_JWKS_TIMEOUT_MS |
5000 |
Per-fetch timeout |
Behaviour that matters in production:
- Refresh on an unknown
kid. A token whosekidis not cached triggers one re-fetch, throttled to at most once every 5 seconds, then a second lookup. A key rotation is therefore picked up without a restart and without a fetch stampede under load. If thekidis still unknown afterwards, the answer is401 Unknown key ID. - An empty
kidmatches any cached key. A token with nokid, or a JWK set with unnamed keys, falls back to the first key in the cache. That is convenient for a single-key set and imprecise for a rotating one. Publishkidon both sides. - A failed pre-fetch is not fatal. Boot logs
JWKS pre-fetch failed (will retry on demand)and carries on, so the broker starts even if the identity provider is briefly down. The first token then pays the fetch. - Only two key types are read.
RSA(from then/ecomponents) andOKPwithcrv: Ed25519(from the raw 32-bytex). Every other entry in the JWK set is skipped silently, so a document that looks populated can yield zero usable keys. Check the key count in the boot log. - The body is capped at 1 MiB and the response may be
Content-Lengthd or chunked. HTTPS chains are verified against the root set bundled into the binary. http://is accepted and warned about. Boot logs that signing keys fetched over cleartext are MITM-forgeable. It is a warning, not a refusal; treat it as an error in your own review.
Claim validation
Signature verification is delegated to the JWT library; every other check is the broker’s own, because the semantics have to match the older implementation exactly.
| Claim | Rule |
|---|---|
exp |
Optional. If present, rejected once now > exp + JWT_CLOCK_SKEW |
nbf |
If present, rejected while now + skew < nbf |
iat |
If present, rejected when iat > now + skew |
iss |
Checked only when JWT_ISSUER is non-empty; must match exactly |
aud |
Checked only when JWT_AUDIENCE is non-empty; accepts a string or an array, and one match is enough |
sub |
Optional. When present it is stamped as the message’s producerSub |
JWT_CLOCK_SKEW defaults to 30 seconds and applies to all three temporal checks.
sub is the only source of producerSub on a stored message. A client that puts
producerSub in a push body does not get it honoured.
Access levels are a role set, not a ladder
Five levels exist: public, read-only, write-only, read-write, admin. They
are not ordered. The checks are role-set membership tests, which is why the
write-only case behaves the way it does.
| Level | Passed by |
|---|---|
public |
Everyone, token or not |
read-only |
read-only, read-write, admin, but not write-only |
write-only |
write-only, read-write, admin, but not read-only |
read-write |
read-write, admin |
admin |
admin |
So a producer holding only write-only passes POST /api/v1/push and is rejected
with 403 Insufficient permissions on every read and every consume route: pop, ack,
transaction, listings, all of it. That is the intended shape for a pure producer;
it is also the single most common surprise when rolling authentication out.
The route classification is prefix-based:
public:/health,/metrics,/metrics/prometheus,/,/assets/*,/favicon*, plus the three dashboard-identity routes/auth/me,/auth/loginand/auth/logout, public so they can answer the auth question rather than be blocked by it (with JWT on,/auth/mereturns401from its own handler)admin: everything under/api/v1/system/and/internal/, everyDELETE /api/v1/consumer-groups/*, everyDELETE /api/v1/resources/queues/*, and/api/v1/stats/refreshread-only:GETon/status,/api/v1/status*,/api/v1/analytics*,/api/v1/resources/*,/api/v1/messages*,/api/v1/consumer-groups*,/api/v1/dlq*,/api/v1/traces*, plus/streams/v1/state/getwrite-only:POST /api/v1/push, and only thatread-write: everything else, including pop, ack, transaction, lease extension,/configure, seeks, subscription changes,POST /api/v1/traces, message deletes, and any route not listed above
The last line is the important one: an unrecognised path defaults to read-write,
not to public. The per-route table with the level for every registered route is in
the reference.
Remapping claims for an external identity provider
The role names and the claims they live in are all configurable, so a token minted by an existing IdP usually needs no custom claims at all, only a mapping.
| Variable | Default | Meaning |
|---|---|---|
JWT_ROLES_CLAIM |
role |
Claim holding a single role string |
JWT_ROLES_ARRAY_CLAIM |
roles |
Claim holding an array of role strings |
JWT_ROLE_ADMIN |
admin |
The value that means admin |
JWT_ROLE_READ_WRITE |
read-write |
The value that means read-write |
JWT_ROLE_READ_ONLY |
read-only |
The value that means read-only |
JWT_ROLE_WRITE_ONLY |
write-only |
The value that means write-only |
Both claims are read on every token, and a role counts as held if it matches the
single-value claim or appears in the array claim. An IdP that emits
{"scope":"queen.write"} needs two variables and no custom mint:
JWT_ROLES_CLAIM=scopeJWT_ROLE_WRITE_ONLY=queen.writeBoth claim names are looked up as literal top-level keys, not as JSON paths. A role
buried in a nested object (the realm_access.roles shape, for instance) is
invisible to the broker; the issuer has to lift it into a top-level claim.
Skipping paths
JWT_SKIP_PATHS defaults to /health,/metrics/prometheus,/metrics,/. A skip entry
matches a request path exactly; an entry longer than one character that ends in /
also matches as a prefix. So /assets/ skips everything below it, while / skips
only the root document.
Skips are evaluated before the route’s access level, so an entry here overrides the table above.
The token on the wire
The credential is read from Authorization. A Bearer prefix is stripped
case-insensitively, and a bare token with no prefix is also accepted. Errors are the
broker’s standard shape, {"error":"…"} with content-type: application/json:
| Status | Message | Cause |
|---|---|---|
| 401 | Authentication required |
No Authorization header on a non-public route |
| 401 | Bad token header |
The token is not a decodable JWT |
| 401 | Unsupported token algorithm |
alg outside the JWT_ALGORITHM set |
| 401 | Invalid token signature |
Signature check failed |
| 401 | Token has expired / Token not yet valid / Token issued in the future |
Temporal checks, with skew applied |
| 401 | Invalid token issuer / Invalid token audience |
JWT_ISSUER / JWT_AUDIENCE mismatch |
| 401 | Unknown key ID |
No JWKS key for the token’s kid, after one refresh |
| 403 | Insufficient permissions |
Valid token, roles do not satisfy the route |
| 500 | HS secret not configured and the two PEM messages |
Server-side key material problem |
Boot-time validation is fatal
When JWT_ENABLED=true, the configuration is validated before the listener binds
and a failure exits the process with a message naming what is missing:
HS256orautowith none ofJWT_SECRET,JWT_PUBLIC_KEY,JWT_JWKS_URLset.RS256,RS384,RS512orEdDSAwith neitherJWT_PUBLIC_KEYnorJWT_JWKS_URLset.- Any
JWT_ALGORITHMvalue outside the supported list.
This is the one authentication mistake the broker refuses to start with. Everything else (a wrong secret, an unreachable JWKS endpoint, a role mapping that matches nothing) starts cleanly and fails per request, so verify with a real token against a real route before you cut traffic over.
What this does not give you
- No tenant separation. A valid admin token sees every queue in the broker. Separation is the tenancy header plus a gateway: multi-tenant hosting.
- No per-queue permissions. The access level is per route, not per resource.
- No revocation. There is no deny-list; a token is valid until
exp. Keep lifetimes short, and rememberexpis optional to the validator. - No transport security. The token travels in a header over whatever the listener speaks, which is plaintext HTTP. See Trust boundaries.