swarm-bao: give the store forwarder's OIDC reader its own bao identity
`swarm-bao-forwarder-oidc` fetches the store container's collector secret, one path, and was the last reader still logging in with `deploy.bao.clientCertFile`: the hive's own leaf, whose policy reads every agent's credentials, the hive's tree and every service's OIDC secret. The four-way split gave grafana's and the swarm collector's readers leaves of their own and left this one behind. It now holds `forwarder-oidc.pem`, minted by `swarm-bao-pki`, and logs in under the `swarm-forwarder-oidc` cert-auth role, whose policy reads `secret/data/swarm/services/<store forwarder client id>/oidc/client` and nothing else. The role is written by `swarm-bao-forwarder-oidc-policy` from the bootstrap token, which gains the two grants that unit calls, and the reader is ordered after it. The subject is reserved as a hive name. A store host whose pair is null is refused at eval rather than falling back to the hive's leaf. The hive's own role and `client.pem` are untouched; nothing is revoked.
This commit is contained in:
parent
46d0b46670
commit
92e1909caf
9 changed files with 229 additions and 47 deletions
|
|
@ -471,6 +471,17 @@ let
|
|||
}
|
||||
];
|
||||
|
||||
# The fifth, and the one the four-way split left on the hive's leaf: the
|
||||
# store's own forwarder. Its path is `forwarderStoreSecretPath` below with
|
||||
# KV v2's `data/` inserted.
|
||||
forwarderOidcReaders = [
|
||||
{
|
||||
name = "swarm-forwarder-oidc";
|
||||
cn = baoDeploy.forwarderOidcCommonName;
|
||||
policyText = readStanza "${credentialMountPath}/data/swarm/services/${cfg.otel.clientId}/oidc/client";
|
||||
}
|
||||
];
|
||||
|
||||
# The role name IS the policy name, as for the three service principals
|
||||
# above: the role attaches the policy by spelling it identically, and one
|
||||
# string for both objects removes the way they drift apart.
|
||||
|
|
@ -703,15 +714,15 @@ let
|
|||
|
||||
# The forwarder's own credential, and the three names it takes on the way
|
||||
# in. The shape is ./swarm-otel.nix's `swarm-bao-otel-oidc` — the unit that
|
||||
# already reads an OIDC client secret out of the store with this host's
|
||||
# certificate and lands it in a collector's container — because that route
|
||||
# is proven and there is no second one worth inventing.
|
||||
# already reads an OIDC client secret out of the store with a leaf of its
|
||||
# own and lands it in a collector's container — because that route is
|
||||
# proven and there is no second one worth inventing.
|
||||
#
|
||||
# Where the publisher on authelia's host leaves it. The `services` segment
|
||||
# is `swarm-secret-client`'s `path::Kind::Service`: a swarm service's client
|
||||
# is registered once for the whole swarm, so its secret is one value. The
|
||||
# prefix is also exactly what a hive certificate's read grant covers, so a
|
||||
# path outside it answers 403 however correct it looks.
|
||||
# `swarm-forwarder-oidc` role's grant is this one path, so any other answers
|
||||
# 403 however correct it looks.
|
||||
forwarderStoreSecretPath = "secret/swarm/services/${cfg.otel.clientId}/oidc/client";
|
||||
# At rest in the container's tree, written by the host unit below — under
|
||||
# /var/lib rather than /run, because a secret that evaporates on reboot
|
||||
|
|
@ -1213,6 +1224,23 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
forwarderOidcCommonName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "swarm-bao-forwarder-oidc";
|
||||
example = "swarm-bao-forwarder-oidc.svc";
|
||||
description = ''
|
||||
Subject the store's `swarm-forwarder-oidc` cert-auth role accepts — the
|
||||
identity `swarm-bao-forwarder-oidc`, the unit that fetches the store's
|
||||
own forwarder's OIDC client secret, presents. Its grant is one path,
|
||||
`swarm/services/<forwarder client id>/oidc/client`, and read only.
|
||||
|
||||
A **fifth** identity rather than reuse of
|
||||
{option}`services.hyperhive.deploy.bao.otelOidcCommonName`: the two
|
||||
read different clients' secrets, and the store's forwarder is not
|
||||
entitled to the swarm collector's.
|
||||
'';
|
||||
};
|
||||
|
||||
matrixTokenClientCertFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
|
|
@ -1318,6 +1346,30 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
forwarderOidcClientCertFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/swarm-bao-pki/forwarder-oidc.pem";
|
||||
description = ''
|
||||
Certificate the unit that fetches the store's own forwarder's OIDC
|
||||
client secret presents to the store. Its subject must be
|
||||
{option}`services.hyperhive.deploy.bao.forwarderOidcCommonName`.
|
||||
|
||||
⚠️ Not the hive's own leaf, for the reason
|
||||
{option}`services.hyperhive.deploy.bao.matrixTokenClientCertFile` gives.
|
||||
'';
|
||||
};
|
||||
|
||||
forwarderOidcClientKeyFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/swarm-bao-pki/forwarder-oidc-key.pem";
|
||||
description = ''
|
||||
Private key for
|
||||
{option}`services.hyperhive.deploy.bao.forwarderOidcClientCertFile`.
|
||||
'';
|
||||
};
|
||||
|
||||
serverCaFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
|
|
@ -1511,6 +1563,28 @@ in
|
|||
something set these back to null.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# Refused rather than degraded: `swarm-bao-forwarder-oidc` renders
|
||||
# wherever the store does and has no mode without a secret, and the
|
||||
# only fallback left would be the hive's leaf — the union grant this
|
||||
# pair exists to end.
|
||||
assertion =
|
||||
baoDeploy.forwarderOidcClientCertFile != null && baoDeploy.forwarderOidcClientKeyFile != null;
|
||||
message = ''
|
||||
The swarm secret store runs on this host, so its forwarder needs a
|
||||
client identity of its own: set both
|
||||
|
||||
services.hyperhive.deploy.bao.forwarderOidcClientCertFile
|
||||
services.hyperhive.deploy.bao.forwarderOidcClientKeyFile
|
||||
|
||||
swarm-bao-forwarder-oidc.service fetches the store forwarder's OIDC
|
||||
client secret out of the store with them. A hive that runs the store
|
||||
normally gets both from ./glue-bao-tls.nix.
|
||||
|
||||
⚠️ Not deploy.bao.clientCertFile. That one is the hive's, and its
|
||||
grant reads every secret in the store; this role reads one path.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# The name every reader dials, made resolvable where the store runs.
|
||||
|
|
@ -1557,6 +1631,7 @@ in
|
|||
"swarm-bao-queue-agent-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
"swarm-bao-forwarder-oidc-policy"
|
||||
"swarm-bao-services-issuer-policy"
|
||||
"swarm-bao-nats-tls-policy"
|
||||
];
|
||||
|
|
@ -1618,14 +1693,14 @@ in
|
|||
# reads a store in the container NEXT DOOR; this one reads the store in
|
||||
# the very container it is delivering into, so "before" is a wait on a
|
||||
# process that cannot start until this finishes. There is no bootstrap
|
||||
# cycle behind it — the identity used here is this host's static
|
||||
# `swarm-bao-pki` certificate, not anything the store mints — only an
|
||||
# ordering one, and the cost is bounded: the file is under /var/lib, so
|
||||
# it survives reboots and only a FIRST boot has the collector starting
|
||||
# before it exists. `LoadCredential` refuses to start a unit whose
|
||||
# source is missing, so that boot is a collector that restarts, says so
|
||||
# each time, and comes up the moment this lands. Loud and self-healing
|
||||
# rather than silently exporting without a credential.
|
||||
# cycle behind it — the identity used here is the static
|
||||
# `forwarder-oidc.pem` leaf from `swarm-bao-pki`, not anything the store
|
||||
# issues — only an ordering one, and the cost is bounded: the file is
|
||||
# under /var/lib, so it survives reboots and only a FIRST boot has the
|
||||
# collector starting before it exists. `LoadCredential` refuses to start
|
||||
# a unit whose source is missing, so that boot is a collector that
|
||||
# restarts, says so each time, and comes up the moment this lands. Loud
|
||||
# and self-healing rather than silently exporting without a credential.
|
||||
systemd.services.swarm-bao-forwarder-oidc = {
|
||||
description = "fetch the secret store forwarder's OIDC client secret from the store";
|
||||
after = [
|
||||
|
|
@ -1656,8 +1731,11 @@ in
|
|||
};
|
||||
environment = {
|
||||
BAO_ADDR = "https://${cfg.domain}:${toString cfg.port}";
|
||||
BAO_CLIENT_CERT = baoDeploy.clientCertFile;
|
||||
BAO_CLIENT_KEY = baoDeploy.clientKeyFile;
|
||||
# 🩸 Its OWN leaf, not `clientCertFile`: the hive's grant reads every
|
||||
# agent's credential and every service's OIDC secret, and this unit
|
||||
# needs one path. The pair is asserted set above.
|
||||
BAO_CLIENT_CERT = baoDeploy.forwarderOidcClientCertFile;
|
||||
BAO_CLIENT_KEY = baoDeploy.forwarderOidcClientKeyFile;
|
||||
}
|
||||
# Absent means the system trust store, which is what a deployment with
|
||||
# a real CA wants and what a self-signed one must not be left with.
|
||||
|
|
@ -1680,7 +1758,7 @@ in
|
|||
# this unit's `path` does not carry — `-token-only` answers on
|
||||
# stdout and skips the helper on both sides.
|
||||
if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then
|
||||
echo "could not log in to the swarm secret store with this host's certificate." >&2
|
||||
echo "could not log in to the swarm secret store with the forwarder's own certificate." >&2
|
||||
cat "$err" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -2206,6 +2284,8 @@ in
|
|||
|
||||
systemd.services.swarm-bao-otel-oidc-policy = readerPolicyUnit "write the collector's OIDC-secret-reader bao policy and cert-auth role" otelOidcReaders;
|
||||
|
||||
systemd.services.swarm-bao-forwarder-oidc-policy = readerPolicyUnit "write the store forwarder's OIDC-secret-reader bao policy and cert-auth role" forwarderOidcReaders;
|
||||
|
||||
# A FOURTH sibling, same shape and same reasons as the two above. This
|
||||
# one is what turns `swarm-services-issuer` from a declaration into a
|
||||
# grant: a bao policy reaches nothing until a login role hands it to a
|
||||
|
|
|
|||
Loading…
Reference in a new issue