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
|
|
@ -57,6 +57,47 @@ let
|
|||
# failing the check it was supposed to pass.
|
||||
operatorGroup = "admins";
|
||||
|
||||
# Whether the swarm collector's client is actually registered here.
|
||||
#
|
||||
# Asked of the client list rather than re-derived from the conditions
|
||||
# that produce it (`otel.enable`, a non-empty audience set, authelia
|
||||
# being co-located). A second copy of that predicate is a second thing
|
||||
# to keep in step, and the two drifting is not a build failure: a rule
|
||||
# naming an unregistered client is refused by authelia's *startup*
|
||||
# validator, so the whole SSO service fails to restart.
|
||||
#
|
||||
# Reading the registration itself also makes the rule correct for a
|
||||
# registrar this module has never heard of — an operator registering
|
||||
# the collector by hand against a provider that is not co-located with
|
||||
# it gets the same rule, from the same expression.
|
||||
# ⚠️ `swarm.otel` named in full, not through a let binding: there are two
|
||||
# otel options one word apart, and only this one is the swarm's collector.
|
||||
collectorClientId = config.services.hyperhive.swarm.otel.clientId;
|
||||
collectorRegistered = lib.any (c: c.id == collectorClientId) cfg.oidc.clients;
|
||||
|
||||
# The forge's metrics endpoint: deny until a collector exists to allow.
|
||||
#
|
||||
# `deny` is not a placeholder, it is the protection. The endpoint is
|
||||
# always served (a swarm-integrated forge always has metrics) and
|
||||
# `default_policy` is `one_factor`, which means *any* authenticated
|
||||
# subject — every operator today, every agent once they hold authelia
|
||||
# accounts. Nor does the audience stand between a browser session and
|
||||
# this data: `authn_strategies` on the authz endpoint also accepts
|
||||
# `CookieSession`, and a cookie carries no audience at all.
|
||||
metricsRule = {
|
||||
domain = forgeCfg.domain;
|
||||
resources = [ "^/metrics$" ];
|
||||
}
|
||||
// (
|
||||
if collectorRegistered then
|
||||
{
|
||||
policy = "one_factor";
|
||||
subject = [ "oauth2:client:${collectorClientId}" ];
|
||||
}
|
||||
else
|
||||
{ policy = "deny"; }
|
||||
);
|
||||
|
||||
# Upstream's `services.authelia.instances.<name>` derives the unit,
|
||||
# user, group and StateDirectory from the instance name
|
||||
# (`authelia` + `-<name>`). Naming them here rather than repeating the
|
||||
|
|
@ -1281,32 +1322,10 @@ in
|
|||
# The metrics rule is listed first so it cannot be shadowed
|
||||
# by a broader domain rule added later.
|
||||
rules =
|
||||
# The forge's metrics endpoint. `deny` is deliberate and
|
||||
# is the whole protection right now: the endpoint is
|
||||
# always served (a swarm-integrated forge always has
|
||||
# metrics), and `default_policy` is `one_factor`, which
|
||||
# means *any* authenticated subject — every operator
|
||||
# today, every agent once they hold authelia accounts.
|
||||
#
|
||||
# Being reachable by a Bearer token is not sufficient on
|
||||
# its own: `authn_strategies` on this endpoint also
|
||||
# accepts `CookieSession`, and a cookie carries no
|
||||
# audience, so the audience is not what stands between a
|
||||
# browser session and this data.
|
||||
#
|
||||
# The collector gets in by REPLACING this with a
|
||||
# client-scoped allow (`subject = ["oauth2:client:<id>"]`)
|
||||
# once such a client is registered. Denying until then is
|
||||
# what makes publishing the endpoint safe on its own —
|
||||
# authelia refuses a subject naming a client that is not
|
||||
# registered, and it does so in a `preStart` validator,
|
||||
# so naming one early takes the whole SSO service down on
|
||||
# the next restart rather than failing the build.
|
||||
lib.optional forgeCfg.behindGateway {
|
||||
domain = forgeCfg.domain;
|
||||
resources = [ "^/metrics$" ];
|
||||
policy = "deny";
|
||||
}
|
||||
# Denied or client-scoped depending on whether a
|
||||
# collector is registered — see `metricsRule` above,
|
||||
# which is where the reasoning for both halves lives.
|
||||
lib.optional forgeCfg.behindGateway metricsRule
|
||||
++ lib.optional uiCfg.enable {
|
||||
domain = uiCfg.domain;
|
||||
subject = [ "group:${operatorGroup}" ];
|
||||
|
|
|
|||
Loading…
Reference in a new issue