swarm-authelia: give each hive client an audience and JWT access tokens
A swarm service that has to tell hives apart needs the token itself to
say which hive presented it. Two client-registration fields were missing
for that, and both are properties of the client rather than of any one
consumer:
- `audience` registers which `aud` values a client may request. It is a
boundary, not a label: asking for one that is not registered is
refused with `invalid_target`, so a hive cannot mint a token for
another hive's slot.
- `accessTokenSignedResponseAlg` switches the access token from
authelia's default opaque handle to an RFC 9068 JWT, which is what
lets a resource server verify a token against `/jwks.json` instead of
asking authelia about every request.
Each hive's audience is its own client id rather than a new per-hive
string. The identity is the directory's, not the consumer's, and the id
is already published as `hiveClientPrefix` precisely so a second
consumer does not carry its own copy of the naming rule.
Both fields default to authelia's own behaviour, so no existing client
changes.
This commit is contained in:
parent
0f801c645f
commit
0b98f0ac8f
1 changed files with 81 additions and 0 deletions
|
|
@ -102,11 +102,26 @@ let
|
|||
# and a field later added to the submodule then existed on the
|
||||
# declared entries and not on these, which is an eval error reachable
|
||||
# only once hive identities are on.
|
||||
#
|
||||
# `audience` is the hive's own client id rather than a second per-hive
|
||||
# string invented here. A swarm service that has to tell hives apart
|
||||
# needs one name per hive that both sides already agree on, and the
|
||||
# client id is that name — published as `hiveClientPrefix` for exactly
|
||||
# this reason. Minting a parallel naming scheme would be a second thing
|
||||
# to keep in step, and the one that drifts is the one nobody tests.
|
||||
#
|
||||
# `RS256` because a resource server that cannot call this provider back
|
||||
# is a real case here: the swarm's telemetry collector verifies tokens
|
||||
# offline against `/jwks.json`, and an opaque token gives it nothing to
|
||||
# verify. The queue's auth-callout responder introspects instead, which
|
||||
# is a different question asked of the same token.
|
||||
hiveClients = lib.mapAttrsToList (name: _: {
|
||||
id = "${cfg.hiveClientPrefix}${name}";
|
||||
description = "HyperHive hive ${name}";
|
||||
kind = "machine";
|
||||
redirectUris = [ ];
|
||||
audience = [ "${cfg.hiveClientPrefix}${name}" ];
|
||||
accessTokenSignedResponseAlg = "RS256";
|
||||
}) hyperhiveCfg.swarm.hives;
|
||||
|
||||
# `swarm-authelia-bridge`'s own identity — distinct from
|
||||
|
|
@ -188,6 +203,15 @@ let
|
|||
+ lib.optionalString (c.tokenEndpointAuthMethod != null) ''
|
||||
printf -- ' token_endpoint_auth_method: %s\n' ${lib.escapeShellArg c.tokenEndpointAuthMethod}
|
||||
''
|
||||
# Flow-style YAML, matching `scopes` below. The values are client ids
|
||||
# and hive names, which `Ident` already constrains to `[a-z0-9-]` — no
|
||||
# character in that set needs quoting in a YAML flow sequence.
|
||||
+ lib.optionalString (c.audience != [ ]) ''
|
||||
printf -- ' audience: [%s]\n' ${lib.escapeShellArg (lib.concatStringsSep ", " c.audience)}
|
||||
''
|
||||
+ lib.optionalString (c.accessTokenSignedResponseAlg != null) ''
|
||||
printf -- ' access_token_signed_response_alg: %s\n' ${lib.escapeShellArg c.accessTokenSignedResponseAlg}
|
||||
''
|
||||
+ (
|
||||
if c.kind == "machine" then
|
||||
''
|
||||
|
|
@ -493,6 +517,63 @@ in
|
|||
the secret is at fault.
|
||||
'';
|
||||
};
|
||||
|
||||
audience = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "hive-alpha" ];
|
||||
description = ''
|
||||
Audiences (`aud`) this client is permitted to request a
|
||||
token for. Empty means it asks for none, which is the
|
||||
right answer for a client whose resource server does not
|
||||
distinguish callers.
|
||||
|
||||
⚠️ Registering an audience only *permits* it — the value
|
||||
lands in a token when the client **asks** for it at the
|
||||
token endpoint, and a client that does not send
|
||||
`audience=` receives a token with `aud: []` however
|
||||
complete this list looks. Measured against authelia
|
||||
4.39.20: the config reads exactly right and the resource
|
||||
server rejects every token, because a config that grants
|
||||
and a request that claims are two separate acts.
|
||||
|
||||
Requesting an audience that is *not* listed here is
|
||||
refused with `invalid_target`, which is what makes this
|
||||
usable as a boundary rather than a label: a client cannot
|
||||
mint a token for a resource slot that is not its own.
|
||||
'';
|
||||
};
|
||||
|
||||
accessTokenSignedResponseAlg = lib.mkOption {
|
||||
type = lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"none"
|
||||
"RS256"
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
example = "RS256";
|
||||
description = ''
|
||||
Signing algorithm for this client's **access** tokens.
|
||||
`null` leaves authelia on its default, which issues an
|
||||
opaque token (`authelia_at_…`) — a database handle that
|
||||
carries no claims and means nothing to anyone but this
|
||||
provider.
|
||||
|
||||
Set `RS256` when the resource server verifies the token
|
||||
*itself* rather than asking this provider about it: that
|
||||
yields an RFC 9068 JWT (`at+jwt`) carrying `aud`, `iss`
|
||||
and `client_id`, verifiable against `/jwks.json` with no
|
||||
round trip.
|
||||
|
||||
⚠️ This is what makes a token readable by an
|
||||
OIDC-verifying consumer at all. A resource server given
|
||||
an opaque token is not *misconfigured* — it is
|
||||
structurally unable to verify it, and says so in terms
|
||||
that point at the verifier rather than at the token's
|
||||
format.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue