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
|
# 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-ca-trust` helper; only the Go SSL_CERT_FILE concat below is
|
||||||
# hive-forge-specific.
|
# 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; };
|
caTrust = import ../lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
||||||
useSelfSigned = caTrust.useSelfSigned;
|
useSelfSigned = caTrust.useSelfSigned;
|
||||||
caContainerPath = caTrust.caContainerPath;
|
caContainerPath = caTrust.caContainerPath;
|
||||||
|
|
@ -314,10 +327,86 @@ in
|
||||||
external DNS, but that's off the CI path).
|
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 {
|
config = lib.mkIf config.services.hyperhive.enable {
|
||||||
assertions = [
|
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;
|
assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl;
|
||||||
message = ''
|
message = ''
|
||||||
|
|
@ -648,6 +737,80 @@ in
|
||||||
fi
|
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