internal/authctl: an account JWT whose signer the servers do not list is not pushed or counted as distributed #26

Merged
mikluko merged 3 commits from mikluko/octant/2387-untrusted-operator-key into main 2026-10-05 07:07:08 +00:00
mikluko commented 2026-10-04 18:28:05 +00:00 (Migrated from github.com)

X2387, gap 8 of Q2376.

A full resolver acknowledges and stores a pushed account JWT before anything checks its issuer, and refuses it only when it is applied, so a push under a NATS operator key the servers do not list read as distributed and broke the account at the next server start.

The auth controller now reads each server's trusted NATS operator claims from $SYS.REQ.SERVER.PING.VARZ, alongside the STATSZ roster poll.

no roster server lists the signer    nothing sent, 0 counted, Distributed False / UntrustedSigner
some list it                         sent, counted at most on those, Distributed False / UntrustedSigner
all list it, or trust unknown        as before

Sent in the mixed case because a signing-key rotation rolls servers one by one, and a restarted server needs the JWT under the new key; refusing until all list it would leave accounts down on restarted servers where the old key was dropped.

Rejected: $SYS.REQ.ACCOUNT.<key>.INFO after the push. It shows what a server applied, but only after the untrusted JWT is already on disk, costs a request per account, and loads every account on every server.

Not caught: a server whose VARZ carries no NATS operator claims (configured by trusted_keys), one that does not answer VARZ, and a system user issued before this change, which lacks the VARZ permission until its creds are reissued. Each is taken to trust the key, which is the behaviour before this change. A JWT an older auth controller already pushed stays on the servers' disks; it is now counted 0 and named by the condition.

nats-server v2.15.0, the version go.mod pins:

  • server/accounts.go:4559-4574 — CLAIMS.UPDATE saves and replies jwt updated with no issuer check
  • server/server.go:2211 — verifyAccountClaims refuses an untrusted issuer, on apply
  • server/monitor.go:1294,1743 — VARZ trusted_operators_claim
  • server/reload.go:1619 — trusted operators are restart-only, so a server's keys hold for the life of its server ID

Shared code: PresetAuthController in internal/jwtplane/user.go gains one subject; the push error at account.go:171 and systemaccount.go:121 goes through ignoreUndelivered.

Tests that fail without the check: TestResolvers_UntrustedSigner, TestTakeover_UntrustedSigner.

X2387, gap 8 of Q2376. A full resolver acknowledges and stores a pushed account JWT before anything checks its issuer, and refuses it only when it is applied, so a push under a NATS operator key the servers do not list read as distributed and broke the account at the next server start. The auth controller now reads each server's trusted NATS operator claims from `$SYS.REQ.SERVER.PING.VARZ`, alongside the STATSZ roster poll. no roster server lists the signer nothing sent, 0 counted, Distributed False / UntrustedSigner some list it sent, counted at most on those, Distributed False / UntrustedSigner all list it, or trust unknown as before Sent in the mixed case because a signing-key rotation rolls servers one by one, and a restarted server needs the JWT under the new key; refusing until all list it would leave accounts down on restarted servers where the old key was dropped. Rejected: `$SYS.REQ.ACCOUNT.<key>.INFO` after the push. It shows what a server applied, but only after the untrusted JWT is already on disk, costs a request per account, and loads every account on every server. Not caught: a server whose VARZ carries no NATS operator claims (configured by `trusted_keys`), one that does not answer VARZ, and a system user issued before this change, which lacks the VARZ permission until its creds are reissued. Each is taken to trust the key, which is the behaviour before this change. A JWT an older auth controller already pushed stays on the servers' disks; it is now counted 0 and named by the condition. nats-server v2.15.0, the version `go.mod` pins: - `server/accounts.go:4559-4574` — CLAIMS.UPDATE saves and replies `jwt updated` with no issuer check - `server/server.go:2211` — `verifyAccountClaims` refuses an untrusted issuer, on apply - `server/monitor.go:1294,1743` — VARZ `trusted_operators_claim` - `server/reload.go:1619` — trusted operators are restart-only, so a server's keys hold for the life of its server ID Shared code: `PresetAuthController` in `internal/jwtplane/user.go` gains one subject; the push error at `account.go:171` and `systemaccount.go:121` goes through `ignoreUndelivered`. Tests that fail without the check: `TestResolvers_UntrustedSigner`, `TestTakeover_UntrustedSigner`.
Sign in to join this conversation.
No description provided.