Watch
0
0
Fork
You've already forked hyperhive
0

swarm-bao: grant the queue responder the callout issuer and user keys' leaves

The `swarm-nats-auth` policy grants create and read, never update, on the
seed and public half of the callout issuer and user keys, beside the xkey's
two leaves: `ensure-callout-keys` creates them under that role, and the
responder reads both seeds as it starts.

The `swarm-secret-publisher` policy, which may write the rest of
`swarm/services/`, is denied both seed paths exactly, as it is the xkey's.
A planted issuer seed would be the authority to admit anyone to the queue.

Both policies are written by the granter on every deploy; nothing about the
granter's own policy changes.
This commit is contained in:
atlas 2026-10-06 11:46:28 +02:00 • committed by mara
commit 66ac034949
2 changed files with 60 additions and 17 deletions

View file

@ -469,9 +469,11 @@ let
# Write-only. It copies secrets in and never reads one back; a read # Write-only. It copies secrets in and never reads one back; a read
# capability would let a file-copier recover every hive's credentials. # capability would let a file-copier recover every hive's credentials.
# #
# The callout xkey's seed sits under `swarm/services/` and is no secret this # The queue callout's three seeds sit under `swarm/services/` and are no
# principal copies, so its exact path, which outranks the prefix, is denied: # secret this principal copies, so their exact paths, which outrank the
# a write there would replace the key every queue request is sealed to. # prefix, are denied. A write to the xkey's would replace the key every
# queue request is sealed to; the issuer's is the authority to admit anyone
# to the queue, and the user's is the one identity exempt from callout.
secretPublisherPolicyText = '' secretPublisherPolicyText = ''
path "${credentialMountPath}/data/swarm/hives/*" { path "${credentialMountPath}/data/swarm/hives/*" {
capabilities = ["create", "update"] capabilities = ["create", "update"]
@ -485,6 +487,14 @@ let
capabilities = ["deny"] capabilities = ["deny"]
} }
path "${credentialMountPath}/data/${issuerSeedLeaf}" {
capabilities = ["deny"]
}
path "${credentialMountPath}/data/${userSeedLeaf}" {
capabilities = ["deny"]
}
path "${credentialMountPath}/data/${controllerClientLeaf}" { path "${credentialMountPath}/data/${controllerClientLeaf}" {
capabilities = ["create", "update"] capabilities = ["create", "update"]
} }
@ -858,10 +868,10 @@ let
# other credential an agent holds. Plus the responder's own OIDC client # other credential an agent holds. Plus the responder's own OIDC client
# secret, which `swarm-bao-nats-auth-oidc` reads under this same leaf. # secret, which `swarm-bao-nats-auth-oidc` reads under this same leaf.
# #
# And the callout xkey's two leaves, which `ensure-xkey` creates when the # And the two leaves of each callout key, which `ensure-callout-keys`
# store holds no seed. `create` without `update`, so a stored pair is never # creates when the store holds no seed. `create` without `update`, so a
# replaced; `read` because creating is read-then-create, and the responder # stored pair is never replaced; `read` because creating is
# loads the seed as it starts. # read-then-create, and the responder loads the seeds as it starts.
natsAuthReaders = [ natsAuthReaders = [
{ {
name = "swarm-nats-auth"; name = "swarm-nats-auth";
@ -869,17 +879,28 @@ let
policyText = policyText =
readStanza "${credentialMountPath}/data/swarm/agents/+/queue" readStanza "${credentialMountPath}/data/swarm/agents/+/queue"
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.nats.clientId}/oidc/client" + readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.nats.clientId}/oidc/client"
+ createStanza "${credentialMountPath}/data/${xkeySeedLeaf}" + lib.concatMapStrings (leaf: createStanza "${credentialMountPath}/data/${leaf}") [
+ createStanza "${credentialMountPath}/data/${xkeyPublicLeaf}"; xkeySeedLeaf
xkeyPublicLeaf
issuerSeedLeaf
issuerPublicLeaf
userSeedLeaf
userPublicLeaf
];
} }
]; ];
# The callout xkey, the nix half of `swarm-nats-auth`'s `xkey::SEED_PATH` # The callout's keys, the nix half of `swarm-nats-auth`'s `xkey::SEED_PATH`
# and `xkey::PUBLIC_PATH`. Both are readable by the `swarm-nats-auth` role # and `xkey::PUBLIC_PATH`, and of the `seed_path` and `public_path` of
# alone: `swarm/public/` marks the half that is no secret, and no policy # `nkey::ISSUER` and `nkey::USER`. All are readable by the `swarm-nats-auth`
# opens it to every token. # role alone: `swarm/public/` marks the halves that are no secret, and no
# policy opens them to every token.
xkeySeedLeaf = "swarm/services/swarm-nats/callout/xkey-seed"; xkeySeedLeaf = "swarm/services/swarm-nats/callout/xkey-seed";
xkeyPublicLeaf = "swarm/public/swarm-nats/callout-xkey"; xkeyPublicLeaf = "swarm/public/swarm-nats/callout-xkey";
issuerSeedLeaf = "swarm/services/swarm-nats/callout/issuer-seed";
issuerPublicLeaf = "swarm/public/swarm-nats/callout-issuer";
userSeedLeaf = "swarm/services/swarm-nats/callout/user-seed";
userPublicLeaf = "swarm/public/swarm-nats/callout-user";
createStanza = path: '' createStanza = path: ''
path "${path}" { path "${path}" {

View file

@ -442,6 +442,24 @@ let
"update" "update"
]; ];
} }
{
# The same denial for the callout's two nkey seeds: a planted issuer
# seed admits anyone to the queue, and a planted user seed is the one
# identity callout does not check. Each is its own exact path, so the
# OIDC leaf beside them is still the prefix's.
name = "the publisher may not write the callout issuer or user seed";
ok =
let
grants = grantsIn baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy.script;
in
(grantFor grants "secret/data/swarm/services/swarm-nats/callout/issuer-seed").caps == [ "deny" ]
&& (grantFor grants "secret/data/swarm/services/swarm-nats/callout/user-seed").caps == [ "deny" ]
&&
(grantFor grants "secret/data/swarm/services/swarm-nats/oidc/client").caps == [
"create"
"update"
];
}
{ {
# The ordering is load-bearing and invisible at runtime: the controller's # The ordering is load-bearing and invisible at runtime: the controller's
# unit creates the KV and cert-auth mounts this one writes into, so # unit creates the KV and cert-auth mounts this one writes into, so
@ -590,9 +608,9 @@ let
# `+` is one path segment, so this reaches `swarm/agents/<agent>/queue` # `+` is one path segment, so this reaches `swarm/agents/<agent>/queue`
# and no other credential an agent holds; `swarm/agents/*` would reach # and no other credential an agent holds; `swarm/agents/*` would reach
# all of them. The responder's own OIDC client secret is the one # all of them. The responder's own OIDC client secret is the one
# `services/` leaf beside it. Its only creates are the callout xkey's # `services/` leaf beside it. Its only creates are the two leaves of
# two leaves, and nothing it holds may update or delete one. # each callout key, and nothing it holds may update or delete one.
name = "the queue responder reads agent queue credentials and its OIDC secret, and may create only the callout xkey's two leaves"; name = "the queue responder reads agent queue credentials and its OIDC secret, and may create only the callout keys' leaves";
ok = ok =
let let
s = baoGrantHere.systemd.services.swarm-bao-nats-auth-policy.script; s = baoGrantHere.systemd.services.swarm-bao-nats-auth-policy.script;
@ -604,7 +622,11 @@ let
&& lib.length (lib.filter lib.isList (builtins.split "capabilities = \\[\"read\"]" s)) == 2 && lib.length (lib.filter lib.isList (builtins.split "capabilities = \\[\"read\"]" s)) == 2
&& &&
lib.sort lib.lessThan creates == [ lib.sort lib.lessThan creates == [
"secret/data/swarm/public/swarm-nats/callout-issuer"
"secret/data/swarm/public/swarm-nats/callout-user"
"secret/data/swarm/public/swarm-nats/callout-xkey" "secret/data/swarm/public/swarm-nats/callout-xkey"
"secret/data/swarm/services/swarm-nats/callout/issuer-seed"
"secret/data/swarm/services/swarm-nats/callout/user-seed"
"secret/data/swarm/services/swarm-nats/callout/xkey-seed" "secret/data/swarm/services/swarm-nats/callout/xkey-seed"
] ]
&& lib.all ( && lib.all (
@ -616,7 +638,7 @@ let
"read" "read"
] ]
) grants ) grants
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 4 && lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 8
&& !(lib.hasInfix "secret/data/swarm/agents/*" s) && !(lib.hasInfix "secret/data/swarm/agents/*" s)
&& !(lib.hasInfix "secret/data/swarm/hives" s) && !(lib.hasInfix "secret/data/swarm/hives" s)
&& !(lib.hasInfix "secret/data/swarm/services/*" s) && !(lib.hasInfix "secret/data/swarm/services/*" s)