diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index 5a6c685e..62b8ab01 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -25,14 +25,15 @@ operator has to place, and where. ### 1 · Forge ```bash -# Provision (or refresh) ruth's own forge account — do this first. Ruth's -# bootstrap bypasses the normal spawn-approval flow ("Spawn sub-agents" -# below), so unlike every other agent it does not get its forge account -# auto-provisioned — this manual step is still load-bearing. -hivectl forge create-user ruth - -# Sub-agents spawned later (via the approval flow in "Spawn sub-agents") -# get their forge accounts auto-provisioned — nothing to run here for them. +# hive-c0re no longer creates agent forge users or mints agent tokens: +# swarm-controller does, for agents created at swarm level +# (`swarmctl agent create`), and stores the token in the swarm secret store, +# where the agent fetches it. An agent that only ever existed on this hive — +# ruth's bootstrap, or the hive's own spawn-approval flow — gets no forge +# user from anything yet. Create that user in the forge's admin UI; once the +# agent has a store identity (`swarmctl agent mint-identity`), swarm-controller +# mints its token within five minutes, or at once with: +swarmctl agent mint-forge-token ruth ``` Swarm SSO creates the human operator's own forge account instead of @@ -267,7 +268,9 @@ See [`tools/hivectl.md`](../tools/hivectl.md) for every `hivectl` verb. - **No forge admin token is stored in any agent state dir.** Agents - hold a regular agent token in their `forge-token` file; sensitive + hold a regular agent token, fetched from the swarm secret store into + `/run/hive-agent-forge-token/token` (or, for an agent without a store + identity, the `forge-token` file hive-c0re wrote before); sensitive creds (the core token) live on the host. - All config changes (forge PRs on `agent-configs/`) go through operator approval — agents can't unilaterally rebuild containers, by design. diff --git a/docs/integrations/forge.md b/docs/integrations/forge.md index 0979081e..0c598116 100644 --- a/docs/integrations/forge.md +++ b/docs/integrations/forge.md @@ -17,7 +17,9 @@ each agent on relevant activity. Two scope sets live in `hive-c0re::forge`: -**`TOKEN_SCOPES`** (per-agent tokens): +**`TOKEN_SCOPES`** (per-agent tokens, and `hivectl forge create-user` +accounts). swarm-controller mints agent tokens with a byte-identical copy, +`forge::agent_token::AGENT_TOKEN_SCOPES`, pinned by a test: | Scope | Why | | -------------------- | ------------------------------------------------------------------------------------------------------- | @@ -46,13 +48,19 @@ hive-c0re to re-mint with the new scopes. ## Per-agent forge accounts -Each agent gets its own Forgejo user + access token, provisioned at -boot by `hive-c0re::forge`. The provisioning flow is idempotent: -`hive-c0re::forge` reuses existing accounts + tokens, so container destroy/recreate -doesn't lose forge identity. It writes the token to -`/forge-token` (one line, no trailing newline) inside the -agent container so `hive-forge` CLI + `forge_notify` poller can -read it without touching c0re's host-side credential store. +Each agent gets its own Forgejo user and access token. swarm-controller +creates the user when it creates the agent, and mints one token named +`swarm-agent` with the admin API into the swarm secret store at +`swarm/agents//forge-token`. A pass at start and every five minutes +re-mints any agent's token that's missing or no longer matches the forge; +a rotation deletes the old `swarm-agent` token first, so each agent holds at +most one. The agent fetches the token under its own store certificate into +`/run/hive-agent-forge-token/token` (`nix/agent-modules/forge-token.nix`), +and `hive-forge`, the git credential helper, the `forge_notify` poller and +the avatar sync read it from there, falling back to `/forge-token`, +the file hive-c0re wrote before. hive-c0re no longer creates agent users or +mints agent tokens; the `hyperhive-` tokens it minted stay on +the forge until removed. Two things live in the `agent-configs` Forgejo organization: @@ -162,7 +170,7 @@ The poller starts disabled and stays that way for any of: - `HIVE_FORGE_URL` not set (no forge configured for this hive), or not parseable as a URL. -- `/forge-token` missing or empty (agent has no forge +- no token in `$HIVE_FORGE_TOKEN_FILE` or `/forge-token` (agent has no forge account — pre-provisioning or destroy-without-purge race). - Initial client construction fails (the typed `forgejo-api` client for the API calls, or the plain reqwest client kept for the diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md index 3a05fb70..2350bdcf 100644 --- a/docs/swarm/credentials.md +++ b/docs/swarm/credentials.md @@ -42,9 +42,9 @@ to the store under its own name, pulling what it needs when it needs it. No process reads a secret on another principal's behalf: the principal that needs a value is the principal that authenticates for it. -Two per-agent credential files — the forge token and the github token — sit -outside this page: they're operator-supplied and never pass through the -store, so the table below doesn't govern them. +One per-agent credential file — the github token — sits outside this page: +it's operator-supplied and never passes through the store, so the table +below doesn't govern it. **Per secret, the target specifies minter, reader, and renewal strategy.** Those three are the contract, and the reader is a process pulling a store @@ -54,16 +54,17 @@ strategy for every credential, including the mTLS leaf. -| store path | minter | reader — pulls at runtime, holds in memory | renewal | -| -------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `swarm/agents//matrix/` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated | -| `swarm/agents//bao-mtls` | `swarm-controller`, at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config | must be stated | -| `swarm/agents//queue` | `swarm-controller`, at agent creation | the agent container itself, under its own certificate — the identity it presents to the swarm queue, naming that one agent rather than its hive | none: the secret is fixed for the life of the agent and is revoked by deleting the path. A rotation mechanism is tracked as separate work, because rotating this credential needs a reconnect path — a queue client holding a revoked secret doesn't find out until it reconnects | -| `swarm/hives//matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated | -| `swarm/hives//matrix/sender-token` | `swarm-matrix-ctl`, in the `hive-matrix` container | `swarm-matrix-ctl` itself, under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | must be stated | -| `swarm/hives//queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated | -| `swarm/services//oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated | -| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated | +| store path | minter | reader — pulls at runtime, holds in memory | renewal | +| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `swarm/agents//matrix/` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated | +| `swarm/agents//bao-mtls` | `swarm-controller`, at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config | must be stated | +| `swarm/agents//queue` | `swarm-controller`, at agent creation | the agent container itself, under its own certificate — the identity it presents to the swarm queue, naming that one agent rather than its hive | none: the secret is fixed for the life of the agent and is revoked by deleting the path. A rotation mechanism is tracked as separate work, because rotating this credential needs a reconnect path — a queue client holding a revoked secret doesn't find out until it reconnects | +| `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/hives//matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated | +| `swarm/hives//matrix/sender-token` | `swarm-matrix-ctl`, in the `hive-matrix` container | `swarm-matrix-ctl` itself, under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | must be stated | +| `swarm/hives//queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated | +| `swarm/services//oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated | +| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated | @@ -95,6 +96,18 @@ value it holds, so running this against an already-migrated agent doesn't drop its queue connection. The certificate half isn't: the agent gets a fresh leaf and picks it up on its next boot. +The forge token needs no such step: `swarm-controller` checks every agent +that has a store identity at start and every five minutes, and mints a token +for any whose stored one is missing or stale. To check one agent now: + +```sh +swarmctl agent mint-forge-token +``` + +An agent without a store identity gets no swarm token and keeps using the +`forge-token` file in its state dir, if it has one; run `mint-identity` for it +first. + ⚠️ **Run this for every existing agent before deploying a hive-side change that makes a container require a credential it may not have.** A container whose credential is absent doesn't start — that's deliberate, and it's what diff --git a/docs/tools/forge.md b/docs/tools/forge.md index d84360f7..108dfa2d 100644 --- a/docs/tools/forge.md +++ b/docs/tools/forge.md @@ -14,7 +14,8 @@ markdown-docs > docs/tools/forge-cli.md`. ## Credentials and repo defaults -- Credentials: `$HYPERHIVE_STATE_DIR/forge-token` +- Credentials: `$HIVE_FORGE_TOKEN_FILE` (the token the agent fetched from + the swarm secret store), else `$HYPERHIVE_STATE_DIR/forge-token` - Active repo resolves, highest priority first: global `-r/--repo` flag > the `origin` remote of the cwd's git checkout > `$HIVE_FORGE_REPO` (last-resort override, unset by default) > a hard error. diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 18881495..cf635f73 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -103,22 +103,22 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for ###### **Subcommands:** -* `create-user` — Create or refresh the Forgejo account + token for `` +* `create-user` — Create or refresh a non-agent Forgejo account + token for `` * `reconcile-config` — Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/` main ## `hivectl forge create-user` -Create or refresh the Forgejo account + token for ``. +Create or refresh a non-agent Forgejo account + token for ``. -For an existing agent, persists the token to its state dir; for a human/other account, prints the token to stdout. Set a password to enable forge web-UI login (otherwise it uses a random throwaway). +Prints the token to stdout. Set a password to enable forge web-UI login (otherwise it uses a random throwaway). Refused for an existing agent: swarm-controller mints an agent's token (`swarmctl agent mint-forge-token `). **Usage:** `hivectl forge create-user [OPTIONS] ` ###### **Arguments:** -* `` — Forgejo username. For agents: the container/agent name (`` in `h-`; manager uses the literal `manager`). For humans: any forgejo username — `mara`, `damocles`, etc +* `` — Forgejo username of a human/other account — `mara`, `damocles`, etc ###### **Options:** diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 409d462b..47e56416 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -29,7 +29,7 @@ at boot. Useful for recovery, ad-hoc re-provisioning, or fixing a single agent without bouncing the daemon. ```bash -hivectl forge create-user iris # provision (or refresh) forge account for agent `iris` +hivectl forge create-user iris # refused: `iris` is an agent; use `swarmctl agent mint-forge-token iris` hivectl forge create-user mara # create forge account for a human user; prints token to stdout hivectl forge create-user mara --password hunter2 # set a web-login password hivectl forge create-user mara --password-stdin # read password from stdin (safer for scripting) @@ -40,8 +40,9 @@ hivectl forge reconcile-config iris --verbose # include the full diff, not ``` - For **agents** (name has a state dir under `/var/lib/hyperhive/agents/`): - `create-user` persists the token to `/forge-token`. Re-running refreshes the - token (idempotent — scope always matches current `TOKEN_SCOPES`). + `create-user` refuses. swarm-controller mints an agent's token into + the swarm secret store; `swarmctl agent mint-forge-token ` checks + and, if needed, re-mints it. - For **non-agents** (humans): creates the account and prints the token to stdout, creating no state dir. Re-running after account already exists re-mints the token and prints it again — safe for password resets. diff --git a/docs/tools/swarmctl-cli.md b/docs/tools/swarmctl-cli.md index 96d0eb20..1810d02d 100644 --- a/docs/tools/swarmctl-cli.md +++ b/docs/tools/swarmctl-cli.md @@ -8,6 +8,7 @@ This document contains the help content for the `swarmctl` command-line program. * [`swarmctl agent`↴](#swarmctl-agent) * [`swarmctl agent create`↴](#swarmctl-agent-create) * [`swarmctl agent mint-identity`↴](#swarmctl-agent-mint-identity) +* [`swarmctl agent mint-forge-token`↴](#swarmctl-agent-mint-forge-token) * [`swarmctl user`↴](#swarmctl-user) * [`swarmctl user add`↴](#swarmctl-user-add) * [`swarmctl user update`↴](#swarmctl-user-update) @@ -45,6 +46,7 @@ Manage agents across the swarm * `create` — Queue creation of a new agent on a hive in this swarm * `mint-identity` — Queue a re-mint of an existing agent's identity at the swarm's secret store +* `mint-forge-token` — Check one agent's forge token, and mint it if it's missing or stale @@ -104,6 +106,28 @@ Queues and returns, the same way `agent create` does — watch the swarm UI's jo +## `swarmctl agent mint-forge-token` + +Check one agent's forge token, and mint it if it's missing or stale. + +swarm-controller does this for every agent with a store identity at start and every five minutes; this is for when waiting isn't an option. It leaves a current token alone. + +Queues and returns, the same way `agent create` does — watch the swarm UI's job view for the outcome. + +**Usage:** `swarmctl agent mint-forge-token [OPTIONS] ` + +###### **Arguments:** + +* `` — Name of an agent that already exists + +###### **Options:** + +* `--controller-socket ` — swarm-controller's unix socket. + + Supplied by the nix module that installs this binary, from the same `socketPath` option the daemon binds; falls back to `SWARM_CONTROLLER_SOCKET`. + + + ## `swarmctl user` Manage subjects in the swarm's SSO provider diff --git a/docs/trust-boundary/security.md b/docs/trust-boundary/security.md index 78a98465..ca8fe746 100644 --- a/docs/trust-boundary/security.md +++ b/docs/trust-boundary/security.md @@ -236,9 +236,12 @@ token policy bounds file reads; network isolation bounds network reach. this directory at a default mode is world-readable, which isn't hypothetical: `plugins/` and `.last-cleanup` already are. - `$HYPERHIVE_STATE_DIR/forge-token` (= `/agents//state/forge-token`) - — written at mode `0600` and chowned to the per-agent uid:gid (see - `hive-c0re/src/forge/mod.rs`'s module doc for exactly where). nixbld users - can't read it. + — the token hive-c0re wrote at mode `0600`, chowned to the per-agent + uid:gid, before swarm-controller took over minting. Nothing writes it any + more; it stays as the fallback for an agent without a store identity. + nixbld users can't read it. The swarm-minted token lives under + `/run/hive-agent-forge-token/` (`0700` directory, `0400` file), outside + the state dir. **Policy**: all credential files written to agent state directories MUST be mode `0600` or stricter. Don't create world-readable secret files in agent state dirs. @@ -291,7 +294,7 @@ known operations; there is no arbitrary command pass-through: | `RemoveServiceDropin` | remove `container@.service.d/` drop-in on destroy | | `DaemonReload` | `systemctl daemon-reload` | | `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin ` | -| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | +| `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | | `RestartMatrixDaemon` | `systemctl --machine=h- restart hive-matrix-daemon.service` | | `ControlInfraContainer` | `systemctl container@.service` — the `InfraContainer` enum is the allowlist, and serde rejects unknown names at the wire boundary (`hive-c0re` has no variant, so no request can name it) | | `SyncAgentTmpfiles` | write `/etc/tmpfiles.d/hyperhive-agents.conf` for the agent set, then `systemd-tmpfiles --create` |