deploy: move the SSO provider toggle
The largest of these moves: sixteen references spelled through `let` aliases across eight modules, plus eight more spelled as a path, plus five documentation pages. authelia is also the clearest case for why the two namespaces exist. `swarm.authelia.url` is needed by *every* hive in the swarm — it says where to send a browser to authenticate — while running the container is the business of exactly one host. The client half and the server half were sharing a namespace whose whole contract is "identical everywhere", and only one of them could honour it. `swarm.authelia.oidc.clients` stays where it is for the same reason: several modules register a client there, gated on authelia running here, and the registry itself is what the service *is* rather than a decision about this machine. One sweep note worth recording: a grep for `swarm.authelia.enable` misses `swarmCfg.authelia.enable`, because the prefix is whatever the reading file bound. Grepping the suffix `.authelia.enable` finds both, and found a reference in swarm.nix that the path-shaped pattern did not.
This commit is contained in:
parent
0b7357d4b8
commit
37ca7676d6
16 changed files with 77 additions and 50 deletions
|
|
@ -14,7 +14,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
|
|||
| `chat.<swarm>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` |
|
||||
| `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
|
||||
| `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
|
||||
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `swarm.authelia.enable` |
|
||||
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `deploy.authelia` |
|
||||
| `<swarm>/` | `<swarm>` | swarm-ui dist (static), behind an authelia subrequest | `deploy.swarm-ui` |
|
||||
|
||||
The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees.
|
||||
|
|
@ -64,7 +64,7 @@ Each location carries a duplicated `auth_basic` block (separate locations don't
|
|||
- `<hive-domain>` → `127.0.0.1`
|
||||
- `forge.<swarm>` → `127.0.0.1` (when forge.behindGateway)
|
||||
- `chat.<swarm>` → `127.0.0.1` (when matrix.gatewayHost set)
|
||||
- `auth.<swarm>` → `127.0.0.1` (when swarm.authelia.enable)
|
||||
- `auth.<swarm>` → `127.0.0.1` (when deploy.authelia)
|
||||
|
||||
`lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off.
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ echo "hunter2" | hivectl gateway create-user mara --password-stdin
|
|||
hivectl gateway list-users
|
||||
```
|
||||
|
||||
### 3 · Swarm SSO (only when `swarm.authelia.enable`)
|
||||
### 3 · Swarm SSO (only when `deploy.authelia`)
|
||||
|
||||
⚠️ **Required to finish the install, not optional.** Authelia treats an
|
||||
empty user store as a fatal startup error, so until this runs the
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ One authelia per swarm, in a `swarm-authelia` container, at
|
|||
provider, differentiated by roles and claims rather than by mechanism —
|
||||
there is one IdP and one auth path.
|
||||
|
||||
- **`swarm.authelia.enable`** — run the container here. Defaults from
|
||||
- **`deploy.authelia`** — run the container here. Defaults from
|
||||
`swarm.enableRequiredServices`.
|
||||
- **`swarm.authelia.url`** — where clients are sent to authenticate.
|
||||
Present on **every** hive, defaulting to this host's own instance only
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ separate `enable` would be a second fact free to disagree with the first.
|
|||
|
||||
authelia binds loopback only. The **gateway** on the host running it
|
||||
publishes it as `auth.<swarm.domain>` — vhost, dnsmasq record and TLS
|
||||
name all follow `swarm.authelia.enable`, so there is nothing to turn on
|
||||
name all follow `deploy.authelia`, so there is nothing to turn on
|
||||
separately. (Details, including why a client hive must not declare that
|
||||
vhost: [`../gateway.md`](../gateway.md).)
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ in
|
|||
[ "services" "hyperhive" "swarm" "ui" "enable" ]
|
||||
[ "services" "hyperhive" "deploy" "swarm-ui" ]
|
||||
)
|
||||
(lib.mkRenamedOptionModule
|
||||
[ "services" "hyperhive" "swarm" "authelia" "enable" ]
|
||||
[ "services" "hyperhive" "deploy" "authelia" ]
|
||||
)
|
||||
];
|
||||
|
||||
options.services.hyperhive.deploy = {
|
||||
|
|
@ -97,6 +101,24 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
authelia = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Run the swarm's authelia in a `swarm-authelia` container on this
|
||||
host. {option}`services.hyperhive.swarm.enableRequiredServices`
|
||||
turns this on — a swarm has one SSO provider, and that says it
|
||||
lives here.
|
||||
|
||||
With it off, this hive is a *client*:
|
||||
{option}`services.hyperhive.swarm.authelia.url` still points at
|
||||
whoever runs it, and no container is created. That asymmetry is
|
||||
why the two live in different namespaces — every hive needs the
|
||||
client half, only one runs the server half.
|
||||
'';
|
||||
};
|
||||
|
||||
controller = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ let
|
|||
# assertion below turns into an eval failure rather than a discovery
|
||||
# request to `null/.well-known/...`.
|
||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||
deployCfg = config.services.hyperhive.deploy;
|
||||
autheliaUrl = autheliaCfg.url;
|
||||
autheliaDiscoveryUrl = "${toString autheliaUrl}/.well-known/openid-configuration";
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ let
|
|||
# authelia, so the secret can be moved without an operator. The other
|
||||
# two cases (swarm side via swarmctl, remote hive) leave
|
||||
# `clientSecretFile` to be set explicitly — see docs/swarm/.
|
||||
ssoLocal = autheliaCfg.enable;
|
||||
ssoLocal = deployCfg.authelia;
|
||||
|
||||
# Where the plaintext lands inside the forge container. Under
|
||||
# /var/lib rather than /run: the forge may start before the delivery
|
||||
|
|
|
|||
|
|
@ -25,13 +25,14 @@ let
|
|||
# below turns into an eval failure rather than a discovery request to
|
||||
# `null/.well-known/…`.
|
||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||
deployCfg = config.services.hyperhive.deploy;
|
||||
autheliaUrl = autheliaCfg.url;
|
||||
|
||||
# The all-local case: this host runs BOTH the homeserver and the swarm's
|
||||
# authelia, so the secret can be moved without an operator. The other
|
||||
# two cases (swarm side, remote hive) leave `clientSecretFile` to be set
|
||||
# explicitly — same split the forge module documents.
|
||||
ssoLocal = autheliaCfg.enable;
|
||||
ssoLocal = deployCfg.authelia;
|
||||
|
||||
# Where the plaintext lands inside the matrix container. Under /var/lib
|
||||
# rather than /run: the homeserver may start before the delivery unit on
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ in
|
|||
# ran its first boot on — so they belong to the mode that asserts this
|
||||
# box is the whole deployment, not to the options' own `default`.
|
||||
#
|
||||
# Deriving them from `swarm.nats.enable` / `swarm.authelia.enable`
|
||||
# Deriving them from `swarm.nats.enable` / `deploy.authelia`
|
||||
# inside those defaults is the mixing this file exists to prevent: the
|
||||
# option would be describing a deployment shape instead of describing
|
||||
# itself, and "what does all-local turn on?" would stop having one
|
||||
|
|
|
|||
|
|
@ -231,9 +231,7 @@ in
|
|||
clientSecretFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default =
|
||||
if
|
||||
config.services.hyperhive.swarm.authelia.enable && config.services.hyperhive.hiveName != null
|
||||
then
|
||||
if config.services.hyperhive.deploy.authelia && config.services.hyperhive.hiveName != null then
|
||||
"${config.services.hyperhive.swarm.authelia.hostClientSecretDir}/"
|
||||
+ "${config.services.hyperhive.swarm.authelia.hiveClientPrefix}${config.services.hyperhive.hiveName}.secret"
|
||||
else
|
||||
|
|
@ -284,6 +282,7 @@ in
|
|||
let
|
||||
otel = config.services.hyperhive.otel;
|
||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||
deployCfg = config.services.hyperhive.deploy;
|
||||
swarmOtelCfg = config.services.hyperhive.swarm.otel;
|
||||
hiveName = config.services.hyperhive.hiveName;
|
||||
listen = "${config.services.hyperhive.network.bridgeIp}:${toString otel.collector.port}";
|
||||
|
|
@ -580,8 +579,8 @@ in
|
|||
# Only when the minting container is on THIS host. Elsewhere the file
|
||||
# is operator-provided and there is no local unit to order against —
|
||||
# naming one that does not exist orders nothing, silently.
|
||||
after = lib.optional autheliaCfg.enable "container@${autheliaCfg.machine}.service";
|
||||
requires = lib.optional autheliaCfg.enable "container@${autheliaCfg.machine}.service";
|
||||
after = lib.optional deployCfg.authelia "container@${autheliaCfg.machine}.service";
|
||||
requires = lib.optional deployCfg.authelia "container@${autheliaCfg.machine}.service";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
|
|
|
|||
|
|
@ -345,22 +345,12 @@ let
|
|||
'';
|
||||
in
|
||||
{
|
||||
# `enable` moved to `services.hyperhive.deploy.authelia` — see
|
||||
# ./deploy.nix. Whether this host runs the swarm's SSO provider is a
|
||||
# deployment decision; what stays here is what authelia IS, including
|
||||
# `url` and the OIDC client registry every hive needs as a *client*
|
||||
# whether or not it runs the container.
|
||||
options.services.hyperhive.swarm.authelia = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Run the swarm's authelia in a `swarm-authelia` container on this
|
||||
host. `services.hyperhive.swarm.enableRequiredServices` turns
|
||||
this on — a swarm has one SSO provider, and that says it lives
|
||||
here.
|
||||
|
||||
With it off, this hive is a *client*: `url` below still points
|
||||
at whoever runs it, and no container is created.
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.authelia;
|
||||
|
|
@ -437,7 +427,7 @@ in
|
|||
|
||||
url = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if cfg.enable then "https://${cfg.domain}" else null;
|
||||
default = if deployCfg.authelia then "https://${cfg.domain}" else null;
|
||||
defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null'';
|
||||
example = "https://auth.example.com";
|
||||
description = ''
|
||||
|
|
@ -820,7 +810,7 @@ in
|
|||
bridgeUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
readOnly = true;
|
||||
default = if cfg.enable then "http://127.0.0.1:${toString cfg.bridgePort}" else null;
|
||||
default = if deployCfg.authelia then "http://127.0.0.1:${toString cfg.bridgePort}" else null;
|
||||
defaultText = lib.literalExpression ''if enable then "http://127.0.0.1:''${bridgePort}" else null'';
|
||||
description = ''
|
||||
Where `swarm-authelia-bridge` answers, **as seen from this
|
||||
|
|
@ -839,7 +829,7 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
|
||||
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia) {
|
||||
# The derived half of the client list, declared the same way an
|
||||
# operator declares one. Everything downstream then reads a single
|
||||
# uniformly-typed `cfg.oidc.clients` and cannot tell the parts apart —
|
||||
|
|
@ -974,7 +964,7 @@ in
|
|||
|
||||
# Authelia's own gateway surface: the vhost that fronts it and the
|
||||
# name the hive resolver answers for. Both live here rather than in
|
||||
# the gateway, and both are inside `cfg.enable` — that guard is the
|
||||
# the gateway, and both are inside `deployCfg.authelia` — that guard is the
|
||||
# load-bearing part.
|
||||
#
|
||||
# ⚠️ Every hive in a swarm knows `authelia.url`, but only the host
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ let
|
|||
# the right behaviour is for `swarmctl user add` to fail saying the
|
||||
# value is unset. A guessed path would resolve cleanly and write a file
|
||||
# nothing reads, which is the failure mode that costs an afternoon.
|
||||
autheliaEnv = lib.optionalAttrs autheliaCfg.enable {
|
||||
autheliaEnv = lib.optionalAttrs deployCfg.authelia {
|
||||
# The CONFIGURED authelia, not whatever is on PATH: the argon2
|
||||
# parameters baked into a hash have to match the verifier's.
|
||||
SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia";
|
||||
|
|
@ -451,7 +451,7 @@ in
|
|||
|
||||
authBridgeUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if autheliaCfg.enable then autheliaCfg.bridgeUrl else null;
|
||||
default = if deployCfg.authelia then autheliaCfg.bridgeUrl else null;
|
||||
defaultText = lib.literalExpression ''
|
||||
authelia's own `bridgeUrl` when this host also runs
|
||||
`swarm-authelia`, else null
|
||||
|
|
@ -499,7 +499,7 @@ in
|
|||
# client list would be a second source of truth for a string whose
|
||||
# mismatch is an opaque 401 from the token endpoint. Same shape as the
|
||||
# queue's own client declaration.
|
||||
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
|
||||
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia [
|
||||
{
|
||||
id = queueClientId;
|
||||
description = "HyperHive swarm controller";
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ let
|
|||
# The all-local case: this host runs BOTH Grafana and the swarm's authelia,
|
||||
# so the minted secret can be moved without an operator. Same split the
|
||||
# forge and matrix modules document.
|
||||
ssoLocal = deployCfg.grafana && autheliaCfg.enable;
|
||||
ssoLocal = deployCfg.grafana && deployCfg.authelia;
|
||||
autheliaUrl = toString autheliaCfg.url;
|
||||
|
||||
# Where the plaintext lands inside the container. Under /var/lib rather
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
let
|
||||
cfg = config.services.hyperhive.swarm.nats;
|
||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||
deployCfg = config.services.hyperhive.deploy;
|
||||
autheliaUrl = autheliaCfg.url;
|
||||
networkCfg = config.services.hyperhive.network;
|
||||
# Read even when the controller runs on a different host: what is needed
|
||||
|
|
@ -514,7 +515,7 @@ in
|
|||
# authorization grant 'client_credentials'`. Introspection — which
|
||||
# is all the responder needs today — worked throughout, which is why
|
||||
# nothing was visibly broken while the comment was untrue.
|
||||
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
|
||||
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia [
|
||||
{
|
||||
id = cfg.clientId;
|
||||
description = "HyperHive swarm queue";
|
||||
|
|
@ -767,7 +768,7 @@ in
|
|||
# being up says nothing about whether its in-container secrets unit
|
||||
# has finished. The wait in the script is what actually closes it;
|
||||
# this only stops us spinning for the full timeout on every boot.
|
||||
++ lib.optional autheliaCfg.enable "container@${autheliaCfg.machine}.service";
|
||||
++ lib.optional deployCfg.authelia "container@${autheliaCfg.machine}.service";
|
||||
requires = lib.optional cfg.autoGenerateCallout "swarm-nats-callout-keys.service";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
|
|
|
|||
|
|
@ -517,7 +517,7 @@ in
|
|||
# declaration would break every hive that runs a collector and
|
||||
# publishes nothing.
|
||||
services.hyperhive.swarm.authelia.oidc.clients =
|
||||
lib.mkIf (autheliaCfg.enable && cfg.publishedScrapeTargets != { })
|
||||
lib.mkIf (deployCfg.authelia && cfg.publishedScrapeTargets != { })
|
||||
[
|
||||
{
|
||||
id = cfg.clientId;
|
||||
|
|
@ -555,7 +555,7 @@ in
|
|||
# after it. On a fresh swarm that is a permanent stall presenting as
|
||||
# "metrics are broken", several layers from its cause.
|
||||
systemd.services.swarm-otel-oidc-secret =
|
||||
lib.mkIf (autheliaCfg.enable && cfg.publishedScrapeTargets != { })
|
||||
lib.mkIf (deployCfg.authelia && cfg.publishedScrapeTargets != { })
|
||||
{
|
||||
description = "deliver the swarm collector's OIDC client secret from authelia";
|
||||
after = [ "container@${autheliaCfg.machine}.service" ];
|
||||
|
|
|
|||
|
|
@ -24,9 +24,13 @@ in
|
|||
description = ''
|
||||
Host the swarm's shared services on this hive. The services that
|
||||
exist once per swarm rather than once per hive and are *optional*
|
||||
— the matrix homeserver, the SSO provider — have their `enable`
|
||||
asserted from this, so a swarm's service host is declared in one
|
||||
place.
|
||||
— the matrix homeserver, the SSO provider, the queue, the metrics
|
||||
and log stores — have their toggle asserted from this, so a
|
||||
swarm's service host is declared in one place.
|
||||
|
||||
Those toggles live in two namespaces and the split is deliberate:
|
||||
{option}`services.hyperhive.deploy.*` for "does THIS host run it",
|
||||
`swarm.*.enable` for the ones not yet moved. See ./deploy.nix.
|
||||
|
||||
The forge is swarm-wide too but has nothing to assert: it is the
|
||||
canonical store for the meta flake and every agent's config repo,
|
||||
|
|
@ -46,7 +50,6 @@ in
|
|||
# operator who hasn't spoken, yields to one who has.
|
||||
config.services.hyperhive.swarm = {
|
||||
matrix.enable = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
authelia.enable = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
# The queue. Added later than the two above and missed at the time —
|
||||
# this file predates the `swarm-nats` container by nine days and had
|
||||
# not been revisited since, so its absence was sequence rather than
|
||||
|
|
@ -75,16 +78,26 @@ in
|
|||
# this is ./otel.nix's existing per-hive option).
|
||||
config.services.hyperhive.otel.enable = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
|
||||
# The rest of the shared services, deriving from the same switch as the
|
||||
# `swarm.*` ones above. They read differently only because "does THIS
|
||||
# host run it" lives in `deploy.*` (./deploy.nix) — `swarm.*` has to be
|
||||
# identical on every host, and these are exactly the values that must
|
||||
# differ.
|
||||
#
|
||||
# authelia: a swarm has one SSO provider, and this says it lives here.
|
||||
# With it off the hive is a *client* — `swarm.authelia.url` still points
|
||||
# at whoever runs it.
|
||||
config.services.hyperhive.deploy.authelia = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
|
||||
# The metrics pair, deriving together on purpose: a store with no UI is
|
||||
# unreadable and a UI with no store is empty, so there is no sensible
|
||||
# deployment that takes one and not the other from this switch. An
|
||||
# operator who wants exactly one still sets it directly, which
|
||||
# `mkDefault` allows.
|
||||
#
|
||||
# The log store derives from the same switch for the same reason as the
|
||||
# rest: a hive that is not the service host is a *client* of it, not a
|
||||
# second one.
|
||||
config.services.hyperhive.deploy.victoriametrics = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
config.services.hyperhive.deploy.victorialogs = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
config.services.hyperhive.deploy.grafana = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
|
||||
# The log store, from the same switch for the same reason as the rest: a
|
||||
# hive that is not the service host is a *client* of it, not a second one.
|
||||
config.services.hyperhive.deploy.victorialogs = lib.mkDefault swarmCfg.enableRequiredServices;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ let
|
|||
# unrepresentable rather than merely detected: a default set is all or
|
||||
# nothing, and the assertion is then only ever about what an operator
|
||||
# typed.
|
||||
queueLocal = swarmCfg.nats.enable && swarmCfg.authelia.enable && cfg.hiveName != null;
|
||||
queueLocal = swarmCfg.nats.enable && deployCfg.authelia && cfg.hiveName != null;
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.swarm.hives = lib.mkOption {
|
||||
|
|
|
|||
Loading…
Reference in a new issue