diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 8aca2d44..b314cd53 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -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 ` is what grants it — the two must + # agree, and one constant is how they stay agreeing. + operatorGroup = "operators"; # Upstream's `services.authelia.instances.` 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