feat(3149): the forge registers authelia as an OIDC login source
Additive, never exclusive: forgejo keeps its local password database and gains a second way in. An identity provider that can take the forge offline when it hiccups is a worse forge than one with two doors. A login source in forgejo is a database ROW, not an `app.ini` key, so this is a unit rather than config. It is ordered AFTER forgejo — unlike its neighbour `forgejo-gpg-init`, which runs before — because on a fresh hive that database does not exist until forgejo has started and migrated; running first would either fail or initialise a schema behind the server's back. Idempotency is by query (`admin auth list`), not by a stamp file: the same reasoning already written down for the GPG key next to it, that a stamp outlives a state wipe and then suppresses the repair. Two assertions rather than defaults, both firing at eval: SSO needs a secret path, and it needs somewhere to discover the provider. Either one missing produces a login button that always fails — a runtime symptom several layers from its cause, which is exactly the trade an eval error is worth making. The secret is read from a path and passed on argv for one exec, because `--secret` is the only input forgejo offers — no `--secret-file`, no env var, though its sibling `forgejo-cli actions register` has both. Inside this container the value is already at rest in the login-source row and the only principals are root and forgejo, so argv widens its readership to nobody new. Accepted deliberately, not overlooked.
This commit is contained in:
parent
4a35e1229b
commit
daa8a2eb4b
1 changed files with 163 additions and 0 deletions
|
|
@ -23,6 +23,19 @@ let
|
|||
# that make the CA reachable are shared with hive-ci via the
|
||||
# `hive-ca-trust` helper; only the Go SSL_CERT_FILE concat below is
|
||||
# hive-forge-specific.
|
||||
# Forgejo's name for the login source. A constant, not an option: it
|
||||
# is the key this module's own idempotency check looks up, so making
|
||||
# it configurable would buy nothing and add a way for the lookup and
|
||||
# the row to disagree.
|
||||
ssoSourceName = "authelia";
|
||||
|
||||
# `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/...`.
|
||||
autheliaUrl = config.services.hyperhive.swarm.authelia.url;
|
||||
autheliaDiscoveryUrl = "${toString autheliaUrl}/.well-known/openid-configuration";
|
||||
|
||||
caTrust = import ../lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
||||
useSelfSigned = caTrust.useSelfSigned;
|
||||
caContainerPath = caTrust.caContainerPath;
|
||||
|
|
@ -314,10 +327,86 @@ in
|
|||
external DNS, but that's off the CI path).
|
||||
'';
|
||||
};
|
||||
|
||||
sso = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Register the swarm's authelia as an OpenID Connect login
|
||||
source on this forge.
|
||||
|
||||
**Additive, never exclusive.** Forgejo keeps its local
|
||||
password database and gains an extra "sign in with" button;
|
||||
this does not disable local login. Deliberate: an identity
|
||||
provider that can take the forge offline when it hiccups is a
|
||||
worse forge than one with two ways in.
|
||||
'';
|
||||
};
|
||||
|
||||
clientId = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "forgejo";
|
||||
description = ''
|
||||
OAuth2 client id this forge identifies itself with. Must match
|
||||
the `id` of the corresponding entry in
|
||||
`services.hyperhive.swarm.authelia.oidc.clients`.
|
||||
'';
|
||||
};
|
||||
|
||||
clientSecretFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/hyperhive/forge-oidc-secret";
|
||||
description = ''
|
||||
Path **inside the forge container** holding the client
|
||||
secret's plaintext.
|
||||
|
||||
A path, never a value: an OIDC client secret has two holders
|
||||
in two containers (authelia keeps a hash, this forge needs the
|
||||
plaintext), and a literal written here would be rendered into
|
||||
the world-readable nix store.
|
||||
|
||||
Required when `enable` is set — deliberately no fallback. A
|
||||
forge that boots with SSO half-configured presents as a login
|
||||
button that always fails, which is harder to diagnose than an
|
||||
eval error.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.services.hyperhive.enable {
|
||||
assertions = [
|
||||
{
|
||||
# Fail at EVAL, not at boot. The alternative failure is a login
|
||||
# button that always 401s, three layers from the missing file.
|
||||
assertion = !cfg.sso.enable || cfg.sso.clientSecretFile != null;
|
||||
message = ''
|
||||
services.hyperhive.swarm.forge.sso.enable requires
|
||||
sso.clientSecretFile — the path (inside the forge 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 unit would ask `null/.well-known/…`.
|
||||
assertion = !cfg.sso.enable || autheliaUrl != null;
|
||||
message = ''
|
||||
services.hyperhive.swarm.forge.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.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl;
|
||||
message = ''
|
||||
|
|
@ -648,6 +737,80 @@ in
|
|||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# Register authelia as an OIDC login source.
|
||||
#
|
||||
# ⚠️ Ordered AFTER forgejo, unlike forgejo-gpg-init above, and
|
||||
# the difference is not stylistic: a login source is a row in
|
||||
# forgejo's database, and on a fresh hive that database does
|
||||
# not exist until forgejo has started and run its migrations.
|
||||
# Running first would either fail or let the CLI initialise a
|
||||
# schema behind the server's back.
|
||||
#
|
||||
# Idempotency is by QUERY, not by a stamp file — same reason
|
||||
# spelled out for the GPG key above: a stamp survives a state
|
||||
# wipe that took the thing it claims exists, and then suppresses
|
||||
# the repair.
|
||||
systemd.services.forgejo-sso-source = lib.mkIf cfg.sso.enable {
|
||||
description = "register authelia as Forgejo's OIDC login source";
|
||||
after = [ "forgejo.service" ];
|
||||
requires = [ "forgejo.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = "forgejo";
|
||||
Group = "forgejo";
|
||||
SyslogIdentifier = "forgejo-sso-source";
|
||||
};
|
||||
# `FORGEJO_CUSTOM` (not `GITEA_CUSTOM` — forgejo renamed it)
|
||||
# is how the CLI finds the app.ini upstream's module wrote.
|
||||
environment.FORGEJO_CUSTOM = "/var/lib/forgejo/custom";
|
||||
path = [
|
||||
cfg.package
|
||||
pkgs.coreutils
|
||||
pkgs.gnugrep
|
||||
];
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
secret=$(cat ${lib.escapeShellArg cfg.sso.clientSecretFile})
|
||||
if [ -z "$secret" ]; then
|
||||
echo "empty OIDC client secret at ${cfg.sso.clientSecretFile}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# `--secret` is the only input forgejo offers: there is no
|
||||
# --secret-file and no env var, though its sibling
|
||||
# `forgejo-cli actions register` has both. So the value is
|
||||
# on this argv for the length of one exec, inside this
|
||||
# container, where forgejo already stores it at rest in the
|
||||
# login-source row — root and forgejo are the only
|
||||
# principals here, and root can read the resting copy
|
||||
# anyway. Accepted deliberately (see docs/swarm/), not
|
||||
# overlooked; upstream gap filed.
|
||||
args="--provider openidConnect \
|
||||
--key ${lib.escapeShellArg cfg.sso.clientId} \
|
||||
--auto-discover-url ${lib.escapeShellArg autheliaDiscoveryUrl} \
|
||||
--scopes ${lib.escapeShellArg "openid profile email groups"}"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
if forgejo admin auth list | grep -q "[[:space:]]${ssoSourceName}[[:space:]]"; then
|
||||
id=$(forgejo admin auth list \
|
||||
| grep "[[:space:]]${ssoSourceName}[[:space:]]" \
|
||||
| cut -f1)
|
||||
forgejo admin auth update-oauth --id "$id" \
|
||||
--name ${lib.escapeShellArg ssoSourceName} \
|
||||
--secret "$secret" $args
|
||||
echo "updated OIDC login source ${ssoSourceName} (id $id)"
|
||||
else
|
||||
forgejo admin auth add-oauth \
|
||||
--name ${lib.escapeShellArg ssoSourceName} \
|
||||
--secret "$secret" $args
|
||||
echo "added OIDC login source ${ssoSourceName}"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue