diff --git a/docs/swarm/builder.md b/docs/swarm/builder.md index 893505636..00d1d73eb 100644 --- a/docs/swarm/builder.md +++ b/docs/swarm/builder.md @@ -355,8 +355,8 @@ principal and create what's missing. A unit creates each secret only while the store holds none, so a second start mints nothing. Both tokens last ten years and nothing renews them. To replace one, -write the new token to its path in the store, in its UI under the OIDC login: -nothing mints over a value that exists. A new signing key invalidates both tokens. +write the new token to its path in the store: nothing mints over a value that +exists. A new signing key invalidates both tokens. The unit creates the address, name and key once and never updates them. After you change `swarm.attic.domain`, or after the server's database goes and the @@ -387,8 +387,8 @@ the swarm at an existing tenant: `services.hyperhive.swarm.attic.cache` to the cache's name on it. Set both options on every host that reads them. 2. Seed the push token in the swarm secret store at - `secret/swarm/services/attic/push-token`, field `value`. Use the store's UI under - the OIDC login, or `bao kv put` with `value=-` and the token on stdin, so the token is in no + `secret/swarm/services/attic/push-token`, field `value`. Use the store's UI, + or `bao kv put` with `value=-` and the token on stdin, so the token is in no command line. The token needs push rights on that cache. Until it exists, every push fails and builds stay green with `cached` false. Seed the hives' pull token the same way at diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md index 1421b0f5a..35bd35445 100644 --- a/docs/swarm/credentials.md +++ b/docs/swarm/credentials.md @@ -556,24 +556,11 @@ The OIDC tab logs in through authelia as the client `swarm-bao-ui` (`swarm.bao.ui.oidc.clientId`). An `admins` member gets a token under `swarm-operator-viewer`: `list` and `read` on `secret/metadata/*`, and `create` and `update` on -`secret/data/swarm/services/review-classifier/anthropic-api-key`, -`secret/data/swarm/services/review-classifier/acp-provider` and the cache's five -leaves under `secret/data/swarm/services/attic/` (`push-token`, `pull-token`, -`url`, `cache` and `public-key`). That shows the key tree and each key's -versions and timestamps, and no value: the store refuses every `secret/data/` -read. The writes are the leaves the operator fills in by hand: the review -classifier's keys, and the cache's tokens and facts, which configure an external -cache or replace the in-swarm cache's own. The cache's host units create those -leaves only while the store holds none, so a saved value survives their next run -and every deploy. The cache's signing key, `attic/jwt-secret`, isn't writable here. -Anything more needs a token. - -To replace one of the cache's leaves in the UI, open `secret` > `swarm` > -`services` > `attic`, pick the leaf and choose **Create new version**. The -form starts empty because the login can't read the current value: type the -key `value` and the new value, and save. A new version replaces the whole -secret, so `value` is the only key to enter. `bao kv put` with the same login -works too. +`secret/data/swarm/services/review-classifier/anthropic-api-key` and +`secret/data/swarm/services/review-classifier/acp-provider`. That shows the key +tree and each key's versions and timestamps, and no value: the store refuses +every `secret/data/` read. The two writes are the review classifier's keys, +which the operator fills in by hand. Anything more needs a token. | piece | written by | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 8078501d1..9e6df0ea0 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -777,28 +777,18 @@ let # `read`. The UI needs no `sys/` grant on top: # `sys/internal/ui/mounts` lists any mount the token holds a capability # under, and the rest it calls is in the `default` policy. - # - # The cache's five leaves are where an external cache is configured, and - # where the operator replaces the in-swarm cache's tokens; its host units - # create them only while none is stored. `atticJwtLeaf` is left out: a new - # signing key invalidates both tokens. - operatorWritableLeaves = [ - classifierApiKeyLeaf - classifierAcpKeyLeaf - atticPushTokenLeaf - ] - ++ atticFactLeaves; operatorViewerPolicyName = "swarm-operator-viewer"; operatorViewerPolicyText = '' path "${credentialMountPath}/metadata/*" { capabilities = ["list", "read"] } - '' - + lib.concatMapStrings (leaf: '' - path "${credentialMountPath}/data/${leaf}" { + path "${credentialMountPath}/data/${classifierApiKeyLeaf}" { capabilities = ["create", "update"] } - '') operatorWritableLeaves; + path "${credentialMountPath}/data/${classifierAcpKeyLeaf}" { + capabilities = ["create", "update"] + } + ''; # authelia's operator group (`operatorGroup` in ./swarm-authelia.nix, the # group its rule for the UI's vhost admits) → the viewer policy. authelia diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index c1b43d95e..9a5f25d39 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -316,15 +316,6 @@ let best: g: if best == null || lib.stringLength g.path > lib.stringLength best.path then g else best ) null globs; - # Whether the policy text in `text` lets its holder read `secret/data/swarm/`, - # by `grantFor`'s rule, so a glob that reaches the leaf counts and a write does not. - grantReads = - leaf: text: - let - g = grantFor (grantsIn text) "secret/data/swarm/${leaf}"; - in - g != null && lib.elem "read" g.caps; - ungranted = grants: units: lib.concatLists ( @@ -2148,42 +2139,20 @@ let { # The signing key is the cache's alone; the push token is also the # builder's, and is read there. The pull token is also the controller's, - # which only reads it (pinned above). The pull token's value is what - # the roster guards, so it counts the units that may read it: the - # operator's write-only grant on it, which replaces the token and - # exposes nothing, stays outside. - name = "no granting unit but the cache's names its signing key, and only it and the controller read its pull token"; + # which only reads it (pinned above). + name = "no granting unit but the cache's names its signing key, and only it and the controller name its pull token"; ok = lib.filter ( u: lib.hasInfix "services/attic/jwt-secret" baoGrantHere.systemd.services.${u}.script ) grantingUnitNames == [ "swarm-bao-attic-policy" ] && lib.filter ( - u: grantReads "services/attic/pull-token" baoGrantHere.systemd.services.${u}.script + u: lib.hasInfix "services/attic/pull-token" baoGrantHere.systemd.services.${u}.script ) grantingUnitNames == [ "swarm-bao-controller-policy" "swarm-bao-attic-policy" ]; } - { - # Control for the property above: `grantReads` tells a read from a write - # on the same path, and the operator's policy does name the pull token. - name = "a grant that only writes the pull token is not a read of it, and one that reads it is"; - ok = - let - stanza = caps: '' - path "secret/data/swarm/services/attic/pull-token" { - capabilities = [${caps}] - } - ''; - viewer = baoGrantHere.systemd.services.swarm-bao-operator-viewer-policy.script; - in - !(grantReads "services/attic/pull-token" (stanza ''"create", "update"'')) - && grantReads "services/attic/pull-token" (stanza ''"read"'') - && grantReads "services/attic/pull-token" (stanza ''"create", "read"'') - && lib.hasInfix "services/attic/pull-token" viewer - && !(grantReads "services/attic/pull-token" viewer); - } { name = "the PKI unit signs the cache's leaf under its own subject"; ok = @@ -2665,7 +2634,7 @@ let # metadata, and a write on the leaves the operator fills by hand. # A `read` under `data/` would hand a browser session a secret's # value, and any `sys/` path more than the UI needs. - name = "the operator viewer policy is list and read on KV metadata, a write on the classifier's two keys and the cache's five, and no data/ read, attic signing key or sys/"; + name = "the operator viewer policy is list and read on KV metadata, a write on the classifier's two keys, and no data/ read or sys/"; ok = let viewer = grantsIn baoGrantHere.systemd.services.swarm-bao-operator-viewer-policy.script; @@ -2692,43 +2661,7 @@ let "update" ]; } - { - path = "secret/data/swarm/services/attic/push-token"; - caps = [ - "create" - "update" - ]; - } - { - path = "secret/data/swarm/services/attic/pull-token"; - caps = [ - "create" - "update" - ]; - } - { - path = "secret/data/swarm/services/attic/url"; - caps = [ - "create" - "update" - ]; - } - { - path = "secret/data/swarm/services/attic/cache"; - caps = [ - "create" - "update" - ]; - } - { - path = "secret/data/swarm/services/attic/public-key"; - caps = [ - "create" - "update" - ]; - } ] - && grantFor viewer "secret/data/swarm/services/attic/jwt-secret" == null && grantFor viewer "secret/data/swarm/agents/x/queue" == null && grantFor viewer "secret/metadata/swarm/agents/x/queue" != null && !(lib.any (g: lib.hasPrefix "secret/data" g.path && lib.elem "read" g.caps) viewer) diff --git a/nix/script-tests/attic-init.sh b/nix/script-tests/attic-init.sh index be8be0bd2..2f2319dfc 100644 --- a/nix/script-tests/attic-init.sh +++ b/nix/script-tests/attic-init.sh @@ -207,26 +207,6 @@ expect "pull token unchanged" "$(stored $PULL)" "$pull" expect "push token unchanged" "$(stored $PUSH)" "$push" expect "address, name and key unchanged" "$(stored $URL)$(stored $CACHE)$(stored $KEY)" "http://cache.examplegridgrid:FAKEKEY" -case="values the operator stored over the minted ones are kept" -for leaf in $PULL $PUSH $URL $CACHE $KEY; do - printf '%s' "operator-$leaf" >"$dir/store/$leaf" -done -: >"$calls" -run "$PROVISION" -expect "exit status" $? 0 -expect "no token minted" "$(n '^atticadm')" 0 -expect "nothing written" "$(n '^bao put')" 0 -expect "the operator's push token is still stored" "$(stored $PUSH)" "operator-$PUSH" -expect "the operator's pull token is still stored" "$(stored $PULL)" "operator-$PULL" -expect "the operator's address, name and key are still stored" \ - "$(stored $URL)$(stored $CACHE)$(stored $KEY)" "operator-${URL}operator-${CACHE}operator-${KEY}" -expect "the cache lookup used the operator's pull token" "$(n "^attic cache info local:grid | token=operator-$PULL")" 1 -printf '%s' "$pull" >"$dir/store/$PULL" -printf '%s' "$push" >"$dir/store/$PUSH" -printf '%s' http://cache.example >"$dir/store/$URL" -printf '%s' grid >"$dir/store/$CACHE" -printf '%s' grid:FAKEKEY >"$dir/store/$KEY" - case="an empty server gets its cache back with the stored tokens" rm -f "$dir/cache-exists" : >"$calls"