diff --git a/CLAUDE.md b/CLAUDE.md index b91f2f1a..c38666f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -193,6 +193,15 @@ hand-maintained per-file tree drifts out of sync with the code. `claude --print` per classification (no tools, no session) and replies with the rows and the overall verdict. Holds no forge credential. Design: [`docs/swarm/review-classifier.md`](docs/swarm/review-classifier.md). +- **`swarm-agent-verify/`** — the check that a presented + `swarm-agent..` token holds the secret stored at + `swarm/agents//queue`, read under the verifier's own certificate. + Shared by `swarm-nats-auth` (the queue's auth callout) and + `swarm-logs-auth`. +- **`swarm-logs-auth/`** — the log store gateway's `auth_request` responder + for agents: nginx sends it `/select/logsql/` requests carrying an agent + token, it answers 200 or 401 via `swarm-agent-verify`. Runs on the log + store's host (`swarm-victorialogs.nix`). - **`swarmctl/`** — swarm-level operator CLI, installed by the swarm-controller module on the host that runs the daemon. Runs as **root and acts directly** — it serves no socket, publishes no HTTP diff --git a/Cargo.lock b/Cargo.lock index adf97684..f5de553e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4789,6 +4789,18 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swarm-agent-verify" +version = "0.1.0" +dependencies = [ + "anyhow", + "subtle", + "swarm-queue-client", + "swarm-secret-client", + "tokio", + "tracing", +] + [[package]] name = "swarm-authelia-bridge" version = "0.1.0" @@ -4870,6 +4882,23 @@ dependencies = [ "reqwest", "serde_json", "swarm-queue-client", + "swarm-secret-client", + "tokio", +] + +[[package]] +name = "swarm-logs-auth" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "clap", + "swarm-agent-verify", + "swarm-queue-client", + "swarm-secret-client", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -4910,7 +4939,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "subtle", + "swarm-agent-verify", "swarm-queue-client", "swarm-secret-client", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 2271332d..7d8112bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "hive-subagent-mcp", "hive-types", "hivectl", + "swarm-agent-verify", "swarm-authelia-bridge", "swarm-authelia-bridge-sock", "swarm-controller", @@ -34,6 +35,7 @@ members = [ "swarm-queue-client", "swarm-review-classifier", "swarm-logs", + "swarm-logs-auth", "swarm-secret-client", "swarmctl", ] @@ -104,6 +106,7 @@ hive-priv-sock = { path = "hive-priv-sock" } hive-runtime = { path = "hive-runtime" } hive-sock-client = { path = "hive-sock-client" } hive-types = { path = "hive-types" } +swarm-agent-verify = { path = "swarm-agent-verify" } swarm-authelia-bridge-sock = { path = "swarm-authelia-bridge-sock" } swarm-matrix-client = { path = "swarm-matrix-client" } swarm-queue-client = { path = "swarm-queue-client" } @@ -234,7 +237,7 @@ futures-util = "0.3" hmac = "0.13" sha2 = "0.11" # Constant-time comparison of a presented secret against the stored one -# (`swarm-nats-auth::agent_token`). Already in the tree through the TLS stack. +# (`swarm-agent-verify`). Already in the tree through the TLS stack. subtle = "2.6" # The NATS protocol client, for the swarm queue's auth-callout responder. # `default-features = false` because the default set is broad - jetstream, kv, diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 0535bd71..7deba5bc 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -182,8 +182,9 @@ the credential from the store again for each attempt. The hive's shared queue client is still delivered into the harness unit's credentials directory, and `swarm.statusPublish.tokenEndpoint` still reaches -the container, but only for `swarm-logs`: the harness never connects with -them. +the container, but only for a `swarm-logs` built before it read the agent's +own queue secret: the harness never connects with them, and a current +`swarm-logs` presents that secret to the log store instead.
What an agent publishes over the queue diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md index c285080e..a1aaf9b7 100644 --- a/docs/swarm/credentials.md +++ b/docs/swarm/credentials.md @@ -55,27 +55,27 @@ of the cell says how. -| store path | minter | reader — pulls at runtime, holds in memory | automatic re-mint | automatic re-pull | -| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `swarm/agents//matrix/main` | `swarm-controller`, with the swarm's appservice token, at agent creation and in a five-minute pass | the agent container itself, under the certificate its hive passed in | ✅ the pass re-mints when the stored token is missing, unknown to the homeserver, or someone else's | ✅ `hive-matrix-daemon` exits when the homeserver rejects its token, and a five-minute timer restarts it, which reads the store again | -| `swarm/agents//matrix/` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated | must be stated | -| `swarm/controller/swarm-controller/matrix/appservice-token` | `swarm-matrix-ctl`, inside the `hive-matrix` container, once | `swarm-controller`, under its own certificate | ❌ `swarm-matrix-ctl` mints it once; the container keeps its copy and republishes it when the store's differs | ✅ the controller reads it on every five-minute matrix pass | -| `swarm/controller/swarm-controller/oidc/client` | authelia, at its first boot; `swarm-secret-publish` copies it in | `swarm-controller`, under its own certificate, once at start | ❌ authelia mints it once. A re-mint is republished by `swarm-secret-publish`'s path unit | ❌ read once at start; the controller holds the old value until it restarts | -| `swarm/controller/swarm-controller/forge/token` | the forge, inside the `hive-forge` container, once; `swarm-bao-forge-publisher` on the forge's host copies it in under its own certificate, with a write-only grant | `swarm-controller`, under its own certificate, at start | ❌ the forge mints it once and keeps its copy in the container; the publisher writes that copy each time the forge container starts and each time the mint writes the file | ❌ the controller reads it at start, and in the background with backoff until it first finds it; a replaced token reaches it with the next restart | -| `swarm/agents//bao-mtls` | the store's agent PKI mount (`deploy.bao.agentPkiMountPath`), which generates the key, at `swarm-controller`'s request at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config; then `hive-agent-bao-identity` in the agent container, under the leaf it holds | ✅ `swarm-controller`'s five-minute pass re-issues a live agent's leaf once it's past half its validity (45 of 90 days, read from the certificate itself) | ✅ `hive-agent-bao-identity` re-reads it every 10 minutes and swaps a new leaf into `/run/hive-agent-bao-identity/`, which every store login in the container presents. It reads under the leaf it holds, so it renews only while that leaf is valid | -| `swarm/agents//queue` | `swarm-controller`, at agent creation | `hive-agent` in the agent container, under the agent's own certificate, held in memory — the identity it presents to the swarm queue, naming that one agent rather than its hive | ✅ `swarm-controller`'s five-minute pass re-mints a live agent's secret once it's 45 days old by `minted_at` on the stored object; a secret with no `minted_at` gets one stamped, value unchanged. The pass skips agents declared `Destroyed` — declaring an agent destroyed deletes every version of the path instead, the undo of the mint rather than another one | ✅ `hive-agent` reads the path before its first connect and again on every reconnect attempt, so a reconnect after a re-mint presents the new secret. An open connection keeps the secret it connected with; after a revocation the agent keeps retrying under the queue client's backoff | -| `swarm/agents//forge-token` | `swarm-controller`, at agent creation and in a pass every 5 minutes over every agent with a store identity | the agent container itself, under its own certificate, fetched to `/run/hive-agent-forge-token/token` | ✅ the controller re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes) | ✅ the agent re-fetches on a 10-minute timer | -| `swarm/agents//forge/