Watch
0
0
Fork
You've already forked hyperhive
0

let the operator's oidc login write the cache's tokens and facts

This commit is contained in:
atlas 2026-10-28 23:46:58 +01:00
commit f22f4ee6ac
5 changed files with 87 additions and 15 deletions

View file

@ -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: nothing mints over a value that
exists. A new signing key invalidates both tokens.
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.
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,
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 under
the OIDC login, 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

View file

@ -556,11 +556,17 @@ 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` 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.
`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.
| piece | written by |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |

View file

@ -777,18 +777,28 @@ 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"]
}
path "${credentialMountPath}/data/${classifierApiKeyLeaf}" {
''
+ lib.concatMapStrings (leaf: ''
path "${credentialMountPath}/data/${leaf}" {
capabilities = ["create", "update"]
}
path "${credentialMountPath}/data/${classifierAcpKeyLeaf}" {
capabilities = ["create", "update"]
}
'';
'') operatorWritableLeaves;
# authelia's operator group (`operatorGroup` in ./swarm-authelia.nix, the
# group its rule for the UI's vhost admits) → the viewer policy. authelia

View file

@ -2634,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 no data/ read or sys/";
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/";
ok =
let
viewer = grantsIn baoGrantHere.systemd.services.swarm-bao-operator-viewer-policy.script;
@ -2661,7 +2661,43 @@ 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)

View file

@ -207,6 +207,26 @@ 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"