Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ec0ce90fd | ||
|
|
4b6214305f |
11 changed files with 69 additions and 155 deletions
|
|
@ -69,11 +69,11 @@ provider, differentiated by roles and claims rather than by mechanism —
|
|||
there is one IdP and one auth path.
|
||||
|
||||
- **`deploy.authelia`** — run the container here.
|
||||
- **`swarm.authelia.url`** — where clients go to authenticate.
|
||||
Present on **every** hive, defaulting to this host's own instance only
|
||||
when this module is the thing running it; otherwise `null`, and a hive
|
||||
joining someone else's swarm sets it explicitly. Null means "no SSO
|
||||
configured", and consumers say so rather than guessing an address.
|
||||
- **`swarm.authelia.url`** — where clients go to authenticate. Present
|
||||
on **every** hive and the same value on all of them:
|
||||
`https://<swarm.authelia.domain>`, whether or not this host runs the
|
||||
container. Set it explicitly when joining a swarm whose IdP is under
|
||||
another name.
|
||||
|
||||
swarm-controller writes the users database, not by hand: hive-c0re
|
||||
creates and destroys agents continuously, so the subject set is dynamic.
|
||||
|
|
|
|||
|
|
@ -573,20 +573,6 @@ in
|
|||
host: see docs/swarm/ for which secret goes where.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# Without a provider URL there is nothing to discover against,
|
||||
# and the rendered unit would ask `null/.well-known/…`.
|
||||
assertion = autheliaUrl != null;
|
||||
message = ''
|
||||
The forge's SSO login source requires
|
||||
services.hyperhive.swarm.authelia.url — the base URL of the
|
||||
swarm's SSO provider.
|
||||
|
||||
It defaults to this host's own instance only when this host
|
||||
runs authelia. A hive that federates with a swarm sets it
|
||||
explicitly to wherever that provider lives.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl;
|
||||
message = ''
|
||||
|
|
|
|||
|
|
@ -968,20 +968,6 @@ in
|
|||
host: see docs/swarm/ for which secret goes where.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# Without a provider URL there is nothing to discover against, and
|
||||
# the rendered config would name `null` as its issuer.
|
||||
assertion = autheliaUrl != null;
|
||||
message = ''
|
||||
This homeserver's SSO login flow requires
|
||||
services.hyperhive.swarm.authelia.url — the base URL of the
|
||||
swarm's SSO provider.
|
||||
|
||||
It defaults to this host's own instance only when this host
|
||||
runs authelia. A hive that federates with a swarm sets it
|
||||
explicitly to wherever that provider lives.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# The callback URL must name the homeserver itself, and with no
|
||||
# gateway vhost there is no public name for it to be built from.
|
||||
|
|
|
|||
|
|
@ -457,23 +457,25 @@ in
|
|||
};
|
||||
|
||||
url = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if deployCfg.authelia.enable then "https://${cfg.domain}" else null;
|
||||
defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null'';
|
||||
type = lib.types.str;
|
||||
default = "https://${cfg.domain}";
|
||||
defaultText = lib.literalExpression ''"https://''${domain}"'';
|
||||
example = "https://auth.example.com";
|
||||
description = ''
|
||||
Base URL clients are sent to for authentication — the half of
|
||||
this module that exists on **every** hive, not just the one
|
||||
running the container.
|
||||
|
||||
Defaults to this host's own instance **only when this module is
|
||||
the thing running it**; in that case the URL is not a guess, it
|
||||
is where this module just put the container. Otherwise `null`,
|
||||
and a hive that federates with a swarm sets it explicitly to
|
||||
wherever the swarm's authelia lives. Null means "no SSO
|
||||
configured" and consumers say so rather than inventing an
|
||||
address — an endpoint baked in as a fallback is one that
|
||||
resolves cleanly and points at the wrong machine.
|
||||
Names {option}`domain`, and does **not** ask whether this host
|
||||
runs the container: a swarm has one SSO provider, so every hive
|
||||
addresses the same name and resolution decides where it is —
|
||||
dnsmasq locally on the host serving the vhost, the real network
|
||||
anywhere else. There is no loopback-vs-remote branch to get
|
||||
wrong, the same way {option}`services.hyperhive.swarm.otel.domain`
|
||||
has none.
|
||||
|
||||
Not having SSO is not a supported deployment: every swarm has an
|
||||
IdP, so this is never `null`.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -950,18 +952,6 @@ in
|
|||
# would reject it, but three layers away and at boot — naming both
|
||||
# sources here is the cheaper failure.
|
||||
++ [
|
||||
{
|
||||
# The bridge introspects by name, so a null URL becomes a nix
|
||||
# coercion error several files from its cause. Only reachable by
|
||||
# enabling authelia and clearing `url` by hand — an assertion
|
||||
# rather than a fallback, because a guessed URL that evaluates
|
||||
# cleanly is worse than a refused build.
|
||||
assertion = cfg.url != null;
|
||||
message =
|
||||
"services.hyperhive.swarm.authelia.url must not be null when authelia "
|
||||
+ "is enabled: swarm-authelia-bridge introspects at "
|
||||
+ "`\${url}/api/oidc/introspection` from inside its container.";
|
||||
}
|
||||
{
|
||||
assertion = lib.length (lib.unique (map (c: c.id) cfg.oidc.clients)) == lib.length cfg.oidc.clients;
|
||||
message =
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@ in
|
|||
|
||||
tokenEndpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = lib.optionalString (autheliaCfg.url != null) "${autheliaCfg.url}/api/oidc/token";
|
||||
default = "${autheliaCfg.url}/api/oidc/token";
|
||||
defaultText = lib.literalExpression ''"''${swarm.authelia.url}/api/oidc/token"'';
|
||||
description = ''
|
||||
The OIDC token endpoint the controller mints its own access
|
||||
|
|
@ -773,12 +773,13 @@ in
|
|||
{
|
||||
assertion = cfg.queue.tokenEndpoint != "";
|
||||
message = ''
|
||||
services.hyperhive.swarm.controller.queue.tokenEndpoint is unset,
|
||||
which means services.hyperhive.swarm.authelia.url is null.
|
||||
services.hyperhive.swarm.controller.queue.tokenEndpoint is unset.
|
||||
|
||||
The controller mints its own access token before it may connect
|
||||
to the queue, so it needs to know where the swarm's identity
|
||||
provider lives — set that URL, or set this endpoint directly.
|
||||
It defaults from services.hyperhive.swarm.authelia.url, so this
|
||||
only happens when it has been cleared by hand. The controller
|
||||
mints its own access token before it may connect to the queue,
|
||||
so it needs to know where the swarm's identity provider lives —
|
||||
set this endpoint directly.
|
||||
'';
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -109,18 +109,6 @@ let
|
|||
# operator sees, not a coercion error from here.
|
||||
domainBase = if swarmDomain == null then "invalid" else swarmDomain;
|
||||
|
||||
# Is SSO configured for this SWARM. Swarm-wide by construction — `swarm.*` is
|
||||
# identical on every host — and the option's own description is what makes
|
||||
# this the right question to ask: a null URL means "no SSO configured".
|
||||
#
|
||||
# 🩸 A subject of an assertion below, NOT a gate. Dropping the OIDC block when
|
||||
# this is false looks conservative and is the outage: `disable_login_form` is
|
||||
# unconditional a few hundred lines down, so a Grafana with no OIDC settings
|
||||
# is a Grafana with no login of any kind, arrived at silently. SSO is a
|
||||
# requirement of running this service, so an unconfigured swarm fails to
|
||||
# build and says which option to set.
|
||||
ssoConfigured = hyperhiveCfg.swarm.authelia.url != null;
|
||||
|
||||
# A reader of the store is defined by holding a certificate the store
|
||||
# accepts, never by standing next to it — the rule
|
||||
# ./glue-matrix-bao-token.nix states in full.
|
||||
|
|
@ -448,34 +436,11 @@ in
|
|||
# host that runs authelia, and this whole block is gated on the host that
|
||||
# runs Grafana. ./glue-grafana-oidc-client.nix is where it moved to.
|
||||
|
||||
# Both arms are what used to be a silent gate, and both fire only where
|
||||
# Grafana runs — this whole block is under `deploy.grafana.enable`. The
|
||||
# binding itself is asserted rather than a copy of its formula, the way
|
||||
# ./swarm-nats.nix's own `autheliaUrl` arm does it: two spellings of one
|
||||
# boolean is two places for a future edit to land in only one.
|
||||
# This arm fires only where Grafana runs — this whole block is under
|
||||
# `deploy.grafana.enable`. SSO itself needs no assertion any more:
|
||||
# `swarm.authelia.url` is non-nullable, so every swarm has one and the
|
||||
# OIDC block below always names a real endpoint.
|
||||
assertions = [
|
||||
{
|
||||
# SSO is not optional for this service, and the reason is a hundred
|
||||
# lines below in `auth.disable_login_form = true`: Grafana ships an
|
||||
# `admin`/`admin` account on a public vhost, so the password box is
|
||||
# off whatever the topology. Emitting no OIDC block when the swarm
|
||||
# names no IdP therefore produces a container with no way in at all —
|
||||
# a state no log names, since nothing failed. Failing the build and
|
||||
# naming the option is the only report that reaches anyone.
|
||||
assertion = ssoConfigured;
|
||||
message = ''
|
||||
services.hyperhive.deploy.grafana.enable requires
|
||||
services.hyperhive.swarm.authelia.url — Grafana's only login is SSO,
|
||||
because its local login form is disabled unconditionally (it ships
|
||||
an admin/admin account and its vhost is on the public gateway).
|
||||
|
||||
It defaults to this host's own instance only when this host runs
|
||||
authelia. A hive that federates with a swarm sets it explicitly to
|
||||
wherever that provider lives. Grafana exchanges its authorization
|
||||
code at `''${url}/api/oidc/token` from inside its container, so a
|
||||
null URL leaves no endpoint to name.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# The other half of one login: the OIDC block names a `$__file{}` that
|
||||
# `swarm-bao-grafana-oidc.service` below writes, and that unit reads
|
||||
|
|
|
|||
|
|
@ -511,18 +511,6 @@ in
|
|||
to have this host mint all four.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = autheliaUrl != null;
|
||||
message = ''
|
||||
services.hyperhive.deploy.nats.enable requires
|
||||
services.hyperhive.swarm.authelia.url — the queue authenticates
|
||||
clients by validating tokens that authelia issued.
|
||||
|
||||
It defaults to this host's own instance only when this host
|
||||
runs authelia. A hive that federates with a swarm sets it
|
||||
explicitly to wherever that provider lives.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# One declaration, two readers. The queue knows which client id it
|
||||
|
|
|
|||
|
|
@ -1027,22 +1027,6 @@ in
|
|||
should not be described as both.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# A hive proves who it is with a token this provider mints, so
|
||||
# there is no version of this collector that runs without one.
|
||||
# Stated as an assertion rather than a fallback because a guessed
|
||||
# issuer URL evaluates cleanly and refuses every hive at runtime.
|
||||
assertion = hyperhiveCfg.swarm.authelia.url != null;
|
||||
message = ''
|
||||
services.hyperhive.deploy.swarm-otel.enable is true but
|
||||
services.hyperhive.swarm.authelia.url is null: every hive
|
||||
authenticates to this collector as itself, and the token comes
|
||||
from the swarm's identity provider.
|
||||
|
||||
Point authelia.url at the swarm's provider, or enable
|
||||
services.hyperhive.swarm.authelia on the host that runs it.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# A port collision between two listeners on one host is a runtime
|
||||
# coin toss with nothing in any log — the failure this whole
|
||||
|
|
|
|||
|
|
@ -500,8 +500,8 @@ in
|
|||
options.services.hyperhive.swarm.statusPublish = {
|
||||
tokenEndpoint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if swarmCfg.authelia.url != null then "${swarmCfg.authelia.url}/api/oidc/token" else null;
|
||||
defaultText = lib.literalExpression ''"''${swarm.authelia.url}/api/oidc/token" when that URL is set, else null'';
|
||||
default = "${swarmCfg.authelia.url}/api/oidc/token";
|
||||
defaultText = lib.literalExpression ''"''${swarm.authelia.url}/api/oidc/token"'';
|
||||
example = "https://auth.example.com/api/oidc/token";
|
||||
description = ''
|
||||
The swarm IdP's OAuth2 token endpoint. This hive mints a
|
||||
|
|
|
|||
|
|
@ -70,17 +70,6 @@ let
|
|||
swarm.authelia.url = "https://auth.example.invalid";
|
||||
};
|
||||
|
||||
# The mirror image: the identity is placed, and the swarm names no IdP. The
|
||||
# other half of "SSO must always be configured", and isolated the same way —
|
||||
# exactly one thing wrong, so the arm reads one refusal.
|
||||
grafanaNoSso = hive {
|
||||
deploy.grafana.enable = true;
|
||||
deploy.grafana.plugins = [ ];
|
||||
deploy.grafana.package = pkgs.emptyDirectory;
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
};
|
||||
|
||||
# Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its
|
||||
# two reasons. An assertion is a config VALUE until something forces it —
|
||||
# `.config` never throws — so a fixture in a state the module refuses is
|
||||
|
|
@ -164,19 +153,7 @@ let
|
|||
&& !(grafanaRefusedFor grafanaNoIdentity "swarm.authelia.url");
|
||||
}
|
||||
{
|
||||
# "SSO must always be configured", as an eval-time refusal rather than a
|
||||
# gate. A null URL used to drop the OIDC block silently, and
|
||||
# `disable_login_form` is unconditional a hundred lines below it, so that
|
||||
# combination produced a Grafana with no SSO and no password box — an
|
||||
# outage whose cause is a boolean that evaluated to false at build time
|
||||
# and left no trace. Same isolation as the arm above, mirrored.
|
||||
name = "a grafana host in a swarm with no IdP is refused, naming swarm.authelia.url";
|
||||
ok =
|
||||
grafanaRefusedFor grafanaNoSso "services.hyperhive.swarm.authelia.url"
|
||||
&& !(grafanaRefusedFor grafanaNoSso "deploy.bao.clientCertFile");
|
||||
}
|
||||
{
|
||||
# Without this the two arms above prove nothing: a refusal that fires on
|
||||
# Without this the arm above proves nothing: a refusal that fires on
|
||||
# every host is not a check, and both of these are hosts a swarm is
|
||||
# expected to have. Read through the same helper, so a message that
|
||||
# stopped naming its option would fail the arms above rather than pass
|
||||
|
|
|
|||
|
|
@ -59,6 +59,15 @@ let
|
|||
swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
|
||||
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
|
||||
};
|
||||
|
||||
# A hive running NOTHING of the swarm's own services — no IdP here, no
|
||||
# `swarm.authelia.url` set by hand. The whole point of the fixture is what it
|
||||
# does *not* say: it is the shape whose IdP address used to be null, and
|
||||
# before that a co-location-derived guess.
|
||||
autheliaNotColocated = hive { };
|
||||
# The same swarm's IdP host, for the arm that the two agree.
|
||||
autheliaColocated = hive { deploy.authelia.enable = true; };
|
||||
|
||||
cases = [
|
||||
{
|
||||
# Reads the RENDERED settings, not the option: `calloutBlocks {…} // {
|
||||
|
|
@ -116,6 +125,34 @@ let
|
|||
name = "a consumer of authelia's host client-secret dir renders it from the deploy namespace";
|
||||
ok = lib.hasInfix "/var/lib/nixos-containers/swarm-authelia/var/lib/authelia-swarm/oidc-clients/" autheliaOldPath.systemd.services.swarm-nats-auth-secrets.script;
|
||||
}
|
||||
{
|
||||
# Config NAMES the IdP; it never computes where the IdP is. Both arms
|
||||
# matter together: the address is the swarm's name on a hive that runs
|
||||
# nothing, and it is the SAME string on the hive that serves the vhost —
|
||||
# so a re-introduced co-location branch shows up as the two disagreeing
|
||||
# rather than as a value that merely looks plausible on one of them.
|
||||
name = "the swarm IdP address is its domain on every hive, co-located or not";
|
||||
ok =
|
||||
autheliaNotColocated.services.hyperhive.swarm.authelia.url
|
||||
== "https://${autheliaNotColocated.services.hyperhive.swarm.authelia.domain}"
|
||||
&&
|
||||
autheliaColocated.services.hyperhive.swarm.authelia.url
|
||||
== autheliaNotColocated.services.hyperhive.swarm.authelia.url;
|
||||
}
|
||||
{
|
||||
# …and the name it resolves through is a DOMAIN, not a host this config
|
||||
# picked. `127.0.0.1` is what the old default rendered on the IdP host,
|
||||
# the bridge address is what the queue's defaults still render, and
|
||||
# neither is an address a client may be handed: the domain is allowed to
|
||||
# resolve differently in different places, which is the whole property.
|
||||
name = "the swarm IdP address names no host address";
|
||||
ok =
|
||||
let
|
||||
url = autheliaNotColocated.services.hyperhive.swarm.authelia.url;
|
||||
bridgeIp = autheliaNotColocated.services.hyperhive.network.bridgeIp;
|
||||
in
|
||||
!(lib.hasInfix "127.0.0.1" url) && !(lib.hasInfix "localhost" url) && !(lib.hasInfix bridgeIp url);
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "nats-authelia" cases
|
||||
|
|
|
|||
Loading…
Reference in a new issue