Compare commits

...
Author SHA1 Message Date
atlas
541dd29820 fix(swarm-authelia): only declare the scrape target where a collector reads it
The comment claimed the option's rule keeps scraper and target on one host
by construction. It does not. Both services default from
enableRequiredServices via mkDefault, which is an invitation to override
rather than a guarantee, so co-location is a property of the auto-deployed
topology and not of the module.

Gating the target on the collector's own enable makes the loopback address
honest: a host running authelia without a collector no longer declares a
target nothing can read. That absence was the part worth fixing, because it
is silent — no error, no metrics, nothing in a log to notice.

This does not make authelia scrapeable from another host. That needs the
endpoint published under a name with a certificate and an audience, which
is separate work; the option's docs now say so where someone splitting the
two would read it.
2026-08-24 12:36:54 +02:00
atlas
31fa891a4e feat(swarm-authelia): expose prometheus metrics and declare the scrape target
The endpoint was off, so nothing reported on the swarm's own SSO. Enabling
it alone would have added no data — the scraper that reads it only landed
with the swarm-tier prometheus receiver.

Loopback only, like the main listener and for a stronger reason: this
endpoint authenticates nothing and reports request volumes and outcomes for
every login on the swarm.

metricsPort is an option rather than a literal because every swarm container
shares the host netns, so two services picking the same port do not conflict
at build time — one loses at runtime with nothing in any log. 9959 is
upstream's default and is unclaimed across nix/.
2026-08-24 12:36:54 +02:00

View file

@ -43,6 +43,7 @@ let
swarmDomain = hyperhiveCfg.swarm.domain;
uiCfg = hyperhiveCfg.swarm.ui;
forgeCfg = hyperhiveCfg.swarm.forge;
otelCfg = hyperhiveCfg.swarm.otel;
# Group an account must hold to reach operator-only surfaces. Named
# here because this module writes the rule that enforces it and
@ -339,6 +340,36 @@ in
'';
};
metricsPort = lib.mkOption {
type = lib.types.port;
default = 9959;
description = ''
TCP port authelia serves its Prometheus metrics on, bound to
loopback. Upstream's default, kept so an operator reading
authelia's documentation finds what they expect.
A separate port from {option}`port` because it is a separate
listener with a different audience: the main one is proxied by
the gateway and reachable from the swarm, this one is scraped by
the collector on this host and by nothing else.
Every swarm container shares the host network namespace, so
two services defaulting to the same port do not conflict at build
time one simply loses at runtime, with nothing in any log. Check
a new value against the others before changing this.
::: {.note}
Loopback means this endpoint is only reachable by a collector on
the *same host*, so the scrape target is declared only when one is
enabled here. Run the swarm's collector elsewhere and authelia's
metrics are simply not collected no error, and nothing in a log
to say so. Making them reachable across hosts is a different piece
of work: the endpoint would have to be published under a name,
with a certificate and an audience.
:::
'';
};
domain = lib.mkOption {
type = lib.types.str;
# Under the SWARM domain, like the forge and matrix: a swarm has one
@ -804,6 +835,26 @@ in
# bridge at that wrong answer.
services.hyperhive.gateway.localNames = [ cfg.domain ];
# Declared here rather than in the collector's module, per the option's
# own rule: an entry exists only where the service that named it runs.
#
# Gated on the collector's `enable` as well, and that second condition is
# what makes the loopback address honest. Both services default from
# `swarm.enableRequiredServices` — but `mkDefault` is an invitation to
# override, not a guarantee, so "they are on the same host" is a property
# of the auto-deployed topology rather than of the module. Without this
# gate, a host running authelia and no collector would declare a target
# nothing can read, and the absence would be silent: no error, no metrics,
# nothing to notice.
#
# It does not make authelia scrapeable from ANOTHER host — that needs the
# endpoint published under a name with a cert and an audience, which is a
# different piece of work. This only stops the config asserting a
# collection that is not happening.
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf otelCfg.enable {
authelia = "127.0.0.1:${toString cfg.metricsPort}";
};
# This swarm-ui quick-links entry, same guard as the vhost/DNS name
# above (only the host actually running the container claims it —
# see `services.hyperhive.swarm.controller.links`'s description for
@ -1110,6 +1161,16 @@ in
};
log.level = "info";
# Prometheus exposition for the swarm collector to scrape.
# Loopback only, like the main listener above and for a
# stronger reason: this endpoint has no authentication of its
# own, and it reports request volumes and outcomes for every
# SSO login on the swarm.
telemetry.metrics = {
enabled = true;
address = "tcp://127.0.0.1:${toString cfg.metricsPort}";
};
# `watch` is load-bearing, not a convenience: authelia reads
# this file once at startup, and `swarm-authelia-bridge` writes
# it to create agent identities while being unable to restart