feat(3167): authelia gates the swarm UI on an operators group

default_policy = one_factor means 'any authenticated user', which is
authentication and not authorisation. The swarm UI is operator-only and
agents are getting authelia accounts of their own, so a session alone
would be enough to open it the day that lands - the vhost's auth_request
would be a check nobody fails.

Adds an access_control rule for the UI's domain requiring
subject = group:operators, present only when the UI is enabled. The
group name is a constant beside the rule: it is also what an operator
types into 'swarmctl user add --group', and a configurable name is one
more way for the rule and the account to disagree silently.
This commit is contained in:
atlas 2026-08-12 17:31:30 +02:00
commit d337fec565

View file

@ -37,6 +37,13 @@ let
hyperhiveCfg = config.services.hyperhive;
hyperhiveDomain = hyperhiveCfg.domain;
swarmDomain = hyperhiveCfg.swarm.domain;
uiCfg = hyperhiveCfg.swarm.ui;
# Group an account must hold to reach operator-only surfaces. Named
# here because this module writes the rule that enforces it and
# `swarmctl user add --group <this>` is what grants it — the two must
# agree, and one constant is how they stay agreeing.
operatorGroup = "operators";
# Upstream's `services.authelia.instances.<name>` derives the unit,
# user, group and StateDirectory from the instance name
@ -536,7 +543,26 @@ in
authentication_backend.file.path = cfg.usersFile;
access_control.default_policy = "one_factor";
# ⚠️ `one_factor` as the DEFAULT means "any authenticated
# user", which is authentication, not authorisation. The
# swarm UI is operator-only, and agents are getting
# authelia accounts of their own — so the day that lands,
# a session alone would be enough to open it. The rule
# below is what makes the distinction real; without it
# the vhost's `auth_request` is a check nobody fails.
#
# The group is a constant rather than an option: it is the
# value an operator types into `swarmctl user add --group`,
# and a configurable name is one more way for the rule and
# the account to disagree silently.
access_control = {
default_policy = "one_factor";
rules = lib.optional uiCfg.enable {
domain = uiCfg.domain;
subject = [ "group:${operatorGroup}" ];
policy = "one_factor";
};
};
# The cookie domain is the SWARM's domain, NOT authelia's
# own host: the session cookie has to be sent to the apps