From 31fa891a4ebfd68f57018fdbc1278742f80715cd Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 19 Aug 2026 21:45:07 +0200 Subject: [PATCH] feat(swarm-authelia): expose prometheus metrics and declare the scrape target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/. --- nix/host-modules/swarm-authelia.nix | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 7d4dfd6f..09c7c5f5 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -339,6 +339,26 @@ 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. + ''; + }; + domain = lib.mkOption { type = lib.types.str; # Under the SWARM domain, like the forge and matrix: a swarm has one @@ -804,6 +824,12 @@ 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, + # which is what keeps scraper and target on one host by construction + # rather than by the all-local deployment happening to co-locate them. + services.hyperhive.swarm.otel.scrapeTargets.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 +1136,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