forge: hand the OIDC client secret to forgejo from /run, not from the container's disk
swarm-bao-forge-oidc fetched the forge's OIDC client secret from bao and wrote it to /var/lib/nixos-containers/hive-forge/var/lib/forgejo-oidc/<id>.secret, on the container's persistent root, where it outlived a bao rotation and was in every backup of that tree. Now the fetch writes it to /run/hive-forge-oidc/<id>.secret on the host (tmpfs, root 0400). The directory is bound read-only into the container at the same path, and forgejo-sso-source loads it through LoadCredential. A tmpfiles rule creates the directory at boot, so the bind source always exists, and removes the old persistent copy. After a reboot the file is absent until the fetch succeeds, and forgejo-sso-source fails to start until then; the fetch starts it again once it writes the file. Logins keep working meanwhile: forgejo holds the secret in its login-source row. deploy.forgejo.sso.clientSecretFile (and its old swarm.forge.sso name) is removed: it only chose where in the container the fetched file landed. The fetch no longer needs the container's forgejo uid, so it no longer fails on a fresh host before the container has created its state dir. Part of #4998.
This commit is contained in:
parent
8bf53f7ed9
commit
09ad7386b2
8 changed files with 106 additions and 130 deletions
|
|
@ -40,17 +40,9 @@ let
|
|||
deploy.forgejo.enable = false;
|
||||
};
|
||||
|
||||
# The forge without authelia: the other half of the split, with its secret's
|
||||
# path and its store identity chosen by hand.
|
||||
forgeNoAuthelia = hive {
|
||||
deploy.forgejo.enable = true;
|
||||
deploy.forgejo.sso.clientSecretFile = "/var/lib/forgejo-oidc/by-hand.secret";
|
||||
deploy.bao.forgePublisherClientCertFile = "/etc/pki/forge-publisher.pem";
|
||||
deploy.bao.forgePublisherClientKeyFile = "/etc/pki/forge-publisher-key.pem";
|
||||
};
|
||||
|
||||
# The forge with neither authelia nor the store, its store identity placed by
|
||||
# hand: the deployment the store fetch exists for.
|
||||
# hand: the other half of the split, and the deployment the store fetch
|
||||
# exists for.
|
||||
forgeAlone = hive {
|
||||
deploy.forgejo.enable = true;
|
||||
deploy.bao.forgePublisherClientCertFile = "/etc/pki/forge-publisher.pem";
|
||||
|
|
@ -134,8 +126,7 @@ let
|
|||
!bare.services.hyperhive.deploy.forgejo.enable
|
||||
&& !(bare.containers ? hive-forge)
|
||||
&& !(bare.systemd.services ? swarm-bao-forge-publisher)
|
||||
&& !(lib.elem "forge.t.local" bare.services.hyperhive.gateway.localNames)
|
||||
&& !(refusedOver bare "deploy.forgejo.sso.clientSecretFile");
|
||||
&& !(lib.elem "forge.t.local" bare.services.hyperhive.gateway.localNames);
|
||||
}
|
||||
{
|
||||
# All three are published targets; ./swarm-otel-identity.nix covers them.
|
||||
|
|
@ -178,7 +169,7 @@ let
|
|||
ok =
|
||||
forgeClientIds servicesForgeElsewhere == [ "forgejo" ]
|
||||
&& forgeClientIds allLocal == [ "forgejo" ]
|
||||
&& forgeClientIds forgeNoAuthelia == [ ];
|
||||
&& forgeClientIds forgeAlone == [ ];
|
||||
}
|
||||
{
|
||||
name = "the forge's OIDC callback is the one forgejo sends";
|
||||
|
|
@ -370,10 +361,8 @@ let
|
|||
}
|
||||
{
|
||||
# The forge's OIDC secret has one route, the store, under the forge
|
||||
# publisher's leaf, landing where `clientSecretFile` points inside the
|
||||
# container. No unit copies it out of authelia's tree, even on a host
|
||||
# that runs authelia, and a forge host without authelia needs no path
|
||||
# set by hand.
|
||||
# publisher's leaf. No unit copies it out of authelia's tree, even on a
|
||||
# host that runs authelia, and nothing defines the removed path option.
|
||||
name = "the forge reads its OIDC secret from the store under the forge publisher's leaf";
|
||||
ok =
|
||||
let
|
||||
|
|
@ -382,12 +371,11 @@ let
|
|||
in
|
||||
!d.authelia.enable
|
||||
&& !d.bao.enable
|
||||
&& d.forgejo.sso.clientSecretFile == "/var/lib/forgejo-oidc/forgejo.secret"
|
||||
&& !(refusedOver forgeAlone "deploy.forgejo.sso.clientSecretFile")
|
||||
&& !(refusedOver allLocal "deploy.forgejo.sso.clientSecretFile")
|
||||
&& u.environment.BAO_CLIENT_CERT == "/etc/pki/forge-publisher.pem"
|
||||
&& u.environment.BAO_CLIENT_KEY == "/etc/pki/forge-publisher-key.pem"
|
||||
&& lib.hasInfix "bao kv get -field=value secret/swarm/services/forgejo/oidc/client" u.script
|
||||
&& lib.hasInfix "/var/lib/nixos-containers/hive-forge/var/lib/forgejo-oidc/forgejo.secret" u.script
|
||||
&& lib.hasInfix "refresh_consumer hive-forge forgejo-sso-source.service" u.script
|
||||
# An unpublished or empty secret fails the unit, so `storeRetry` retries it.
|
||||
&& !(lib.hasInfix "exit 0" u.script)
|
||||
|
|
@ -400,13 +388,27 @@ let
|
|||
&& !(bare.systemd.services ? swarm-bao-forge-oidc);
|
||||
}
|
||||
{
|
||||
# An operator-chosen path is where the fetched secret lands.
|
||||
name = "the forge's OIDC secret lands where clientSecretFile points";
|
||||
# The fetched secret lives in a host tmpfs directory bound read-only into
|
||||
# the container, and reaches the login source as a credential. Nothing
|
||||
# writes it under the container's persistent root, and the copy an
|
||||
# earlier fetch left there is removed.
|
||||
name = "the forge's OIDC secret reaches its login source from /run, not from persistent disk";
|
||||
ok =
|
||||
let
|
||||
s = (oidcFetch forgeNoAuthelia).script;
|
||||
s = (oidcFetch forgeAlone).script;
|
||||
c = forgeAlone.containers.hive-forge;
|
||||
src = c.config.systemd.services.forgejo-sso-source;
|
||||
rules = forgeAlone.systemd.tmpfiles.rules;
|
||||
in
|
||||
lib.hasInfix "/var/lib/nixos-containers/hive-forge/var/lib/forgejo-oidc/by-hand.secret" s;
|
||||
lib.hasInfix "atomic_write_secret 0400" s
|
||||
&& lib.hasInfix "/run/hive-forge-oidc/forgejo.secret" s
|
||||
&& !(lib.hasInfix "/var/lib/nixos-containers/hive-forge" s)
|
||||
&& c.bindMounts."/run/hive-forge-oidc".hostPath == "/run/hive-forge-oidc"
|
||||
&& c.bindMounts."/run/hive-forge-oidc".isReadOnly
|
||||
&& lib.elem "oidc-client-secret:/run/hive-forge-oidc/forgejo.secret" src.serviceConfig.LoadCredential
|
||||
&& lib.hasInfix "$CREDENTIALS_DIRECTORY/oidc-client-secret" src.script
|
||||
&& lib.elem "d /run/hive-forge-oidc 0700 root root -" rules
|
||||
&& lib.elem "r /var/lib/nixos-containers/hive-forge/var/lib/forgejo-oidc/forgejo.secret" rules;
|
||||
}
|
||||
{
|
||||
# Beside the store, the fetch waits for the unit writing the forge
|
||||
|
|
|
|||
|
|
@ -66,12 +66,10 @@ let
|
|||
# address set by hand: what every hive but one in a multi-host swarm looks
|
||||
# like. The controller is on too, since it may run away from the queue.
|
||||
#
|
||||
# The forge's secret and the controller's store identity are what a hive
|
||||
# away from authelia and the store already has to be handed, and none is an
|
||||
# address; without them this hive would fail assertions that have nothing to
|
||||
# do with the queue.
|
||||
# The controller's store identity is what a hive away from the store already
|
||||
# has to be handed, and it is not an address; without it this hive would
|
||||
# fail assertions that have nothing to do with the queue.
|
||||
remoteSecrets = {
|
||||
deploy.forgejo.sso.clientSecretFile = "/var/lib/forgejo-oidc/by-hand.secret";
|
||||
deploy.swarm-controller.baoClientCertFile = "/var/lib/swarm-controller/bao-client.pem";
|
||||
deploy.swarm-controller.baoClientKeyFile = "/var/lib/swarm-controller/bao-client-key.pem";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ let
|
|||
otelNoIdentity = hive {
|
||||
deploy.swarm-otel.enable = true;
|
||||
swarm.authelia.url = "https://auth.example.invalid";
|
||||
deploy.forgejo.sso.clientSecretFile = "/var/lib/forgejo-oidc/by-hand.secret";
|
||||
};
|
||||
|
||||
# 🩸 The shape the degrade above must not swallow: a collector on a host that
|
||||
|
|
|
|||
Loading…
Reference in a new issue