swarm-queue-client: request the bearer-authz scope when minting an agent token

`swarm-logs query` got a bare nginx 401 from the swarm log store on every
query. The agent OIDC client is registered for `authelia.bearer.authz`
(`swarm-authelia.nix`'s `agentClients` sets `bearerAuthz`), but registration
is not issuance: the token request asked for no scope, so the token came back
carrying none, and authelia's `/api/authz/auth-request` refuses that exactly
as it refuses an unauthenticated caller.

The same failure is already recorded in `swarm-otel.nix` against the
collector's client, on the same scope string — prometheus asks for no scopes
unless told to, and every scrape was refused at introspection. This is that
bug one layer down, so it gets the same shape of fix.

`scope` becomes an opt-in parameter alongside `audience`, not a hardcoded
value or a config field: the two travel together (registered ≠ requested
applies to both) and only the destination decides whether either is needed.
`None` keeps every other caller byte-identical — the NATS connect callback,
`auth.rs`'s bridge client and the OTLP push client all pass it.

Refs #4464
This commit is contained in:
atlas 2026-09-17 09:51:44 +02:00
commit d8f6d99bf9
5 changed files with 118 additions and 28 deletions

View file

@ -50,6 +50,11 @@ hold `authelia.bearer.authz`. Both are set in `swarm-authelia.nix`'s
`agentClients`; without them authelia answers `invalid_target` at the token
endpoint, or the gateway answers 401 with no explanation.
⚠️ Registration is not issuance, so the token request _asks_ for both: the
audience and the scope are named in the `client_credentials` form, because a
client that is registered for a scope it does not request is handed a token
carrying none, and the gateway refuses that with the same bare 401.
## Configuration
Supplied by `nix/agent-modules/logs.nix`, which wraps the binary — the same

View file

@ -46,15 +46,27 @@ pub fn run(
format: Format,
out: &mut impl Write,
) -> Result<()> {
// Minted per invocation, with the query URL as the audience. Both halves
// are load-bearing: authelia refuses a token carrying no audience at the
// authz endpoint the gateway's `auth_request` calls, and the audience it
// checks is the URL being requested — so the string sent here and the
// string requested below must be one binding, which is why `Config` holds
// exactly one.
let token = swarm_queue_client::mint_token_for_blocking(&cfg.queue, Some(&cfg.query_url))
.map_err(|e| anyhow::anyhow!("{}", swarm_queue_client::chain(&e)))
.context("minting an access token for the swarm log store")?;
// Minted per invocation, with the query URL as the audience and the
// `authelia.bearer.authz` scope. All three are load-bearing: authelia
// refuses a token carrying no audience at the authz endpoint the
// gateway's `auth_request` calls, and the audience it checks is the URL
// being requested — so the string sent here and the string requested
// below must be one binding, which is why `Config` holds exactly one.
//
// The scope has to be ASKED for, not merely registered: the agent client
// is granted `authelia.bearer.authz` by the `agentClients` entry in
// `swarm-authelia.nix`, but an OAuth2 server issues no scope the client
// never requested, and a scopeless token is refused at the authz endpoint
// exactly as an unauthenticated one is — a bare nginx 401 with nothing in
// it that names the scope. `swarm-otel.nix` records the same failure
// against the collector's client, on the same string.
let token = swarm_queue_client::mint_token_for_blocking(
&cfg.queue,
Some(&cfg.query_url),
Some("authelia.bearer.authz"),
)
.map_err(|e| anyhow::anyhow!("{}", swarm_queue_client::chain(&e)))
.context("minting an access token for the swarm log store")?;
let http = build_http_client(cfg)?;