Watch
0
0
Fork
You've already forked hyperhive
0

bao reader policy, responder units and docs for the per-surface grant

This commit is contained in:
atlas 2026-11-01 17:40:09 +01:00
commit a73946102f
10 changed files with 103 additions and 63 deletions

View file

@ -289,6 +289,8 @@ host access it has.
| `execution` | the `bash` MCP server (`mcp__bash__run`/`status`/`kill`), rendered into the agent's MCP config only with it; a subagent gets Claude's own `Bash` instead. See `docs/tools/bash.md`. |
| `web_tools` | the Claude built-ins `WebFetch`/`WebSearch` |
| `manage_root_agent` | every agent's state (rw) bind-mounted into the holder's container, plus `/meta` (ro) |
| `logs` | the log store: `swarm-logs` queries. Checked by `swarm-logs-auth` at the gateway, not by a tool list. Off by default. |
| `metrics` | the metrics store: `swarm-metrics` queries. Checked by `swarm-metrics-agent-auth`, the same binary, for the metrics gateway. Off by default. |
**Always-on tools** — `Permission::ALWAYS_ON_TOOLS` exposes `set_status`,
`compact`, and `mark_todos_done` to every agent regardless of its
@ -311,6 +313,23 @@ the default (`Permission::DEFAULT`): `messaging`, `meta`, `inbox`,
none. swarm-controller logs every refused `PUT` or `DELETE` on that route at `warn` as
`agent permissions: request refused`, with the fields `op`, `agent`, `status` and `detail`.
**`logs` and `metrics`** — off for every agent until granted, and separate
because logs are the more sensitive. Grant one with
`swarmctl agent permissions set <agent>` (listing it with the others) or the
create-agent screen. An agent without it gets a 403 from the store's gateway
when `swarm-logs` or `swarm-metrics` queries; a bad secret is still a 401. The
controller refuses the grant, with a 409 naming the hive, unless the agent's
hive advertises `tolerant-permissions` in its status snapshot
(`swarm_wire::hive_status`): a hive that predates the permission would fail to
decode the record. It also mirrors the two grants to the store leaf
`swarm/agents/<agent>/observability` (`swarm_secret_client::observability`),
which the gateways read; the gateway denies when it can't read that leaf. A
revoked grant stops working within `authVerdictTtl` (60 seconds by default) of
the agent's last allowed query. Grant it only after the agent runs a harness
that skips names it doesn't know (its next update after the release that adds
these two); an older harness that reads such a record holds no permissions at
its next start.
**Internet flag** — the record also carries `unrestricted_internet`, a
boolean that isn't a `Permission`. An absent value means on, so every agent
that existed before the field keeps the open access it had. `POST /api/agents`

View file

@ -17,7 +17,7 @@ the rest of the cell says how.
<!-- vale write-good.Passive = NO -->
| store path | minter | reader | automatic re-mint | automatic re-pull |
| ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `swarm/agents/<agent>/matrix/main` | `swarm-controller`, at agent creation and every 5 minutes | the agent | ✅ when missing or rejected by the homeserver | ✅ the daemon restarts within 5 minutes of a rejection |
| `swarm/agents/<agent>/matrix/<account>` | `swarm-controller`, from a credential you hand it | the agent | must be stated | ✅ retried within 2 to 16 minutes after a rejection |
| `swarm/agents/<agent>/matrix-recovery/<account>` | `swarm-controller`, a random passphrase per account | the agent | ❌ never replaced: the account's key backup opens only with this passphrase | ❌ read when the account comes up |
@ -29,6 +29,7 @@ the rest of the cell says how.
| `swarm/controller/swarm-controller/forge/webhook-secret` | `swarm-controller`, at start, from its old `webhook-secret` file; a new one only when there is no file | `swarm-controller` | ❌ minted once; a rotated secret fails every delivery until the forge hooks are re-registered | ❌ read at start |
| `swarm/agents/<agent>/bao-mtls` | the store's agent PKI mount (`deploy.bao.agentPkiMountPath`), at agent creation, and by the renewal pass when none is stored or its cert-auth role is missing | `hive-c0re`, then the agent | ✅ re-issued every 5 minutes once past half its 7-day validity; minted when none is stored or its cert-auth role is missing, for agents not `Destroyed` | ✅ the agent re-reads every 10 minutes; it renews only while its current leaf is valid |
| `swarm/agents/<agent>/queue` | `swarm-controller`, at agent creation | the agent; the queue, log and metrics auth services | ✅ re-minted at 45 days; skipped for `Destroyed` agents, whose path is deleted | ✅ read on every reconnect; an open connection keeps the old secret for up to an hour; the log and metrics stores honour a replaced secret for up to `deploy.victorialogs.authVerdictTtl` and `deploy.victoriametrics.agentAuthVerdictTtl` (60 s by default) |
| `swarm/agents/<agent>/observability` | `swarm-controller`, when an operator sets, clears or creates an agent's permissions with `logs` or `metrics` in the old or new list, and when it destroys the agent; the leaf stays absent for an agent that never held either | the agent (read-only, its own prefix); `swarm-logs-auth` and `swarm-metrics-agent-auth`, which deny when it's absent or unreadable | ❌ the controller rewrites it with the permission; nothing refreshes it | ✅ the stores honour a changed grant for up to `deploy.victorialogs.authVerdictTtl` and `deploy.victoriametrics.agentAuthVerdictTtl` (60 s by default) |
| `swarm/agents/<agent>/forge-token` | `swarm-controller`, at agent creation and every 5 minutes | the agent | ✅ when missing or no longer matching the forge | ✅ every 10 minutes |
| `swarm/agents/<agent>/forge/<label>` | `swarm-controller`, when you link an external forge account in the swarm UI | the agent | ❌ your token; delete it and link the label again | ✅ every 2 minutes |
| `swarm/agents/<agent>/github-token` | `swarm-controller`, when you link a GitHub account in the swarm UI | the agent | ❌ your token; delete it and link again | ✅ every 2 minutes |

