swarm-otel: scrape authelia through its published name
Authelia's metrics were scraped over loopback, declared only on a host running both authelia and the collector, so a split deploy collected none. Its target is now declared on every host as `https://<swarm.authelia.domain>/metrics`, next to forgejo's, and authelia's vhost serves `= /metrics` behind the same collector auth subrequest as the forge's. The loopback entry is gone, so a single host still scrapes authelia once, now by name. The forge-only `metricsRule` becomes `metricsRules`, derived from every `publishedScrapeTargets` entry. Each target gets the collector-scoped `one_factor` rule followed by an unconditional `deny`. The `deny` is needed because authelia 4.39 skips a rule whose `subject` does not match and moves on. With nothing after the collector rule, any other authenticated session reached `default_policy = one_factor` and was admitted. That was already true of the forge's `/metrics`, and this change closes it there too. Part of #5205
This commit is contained in:
parent
1219091788
commit
393e84afd0
4 changed files with 194 additions and 42 deletions
|
|
@ -101,7 +101,30 @@ let
|
|||
# authelia with neither the forge nor the collector beside it.
|
||||
inherit (shared) autheliaOnly;
|
||||
|
||||
# Every swarm service on one host, the collector and authelia included.
|
||||
inherit (shared) allLocal;
|
||||
|
||||
forgeMetricsUrl = "https://forge.t.local/metrics";
|
||||
autheliaMetricsUrl = "https://auth.t.local/metrics";
|
||||
|
||||
# The policy authelia applies to `subject` requesting `https://<domain><path>`:
|
||||
# the first rule whose domain, resources and subject all match, else
|
||||
# `default_policy`. ⚠️ A rule with a `subject` list does NOT match a subject
|
||||
# outside it — authelia moves on to the next rule — which is the semantics a
|
||||
# check of the rule's own fields cannot see.
|
||||
autheliaPolicy =
|
||||
m: subject: domain: path:
|
||||
let
|
||||
ac = m.containers.swarm-authelia.config.services.authelia.instances.swarm.settings.access_control;
|
||||
matches =
|
||||
r:
|
||||
r.domain == domain
|
||||
&& lib.any (re: builtins.match re path != null) (r.resources or [ ".*" ])
|
||||
&& (!(r ? subject) || builtins.elem subject r.subject);
|
||||
hit = lib.findFirst matches null ac.rules;
|
||||
in
|
||||
if hit == null then ac.default_policy else hit.policy;
|
||||
|
||||
cases = [
|
||||
{
|
||||
# The collector's non-assertion, the deliberate mirror of Grafana's
|
||||
|
|
@ -196,6 +219,79 @@ let
|
|||
subject = [ "oauth2:client:${h.swarm.otel.clientId}" ];
|
||||
} rules;
|
||||
}
|
||||
{
|
||||
# Authelia's half of the same move: its target is published by name, so
|
||||
# a collector anywhere in the swarm has a job for it.
|
||||
name = "a collector on a host without authelia scrapes authelia by name";
|
||||
ok =
|
||||
let
|
||||
s = otelSettings otelRemoteAuthelia;
|
||||
job = lib.findFirst (j: j.job_name == "authelia") null s.receivers.prometheus.config.scrape_configs;
|
||||
in
|
||||
!otelRemoteAuthelia.services.hyperhive.deploy.authelia.enable
|
||||
&& job != null
|
||||
&& job.static_configs == [ { targets = [ "auth.t.local" ]; } ]
|
||||
&& job.metrics_path == "/metrics"
|
||||
&& job.oauth2.endpoint_params.audience == autheliaMetricsUrl;
|
||||
}
|
||||
{
|
||||
# The location the collector's job requests: gated, and proxying to the
|
||||
# loopback listener that has no authentication of its own.
|
||||
name = "authelia's vhost serves /metrics only behind the collector's auth subrequest";
|
||||
ok =
|
||||
let
|
||||
h = autheliaOnly.services.hyperhive;
|
||||
locs = autheliaOnly.services.nginx.virtualHosts.${h.swarm.authelia.domain}.locations;
|
||||
authz = locs."= /__authelia_metrics_authz";
|
||||
in
|
||||
locs."= /metrics".proxyPass == "http://127.0.0.1:${toString h.swarm.authelia.metricsPort}/metrics"
|
||||
&& lib.hasInfix "auth_request /__authelia_metrics_authz;" locs."= /metrics".extraConfig
|
||||
&& authz.proxyPass == "https://${h.swarm.authelia.domain}/api/authz/auth-request"
|
||||
&& lib.hasInfix "internal;" authz.extraConfig;
|
||||
}
|
||||
{
|
||||
# Every published target admits the collector's client and refuses any
|
||||
# other authenticated subject. Read through authelia's first-match
|
||||
# semantics: a client-scoped rule alone lets an operator's session fall
|
||||
# through to `default_policy = one_factor`.
|
||||
name = "every published metrics URL admits the collector and refuses a plain session";
|
||||
ok =
|
||||
let
|
||||
h = autheliaOnly.services.hyperhive;
|
||||
collector = "oauth2:client:${h.swarm.otel.clientId}";
|
||||
targets = lib.mapAttrsToList (
|
||||
_: url: builtins.match "https://([^/]+)(/.*)" url
|
||||
) h.swarm.otel.publishedScrapeTargets;
|
||||
policy =
|
||||
subject: t: autheliaPolicy autheliaOnly subject (builtins.elemAt t 0) (builtins.elemAt t 1);
|
||||
in
|
||||
lib.sort lib.lessThan (lib.attrValues h.swarm.otel.publishedScrapeTargets) == [
|
||||
autheliaMetricsUrl
|
||||
forgeMetricsUrl
|
||||
]
|
||||
&& lib.all (t: policy collector t == "one_factor") targets
|
||||
&& lib.all (t: policy "user:someone" t == "deny") targets
|
||||
&& lib.all (t: policy "group:admins" t == "deny") targets;
|
||||
}
|
||||
{
|
||||
# Single host: authelia is scraped once, by its published name. A
|
||||
# loopback job beside it would scrape the same listener twice — and share
|
||||
# its job name, which ./swarm-otel.nix refuses.
|
||||
name = "a host running authelia and the collector scrapes authelia once, by name";
|
||||
ok =
|
||||
let
|
||||
o = allLocal.services.hyperhive.swarm.otel;
|
||||
jobs =
|
||||
lib.filter (j: j.job_name == "authelia")
|
||||
(otelSettings allLocal).receivers.prometheus.config.scrape_configs;
|
||||
in
|
||||
allLocal.services.hyperhive.deploy.authelia.enable
|
||||
&& allLocal.services.hyperhive.deploy.swarm-otel.enable
|
||||
&& !(o.scrapeTargets ? authelia)
|
||||
&& (o.publishedScrapeTargets.authelia or null) == autheliaMetricsUrl
|
||||
&& lib.length jobs == 1
|
||||
&& (lib.head jobs).static_configs == [ { targets = [ "auth.t.local" ]; } ];
|
||||
}
|
||||
{
|
||||
# Read against the roster the fixture declares rather than against
|
||||
# names spelled here: an arm naming `h1` passes on a single-hive
|
||||
|
|
|
|||
Loading…
Reference in a new issue