diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 4a42698e..f0916b0b 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -469,9 +469,11 @@ let # Write-only. It copies secrets in and never reads one back; a read # 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 - # principal copies, so its exact path, which outranks the prefix, is denied: - # a write there would replace the key every queue request is sealed to. + # The queue callout's three seeds sit under `swarm/services/` and are no + # secret this principal copies, so their exact paths, which outrank the + # 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 = '' path "${credentialMountPath}/data/swarm/hives/*" { capabilities = ["create", "update"] @@ -485,6 +487,14 @@ let capabilities = ["deny"] } + path "${credentialMountPath}/data/${issuerSeedLeaf}" { + capabilities = ["deny"] + } + + path "${credentialMountPath}/data/${userSeedLeaf}" { + capabilities = ["deny"] + } + path "${credentialMountPath}/data/${controllerClientLeaf}" { capabilities = ["create", "update"] } @@ -858,10 +868,10 @@ let # other credential an agent holds. Plus the responder's own OIDC client # 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 - # store holds no seed. `create` without `update`, so a stored pair is never - # replaced; `read` because creating is read-then-create, and the responder - # loads the seed as it starts. + # And the two leaves of each callout key, which `ensure-callout-keys` + # creates when the store holds no seed. `create` without `update`, so a + # stored pair is never replaced; `read` because creating is + # read-then-create, and the responder loads the seeds as it starts. natsAuthReaders = [ { name = "swarm-nats-auth"; @@ -869,17 +879,28 @@ let policyText = readStanza "${credentialMountPath}/data/swarm/agents/+/queue" + readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.nats.clientId}/oidc/client" - + createStanza "${credentialMountPath}/data/${xkeySeedLeaf}" - + createStanza "${credentialMountPath}/data/${xkeyPublicLeaf}"; + + lib.concatMapStrings (leaf: createStanza "${credentialMountPath}/data/${leaf}") [ + xkeySeedLeaf + xkeyPublicLeaf + issuerSeedLeaf + issuerPublicLeaf + userSeedLeaf + userPublicLeaf + ]; } ]; - # The callout xkey, the nix half of `swarm-nats-auth`'s `xkey::SEED_PATH` - # and `xkey::PUBLIC_PATH`. Both are readable by the `swarm-nats-auth` role - # alone: `swarm/public/` marks the half that is no secret, and no policy - # opens it to every token. + # The callout's keys, the nix half of `swarm-nats-auth`'s `xkey::SEED_PATH` + # and `xkey::PUBLIC_PATH`, and of the `seed_path` and `public_path` of + # `nkey::ISSUER` and `nkey::USER`. All are readable by the `swarm-nats-auth` + # 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"; 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: '' path "${path}" { diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index 3595dc0d..4907d2e4 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -442,6 +442,24 @@ let "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 # 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//queue` # and no other credential an agent holds; `swarm/agents/*` would reach # 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 - # two leaves, 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"; + # `services/` leaf beside it. Its only creates are the two leaves of + # 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 keys' leaves"; ok = let 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.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/services/swarm-nats/callout/issuer-seed" + "secret/data/swarm/services/swarm-nats/callout/user-seed" "secret/data/swarm/services/swarm-nats/callout/xkey-seed" ] && lib.all ( @@ -616,7 +638,7 @@ let "read" ] ) 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/hives" s) && !(lib.hasInfix "secret/data/swarm/services/*" s)