View file

@ -1,6 +1,6 @@
| `deploy.victoriametrics.agentAuthRate` | Default `50r/s` per client address, for agent reads checked by `swarm-metrics-agent-auth`; over it and `agentAuthBurst`, the gateway answers 403. |
| `deploy.victoriametrics.agentAuthBurst` | Default `500` requests above `agentAuthRate` before the 403s start. |
| `deploy.victoriametrics.agentAuthVerdictTtl` | Default `60` seconds that `swarm-metrics-agent-auth` keeps granting an agent secret it verified. A replaced or deleted secret keeps working at the metrics store for up to that long. |
| `deploy.victoriametrics.agentAuthVerdictTtl` | Default `60` seconds that `swarm-metrics-agent-auth` keeps granting an agent secret it verified. A replaced or deleted secret keeps working at the metrics store for up to that long. A revoked `metrics` permission does too. |
| `deploy.victoriametrics.agentAuthPackage` | The `swarm-logs-auth` build that runs as `swarm-metrics-agent-auth`. Set on its own: `deploy.victorialogs.authPackage` doesn't reach it. |
# Swarm-wide services
@ -249,7 +249,8 @@ vhost does it:
store, and `swarm-metrics-auth` on the store's host checks it against the
same store path. An agent's `swarm-metrics` sends its
`swarm-agent.<agent>.<secret>` bearer, which `swarm-metrics-agent-auth` on
the store's host checks against `swarm/agents/<agent>/queue`. A request
the store's host checks against `swarm/agents/<agent>/queue`. It also needs the `metrics`
permission, or the answer is a 403. A request
without a bearer needs an operator's authelia session (group `admins`);
without one it gets a 401, not a login redirect.
- **vmui**: an operator's authelia session, with a redirect to the login.
@ -306,11 +307,11 @@ no setting here changes that, so Explore is the log browser for this
swarm.
| Option | When you'd touch it |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deploy.victorialogs.retentionPeriod` | Default `30d`, far shorter than the metrics store's — logs are bulkier per unit of value and are typically read within days of ingestion. Raise it if you need to answer questions about last quarter. |
| `deploy.victorialogs.authRate` | Default `50r/s` per client address, for agent queries checked by `swarm-logs-auth`; over it and `authBurst`, the gateway answers 403. Agents on one host share the address; the default covers about a hundred cold-starting together. |
| `deploy.victorialogs.authBurst` | Default `500` requests above `authRate` before the 403s start. |
| `deploy.victorialogs.authVerdictTtl` | Default `60` seconds that `swarm-logs-auth` keeps granting an agent secret it verified, without asking the secret store. A replaced or deleted secret keeps working at the log store for up to that long. |
| `deploy.victorialogs.authVerdictTtl` | Default `60` seconds that `swarm-logs-auth` keeps granting an agent secret it verified, without asking the secret store. A replaced or deleted secret keeps working at the log store for up to that long. A revoked `logs` permission does too. |
| `deploy.victorialogs.grafanaAuthPackage` | The `swarm-metrics-auth` build that runs as `swarm-logs-grafana-auth`. Set on its own: `deploy.victoriametrics.authPackage` doesn't reach it. |
| `swarm.victorialogs.domain` | Only to rename it. |
| `deploy.victorialogs.port` | Only if something else on the services host already claims `9428`. |
@ -320,7 +321,8 @@ the only way in, and the collector is the only intended writer.
**Who may query.** `/select/logsql/` takes one of three credentials. An
agent presents its `swarm-agent.<agent>.<secret>` bearer, checked by
`swarm-logs-auth`. Grafana's datasource presents the same bearer as its
`swarm-logs-auth`. The agent must also hold the `logs` permission,
which is off by default; without it the answer is a 403 (`docs/process/conventions.md`, Permissions). Grafana's datasource presents the same bearer as its
metrics datasource, which `swarm-bao-grafana-metrics-bearer` creates, and a
second `swarm-metrics-auth` instance (`swarm-logs-grafana-auth`) on the log
store's host checks it against the same store path; the gateway tells it from

