bao: let the UI's admins login write the review classifier's API key
The docs tell the operator to write `secret/swarm/services/review-classifier/anthropic-api-key` by hand in the bao UI, but the UI's OIDC login for authelia's `admins` group lands on `swarm-operator-viewer`, which is list+read on `secret/metadata/*` and nothing under `secret/data/`. bao refuses the write, so an admin can't set the key and every classifier run fails with "no API key". The viewer policy gains `create` and `update` on that one leaf, no `read`: a UI session still never sees a secret value. The leaf is a named binding, shared with the classifier's reader policy. module-eval pins the viewer's two stanzas and that no `data/` grant carries `read`. docs/swarm/secrets.md and services.md describe the write. Closes #5148
This commit is contained in:
parent
a669cd96b6
commit
133d5578d2
4 changed files with 32 additions and 11 deletions
|
|
@ -524,9 +524,12 @@ stream passthrough readers use on that host comes up with the store sealed.
|
||||||
|
|
||||||
The OIDC tab logs in through authelia as the client `swarm-bao-ui`
|
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.bao.ui.oidc.clientId`). An `admins` member gets a token under
|
||||||
`swarm-operator-viewer`: `list` and `read` on `secret/metadata/*`. That shows
|
`swarm-operator-viewer`: `list` and `read` on `secret/metadata/*`, and
|
||||||
|
`create` and `update` on
|
||||||
|
`secret/data/swarm/services/review-classifier/anthropic-api-key`. That shows
|
||||||
the key tree and each key's versions and timestamps, and no value: the store
|
the key tree and each key's versions and timestamps, and no value: the store
|
||||||
refuses every `secret/data/` read. Anything more needs a token.
|
refuses every `secret/data/` read. The one write is the review classifier's API
|
||||||
|
key, which the operator fills in by hand. Anything more needs a token.
|
||||||
|
|
||||||
| piece | written by |
|
| piece | written by |
|
||||||
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,8 @@ identity: the host that runs the store has it, and a services host with the
|
||||||
store elsewhere gets the classifier only once it holds a copy of the pair (see
|
store elsewhere gets the classifier only once it holds a copy of the pair (see
|
||||||
below). Set `deploy.review-classifier.enable` directly to override either way.
|
below). Set `deploy.review-classifier.enable` directly to override either way.
|
||||||
|
|
||||||
The operator writes the Anthropic key by hand, in the secret store's UI at
|
The operator writes the Anthropic key by hand, in the secret store's UI under
|
||||||
|
the OIDC login, at
|
||||||
`secret/swarm/services/review-classifier/anthropic-api-key`, field `api_key`.
|
`secret/swarm/services/review-classifier/anthropic-api-key`, field `api_key`.
|
||||||
Nothing writes it for you: until it exists the service runs and every run fails
|
Nothing writes it for you: until it exists the service runs and every run fails
|
||||||
with `no API key…`. The service reads the key on every run, so writing or
|
with `no API key…`. The service reads the key on every run, so writing or
|
||||||
|
|
|
||||||
|
|
@ -577,13 +577,18 @@ let
|
||||||
# classifier's reviews with it; no other principal's policy reaches it.
|
# classifier's reviews with it; no other principal's policy reaches it.
|
||||||
classifierForgeTokenLeaf = "swarm/services/review-classifier/forge/token";
|
classifierForgeTokenLeaf = "swarm/services/review-classifier/forge/token";
|
||||||
|
|
||||||
|
# The review classifier's Anthropic API key. The operator writes it through
|
||||||
|
# the UI's OIDC login; the classifier reads it on every run.
|
||||||
|
classifierApiKeyLeaf = "swarm/services/review-classifier/anthropic-api-key";
|
||||||
|
|
||||||
# The UI's OIDC client secret. The publisher writes it; the granter reads it
|
# The UI's OIDC client secret. The publisher writes it; the granter reads it
|
||||||
# into the `oidc` auth method's config, which is the only copy bao uses.
|
# into the `oidc` auth method's config, which is the only copy bao uses.
|
||||||
baoUiClientLeaf = "swarm/services/${cfg.ui.oidc.clientId}/oidc/client";
|
baoUiClientLeaf = "swarm/services/${cfg.ui.oidc.clientId}/oidc/client";
|
||||||
|
|
||||||
# What an OIDC login through the UI gets: the key tree and each key's KV
|
# What an OIDC login through the UI gets: the key tree and each key's KV
|
||||||
# metadata, never a value — KV v2 serves values under `data/`, which no
|
# metadata, and a write on the one leaf the operator fills by hand. Never a
|
||||||
# stanza here names. The UI needs no `sys/` grant on top:
|
# value: KV v2 serves values under `data/`, and the one `data/` stanza has
|
||||||
|
# no `read`. The UI needs no `sys/` grant on top:
|
||||||
# `sys/internal/ui/mounts` lists any mount the token holds a capability
|
# `sys/internal/ui/mounts` lists any mount the token holds a capability
|
||||||
# under, and the rest it calls is in the `default` policy.
|
# under, and the rest it calls is in the `default` policy.
|
||||||
operatorViewerPolicyName = "swarm-operator-viewer";
|
operatorViewerPolicyName = "swarm-operator-viewer";
|
||||||
|
|
@ -591,6 +596,9 @@ let
|
||||||
path "${credentialMountPath}/metadata/*" {
|
path "${credentialMountPath}/metadata/*" {
|
||||||
capabilities = ["list", "read"]
|
capabilities = ["list", "read"]
|
||||||
}
|
}
|
||||||
|
path "${credentialMountPath}/data/${classifierApiKeyLeaf}" {
|
||||||
|
capabilities = ["create", "update"]
|
||||||
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# authelia's operator group (`operatorGroup` in ./swarm-authelia.nix, the
|
# authelia's operator group (`operatorGroup` in ./swarm-authelia.nix, the
|
||||||
|
|
@ -967,7 +975,7 @@ let
|
||||||
name = "swarm-review-classifier";
|
name = "swarm-review-classifier";
|
||||||
cn = baoDeploy.reviewClassifierCommonName;
|
cn = baoDeploy.reviewClassifierCommonName;
|
||||||
policyText =
|
policyText =
|
||||||
readStanza "${credentialMountPath}/data/swarm/services/review-classifier/anthropic-api-key"
|
readStanza "${credentialMountPath}/data/${classifierApiKeyLeaf}"
|
||||||
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.review-classifier.clientId}/oidc/client";
|
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.review-classifier.clientId}/oidc/client";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1836,10 +1836,11 @@ let
|
||||||
|
|
||||||
# ── the UI's OIDC login ─────────────────────────────────────────────────
|
# ── the UI's OIDC login ─────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
# What an `admins` login through the UI holds: the key tree and KV
|
# What an `admins` login through the UI holds: the key tree, KV
|
||||||
# metadata. Any `data/` path would hand a browser session every secret
|
# metadata, and a write on the one leaf the operator fills by hand.
|
||||||
# in the store, and any `sys/` one more than the UI needs.
|
# A `read` under `data/` would hand a browser session a secret's
|
||||||
name = "the operator viewer policy is list and read on KV metadata, and nothing under data/ or sys/";
|
# 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 API key, and no data/ read or sys/";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
viewer = grantsIn baoGrantHere.systemd.services.swarm-bao-operator-viewer-policy.script;
|
viewer = grantsIn baoGrantHere.systemd.services.swarm-bao-operator-viewer-policy.script;
|
||||||
|
|
@ -1852,10 +1853,18 @@ let
|
||||||
"read"
|
"read"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
path = "secret/data/swarm/services/review-classifier/anthropic-api-key";
|
||||||
|
caps = [
|
||||||
|
"create"
|
||||||
|
"update"
|
||||||
|
];
|
||||||
|
}
|
||||||
]
|
]
|
||||||
&& grantFor viewer "secret/data/swarm/agents/x/queue" == null
|
&& grantFor viewer "secret/data/swarm/agents/x/queue" == null
|
||||||
&& grantFor viewer "secret/metadata/swarm/agents/x/queue" != null
|
&& grantFor viewer "secret/metadata/swarm/agents/x/queue" != null
|
||||||
&& !(lib.any (g: lib.hasPrefix "secret/data" g.path || lib.hasPrefix "sys/" g.path) viewer);
|
&& !(lib.any (g: lib.hasPrefix "secret/data" g.path && lib.elem "read" g.caps) viewer)
|
||||||
|
&& !(lib.any (g: lib.hasPrefix "sys/" g.path) viewer);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# Route (a): enabling an auth method stays a bootstrap-token act. The
|
# Route (a): enabling an auth method stays a bootstrap-token act. The
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue