forge: fetch the OIDC client secret from bao instead of a host-side copy
The forge's login source read its OIDC client secret from a file that `hive-forge-oidc-secret` copied out of authelia's container tree. That unit only existed where authelia ran on the forge's host (`ssoLocal`), so a forge anywhere else needed the file placed by hand. The publisher on authelia's host now carries the forge's client secret to `secret/swarm/services/<forge client id>/oidc/client`, like every other swarm service's. On the forge's host `swarm-bao-forge-oidc`, a copy of `swarm-bao-matrix-oidc`, reads it under the forge publisher's leaf (the forge host's one store identity, whose policy gains read on that one path) and writes the file `forgejo-sso-source` reads, re-running the login source when the value changes. It retries on the shared store-retry shape while the store is down or the secret is not yet published. `hive-forge-oidc-secret` is removed, `clientSecretFile` defaults to the in-container path wherever the forge runs, and the `ssoLocal` gate on the login source's resolver ordering goes with it: the login source fetches the issuer's discovery document on every forge host. Refs #5015
This commit is contained in:
parent
6bf4792d61
commit
f6ca5f1433
10 changed files with 332 additions and 159 deletions
|
|
@ -1373,16 +1373,19 @@ let
|
|||
lib.hasInfix "/ci-runner.pem ]" s && lib.hasInfix "swarm-ci-runner \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
# The forge's host is the one writer of the controller's forge token.
|
||||
# Pinned as the whole stanza, so an added `read` fails: it copies a file
|
||||
# in and never reads one back.
|
||||
name = "the forge publisher's grant is writing the controller's forge token and nothing else";
|
||||
# The forge's host is the one writer of the controller's forge token,
|
||||
# and reads the forge's own OIDC client secret under the same leaf.
|
||||
# Pinned as two whole stanzas, so an added `read` on the token or a
|
||||
# write or wildcard on the secret fails.
|
||||
name = "the forge publisher's grant is writing the controller's forge token and reading the forge's OIDC secret, and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-forge-publisher-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/forge/token\" {\n capabilities = [\"create\", \"update\"]\n}" s
|
||||
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 1
|
||||
&& lib.hasInfix "path \"secret/data/swarm/services/forgejo/oidc/client\" {\n capabilities = [\"read\"]\n}" s
|
||||
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 2
|
||||
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
|
||||
&& lib.hasInfix "auth/cert/certs/swarm-forge-publisher" s
|
||||
&& lib.hasInfix "allowed_common_names=swarm-forge-publisher" s
|
||||
&& lib.hasInfix "token_policies=swarm-forge-publisher" s;
|
||||
|
|
@ -1413,6 +1416,16 @@ let
|
|||
"swarm-bao-forge-publisher-policy"
|
||||
];
|
||||
}
|
||||
{
|
||||
# The forge's OIDC client secret by name: the forge publisher reads it,
|
||||
# and the secret publisher writes it under its `swarm/services/*` grant,
|
||||
# which names no service.
|
||||
name = "no granting unit but the forge publisher's names the forge's OIDC secret";
|
||||
ok =
|
||||
lib.filter (
|
||||
u: lib.hasInfix "swarm/services/forgejo/oidc/client" baoGrantHere.systemd.services.${u}.script
|
||||
) grantingUnitNames == [ "swarm-bao-forge-publisher-policy" ];
|
||||
}
|
||||
{
|
||||
name = "the PKI unit signs the forge publisher's leaf under its own subject";
|
||||
ok =
|
||||
|
|
|
|||
|
|
@ -40,10 +40,21 @@ let
|
|||
deploy.forgejo.enable = false;
|
||||
};
|
||||
|
||||
# The forge without authelia: the other half of the split.
|
||||
# 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.
|
||||
forgeAlone = hive {
|
||||
deploy.forgejo.enable = true;
|
||||
deploy.bao.forgePublisherClientCertFile = "/etc/pki/forge-publisher.pem";
|
||||
deploy.bao.forgePublisherClientKeyFile = "/etc/pki/forge-publisher-key.pem";
|
||||
};
|
||||
|
||||
# The runner alone: no hive, no forge, no store, its store identity placed
|
||||
|
|
@ -105,6 +116,7 @@ let
|
|||
refusedOver = m: needle: lib.any (a: !a.assertion && lib.hasInfix needle a.message) m.assertions;
|
||||
|
||||
publisher = m: m.systemd.services.swarm-bao-forge-publisher;
|
||||
oidcFetch = m: m.systemd.services.swarm-bao-forge-oidc;
|
||||
|
||||
forgeSettings = forgeHere.containers.hive-forge.config.services.forgejo.settings;
|
||||
|
||||
|
|
@ -329,6 +341,79 @@ let
|
|||
&& lib.hasInfix "exit 1" s
|
||||
&& !(lib.hasInfix "bao kv put" s);
|
||||
}
|
||||
{
|
||||
# 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.
|
||||
name = "the forge reads its OIDC secret from the store under the forge publisher's leaf";
|
||||
ok =
|
||||
let
|
||||
u = oidcFetch forgeAlone;
|
||||
d = forgeAlone.services.hyperhive.deploy;
|
||||
in
|
||||
!d.authelia.enable
|
||||
&& !d.bao.enable
|
||||
&& d.forgejo.sso.clientSecretFile == "/var/lib/forgejo-oidc/forgejo.secret"
|
||||
&& !(refusedOver forgeAlone "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)
|
||||
&& u.serviceConfig.Restart == "on-failure"
|
||||
&& lib.elem "container@hive-forge.service" u.before
|
||||
&& lib.elem "multi-user.target" u.wantedBy
|
||||
&& (oidcFetch allLocal).environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/forge-publisher.pem"
|
||||
&& !(forgeAlone.systemd.services ? hive-forge-oidc-secret)
|
||||
&& !(allLocal.systemd.services ? hive-forge-oidc-secret)
|
||||
&& !(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";
|
||||
ok =
|
||||
let
|
||||
s = (oidcFetch forgeNoAuthelia).script;
|
||||
in
|
||||
lib.hasInfix "/var/lib/nixos-containers/hive-forge/var/lib/forgejo-oidc/by-hand.secret" s;
|
||||
}
|
||||
{
|
||||
# Beside the store, the fetch waits for the unit writing the forge
|
||||
# publisher's role, the leaf it logs in with.
|
||||
name = "the forge's OIDC secret fetch is ordered after the forge publisher's policy unit";
|
||||
ok =
|
||||
let
|
||||
u = oidcFetch allLocal;
|
||||
in
|
||||
lib.elem "swarm-bao-forge-publisher-policy.service" u.after
|
||||
&& lib.elem "swarm-bao-forge-publisher-policy.service" u.wants;
|
||||
}
|
||||
{
|
||||
# The absence arm: no publisher leaf, no secret, and the unit says which
|
||||
# options to set.
|
||||
name = "a forge host with no publisher leaf renders an OIDC secret fetch that fails naming the options";
|
||||
ok =
|
||||
let
|
||||
u = oidcFetch forgeHere;
|
||||
in
|
||||
!(u.environment ? BAO_CLIENT_CERT)
|
||||
&& lib.hasInfix "forgePublisherClientCertFile" u.script
|
||||
&& lib.hasInfix "exit 1" u.script
|
||||
&& !(lib.hasInfix "bao login" u.script);
|
||||
}
|
||||
{
|
||||
# The login source fetches the issuer's discovery document by name
|
||||
# wherever the forge runs, so it waits for the resolver on a forge host
|
||||
# without authelia too.
|
||||
name = "the login source waits for the container's resolver on a forge host without authelia";
|
||||
ok = lib.elem "forgejo-sso-source.service" (
|
||||
forgeAlone.containers.hive-forge.config.systemd.services.swarm-bridge-dns.before
|
||||
);
|
||||
}
|
||||
{
|
||||
# The host copy is removed where the forge runs: nothing reads it.
|
||||
name = "the forge's host removes its old copy of the controller's forge token";
|
||||
|
|
|
|||
|
|
@ -189,10 +189,10 @@ let
|
|||
]);
|
||||
}
|
||||
{
|
||||
# The other half of the queue's and the homeserver's one route: the
|
||||
# publisher on an IdP host that runs neither carries both secrets to the
|
||||
# paths their readers fetch.
|
||||
name = "the publisher on an authelia host carries the queue's and homeserver's secrets";
|
||||
# The other half of the queue's, the homeserver's and the forge's one
|
||||
# route: the publisher on an IdP host that runs none of them carries each
|
||||
# secret to the path its reader fetches.
|
||||
name = "the publisher on an authelia host carries the queue's, homeserver's and forge's secrets";
|
||||
ok =
|
||||
let
|
||||
s = secretPublisherRemote.systemd.services.swarm-secret-publish.script;
|
||||
|
|
@ -200,7 +200,10 @@ let
|
|||
lib.hasInfix "secret/swarm/services/swarm-nats/oidc/client" s
|
||||
&& lib.hasInfix "/swarm-nats.secret" s
|
||||
&& lib.hasInfix "secret/swarm/services/tuwunel/oidc/client" s
|
||||
&& lib.hasInfix "/tuwunel.secret" s;
|
||||
&& lib.hasInfix "/tuwunel.secret" s
|
||||
&& !secretPublisherRemote.services.hyperhive.deploy.forgejo.enable
|
||||
&& lib.hasInfix "secret/swarm/services/forgejo/oidc/client" s
|
||||
&& lib.hasInfix "/forgejo.secret" s;
|
||||
}
|
||||
{
|
||||
# Both ends of a wire nothing at eval time carries end to end: the
|
||||
|
|
|
|||
Loading…
Reference in a new issue