swarm-authelia: admit only the collector anywhere on the metrics store's name
No access rule covered the push route on `metrics.<swarm>`, so a request there fell to `default_policy = one_factor`: any logged-in session cookie for the swarm could write samples into VictoriaMetrics. The name now gets two rules with no `resources`: the collector's client at `one_factor`, then `deny`. Only the push route and `/metrics` on that vhost send an authelia subrequest (the read API, Grafana's datasource included, is served with no check), and the collector is the one caller of both, so a domain-wide pair covers them with no path or query regex to slip past. It replaces the per-path `/metrics` pair the published target would otherwise derive for that name. Both rules sit ahead of the apex and store-UI rules, and authelia matches `domain` exactly, so they shadow no other name. Authelia 4.39.20, read from source: a rule with no `domain` wildcard matches with `strings.EqualFold` (access_control_domain.go), an empty `resources` or `subject` list matches everything (access_control_rule.go), and the first matching rule wins (authorizer.go). Fixes #5218
This commit is contained in:
parent
338a767a61
commit
223cca2b56
4 changed files with 108 additions and 39 deletions
|
|
@ -61,7 +61,7 @@ let
|
|||
collectorClientId = config.services.hyperhive.swarm.otel.clientId;
|
||||
collectorRegistered = lib.any (c: c.id == collectorClientId) cfg.oidc.clients;
|
||||
|
||||
# Every published metrics endpoint: the collector's client, and nobody else.
|
||||
# `target`, for the collector's client and nobody else.
|
||||
#
|
||||
# The `deny` is the protection, and it has to be a rule of its own. A rule
|
||||
# with a `subject` does not match a subject outside it: authelia skips it and
|
||||
|
|
@ -71,38 +71,52 @@ let
|
|||
# between a browser session and this data: `authn_strategies` on the authz
|
||||
# endpoint also accepts `CookieSession`, and a cookie carries no audience.
|
||||
# Until a collector is registered, the `deny` is the only rule.
|
||||
#
|
||||
collectorOnly =
|
||||
target:
|
||||
lib.optional collectorRegistered (
|
||||
target
|
||||
// {
|
||||
policy = "one_factor";
|
||||
subject = [ "oauth2:client:${collectorClientId}" ];
|
||||
}
|
||||
)
|
||||
++ [ (target // { policy = "deny"; }) ];
|
||||
|
||||
# The metrics store's name, every path. Only its push route and `/metrics`
|
||||
# consult authelia (its read API is served with no check), and the
|
||||
# collector is the one caller of both, so the rules take no `resources`
|
||||
# and leave no path or query form to fall through to `default_policy`.
|
||||
# ⚠️ An authenticated location added to that vhost for anyone else is
|
||||
# refused here until it gets a rule ahead of the `deny`.
|
||||
metricsStoreDomain = config.services.hyperhive.swarm.victoriametrics.domain;
|
||||
metricsStoreRules = collectorOnly { domain = metricsStoreDomain; };
|
||||
|
||||
# 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.
|
||||
metricsRules = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
job: url:
|
||||
let
|
||||
# The split `parsePublished` in ./swarm-otel.nix makes for the scrape
|
||||
# job; the two must agree on what the URL's host and path are.
|
||||
parts = builtins.match "https://([^/]+)(/.*)" url;
|
||||
target = {
|
||||
domain = builtins.elemAt parts 0;
|
||||
# ⚠️ Authelia matches `resources` against the cleaned path PLUS the
|
||||
# query string, while nginx's `= <path>` location ignores the query.
|
||||
# A bare `^<path>$` therefore misses `<path>?x`, which the location
|
||||
# still serves, and that request falls through to `default_policy`.
|
||||
resources = [ "^${lib.escapeRegex (builtins.elemAt parts 1)}([?].*)?$" ];
|
||||
};
|
||||
in
|
||||
if parts == null then
|
||||
throw "swarm.otel.publishedScrapeTargets.${job} = \"${url}\" is not an https URL with a path; authelia's access rule for it cannot be derived."
|
||||
else
|
||||
lib.optional collectorRegistered (
|
||||
target
|
||||
// {
|
||||
policy = "one_factor";
|
||||
subject = [ "oauth2:client:${collectorClientId}" ];
|
||||
# An entry on the metrics store's name is covered by `metricsStoreRules`.
|
||||
metricsRules = lib.filter (r: r.domain != metricsStoreDomain) (
|
||||
lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
job: url:
|
||||
let
|
||||
# The split `parsePublished` in ./swarm-otel.nix makes for the scrape
|
||||
# job; the two must agree on what the URL's host and path are.
|
||||
parts = builtins.match "https://([^/]+)(/.*)" url;
|
||||
in
|
||||
if parts == null then
|
||||
throw "swarm.otel.publishedScrapeTargets.${job} = \"${url}\" is not an https URL with a path; authelia's access rule for it cannot be derived."
|
||||
else
|
||||
collectorOnly {
|
||||
domain = builtins.elemAt parts 0;
|
||||
# ⚠️ Authelia matches `resources` against the cleaned path PLUS the
|
||||
# query string, while nginx's `= <path>` location ignores the query.
|
||||
# A bare `^<path>$` therefore misses `<path>?x`, which the location
|
||||
# still serves, and that request falls through to `default_policy`.
|
||||
resources = [ "^${lib.escapeRegex (builtins.elemAt parts 1)}([?].*)?$" ];
|
||||
}
|
||||
)
|
||||
++ [ (target // { policy = "deny"; }) ]
|
||||
) config.services.hyperhive.swarm.otel.publishedScrapeTargets
|
||||
) config.services.hyperhive.swarm.otel.publishedScrapeTargets
|
||||
)
|
||||
);
|
||||
|
||||
# Upstream's `services.authelia.instances.<name>` derives the unit,
|
||||
|
|
@ -1138,9 +1152,10 @@ in
|
|||
# shadowed by a broader domain rule added later.
|
||||
rules =
|
||||
# The collector's client where one is registered, then
|
||||
# `deny` — see `metricsRules` above, which is where the
|
||||
# `deny` — see `collectorOnly` above, which is where the
|
||||
# reasoning for both halves lives.
|
||||
metricsRules
|
||||
++ metricsStoreRules
|
||||
# Also guarded on the domain being set: without it a null
|
||||
# apex would render a rule matching the string "null".
|
||||
++ lib.optional (deployCfg.swarm-controller.enable && swarmDomain != null) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue