Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-logs
Repository files (latest commit first)
Filename Latest commit message Latest commit date
atlas c2c61b6146 hive status publishing: read the hive's OIDC secret from the store, retry until it is there
hive-c0re publishes this hive's status to the swarm queue as the OIDC
client `hive-<name>`. Its secret came from
`deploy.hive-controller.statusPublish.clientSecretFile`, which defaulted
to authelia's minted file only on a host running both authelia and the
queue. Every other hive had no secret and never published, so the
controller showed it `never_reported`.

Now `swarm-secret-publish`, beside authelia, also copies each hive's
`hive-<name>` secret to `swarm/hives/<hive>/oidc/client`. Its policy
already allows `swarm/hives/*`. hive-c0re reads that path under the
hive's own store leaf, which the existing `swarm/hives/<hive>/*` grant
covers, and holds the value in memory.

The shared queue client no longer remembers a failed first connect for
the life of the process. A failed attempt is retried with a backoff
from 5s, doubling to a cap of 5 minutes; the dashboard banner stays up
until an attempt succeeds. The two status publishers wait for the
client instead of giving up on the first `None`, so a secret published
after boot is picked up without a restart.

Removed: the `statusPublish.clientSecretFile` option (both spellings,
now removed options with a message), its same-host default, its
LoadCredential, the assertion tying it to the other two coordinates,
and `swarm_queue_client::QueueConfig::from_env`, whose only caller was
this read. Publishing is now on wherever the queue URL and token
endpoint are set and the hive holds a store leaf.

Refs #5015
2026-10-10 01:05:33 +02:00
..
src hive status publishing: read the hive's OIDC secret from the store, retry until it is there 2026-10-10 01:05:33 +02:00
Cargo.toml swarm-logs: an agent's CLI for the swarm log store 2026-09-17 01:02:14 +02:00
README.md swarm-queue-client: request the bearer-authz scope when minting an agent token 2026-09-17 09:51:44 +02:00

swarm-logs

An agent's CLI for the swarm log store. One verb — swarm-logs query '<LogsQL>' — so reading logs pipes and greps like any other command instead of being a hand-rolled token request plus a curl, per query.

Read-only and query-only. There is no ingest path here (that is the collector's), and no tail: streaming is a different LogsQL endpoint with a different response shape, left for a follow-up rather than folded in.

Distinct from hive-metric, the other half of the same surface from an agent's seat: that one writes a custom metric, this one reads logs.

⚠️ There is no scoping

The gateway forwards the query unmodified, so any authenticated caller reads the whole swarm's logs — every hive's, not only its own. That is the rule in force rather than an omission here; when read permissions exist they attach at the gateway location, not in this binary.

How it authenticates

The agent container already holds one identity, the per-hive machine client hive-<name>-agent, delivered as a systemd credential pair. This binary presents a client_credentials access token minted from it as Authorization: Bearer.

Minting is not implemented here — swarm_queue_client::mint_token_for_blocking already owns the request, its error type and its CA handling, and a token-endpoint fix has to be findable in one place. What this crate adds is the agent-shaped half: the client id arrives as a file beside the secret, so nothing outside nix/agent-modules/queue.nix spells hive-<name>-agent twice. That is the same problem hive-agent/src/swarm_queue.rs solves, and src/auth.rs is its decide restated over this binary's inputs.

🩸 Every credential here is a path. The secret's contents are read at the moment of the request, inside swarm-queue-client, and are never bound to a name in this crate, logged, or rendered into an error.

The audience is the URL

swarm-logs mints its token with the query URL as the requested audience, because that is what authelia checks at the authz endpoint the gateway's auth_request calls. The same rule swarm-otel.nix states over its own push targets: one binding for the address and the audience, since two spellings of one address present as a valid token refused at the store.

⚠️ That requires the agent client to be registered for that audience and to hold authelia.bearer.authz. Both are set in swarm-authelia.nix's agentClients; without them authelia answers invalid_target at the token endpoint, or the gateway answers 401 with no explanation.

⚠️ Registration is not issuance, so the token request asks for both: the audience and the scope are named in the client_credentials form, because a client that is registered for a scope it does not request is handed a token carrying none, and the gateway refuses that with the same bare 401.

Configuration

Supplied by nix/agent-modules/logs.nix, which wraps the binary — the same shape swarmctl is configured in, and for the same reason: every value is derived from an option that module owns, so a default here would be an address we hope points at something.

variable what
HIVE_AGENT_LOGS_QUERY_URL the LogsQL query endpoint; also the token's audience
HIVE_AGENT_OIDC_TOKEN_ENDPOINT authelia's token endpoint
HIVE_AGENT_OIDC_CLIENT_ID_FILE path of the delivered client-id credential
HIVE_AGENT_OIDC_CLIENT_SECRET_FILE path of the delivered secret credential
HIVE_AGENT_OIDC_CA_FILE optional extra trust anchor; unset in this tree

HIVE_AGENT, the same prefix the harness reads, because it is the same identity — an agent authenticates as its hive's agent client whether the caller is the harness or a CLI the agent typed. The first four are all-or-none: a half-set environment is a deployment bug, and this binary says so by name rather than behaving like an unconfigured one.

Output

$ swarm-logs query 'atlas-otel-probe' --limit 5

One log message per line by default (the record's _msg), which is what a grep pattern is written against. --format json passes the store's NDJSON through unmodified, for _time, _stream and jq.

A record the projection cannot understand — no string _msg, or not JSON at all — is printed whole rather than dropped. Silently losing a row would make an incomplete answer indistinguishable from a complete one.

A non-200 is reported with its body and a non-zero exit, never as an empty result. That is the whole reason the gateway grew a separate machine route: the operator's browser route answers an unauthenticated caller with authelia's login page as a 200 with an HTML body, and a client that reads only the status code records a query that succeeded and matched nothing.