From 2489830772cc4ceef960f6d8306fe54dbdf7d086 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 17 Oct 2026 13:31:11 +0200 Subject: [PATCH] authelia: allow only the collector to write to the log store's ingest path --- docs/swarm/services.md | 6 ++-- nix/host-modules/swarm-authelia.nix | 11 +++++++ nix/module-eval/swarm-otel-identity.nix | 43 +++++++++++++++++++------ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/docs/swarm/services.md b/docs/swarm/services.md index d01d5298..2daee96b 100644 --- a/docs/swarm/services.md +++ b/docs/swarm/services.md @@ -290,8 +290,10 @@ accepts the new value within a minute. The collector also scrapes this store's own metrics, at `https:///metrics`, so a collector on a host -without the store still gets them. The gateway admits only the collector's -client to that path; any other session gets `deny`. +without the store still gets them. On this name the gateway admits only the +collector's client to `/metrics` and the ingest route +`/insert/opentelemetry/v1/logs`; any other session gets `deny` on both. The +read side stays open to any authenticated caller. ### Telemetry collector (OTEL) diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 6a25df51..a88b5ac0 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -134,6 +134,16 @@ let } ]; + # The log store's ingest routes: the collector's client, nobody else. A + # prefix rather than the one route the vhost serves, so no spelling of it, + # and no route added under `/insert/`, reaches `default_policy`. The rest of + # the name (read API, vmui) stays open to any authenticated caller; its + # `/metrics` is covered by `metricsRules`. + logsStoreRules = collectorOnly { + domain = config.services.hyperhive.swarm.victorialogs.domain; + resources = [ "^/insert/.*" ]; + }; + # One pair per `swarm.otel.publishedScrapeTargets` entry, read off the same # URL the collector requests and its token is minted for, so a target # cannot be published without the rules that confine it to the collector. @@ -1342,6 +1352,7 @@ in # reasoning for both halves lives. metricsRules ++ metricsStoreRules + ++ logsStoreRules # The swarm UI on the apex. Not keyed to a deploy flag: its # vhost is on the swarm controller's host, which need not # be this one. Guarded on the domain being set: without it diff --git a/nix/module-eval/swarm-otel-identity.nix b/nix/module-eval/swarm-otel-identity.nix index 26e575d2..48b027b5 100644 --- a/nix/module-eval/swarm-otel-identity.nix +++ b/nix/module-eval/swarm-otel-identity.nix @@ -387,8 +387,11 @@ let rules = autheliaOnly.containers.swarm-authelia.config.services.authelia.instances.swarm.settings.access_control.rules; # The metrics store's own name is the case above's; its rules carry - # `resources` for its push route and `/metrics` together. - metricsRules = lib.filter (r: r ? resources && r.domain != "metrics.t.local") rules; + # `resources` for its push route and `/metrics` together. The log + # store's `/insert/` ingest rules are the case below's. + metricsRules = lib.filter ( + r: r ? resources && r.domain != "metrics.t.local" && r.resources != [ "^/insert/.*" ] + ) rules; re = lib.head (lib.head metricsRules).resources; table = [ { @@ -514,10 +517,11 @@ let && checksOf locs."^~ /select/logsql/" == [ "auth_request /__swarm_logs_auth;" ]; } { - # The `deny` that confines the log store's `/metrics` is scoped to that - # path, and comes after the collector's rule: every other path on the - # name still reaches `default_policy`, as it did with no rule at all. - name = "authelia confines the log store's /metrics to the collector and leaves its other paths alone"; + # The `deny`s that confine the log store's `/metrics` and its `/insert/` + # ingest prefix are each scoped to their path and come after the + # collector's rule: every other path on the name still reaches + # `default_policy`. + name = "authelia confines the log store's /metrics and ingest to the collector and leaves its other paths alone"; ok = let h = autheliaOnly.services.hyperhive; @@ -526,19 +530,38 @@ let autheliaOnly.containers.swarm-authelia.config.services.authelia.instances.swarm.settings.access_control.rules; logsRules = lib.filter (r: r.domain == "logs.t.local") rules; policy = subject: autheliaPolicy autheliaOnly subject "logs.t.local"; + ingest = "/insert/opentelemetry/v1/logs"; in map (r: r.policy) logsRules == [ "one_factor" "deny" + "one_factor" + "deny" ] - && lib.all (r: r.resources == [ "^/metrics([?].*)?$" ]) logsRules - && (lib.head logsRules).subject == [ collector ] + && + map (r: r.resources) logsRules == [ + [ "^/metrics([?].*)?$" ] + [ "^/metrics([?].*)?$" ] + [ "^/insert/.*" ] + [ "^/insert/.*" ] + ] + && + map (r: r.subject or null) logsRules == [ + [ collector ] + null + [ collector ] + null + ] && policy collector "/metrics" == "one_factor" && policy "user:someone" "/metrics" == "deny" && policy "user:someone" "/metrics?x=1" == "deny" + && policy collector ingest == "one_factor" + && policy collector "${ingest}?_msg_field=MESSAGE" == "one_factor" + && policy "user:someone" ingest == "deny" + && policy "user:someone" "${ingest}?_msg_field=MESSAGE" == "deny" + && policy "user:someone" "/insert/jsonline" == "deny" && policy "user:someone" "/" == "one_factor" - && policy "user:someone" "/select/logsql/query" == "one_factor" - && policy "user:someone" "/insert/opentelemetry/v1/logs" == "one_factor"; + && policy "user:someone" "/select/logsql/query" == "one_factor"; } { # Single host: the store is scraped once, by its published name.