View file

@ -256,7 +256,7 @@ Replace the permissions the swarm grants an agent; the internet flag stays unles
###### **Options:**
* `--permissions <PERMISSIONS>` — The permissions to grant, comma-separated: any of `messaging`, `meta`, `inbox`, `execution`, `web_tools`, `manage_root_agent`. `--permissions ''` grants none
* `--permissions <PERMISSIONS>` — The permissions to grant, comma-separated: any of `messaging`, `meta`, `inbox`, `execution`, `web_tools`, `manage_root_agent`, `logs`, `metrics`. `--permissions ''` grants none
* `--unrestricted-internet <BOOL>` — Whether the agent's traffic may leave the host unfiltered. Left out, the stored value stays
Possible values: `true`, `false`

View file

@ -89,7 +89,9 @@ reaches that one leaf per agent: the queue's auth callout (`swarm-nats-auth`),
the log store's agent check (`swarm-logs-auth`, on the log store's host), and
the metrics store's (`swarm-metrics-agent-auth`, under the `swarm-metrics-auth`
leaf on the metrics store's host). Any one compromised can present any agent's
queue secret, at the queue and at both stores' read routes alike.
queue secret, at the queue and at both stores' read routes alike. The two store checks also read the
agent's `swarm/agents/<agent>/observability` leaf, which only the controller writes, and let
the agent through only with the `logs` or `metrics` permission it names.
### Per-hive credentials live under the hive kind

View file

@ -1219,7 +1219,7 @@ let
# The log store's auth responders: the agent one checks the secret an agent
# presents against the one stored for it (the same `+` leaf as the queue
# responder's), the other checks the bearer Grafana presents against
# responder's) and reads the agent's mirrored `logs`/`metrics` grant, the other checks the bearer Grafana presents against
# Grafana's metrics bearer; and the store container's journal forwarder
# reads its own client secret under the same leaf. Nothing else.
logsAuthReaders = [
@ -1229,6 +1229,7 @@ let
batchToken = true;
policyText =
readStanza "${credentialMountPath}/data/swarm/agents/+/queue"
+ readStanza "${credentialMountPath}/data/swarm/agents/+/observability"
+ readStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}"
# The journal forwarder in the store's container, under this same leaf.
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.victorialogs.otel.clientId}/oidc/client";
@ -1237,7 +1238,7 @@ let
# The metrics store's responders under one leaf: the agent one checks the
# secret an agent presents against the one stored for it (the same `+` leaf
# as the log store's), the other checks the bearer Grafana presents; and the
# as the log store's) and reads the agent's mirrored grant, the other checks the bearer Grafana presents; and the
# host unit that fetches the store container's journal forwarder secret.
# Nothing else.
metricsAuthReaders = [
@ -1247,6 +1248,7 @@ let
batchToken = true;
policyText =
readStanza "${credentialMountPath}/data/swarm/agents/+/queue"
+ readStanza "${credentialMountPath}/data/swarm/agents/+/observability"
+ readStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}"
# The metrics store container's journal forwarder, fetched on its host.
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.victoriametrics.otel.clientId}/oidc/client";

View file

