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
|
|
@ -67,6 +67,14 @@ let
|
|||
deploy.bao.queueAgentClientKeyFile = lib.mkForce null;
|
||||
};
|
||||
|
||||
# The store with the forwarder's own pair taken away. The forwarder renders
|
||||
# wherever the store does, so this is the deployment the assertion refuses.
|
||||
baoNoForwarderIdentity = hive {
|
||||
deploy.bao.enable = true;
|
||||
deploy.bao.forwarderOidcClientCertFile = lib.mkForce null;
|
||||
deploy.bao.forwarderOidcClientKeyFile = lib.mkForce null;
|
||||
};
|
||||
|
||||
# All four readers against a store they do not run, each with a leaf placed
|
||||
# by hand. The deployment in which there is no local policy unit to wait for.
|
||||
baoRemoteReaders = hive {
|
||||
|
|
@ -454,6 +462,26 @@ let
|
|||
&& !(lib.hasInfix "swarm-grafana" s)
|
||||
&& !(lib.hasInfix "sys/policies/acl" s);
|
||||
}
|
||||
{
|
||||
# The fifth, and the one that stayed on the hive's leaf longest: the
|
||||
# store's own forwarder. Its client id is `swarm-bao-collector`, so the
|
||||
# arm naming `swarm-collector/` is the swarm collector's secret, which
|
||||
# this principal is not entitled to.
|
||||
name = "the store forwarder's OIDC reader's grant is its own client secret and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-forwarder-oidc-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/services/swarm-bao-collector/oidc/client\" {" s
|
||||
&& lib.hasInfix "capabilities = [\"read\"]" s
|
||||
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 1
|
||||
&& !(lib.hasInfix "secret/data/swarm/agents" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
|
||||
&& !(lib.hasInfix "services/swarm-collector/" s)
|
||||
&& !(lib.hasInfix "swarm-grafana" s)
|
||||
&& !(lib.hasInfix "sys/policies/acl" s);
|
||||
}
|
||||
{
|
||||
# 🩸 The half that makes the policies above bind: a policy grants only
|
||||
# through a token that carries it, and a token is minted by a cert-auth
|
||||
|
|
@ -463,7 +491,7 @@ let
|
|||
# The per-hive subjects carry the hive name because their paths do; the
|
||||
# two service subjects do not, because an OIDC client is registered once
|
||||
# per swarm. Pinned so neither shape is tidied into the other.
|
||||
name = "each of the four readers logs in under a subject of its own";
|
||||
name = "each of the five readers logs in under a subject of its own";
|
||||
ok =
|
||||
let
|
||||
subjectOf =
|
||||
|
|
@ -481,7 +509,8 @@ let
|
|||
subjectOf "swarm-bao-matrix-token-policy" "swarm-matrix-token-h1" "swarm-bao-matrix-token-h1"
|
||||
&& subjectOf "swarm-bao-queue-agent-policy" "swarm-queue-agent-h1" "swarm-bao-queue-agent-h1"
|
||||
&& subjectOf "swarm-bao-grafana-oidc-policy" "swarm-grafana-oidc" "swarm-bao-grafana-oidc"
|
||||
&& subjectOf "swarm-bao-otel-oidc-policy" "swarm-otel-oidc" "swarm-bao-otel-oidc";
|
||||
&& subjectOf "swarm-bao-otel-oidc-policy" "swarm-otel-oidc" "swarm-bao-otel-oidc"
|
||||
&& subjectOf "swarm-bao-forwarder-oidc-policy" "swarm-forwarder-oidc" "swarm-bao-forwarder-oidc";
|
||||
}
|
||||
{
|
||||
# 🩸 The consuming side, and the arm that would catch the regression that
|
||||
|
|
@ -492,7 +521,7 @@ let
|
|||
#
|
||||
# Each pair is asserted whole: a certificate with no key authenticates
|
||||
# nothing, so a half-set pair is a reader that does not render.
|
||||
name = "each of the four readers presents its own leaf, never the hive's";
|
||||
name = "each of the five readers presents its own leaf, never the hive's";
|
||||
ok =
|
||||
let
|
||||
b = baoGrantWithConsumers.services.hyperhive.deploy.bao;
|
||||
|
|
@ -509,6 +538,8 @@ let
|
|||
b.grafanaOidcClientKeyFile
|
||||
b.otelOidcClientCertFile
|
||||
b.otelOidcClientKeyFile
|
||||
b.forwarderOidcClientCertFile
|
||||
b.forwarderOidcClientKeyFile
|
||||
];
|
||||
envOf = unit: baoGrantWithConsumers.systemd.services.${unit}.environment;
|
||||
presents =
|
||||
|
|
@ -521,7 +552,8 @@ let
|
|||
&& presents "swarm-bao-matrix-token" b.matrixTokenClientCertFile b.matrixTokenClientKeyFile
|
||||
&& presents "swarm-bao-queue-agent" b.queueAgentClientCertFile b.queueAgentClientKeyFile
|
||||
&& presents "swarm-bao-grafana-oidc" b.grafanaOidcClientCertFile b.grafanaOidcClientKeyFile
|
||||
&& presents "swarm-bao-otel-oidc" b.otelOidcClientCertFile b.otelOidcClientKeyFile;
|
||||
&& presents "swarm-bao-otel-oidc" b.otelOidcClientCertFile b.otelOidcClientKeyFile
|
||||
&& presents "swarm-bao-forwarder-oidc" b.forwarderOidcClientCertFile b.forwarderOidcClientKeyFile;
|
||||
}
|
||||
{
|
||||
# The minting side of the same claim. A role matching a subject nothing
|
||||
|
|
@ -547,6 +579,8 @@ let
|
|||
"swarm-bao-grafana-oidc \"\" clientAuth"
|
||||
"/otel-oidc.pem ]"
|
||||
"swarm-bao-otel-oidc \"\" clientAuth"
|
||||
"/forwarder-oidc.pem ]"
|
||||
"swarm-bao-forwarder-oidc \"\" clientAuth"
|
||||
];
|
||||
}
|
||||
{
|
||||
|
|
@ -555,7 +589,7 @@ let
|
|||
# still asserted, exactly as the three service principals above behave in
|
||||
# this deployment. A unit that vanished here would take the policy with
|
||||
# it and leave nothing to diagnose.
|
||||
name = "with no client CA the four readers get policies but no login roles";
|
||||
name = "with no client CA the five readers get policies but no login roles";
|
||||
ok =
|
||||
let
|
||||
units = [
|
||||
|
|
@ -563,6 +597,7 @@ let
|
|||
"swarm-bao-queue-agent-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
"swarm-bao-forwarder-oidc-policy"
|
||||
];
|
||||
scriptOf = unit: baoGrantNoClientCa.systemd.services.${unit}.script;
|
||||
in
|
||||
|
|
@ -578,7 +613,7 @@ let
|
|||
# client certificate and the host is the side that has one, so a unit
|
||||
# rendered inside the store's container would have neither an identity
|
||||
# nor a route. Plus the ordering that makes the mounts exist first.
|
||||
name = "the four readers' granting units are ordered after the mounts and rendered on the host";
|
||||
name = "the five readers' granting units are ordered after the mounts and rendered on the host";
|
||||
ok =
|
||||
let
|
||||
units = [
|
||||
|
|
@ -586,6 +621,7 @@ let
|
|||
"swarm-bao-queue-agent-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
"swarm-bao-forwarder-oidc-policy"
|
||||
];
|
||||
in
|
||||
lib.all (
|
||||
|
|
@ -598,7 +634,11 @@ let
|
|||
# The other end of those units: each reader logs in against the role its
|
||||
# own policy unit writes, so it has to wait for that unit. Ordering and
|
||||
# never a requirement, since the policy unit skips once the token is gone.
|
||||
name = "each of the four readers is ordered after the unit writing its role";
|
||||
#
|
||||
# The forwarder is listed apart from `policyReaders`: it renders wherever
|
||||
# the store does, so it is never absent on a store host and never present
|
||||
# on a remote one, and the two cases below would fail on it for that.
|
||||
name = "each of the five readers is ordered after the unit writing its role";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantWithConsumers.systemd.services;
|
||||
|
|
@ -611,7 +651,7 @@ let
|
|||
&& lib.elem policy s.${reader}.wants
|
||||
&& !(lib.elem policy s.${reader}.requires);
|
||||
in
|
||||
lib.all waitsFor policyReaders;
|
||||
lib.all waitsFor (policyReaders ++ [ "swarm-bao-forwarder-oidc" ]);
|
||||
}
|
||||
{
|
||||
# The ordering is set apart from each reader's own definition, so it can
|
||||
|
|
@ -643,7 +683,7 @@ let
|
|||
# `baoGrantNoStore` makes for the controller's, one file over. Without
|
||||
# this arm `lib.mkIf haveBootstrapToken` could be dropped from the shared
|
||||
# builder and every other case here would still pass.
|
||||
name = "without a bootstrap token none of the four readers' granting units render";
|
||||
name = "without a bootstrap token none of the five readers' granting units render";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantNoStore.systemd.services;
|
||||
|
|
@ -651,7 +691,27 @@ let
|
|||
!(s ? swarm-bao-matrix-token-policy)
|
||||
&& !(s ? swarm-bao-queue-agent-policy)
|
||||
&& !(s ? swarm-bao-grafana-oidc-policy)
|
||||
&& !(s ? swarm-bao-otel-oidc-policy);
|
||||
&& !(s ? swarm-bao-otel-oidc-policy)
|
||||
&& !(s ? swarm-bao-forwarder-oidc-policy);
|
||||
}
|
||||
{
|
||||
# 🩸 The refusal half of the forwarder's own leaf. It renders wherever the
|
||||
# store does and has no mode without a secret, so a null pair has one
|
||||
# fallback left — the hive's leaf and its union grant. Refused at eval,
|
||||
# with both options named.
|
||||
name = "a store host without the forwarder's own pair is refused, naming both options";
|
||||
ok =
|
||||
let
|
||||
refused = lib.filter (a: !a.assertion) baoNoForwarderIdentity.assertions;
|
||||
names =
|
||||
a:
|
||||
lib.hasInfix "services.hyperhive.deploy.bao.forwarderOidcClientCertFile" a.message
|
||||
&& lib.hasInfix "services.hyperhive.deploy.bao.forwarderOidcClientKeyFile" a.message;
|
||||
in
|
||||
lib.any names refused
|
||||
# The control: the same store with the pair in place trips no such
|
||||
# assertion, so the arm above is not firing on every store host.
|
||||
&& !(lib.any (a: !a.assertion && names a) baoGrantHere.assertions);
|
||||
}
|
||||
{
|
||||
# The policy authorising this route lives in another file, and nothing
|
||||
|
|
@ -760,7 +820,7 @@ let
|
|||
{
|
||||
# What makes the case above mean something: discovery by token path
|
||||
# reaches every unit that uses the token today, and each yields calls.
|
||||
name = "the bootstrap-policy check sees all nine units that use the token, and parses calls from each";
|
||||
name = "the bootstrap-policy check sees all ten units that use the token, and parses calls from each";
|
||||
ok =
|
||||
lib.all (n: bootstrapUnits ? ${n}) [
|
||||
"swarm-bao-controller-policy"
|
||||
|
|
@ -770,6 +830,7 @@ let
|
|||
"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"
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue