Every other crate has one and Cargo.toml already named it. Follows the sibling shape: what it is, why it is a crate rather than more config, the invariant that is easy to break (issuer_account must be absent, and Token::new_user reintroduces it), and what the tests do NOT cover - the introspection endpoint under test is a stub, so nothing here says anything about the real authelia integration.
66 lines
3.3 KiB
Markdown
66 lines
3.3 KiB
Markdown
# swarm-nats-auth
|
|
|
|
The auth-callout responder for the swarm's NATS queue — the half that lets
|
|
the server say *yes*.
|
|
|
|
`nix/host-modules/swarm-nats.nix` configures `nats-server` with an
|
|
`auth_callout` block. **That block with no responder is the fail-closed
|
|
state**, and it is the measured one rather than the obvious one: on the pinned
|
|
nats-server, both `authorization { }` and `authorization { users: [] }` answer
|
|
PONG to an anonymous client, while an `auth_callout` block sets
|
|
`auth_required` and refuses every client no responder has approved. So the
|
|
module ships the final config from the start and this crate only adds the
|
|
ability to approve. No interim hole is ever opened.
|
|
|
|
## Why a crate and not more config
|
|
|
|
The reply is a **signed NATS user JWT**. Signing needs the account nkey seed
|
|
and the JWT framing, which is program work — which is why the container and
|
|
its config landed first and this arrived separately, rather than the pair
|
|
being one change.
|
|
|
|
## The invariant that is easy to break
|
|
|
|
⛔ **`issuer_account` must be absent from the issued user token.** It is an
|
|
operator-mode field. The module renders *server-config* mode (`accounts
|
|
{ AUTH, APP }`, no operator), where its mere presence makes the server refuse
|
|
the client — `Error non operator mode account "AUTH": attempted to use
|
|
issuer_account` — while the responder cheerfully reports `granted=true`. The
|
|
account is named by the claims' `aud` instead.
|
|
|
|
`nats_jwt::Token::new_user` always sets it, so reaching for that constructor
|
|
reintroduces the bug. `nats-jwt` is a **dev-dependency**: it cannot express
|
|
`aud` on either token, and its role here is as the encoder's *test oracle*,
|
|
not part of the path that runs.
|
|
|
|
## Rules the code follows
|
|
|
|
- **A denial is a signed response carrying an error, never silence.** The
|
|
server cannot tell an absent responder from a refusing one, so staying quiet
|
|
turns every rejection into a timeout and hides an outage inside what looks
|
|
like ordinary denials.
|
|
- **Anything that is not an explicit `{"active": true}` denies** — including
|
|
an introspection call that could not be *made*. The failure modes of an HTTP
|
|
call are exactly the conditions under which an attacker would most like this
|
|
to fall open.
|
|
- **Every credential is a path, never a value.** A value in nix config lands
|
|
in the world-readable store; a value in `argv` is readable via
|
|
`/proc/<pid>/cmdline`, which is `0444`. Paths are not secrets, so passing
|
|
them as flags is fine.
|
|
- **The introspection timeout is pinned below the server's `authorization.
|
|
timeout`**, with a test asserting the relation — a responder that answers
|
|
after the server gave up is indistinguishable from one that never answered.
|
|
|
|
## What the tests do and do not cover
|
|
|
|
Unit tests cover the JWT framing, including byte-equality against `nats-jwt`
|
|
on the one shape that crate models. They are **not** sufficient on their own:
|
|
this crate's shape is decided by a server that parses what it emits, so the
|
|
change was also driven against a real `nats-server` — every refusal repeated
|
|
*with the responder live*, because a responder that says yes to everyone
|
|
passes "a client can connect" perfectly. That harness lives outside this repo;
|
|
the PR that added this crate links it.
|
|
|
|
⚠️ **Its introspection endpoint is a stub.** Those runs prove this crate's own
|
|
behaviour and nothing about the real authelia integration, which needs a
|
|
deployment.
|