feat(#3517): a named capability for authelia's bearer-authz scope

A scraper reaches a service published behind the gateway by presenting an
access token to authelia's authz endpoint, which requires the client to
carry authelia.bearer.authz. Nothing could express that: scopes are
derived from kind, and a machine client rendered an empty list.

A named capability rather than a free-form scopes list, for the reason
the derivation exists — authelia refuses some scope/grant combinations
outright, openid with client_credentials among them, and a list makes
those expressible again.

The two assertions carry their weight: authelia checks the same
obligations, but in its preStart validator, so a violation builds and
deploys cleanly and then fails to restart with swarm SSO attached to it.
This commit is contained in:
atlas 2026-08-24 12:05:01 +02:00
commit da88d450dd

View file

@ -217,7 +217,7 @@ let
if c.kind == "machine" then
''
printf -- ' grant_types: ["client_credentials"]\n'
printf -- ' scopes: []\n'
printf -- ' scopes: [${lib.optionalString c.bearerAuthz "authelia.bearer.authz"}]\n'
''
else
''
@ -575,6 +575,34 @@ in
'';
};
bearerAuthz = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Grant this client the `authelia.bearer.authz` scope, so it
may present its access token to authelia's authz endpoint
and be authorised by an `access_control` rule how a
scraper reaches a service published behind the gateway.
A named capability rather than a free-form `scopes` list,
for the same reason `kind` derives the rest: authelia
refuses some scope/grant combinations outright (`openid`
with `client_credentials` among them), and a list would
make those combinations expressible again. This admits the
one value that is legal here and nothing else.
::: {.note}
Setting this obliges two other options, and the assertions
below enforce it. Authelia checks the same thing, but only
in its `preStart` validator which means a violation
builds and deploys cleanly and then fails to restart,
taking swarm SSO down. The assertions move that to
evaluation, where a wrong value costs nothing.
:::
'';
};
accessTokenSignedResponseAlg = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
@ -820,6 +848,54 @@ in
+ "services.hyperhive.swarm.hives; rename the hive or the "
+ "colliding client.";
}
# The two obligations `authelia.bearer.authz` carries. Authelia
# enforces both itself — but in its `preStart` validator, so a
# violation produces a green `nixos-rebuild switch` and an authelia
# that then refuses to come back up, taking swarm SSO with it.
# Asserting here moves the same failure to evaluation, where a
# wrong value costs a build and nothing else.
{
assertion = lib.all (c: !c.bearerAuthz || c.audience != [ ]) cfg.oidc.clients;
message =
"services.hyperhive.swarm.authelia.oidc.clients: "
+ lib.concatStringsSep ", " (
map (c: "client '${c.id}'") (lib.filter (c: c.bearerAuthz && c.audience == [ ]) cfg.oidc.clients)
)
+ " sets bearerAuthz but declares no audience. The audience is "
+ "what authorises a bearer token at a given URL, so without "
+ "one the scope grants access to nothing and authelia refuses "
+ "the configuration outright.";
}
{
assertion = lib.all (
c:
!c.bearerAuthz
|| lib.elem c.tokenEndpointAuthMethod [
"client_secret_basic"
"client_secret_jwt"
"private_key_jwt"
]
) cfg.oidc.clients;
message =
"services.hyperhive.swarm.authelia.oidc.clients: "
+ lib.concatStringsSep ", " (
map (c: "client '${c.id}' (tokenEndpointAuthMethod = ${toString c.tokenEndpointAuthMethod})") (
lib.filter (
c:
c.bearerAuthz
&& !lib.elem c.tokenEndpointAuthMethod [
"client_secret_basic"
"client_secret_jwt"
"private_key_jwt"
]
) cfg.oidc.clients
)
)
+ ". A confidential client carrying authelia.bearer.authz must "
+ "authenticate with client_secret_basic, client_secret_jwt or "
+ "private_key_jwt. Notably client_secret_post is refused, and "
+ "it is what an OAuth2 client library may reach for first.";
}
];
# Authelia's own gateway surface: the vhost that fronts it and the