@ -216,8 +216,9 @@ in
without reading the secret store. A denial is never remembered.
This is also how long a re-minted or deleted queue secret keeps working
at the log store's query route after its last successful check. `0`
checks the store on every query.
at the log store's query route after its last successful check. It is
equally how long a revoked `logs` permission keeps working. `0` checks
the store on every query.
'';
};
@ -514,6 +515,7 @@ in
"--socket ${logsAuthSocket}"
# The role ./swarm-bao.nix writes on the store's `cert` mount.
"--store-cert-role swarm-logs-auth"
"--surface logs"
"--verdict-ttl ${toString deployCfg.victorialogs.authVerdictTtl}"
];
# Paths only: `argv` is world-readable via /proc/<pid>/cmdline.

View file

@ -295,8 +295,9 @@ in
remembered.
This is also how long a re-minted or deleted queue secret keeps working
at the metrics store's read API after its last successful check. `0`
checks the store on every query.
at the metrics store's read API after its last successful check. It is
equally how long a revoked `metrics` permission keeps working. `0` checks
the store on every query.
'';
};
@ -610,6 +611,7 @@ in
"${deployCfg.victoriametrics.agentAuthPackage}/bin/swarm-logs-auth"
"--socket ${agentAuthSocket}"
"--store-cert-role swarm-metrics-auth"
"--surface metrics"
"--verdict-ttl ${toString deployCfg.victoriametrics.agentAuthVerdictTtl}"
];
LoadCredential = lib.mapAttrsToList (id: path: "${id}:${path}") metricsAuthStoreFiles;

View file

@ -835,7 +835,7 @@ let
# Every agent's queue secret, Grafana's metrics bearer and the store
# container forwarder's client secret, all read-only, and nothing else:
# this principal checks a presented secret and does nothing with it.
name = "the log store responder's grant is every agent's queue secret, Grafana's bearer and its container forwarder's client secret, read-only, and nothing else";
name = "the log store responder's grant is every agent's queue secret and log/metric grant, Grafana's bearer and its container forwarder's client secret, read-only, and nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-logs-auth-policy.script;
@ -845,6 +845,10 @@ let
path = "secret/data/swarm/agents/+/queue";
caps = [ "read" ];
}
{
path = "secret/data/swarm/agents/+/observability";
caps = [ "read" ];
}
{
path = "secret/data/swarm/services/swarm-grafana/metrics/bearer";
caps = [ "read" ];
@ -871,7 +875,7 @@ let
# creates and the store container forwarder's client secret, and nothing
# else: this principal checks presented bearers, and its host fetches
# one secret for the container.
name = "the metrics store responder's grant is agents' queue secrets, Grafana's metrics bearer and its container forwarder's client secret, read-only, and nothing else";
name = "the metrics store responder's grant is agents' queue secrets and log/metric grants, Grafana's metrics bearer and its container forwarder's client secret, read-only, and nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-metrics-auth-policy.script;
@ -881,6 +885,10 @@ let
path = "secret/data/swarm/agents/+/queue";
caps = [ "read" ];
}
{
path = "secret/data/swarm/agents/+/observability";
caps = [ "read" ];
}
{
path = "secret/data/swarm/services/swarm-grafana/metrics/bearer";
caps = [ "read" ];

View file

@ -998,6 +998,7 @@ let
&& !(hasStorePath "${logs.authPackage}" agentUnit.serviceConfig.ExecStart)
&& hasStorePath "${v.authPackage}/bin/swarm-metrics-auth " unit.serviceConfig.ExecStart
&& lib.hasInfix "--store-cert-role swarm-metrics-auth" agentUnit.serviceConfig.ExecStart
&& lib.hasInfix "--surface metrics" agentUnit.serviceConfig.ExecStart
&& lib.hasInfix "--verdict-ttl ${toString v.agentAuthVerdictTtl}" agentUnit.serviceConfig.ExecStart
&& agentUnit.serviceConfig.LoadCredential == unit.serviceConfig.LoadCredential
&& lib.hasInfix "limit_req_zone $binary_remote_addr zone=swarm_metrics_agent_auth:1m rate=${v.agentAuthRate};" http
@ -1143,6 +1144,7 @@ let
&& lib.hasInfix "internal;" l."= /__swarm_logs_agent_auth".extraConfig
&& l."= /__swarm_logs_agent_auth".proxyPass == "http://unix:/run/swarm-logs-auth/auth.sock:/"
&& lib.hasInfix "--socket /run/swarm-logs-auth/auth.sock" unit.serviceConfig.ExecStart
&& lib.hasInfix "--surface logs" unit.serviceConfig.ExecStart
&& lib.hasInfix "internal;" l."= /__swarm_logs_grafana_auth".extraConfig
&&
l."= /__swarm_logs_grafana_auth".proxyPass == "http://unix:/run/swarm-logs-grafana-auth/auth.sock:/"