feat(3150): wire tuwunel's identity_provider + register the authelia client
Stage 1's actual login path. The provider entry is rendered only when sso.enable is set, via optionalAttrs rather than an empty list, so a hive that has not opted in renders byte-identical tuwunel settings. The secret reaches tuwunel through LoadCredential rather than as a direct path: upstream requires the file to survive systemd sandboxing and suggests /etc/tuwunel/, which this container has no writable etc for. The registration token two units below already solves it the same way, and for the same reason -- DynamicUser and PrivateUsers stay intact. Three assertions fail at eval instead of at boot. tuwunel reads identity providers from its config file, so a half-configured one does not hide a login button, it can stop the homeserver from starting.
This commit is contained in:
parent
9fa75a7f2f
commit
59ecefe0e1
1 changed files with 151 additions and 2 deletions
|
|
@ -22,6 +22,43 @@ let
|
|||
# it is rebuilt from the current CA every boot rather than going stale.
|
||||
matrixCaBundle = "/run/hive-matrix-ca/ca-bundle.crt";
|
||||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||||
|
||||
# `url` is the half of the authelia module that exists on EVERY hive —
|
||||
# null when no SSO provider is configured anywhere, which the assertion
|
||||
# below turns into an eval failure rather than a discovery request to
|
||||
# `null/.well-known/…`.
|
||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||
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 = cfg.sso.enable && autheliaCfg.enable;
|
||||
|
||||
# Where the plaintext lands inside the matrix container. Under /var/lib
|
||||
# rather than /run: the homeserver may start before the delivery unit on
|
||||
# a later boot, and a secret that evaporates on reboot turns a working
|
||||
# login into an intermittent one.
|
||||
matrixSecretPath = "/var/lib/tuwunel-oidc/${cfg.sso.clientId}.secret";
|
||||
|
||||
# ⚠️ tuwunel does NOT read the path above directly, and this indirection
|
||||
# is not ceremony. Upstream's own words: "under systemd the path must be
|
||||
# visible to the service after sandboxing (ReadWritePaths / ProtectHome),
|
||||
# typically by placing the file under /etc/tuwunel/" — which this
|
||||
# container has no writable etc for. `LoadCredential` is the answer
|
||||
# already in use two units below for the registration token, and for the
|
||||
# same reason: it keeps `DynamicUser=true` + `PrivateUsers=true` intact
|
||||
# with no host-side chown or GID pinning.
|
||||
matrixSecretCredential = "/run/credentials/tuwunel.service/oidc_client_secret";
|
||||
|
||||
# Format-locked by tuwunel, not chosen here: the callback host must point
|
||||
# directly at the matrix server and the path is fixed at
|
||||
# `/_matrix/client/unstable/login/sso/callback/<client_id>`. Built once
|
||||
# and read twice — by the homeserver's own config and by the client entry
|
||||
# handed to authelia — because a redirect-URI mismatch is a rejected
|
||||
# login with no error text worth reading.
|
||||
ssoCallbackUrl = "https://${toString cfg.gatewayHost}/_matrix/client/unstable/login/sso/callback/${cfg.sso.clientId}";
|
||||
# Falls back to the SWARM domain: a swarm runs one homeserver, so its
|
||||
# identifier belongs to the swarm rather than to whichever hive happens
|
||||
# to host it — otherwise moving the container between hives would look
|
||||
|
|
@ -401,7 +438,15 @@ in
|
|||
default = false;
|
||||
description = ''
|
||||
Let this homeserver delegate login to the swarm's authelia,
|
||||
as an OIDC relying party ("matrix next-gen auth").
|
||||
as an OIDC relying party — matrix SSO (`m.login.sso`), an
|
||||
extra flow offered alongside password login.
|
||||
|
||||
⚠️ Not to be confused with tuwunel's `oidc_*` settings, which
|
||||
point the other way: those make this homeserver an
|
||||
*authorization server* for matrix clients. This option makes
|
||||
it a *client* of an external identity provider. The two
|
||||
families share the protocol's name and answer opposite
|
||||
questions.
|
||||
|
||||
This **adds** a way in. Password login keeps working: an
|
||||
identity provider that can take the homeserver offline when
|
||||
|
|
@ -550,8 +595,80 @@ in
|
|||
"matrix.example.com" or "homeserver.internal".
|
||||
'';
|
||||
}
|
||||
{
|
||||
# Fail at EVAL, not at boot. tuwunel reads its identity providers
|
||||
# from the config file, so a half-configured one does not hide a
|
||||
# login button — it can stop the homeserver from starting at all.
|
||||
assertion = !cfg.sso.enable || cfg.sso.clientSecretFile != null;
|
||||
message = ''
|
||||
services.hyperhive.swarm.matrix.sso.enable requires
|
||||
sso.clientSecretFile — the path (inside the matrix container)
|
||||
holding the OIDC client secret's plaintext.
|
||||
|
||||
On a hive that also runs the swarm's authelia this is wired up
|
||||
for you. Set it explicitly when authelia lives on another
|
||||
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 = !cfg.sso.enable || autheliaUrl != null;
|
||||
message = ''
|
||||
services.hyperhive.swarm.matrix.sso.enable 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.
|
||||
assertion = !cfg.sso.enable || cfg.gatewayHost != null;
|
||||
message = ''
|
||||
services.hyperhive.swarm.matrix.sso.enable requires
|
||||
services.hyperhive.swarm.matrix.gatewayHost.
|
||||
|
||||
tuwunel's SSO callback URL is format-locked to
|
||||
`<homeserver>/_matrix/client/unstable/login/sso/callback/<client_id>`,
|
||||
and the identity provider redirects a browser to it — so it
|
||||
has to be a name the browser can reach, which is exactly what
|
||||
`gatewayHost` is. With it null the homeserver is direct on
|
||||
httpPort and has no such name.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# One declaration, two readers. The homeserver knows its own callback
|
||||
# URL; making the operator restate it in authelia's client list would
|
||||
# be a second source of truth for a string whose mismatch is a silent
|
||||
# rejected login.
|
||||
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf ssoLocal [
|
||||
{
|
||||
id = cfg.sso.clientId;
|
||||
description = "HyperHive matrix";
|
||||
redirectUris = [ ssoCallbackUrl ];
|
||||
}
|
||||
];
|
||||
|
||||
# Same case, same reasoning: this host minted the secret, so it can say
|
||||
# where the homeserver will find it.
|
||||
services.hyperhive.swarm.matrix.sso.clientSecretFile = lib.mkIf ssoLocal (
|
||||
lib.mkDefault matrixSecretPath
|
||||
);
|
||||
|
||||
# ⚠️ Deliberately NO `networking.hosts` entry for authelia's name, and
|
||||
# the difference from hive-forge (which needs one) is worth stating:
|
||||
# that container resolves through the host's resolvers, where the swarm
|
||||
# domain has no records. This one resolves through the hive's dnsmasq
|
||||
# at `bridgeIp` (see the static resolv.conf below), and every
|
||||
# `gateway.localNames` entry — authelia's domain among them — is
|
||||
# already mapped there. Adding a loopback override would only create a
|
||||
# second answer that can disagree with the first.
|
||||
|
||||
# Activation-time token generation — without this the bind-mount
|
||||
# would hand tuwunel an empty file on first boot and break every
|
||||
# registration until restart. Idempotent;
|
||||
|
|
@ -663,6 +780,34 @@ in
|
|||
# Tuwunel's default suffix is " 💕" — suppress it so agent
|
||||
# display names are clean (just the agent name, no emoji).
|
||||
new_user_displayname_suffix = "";
|
||||
}
|
||||
# `optionalAttrs`, not a key set to `[]`: with SSO off the
|
||||
# rendered settings must be *exactly* what they were before
|
||||
# this option existed, and an empty list is still a key.
|
||||
// lib.optionalAttrs cfg.sso.enable {
|
||||
# tuwunel's OIDC server and this list are the two ends of one
|
||||
# pipe: `oidc_native_auth` stays false (its default), which
|
||||
# upstream defines as "the OIDC server runs only to broker
|
||||
# for a configured identity_provider". So the client-facing
|
||||
# half needs no configuration — only the upstream half does.
|
||||
identity_provider = [
|
||||
{
|
||||
# A free, case-insensitive string, not an enum: a
|
||||
# recognised brand gets defaults and provider-specific
|
||||
# workarounds, an unrecognised one simply gets neither.
|
||||
# Which is why `issuer_url` below is not optional for us
|
||||
# — the pre-supplied issuers cover public providers only.
|
||||
brand = "authelia";
|
||||
client_id = cfg.sso.clientId;
|
||||
client_secret_file = matrixSecretCredential;
|
||||
issuer_url = toString autheliaUrl;
|
||||
callback_url = ssoCallbackUrl;
|
||||
# Explicit, though a lone provider is auto-defaulted:
|
||||
# relying on that logs a warning every startup, and a
|
||||
# recurring warning that is expected is one nobody reads.
|
||||
default = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
# Keeps DynamicUser=true + PrivateUsers=true intact — no
|
||||
|
|
@ -670,7 +815,11 @@ in
|
|||
# See `man systemd.exec` → LoadCredential.
|
||||
systemd.services.tuwunel.serviceConfig.LoadCredential = [
|
||||
"registration_token:${toString cfg.registrationTokenFile}"
|
||||
];
|
||||
]
|
||||
# Same mechanism, second secret. tuwunel re-reads this file on
|
||||
# every OAuth exchange, not just at startup, so it has to outlive
|
||||
# the unit's start — a credentials path does.
|
||||
++ lib.optional cfg.sso.enable "oidc_client_secret:${toString cfg.sso.clientSecretFile}";
|
||||
|
||||
# Federation TLS against a peer whose cert chains to the swarm
|
||||
# root: tuwunel's outbound client is reqwest with the `rustls`
|
||||
|
|
|
|||
Loading…
Reference in a new issue