drop the shared hive agent login
Every agent now connects to the queue and reads logs with its own secret from the store (#5181), so the per-hive `hive-<h>-agent` OIDC client has no reader left. Remove it end to end: - hive-c0re no longer forwards `hive-queue-agent-{secret,client-id}` into containers, and no longer forwards HIVE_AGENT_OIDC_TOKEN_ENDPOINT; the agent module that loaded them (nix/agent-modules/queue.nix) is gone, its options refused with a removal message. - The host reader (glue-queue-agent-credential.nix, swarm-bao-queue-agent), its store role and policy unit (swarm-bao-queue-agent-policy), its PKI leaf and the deploy.bao.queueAgent* / queue.agentCredentialDir options are gone; the options are refused with a removal message. - authelia mints no `hive-<h>-agent` client, and swarm-secret-publish no longer writes swarm/hives/<h>/queue/agent. - swarm-secret-client drops queue::agent_client_path / queue::Credential. - HIVE_AGENT_NATS_URL is now gated on the agents' queue address alone. The log store's authelia check stays for operators. swarm-nats-auth still refuses a `-agent` client id; that refusal and `agentClientSuffix` are left for a follow-up. Refs #4459 Refs #4765
This commit is contained in:
parent
d19bd83aa5
commit
868c6cf96c
41 changed files with 286 additions and 1410 deletions
|
|
@ -303,8 +303,7 @@ subjects it publishes to, the credential may answer the one request it was just
|
|||
sent, which is how a command gets its reply. The harness and the subagent daemon
|
||||
connect with `_INBOX.agent.<agent>` as their inbox prefix, so the replies to the
|
||||
icon write and the schedule read arrive. An agent can't read another agent's
|
||||
terminal, state, input, commands or replies. The queue refuses the hive's shared
|
||||
agent client outright.
|
||||
terminal, state, input, commands or replies.
|
||||
|
||||
Hives and the swarm controller get a subscribe allow-list too, each with its
|
||||
own reply inbox `_INBOX.client.<client-id>.>`, which `swarm_queue_client::connect`
|
||||
|
|
|
|||
|
|
@ -121,13 +121,12 @@ them and names them in config. The service principals' leaves are at the top of
|
|||
`/var/lib/swarm-bao-pki/`, named after the principal.
|
||||
|
||||
For every hive in `swarm.hives` other than its own, the store's host signs
|
||||
that hive's three leaves into `/var/lib/swarm-bao-pki/hives/<hive>/`:
|
||||
that hive's two leaves into `/var/lib/swarm-bao-pki/hives/<hive>/`:
|
||||
|
||||
| file | subject | name it in |
|
||||
| ---------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `client.pem`, `client-key.pem` | `<hive>` | `deploy.bao.clientCertFile`, `.clientKeyFile` |
|
||||
| `queue-agent.pem`, `queue-agent-key.pem` | `<deploy.bao.queueAgentCommonNamePrefix>-<hive>` | `deploy.bao.queueAgentClientCertFile`, `.queueAgentClientKeyFile` |
|
||||
| `hive-leaf.pem`, `hive-leaf-key.pem` | `<deploy.bao.hiveLeafCommonNamePrefix>-<hive>` | `deploy.hive-controller.tls.baoHiveIdentityCertFile`, `.baoHiveIdentityKeyFile` |
|
||||
| file | subject | name it in |
|
||||
| ------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `client.pem`, `client-key.pem` | `<hive>` | `deploy.bao.clientCertFile`, `.clientKeyFile` |
|
||||
| `hive-leaf.pem`, `hive-leaf-key.pem` | `<deploy.bao.hiveLeafCommonNamePrefix>-<hive>` | `deploy.hive-controller.tls.baoHiveIdentityCertFile`, `.baoHiveIdentityKeyFile` |
|
||||
|
||||
Copy `/var/lib/swarm-bao-pki/ca.pem` with them and name it in
|
||||
`deploy.bao.serverCaFile`. A hive added to `swarm.hives` gets its directory on
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ the rest of the cell says how.
|
|||
| `swarm/agents/<agent>/claude-credentials` | `swarm-claude-login`, when you log the agent in from its card; one version per login | the agent, while it waits for a login | ❌ log in again | ✅ every 30 seconds while waiting |
|
||||
| `swarm/hives/<hive>/matrix/sender-token` | `swarm-controller`, every 5 minutes | `swarm-controller` and the hive | ✅ when missing or rejected by the homeserver | ✅ the hive re-reads on every sweep |
|
||||
| `swarm/hives/<hive>/forge/token` | `swarm-controller`, every 5 minutes | `swarm-controller` and the hive | ✅ when missing or no longer matching the forge | ✅ read on every forge call |
|
||||
| `swarm/hives/<hive>/queue/agent` | authelia | `swarm-bao-queue-agent` on the hive's host | must be stated | must be stated |
|
||||
| `swarm/hives/<hive>/oidc/client` | authelia, first boot; `swarm-secret-publish` copies it in | the hive | ❌ minted once; `swarm-secret-publish` republishes a re-mint | ✅ re-read when authelia refuses it |
|
||||
| `swarm/services/<clientId>/oidc/client` | authelia | that service | must be stated | must be stated |
|
||||
| `swarm/services/ci-runner/registration-token` | `swarm-controller`, from the forge, at start and every 5 minutes | `swarm-bao-ci-runner` on the runner's host | ❌ the forge keeps one token until an admin resets it; the controller then writes the new one within 5 minutes | ✅ every 10 minutes; restarts the runner on a change |
|
||||
|
|
|
|||
|
|
@ -107,9 +107,8 @@ with nothing to authenticate with still receives every hive's telemetry —
|
|||
its pushes to the stores go out unauthenticated and get refused there, and its
|
||||
scrape jobs for the published targets read a credential file nothing
|
||||
loads, so they can't get a token. `swarm-bao-otel-oidc.service` therefore doesn't render at all without
|
||||
`deploy.bao.otelOidcClientCertFile` / `otelOidcClientKeyFile`, the same choice
|
||||
`glue-queue-agent-credential.nix` makes for its own reader, rather than the hard assertion `swarm-grafana.nix`
|
||||
uses for its non-optional one. The operator can still deliver the secret by
|
||||
`deploy.bao.otelOidcClientCertFile` / `otelOidcClientKeyFile`, rather than the hard assertion
|
||||
`swarm-grafana.nix` uses for its non-optional one. The operator can still deliver the secret by
|
||||
hand, at `services.hyperhive.deploy.swarm-otel.clientSecretFile`, on a
|
||||
collector this unit never reaches.
|
||||
|
||||
|
|
@ -259,7 +258,6 @@ such as the root token.
|
|||
| the hive's own store identity | `glue-bao-tls.nix` on the store's host; placed by hand on every other host ([`ca.md`](ca.md#the-gateway-leaf-comes-from-the-secret-store)) | `deploy.hive-controller.tls.baoHiveIdentityCertFile` / `baoHiveIdentityKeyFile` |
|
||||
| the forge's copy of its OIDC secret | `swarm-bao-forge-oidc.service` reads it out of the swarm secret store under the forge publisher's leaf, wherever authelia runs | `/run/hive-forge-oidc/<id>.secret` on the host (tmpfs), bound into the forge container, handed to `forgejo-sso-source` through `LoadCredential` |
|
||||
| the homeserver's copy of its OIDC secret | `swarm-bao-matrix-oidc.service` reads it out of the swarm secret store under matrix-ctl's leaf, wherever authelia runs | `/run/hive-matrix-oidc/<id>.secret` on the host (tmpfs), bound into the homeserver container, handed to tuwunel through `LoadCredential` |
|
||||
| the agent containers' queue credential | authelia, published to the store by `swarm-secret-publish` | `<deploy.hive-controller.queue.agentCredentialDir>/secret` (`0600`) and `/client_id` (`0644`) |
|
||||
|
||||
The forge and the homeserver read their copies out of the store, the way
|
||||
Grafana does below: the forge under the forge publisher's leaf, the
|
||||
|
|
@ -268,34 +266,8 @@ publisher in authelia's container has written the secret, rather than skipping.
|
|||
A silent skip produces a service whose login button always fails, which is a
|
||||
symptom many layers from its cause.
|
||||
|
||||
The store's **first reader** is the agent containers' queue credential:
|
||||
`glue-queue-agent-credential.nix` lands it as two files, the client secret and
|
||||
the client id it authenticates, because that's how a queue client reads them —
|
||||
the secret by path, the id by value. No local fallback exists, and none is
|
||||
possible, so absent files mean an agent built before `swarm-logs` read its own
|
||||
queue secret has no working `swarm-logs`, which is what a swarm looks like
|
||||
before the publisher in authelia's container has run.
|
||||
The reader runs before `hive-c0re.service`, which wants it (doesn't require
|
||||
it), so an agent container never renders ahead of the credential; an
|
||||
unreachable store delays the daemon's start rather than failing it.
|
||||
|
||||
That credential still has one hop left, because the reader of it lives inside
|
||||
an agent container. **It crosses as a systemd credential, not as a bind
|
||||
mount**, and the mode decides that: the secret is `root:0600` and a harness
|
||||
runs as its own unprivileged agent user, so a bind would deliver a file that
|
||||
user can't open. `hive-c0re` stats the two files — the directory is `0755`, so
|
||||
it needs no read access to either — and hands them to
|
||||
`systemd-nspawn --load-credential` through `hive-priv`, which runs as root.
|
||||
Inside, `hive-agent.service` names the same two ids in `LoadCredential=`, which
|
||||
lands them in its own `$CREDENTIALS_DIRECTORY`, owned by the agent user and by
|
||||
nobody else, where a `swarm-logs` built before it read the agent's own queue
|
||||
secret reads them. The harness itself doesn't: it connects to the queue with the agent's own credential. `hive-c0re`
|
||||
never reads the bytes at any point: it runs as `hive-core`, which is the reason
|
||||
a copy wasn't an option either. When the files aren't there the daemon forwards
|
||||
nothing and says so in its journal, and the container starts without them.
|
||||
|
||||
The **second reader** is Grafana's OIDC client secret, and it's the first one
|
||||
that's a _swarm service's_ own credential rather than a hive's. It has exactly
|
||||
The store's **first reader** is Grafana's OIDC client secret, a _swarm
|
||||
service's_ own credential rather than a hive's. It has exactly
|
||||
one delivery route: `swarm-bao-grafana-oidc.service` reads
|
||||
`swarm/services/<client-id>/oidc/client` out of the store, on **every** host
|
||||
that runs Grafana. Where authelia is doesn't enter into it — the publisher in
|
||||
|
|
@ -338,7 +310,7 @@ the password, and until it has, the store refuses `swarm-bao-grafana-admin` on
|
|||
the Grafana host, which retries, while Grafana stays down because its config
|
||||
reads that file.
|
||||
|
||||
The **third reader** is the swarm collector's own OIDC client secret, the
|
||||
The **second reader** is the swarm collector's own OIDC client secret, the
|
||||
same shape one level down: `swarm-bao-otel-oidc.service` reads
|
||||
`swarm/services/<client-id>/oidc/client` out of the store on every host that
|
||||
runs the collector, and registration lives separately in
|
||||
|
|
@ -347,13 +319,12 @@ differs is what an absent credential means: the collector still receives
|
|||
telemetry with none, so `swarm-otel.nix` doesn't assert
|
||||
`deploy.bao.otelOidcClientCertFile` / `otelOidcClientKeyFile` the way Grafana
|
||||
does — it
|
||||
doesn't render the reading unit without them, the shape
|
||||
`glue-queue-agent-credential.nix` uses for its own read.
|
||||
doesn't render the reading unit without them.
|
||||
|
||||
The **fourth reader** is the secret store forwarder's OIDC client secret:
|
||||
The **third reader** is the secret store forwarder's OIDC client secret:
|
||||
`swarm-bao-forwarder-oidc.service`, on the store's own host, registered by
|
||||
`glue-swarm-bao-otel-oidc-client.nix` on authelia's. It differs from the
|
||||
third in both directions. It renders unconditionally, because the export it
|
||||
second in both directions. It renders unconditionally, because the export it
|
||||
authenticates has no unauthenticated mode to degrade into — and it orders
|
||||
itself **after** `container@swarm-bao`, because the store it reads runs in the
|
||||
container it delivers into. Nothing circular sits behind that: the identity it
|
||||
|
|
@ -412,14 +383,13 @@ next controller restart.
|
|||
### Per-principal identities
|
||||
|
||||
Bao matches a cert-auth role on the certificate's subject, so a certificate is an
|
||||
identity and sharing one merges the identities. Each of these eleven leaves has
|
||||
identity and sharing one merges the identities. Each of these ten leaves has
|
||||
a role and a policy of its own, and reaches one path, except the review
|
||||
classifier's, the CI runner's, the builder's, the CI run poller's, the claude
|
||||
login minter's, the forge host's and Grafana's, which reach more than one:
|
||||
|
||||
| unit | option pair under `deploy.bao.` | reads, or writes |
|
||||
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `swarm-bao-queue-agent` | `queueAgentClientCertFile` / `queueAgentClientKeyFile` | `swarm/hives/<hive>/queue/agent` |
|
||||
| `swarm-bao-grafana-oidc`, `swarm-bao-grafana-admin`, and Grafana's journal forwarder in its container | `grafanaOidcClientCertFile` / `grafanaOidcClientKeyFile` | reads `swarm/services/<grafana client id>/oidc/client` and `swarm/services/<grafana forwarder client id>/oidc/client`; creates and reads `swarm/services/<grafana client id>/admin/password` |
|
||||
| `swarm-bao-otel-oidc` | `otelOidcClientCertFile` / `otelOidcClientKeyFile` | `swarm/services/<collector client id>/oidc/client` |
|
||||
| `swarm-bao-forwarder-oidc` | `forwarderOidcClientCertFile` / `forwarderOidcClientKeyFile` | `swarm/services/<store forwarder client id>/oidc/client` |
|
||||
|
|
@ -431,12 +401,8 @@ login minter's, the forge host's and Grafana's, which reach more than one:
|
|||
| `swarm-ci-metrics` | `ciMetricsClientCertFile` / `ciMetricsClientKeyFile` | `swarm/services/ci-metrics/forge/token` and `swarm/services/<poller client id>/oidc/client` |
|
||||
| `swarm-bao-forge-oidc` and the forge's controller-token mint and its journal forwarder in its container | `forgePublisherClientCertFile` / `forgePublisherClientKeyFile` | writes `swarm/controller/swarm-controller/forge/token`; reads `swarm/services/<forge client id>/oidc/client` and `swarm/services/<forge forwarder client id>/oidc/client` |
|
||||
|
||||
The first exists **per hive**, because the path it reads carries a hive name
|
||||
and every hive runs its own reader. Its subject is
|
||||
`<deploy.bao.queueAgentCommonNamePrefix>-<hive>`; `swarm.nix` reserves that
|
||||
composed spelling as a hive name, so nobody can name a hive into another hive's
|
||||
role. The other nine reach a path that names a swarm service rather than a hive,
|
||||
so one role each is enough and their subjects are the flat
|
||||
Each reaches a path that names a swarm service rather than a hive, so one role
|
||||
each is enough and their subjects are the flat
|
||||
`deploy.bao.grafanaOidcCommonName`, `deploy.bao.otelOidcCommonName`,
|
||||
`deploy.bao.forwarderOidcCommonName`, `deploy.bao.ciRunnerCommonName`,
|
||||
`deploy.bao.reviewClassifierCommonName`, `deploy.bao.builderCommonName`,
|
||||
|
|
@ -449,7 +415,7 @@ OIDC client secret it reads.
|
|||
On a host that mints its own PKI, `glue-bao-tls.nix` signs all nine and defaults
|
||||
all eighteen options, and there is nothing to do. Elsewhere you copy each leaf from
|
||||
the store's host and name it here: a service principal's from the top of
|
||||
`/var/lib/swarm-bao-pki/`, another hive's queue-agent pair from
|
||||
`/var/lib/swarm-bao-pki/`, another hive's leaves from
|
||||
`/var/lib/swarm-bao-pki/hives/<hive>/` ([bao.md § TLS](bao.md#tls) lists that
|
||||
directory). That's one file per principal rather than one file
|
||||
shared by nine, which is the whole of what this buys. `swarm-ci-runner.nix`
|
||||
|
|
@ -461,7 +427,7 @@ naming both options, and the controller runs without forge access and the forge
|
|||
without a login source until an operator places the leaf.
|
||||
|
||||
On a host without the store, where the forwarder never runs, forgetting one of
|
||||
the other nine isn't a quiet degrade. A hand-configured remote-store hive that
|
||||
the others isn't a quiet degrade. A hand-configured remote-store hive that
|
||||
names the hive's own `clientCertFile` and misses a principal's pair has the
|
||||
build refused, by the module of the reader it missed, naming that pair — see
|
||||
[what a missing leaf costs](#what-a-missing-leaf-costs) below.
|
||||
|
|
@ -475,7 +441,7 @@ their readers hold a handle for longer than five minutes.
|
|||
|
||||
### What a missing leaf costs
|
||||
|
||||
Each of the three modules asserts its own pair, on the same condition: **this host
|
||||
Each of the four modules asserts its own pair, on the same condition: **this host
|
||||
already reads the store** — it holds `deploy.bao.clientCertFile` and
|
||||
`clientKeyFile` — **and the consumer is on, and this principal's pair is null**.
|
||||
The refusal names the two options, so an operator acts on the message without
|
||||
|
|
@ -492,9 +458,7 @@ that matters on its own:
|
|||
- **A host that runs the store** gets every pair from `glue-bao-tls.nix` as
|
||||
defaults, so the third clause is false. Nothing to set, nothing to refuse.
|
||||
- **A consumer that's off** — no collector, no Grafana — has no
|
||||
unit to skip, so the second clause is false. Only the queue-credential reader
|
||||
has no toggle of its own to check: every hive runs one for its own agents, so
|
||||
reading the store at all is what asks for it.
|
||||
unit to skip, so the second clause is false.
|
||||
|
||||
The collector keeps a separate degrade underneath this, unchanged: a host with no
|
||||
store identity runs a collector with no OIDC client secret and still receives
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ Four consequences worth stating plainly:
|
|||
|
||||
Everything above is a person in a browser. A swarm also has callers that
|
||||
hold no session and follow no redirect: the telemetry collector, the
|
||||
queue's auth-callout responder, and each hive's own agents.
|
||||
queue's auth-callout responder, and each hive.
|
||||
|
||||
**One client per hive, not one per service.** A hive's identity belongs to
|
||||
the directory rather than to whichever service happens to consume it, so a
|
||||
|
|
|
|||
Loading…
Reference in a new issue