An agent can reach VictoriaLogs only through the gateway, and since the
machine query route landed the way to read it has been to hand-roll a
client_credentials token request and a curl, per query. This is the CLI
that closes that: `swarm-logs query '<LogsQL>'`, matched log lines on
stdout, so the answer pipes into grep like any other command's.
Built to the plan posted on the tracker thread: own crate, own
docs/tools reference generated off the clap tree, `query` as the one
verb, and the JSON error body surfaced on a non-200 rather than
swallowed. No `tail`: streaming is a different endpoint with a different
response shape, and folding it in here would be a fatter scope than the
ask.
Minting the token is NOT implemented here — swarm-queue-client already
owns the client_credentials 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's
swarm_queue module solves, and swarm-logs/src/auth.rs is its `decide`
restated over this binary's inputs.
⚠️ The plan named one thing to verify empirically before calling the auth
settled: whether authelia's bearer policy for the logs vhost accepts the
agent client's audience. Measured from inside a container: it does not.
The client minted a token fine but with `aud: []` and `scp: []`, asking
for the logs URL as an audience answered `invalid_target`, and presenting
the audience-less token to the gateway answered a bare 401. So
swarm-authelia.nix's agentClients gains `authelia.bearer.authz` and the
query URL as a second audience — authelia authorises a bearer token by
the URL being requested, and that URL is now one binding read by three
places rather than three spellings of one address.
The URL reaches an agent the same way its queue coordinates do: computed
on the host (a container cannot derive a gateway address), forwarded by
hive_c0re::meta into the container's option set, and consumed by a new
agent module that installs the binary *wrapped* with its coordinates —
the shape swarm-controller.nix installs swarmctl in. Gated on the queue
credential as well as on the URL: a binary that can only answer 401 is
worse than no binary, because an agent reads a 401 as "no logs", which is
the exact confusion the store's machine route was added to end.
136 lines
6.1 KiB
Nix
136 lines
6.1 KiB
Nix
# Swarm-queue coordinates for this agent's harness.
|
|
#
|
|
# Three of the four the harness needs are addresses (this file's two options
|
|
# plus the secret's path); the fourth, the client id, arrives as a file beside
|
|
# the secret so a reader never spells `hive-<name>-agent` a second time.
|
|
#
|
|
# ⚠️ The credential arrives as a systemd credential and NOT as a bind mount,
|
|
# and the mode is why: the host file is `root:0600` and this unit runs as the
|
|
# unprivileged agent user. nspawn's `--load-credential` (written by
|
|
# `hive_c0re::lifecycle::host_config`) is read by the container manager as
|
|
# root and re-exposed under this unit's own `User=`; a bind would deliver a
|
|
# file the harness cannot open.
|
|
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.hyperhive.queue;
|
|
configured = cfg.natsUrl != null && cfg.tokenEndpoint != null;
|
|
|
|
# The two ids `hive_c0re::lifecycle::host_config` forwards under. Neither
|
|
# side can discover the other's spelling, so a rename is a rename there too.
|
|
secretCredential = "hive-queue-agent-secret";
|
|
clientIdCredential = "hive-queue-agent-client-id";
|
|
|
|
# Where systemd materialises this unit's credentials. `%d` above is the
|
|
# same directory, but `%d` only expands *inside* a unit — a consumer that
|
|
# is not one (the `swarm-logs` wrapper, see ./logs.nix) needs the literal,
|
|
# and deriving it from the unit name here is what keeps the two from
|
|
# disagreeing about which unit's credentials they mean.
|
|
credentialsDir = "/run/credentials/hive-agent.service";
|
|
in
|
|
{
|
|
options.hyperhive.queue = {
|
|
natsUrl = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "nats://10.42.0.1:4222";
|
|
description = ''
|
|
Where the swarm queue listens, as this container reaches it.
|
|
|
|
Set by the generated meta flake from the host's
|
|
{option}`services.hyperhive.deploy.hive-controller.queue.agentNatsUrl`,
|
|
which is the bridge address rather than a loopback one — inside this
|
|
container `127.0.0.1` is the agent itself.
|
|
|
|
`null` means this hive has not been given the queue's address for its
|
|
agents, and the harness then declares no credential and logs that it
|
|
has none. It is deliberately not defaulted to anything: a guessed
|
|
address builds fine and talks to the wrong machine.
|
|
'';
|
|
};
|
|
|
|
tokenEndpoint = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "https://auth.example.com/api/oidc/token";
|
|
description = ''
|
|
The swarm IdP's OAuth2 token endpoint. The harness mints a
|
|
`client_credentials` token there and presents it to the queue, which
|
|
authenticates it as the client named in the delivered credential.
|
|
|
|
Set together with {option}`hyperhive.queue.natsUrl` or not at all —
|
|
the harness treats a half-set pair as a deployment bug rather than as
|
|
"no queue coordinates".
|
|
'';
|
|
};
|
|
|
|
# One declaration, two readers. The credential ids are this module's —
|
|
# `hive_c0re::lifecycle::host_config` and the `LoadCredential=` below are
|
|
# the pair that has to agree on them — and a second consumer that spelled
|
|
# them again would be a second source of truth for a string whose
|
|
# mismatch is a file that is simply not there.
|
|
clientIdFile = lib.mkOption {
|
|
type = lib.types.str;
|
|
readOnly = true;
|
|
default = "${credentialsDir}/${clientIdCredential}";
|
|
description = ''
|
|
Path the agent's OIDC client id is delivered at, for a consumer
|
|
outside the harness unit. Read-only: it is a fact about where the
|
|
credential lands, not a knob — see {option}`hyperhive.logs.queryUrl`
|
|
for the consumer this exists for.
|
|
'';
|
|
};
|
|
|
|
clientSecretFile = lib.mkOption {
|
|
type = lib.types.str;
|
|
readOnly = true;
|
|
default = "${credentialsDir}/${secretCredential}";
|
|
description = ''
|
|
Path the agent's OIDC client secret is delivered at. Read-only for
|
|
the same reason as {option}`hyperhive.queue.clientIdFile`.
|
|
|
|
🩸 A PATH and never a value. The file is `0400` to the agent user and
|
|
is read at the moment of a token request; nothing in this tree puts
|
|
its contents in an environment variable, where `/proc/<pid>/environ`
|
|
would publish them to every process in the container.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf configured {
|
|
systemd.services.hive-agent = {
|
|
# Bare ids, no paths: this is the terse `LoadCredential=` form that
|
|
# inherits a credential the service *manager* received, which is what
|
|
# the container manager passed in. `man systemd.exec` also makes that
|
|
# form non-fatal when the credential is absent, which is exactly the
|
|
# behaviour a hive whose publisher has not run yet needs — the unit
|
|
# starts, finds no id, and says so.
|
|
serviceConfig.LoadCredential = [
|
|
secretCredential
|
|
clientIdCredential
|
|
];
|
|
environment = {
|
|
# `%d` is `$CREDENTIALS_DIRECTORY`, per-unit and owned by `User=`.
|
|
# Same shape hive-c0re's own queue client is handed its secret in
|
|
# (`nix/host-modules/hive-c0re/environment.nix`) — the harness reads
|
|
# a path and never a value.
|
|
HIVE_AGENT_OIDC_CLIENT_SECRET_FILE = "%d/${secretCredential}";
|
|
# The id is public (it is sent to the token endpoint on every
|
|
# connection) but still arrives as a path, because it arrives *with*
|
|
# the secret. `QueueConfig::from_env` wants it as a value, so the
|
|
# harness reads this file itself — see `hive-agent`'s `swarm_queue`.
|
|
HIVE_AGENT_OIDC_CLIENT_ID_FILE = "%d/${clientIdCredential}";
|
|
};
|
|
# No `HIVE_AGENT_OIDC_CA_FILE`. The hive's own client needs one because
|
|
# the host does not trust the swarm's CA, but an agent does: the meta
|
|
# flake embeds the hive CA and the swarm root it is issued under into
|
|
# this container's `security.pki.certificateFiles` at build time, and
|
|
# reqwest's rustls backend verifies against the system store. A path
|
|
# here would need the bundle delivered as a third credential to say
|
|
# nothing new.
|
|
};
|
|
};
|
|
}
|