feat(#3517): register the swarm collector as an audienced oauth2 client
The forge's `/metrics` is published behind the gateway and denied to everyone, waiting on a client to allow. This is that client. An audience is a URL: authelia validates a bearer token against the address being requested, and a client may only request an audience it is registered for, so registration is the authorisation. The URLs are owned by the services that publish them while audiences attach to one client, so services contribute to a list and this module builds the single entry — the `gateway.localNames` split, forced here by client definitions concatenating rather than merging into a shared entry. The access-control rule asks the client list whether the collector is registered rather than re-deriving the conditions that register it. The two drifting is not a build failure: authelia refuses a rule naming an unknown client in its startup validator, so SSO fails to restart.
This commit is contained in:
parent
da88d450dd
commit
3895a1e21d
3 changed files with 145 additions and 26 deletions
|
|
@ -223,6 +223,57 @@ in
|
|||
scrape is the inert configuration this option exists to avoid.
|
||||
'';
|
||||
};
|
||||
|
||||
collectorAudiences = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = lib.literalExpression ''[ "https://forge.example.com/metrics" ]'';
|
||||
description = ''
|
||||
URLs this collector may mint an access token for, contributed by
|
||||
the modules that publish them.
|
||||
|
||||
A service module says **which URL it exposes**; this module turns
|
||||
the set into the collector's single OAuth2 client — the same split
|
||||
as `services.hyperhive.gateway.localNames`, and for the same
|
||||
reason. The collector has no business knowing another service's
|
||||
routes, and a service cannot reach into the client list to add
|
||||
itself: client definitions concatenate rather than merging into a
|
||||
shared entry, so the entry has to be built in one place from a
|
||||
list many modules can write.
|
||||
|
||||
⚠️ An audience is a **URL**, not a label. Authelia validates a
|
||||
bearer token against the address being requested, so a token
|
||||
minted for one target is refused at another and a client may only
|
||||
request an audience it is registered for — registration is the
|
||||
authorisation. A near miss (a trailing slash, `http` for `https`)
|
||||
presents as a valid token rejected at the target, several layers
|
||||
from its cause.
|
||||
|
||||
⚠️ Deliberately not part of `scrapeTargets`. An entry there is
|
||||
trusted because the scraper and the target share a host; an entry
|
||||
here is trusted because it presents a credential. Folding the two
|
||||
together would leave a reader unable to tell which of those a
|
||||
given target relies on.
|
||||
'';
|
||||
};
|
||||
|
||||
clientId = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "swarm-collector";
|
||||
description = ''
|
||||
OAuth2 client id this collector authenticates as. Published so
|
||||
authelia's `access_control` rules can name it without carrying a
|
||||
second copy of the string, exactly as
|
||||
`services.hyperhive.swarm.authelia.hiveClientPrefix` is published
|
||||
for the queue's responder.
|
||||
|
||||
Two spellings drifting apart is not a build failure: authelia
|
||||
refuses a rule naming an unregistered client in its startup
|
||||
validator, so the swarm's SSO service fails to *restart* — long
|
||||
after the change that caused it evaluated cleanly.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) {
|
||||
|
|
@ -267,6 +318,43 @@ in
|
|||
# telemetry.
|
||||
services.hyperhive.swarm.authelia.oidc.hiveIdentities = true;
|
||||
|
||||
# The collector's own identity, for the other direction: the hive
|
||||
# identities above are how this collector authenticates its *callers*,
|
||||
# this is how it authenticates *itself* to a service published behind
|
||||
# the gateway.
|
||||
#
|
||||
# Only where authelia is co-located. A client is a row in this host's
|
||||
# provider config, so declaring one against a remote provider would
|
||||
# render nothing while reading as done; a swarm whose authelia lives
|
||||
# elsewhere registers it there.
|
||||
#
|
||||
# ⚠️ Also conditional on the audience set being non-empty, and that is
|
||||
# the shipped case rather than an edge — nothing contributes a URL
|
||||
# until some service publishes one behind the gateway. Authelia
|
||||
# refuses a bearer-authz client with no audience, so an unconditional
|
||||
# declaration would break every hive that runs a collector and
|
||||
# publishes nothing.
|
||||
services.hyperhive.swarm.authelia.oidc.clients =
|
||||
lib.mkIf (autheliaCfg.enable && cfg.collectorAudiences != [ ])
|
||||
[
|
||||
{
|
||||
id = cfg.clientId;
|
||||
description = "HyperHive swarm collector";
|
||||
kind = "machine";
|
||||
# Grants `authelia.bearer.authz`, without which the authz
|
||||
# endpoint refuses an otherwise valid token and blames the
|
||||
# token rather than the missing grant.
|
||||
bearerAuthz = true;
|
||||
audience = cfg.collectorAudiences;
|
||||
# Stated rather than left on authelia's default, because the
|
||||
# two agreeing today is not the same as this being the
|
||||
# required value: authelia permits only basic / JWT methods
|
||||
# for a confidential client holding that scope, and enforces
|
||||
# it in the startup validator.
|
||||
tokenEndpointAuthMethod = "client_secret_basic";
|
||||
}
|
||||
];
|
||||
|
||||
# The CA bind source is written at runtime by a host unit, so the
|
||||
# container has to start after it — otherwise nspawn sets up a mount
|
||||
# over a file that does not exist yet.
|
||||
|
|
|
|||
Loading…
Reference in a new issue