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
|
# and a field later added to the submodule then existed on the
|
||||||
# declared entries and not on these, which is an eval error reachable
|
# declared entries and not on these, which is an eval error reachable
|
||||||
# only once hive identities are on.
|
# 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: _: {
|
hiveClients = lib.mapAttrsToList (name: _: {
|
||||||
id = "${cfg.hiveClientPrefix}${name}";
|
id = "${cfg.hiveClientPrefix}${name}";
|
||||||
description = "HyperHive hive ${name}";
|
description = "HyperHive hive ${name}";
|
||||||
kind = "machine";
|
kind = "machine";
|
||||||
redirectUris = [ ];
|
redirectUris = [ ];
|
||||||
|
audience = [ "${cfg.hiveClientPrefix}${name}" ];
|
||||||
|
accessTokenSignedResponseAlg = "RS256";
|
||||||
}) hyperhiveCfg.swarm.hives;
|
}) hyperhiveCfg.swarm.hives;
|
||||||
|
|
||||||
# `swarm-authelia-bridge`'s own identity — distinct from
|
# `swarm-authelia-bridge`'s own identity — distinct from
|
||||||
|
|
@ -188,6 +203,15 @@ let
|
||||||
+ lib.optionalString (c.tokenEndpointAuthMethod != null) ''
|
+ lib.optionalString (c.tokenEndpointAuthMethod != null) ''
|
||||||
printf -- ' token_endpoint_auth_method: %s\n' ${lib.escapeShellArg c.tokenEndpointAuthMethod}
|
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
|
if c.kind == "machine" then
|
||||||
''
|
''
|
||||||
|
|
@ -493,6 +517,63 @@ in
|
||||||
the secret is at fault.
|
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