# This agent's own identity at the swarm secret store, and the check that # proves it works. # # `swarm-controller` mints the leaf at agent creation and publishes it; this # agent's hive collects it under the hive's own certificate and hands it in as # systemd credentials (`hive_c0re::lifecycle::agent_identity`). Nothing here # fetches anything from the store — by the time this container boots, its # identity is already inside it. # # ⚠️ The identity arrives as credentials and NOT as a bind mount, and the mode # is why: the host file is `0600` to the hive daemon and this unit runs as the # unprivileged agent user. nspawn's `--load-credential` is read by the # container manager as root and re-exposed under this unit's own `User=`; a # bind would deliver a private key this user cannot open. Same answer, same # reason, as ./queue.nix's credential pair. # # This is the courier's other end: the hive-side collector has no purpose # without it, so the two ship together and neither is reachable alone. # # 🩸 This unit fails LOUDLY where the hive-side readers degrade quietly, and # that is the opposite default on purpose. A missing OIDC client secret means # a hive whose publisher has yet to run; a missing or refused certificate # means an agent that believes it can reach the store and cannot, which every # later pull would report as its own unrelated failure. The one place that # knows the real cause is the login itself, so this is where it is said. { pkgs, lib, config, ... }: let cfg = config.services.hyperhive.agent.bao; # This container's agent name. The same string the hive published the # identity under, because the agent's unix user is named for the agent — # see ./user.nix. agentName = config.services.hyperhive.agent.user.name; # The three ids `hive_c0re::lifecycle::agent_identity` forwards under. # Neither side can discover the other's spelling, so a rename is a rename # there too. certCredential = "hive-agent-bao-cert"; keyCredential = "hive-agent-bao-key"; serverCaCredential = "hive-agent-bao-server-ca"; unitName = "hive-agent-bao-identity"; # Where the identity lives in the store, spelled from the same pieces the # publisher uses. `swarm_secret_client::mtls::identity_path` builds # `swarm/agents//bao-mtls` and `path::MOUNT` is `secret`; this # literal is the nix half of that one agreement, exactly as # ../host-modules/glue-queue-agent-credential.nix spells its own. identityPath = "secret/swarm/agents/${agentName}/bao-mtls"; # The address is the whole switch — no separate `enable`, the same shape # ./queue.nix and ./logs.nix gate themselves with. A hive that has a store # forwards its address and every agent on it gets the check; a hive that has # none forwards nothing and no agent does. An `enable` beside it would be a # knob whose only correct setting is whatever the address already says, and # its default would decide whether the hive-side courier delivers into a # container that reads what it is given or into one that never looks. configured = cfg.addr != null; in { options.services.hyperhive.agent.bao = { addr = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "https://bao.example.com:8200"; description = '' Where the swarm secret store listens, as this container reaches it. Set by the generated meta flake from the host's own `BAO_ADDR`, which is the address this hive already uses, and setting it is what generates `${unitName}.service`: at boot that unit logs in with the certificate its hive delivered and reads this agent's own path back, failing if either step does not succeed. `systemctl status ${unitName}` inside the container is then the answer to "can this agent reach the store as itself", which nothing else in the tree reports. `null` means the hive was given no store. No unit is generated then, because an agent whose swarm never minted an identity has nothing to log in with, and a failed unit at every boot would say that in the loudest possible way about a deployment that never asked for it. ''; }; }; config = lib.mkIf configured { systemd.services.${unitName} = { description = "prove this agent can authenticate to the swarm secret store as itself"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; path = [ pkgs.openbao pkgs.coreutils ]; # Sized for a store that comes up around the same time this container # does, not for one that is sealed: a few short attempts cover the race, # and a longer window would only delay the report of a real failure. # # `StartLimit*` are `[Unit]` settings, so they go here and not in # `serviceConfig` — systemd ignores them under `[Service]`. The window # has to exceed `RestartSec` times the burst. startLimitBurst = 4; startLimitIntervalSec = 300; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; TimeoutStartSec = 30; Restart = "on-failure"; RestartSec = 15; User = agentName; Group = agentName; # Bare ids, no paths: the terse `LoadCredential=` form that inherits a # credential the service *manager* received, which is what the # container manager passed in. ./queue.nix states the same shape. LoadCredential = [ certCredential keyCredential serverCaCredential ]; }; environment = { BAO_ADDR = cfg.addr; # `%d` is `$CREDENTIALS_DIRECTORY`, per-unit and owned by `User=`. BAO_CLIENT_CERT = "%d/${certCredential}"; BAO_CLIENT_KEY = "%d/${keyCredential}"; }; script = '' set -euo pipefail # Absent credentials are the first thing checked, because every later # message would blame the store for a delivery that never happened. # 🩸 The files are named and never read here: what went wrong is a # property of the path, and the bytes at it are a private key. missing= for id in ${lib.escapeShellArg certCredential} ${lib.escapeShellArg keyCredential}; do if [ ! -s "$CREDENTIALS_DIRECTORY/$id" ]; then missing="$missing $id" fi done if [ -n "$missing" ]; then echo "this agent has no store identity: the hive delivered no$missing." >&2 echo "swarm-controller publishes it at ${identityPath} when the agent is created, and the hive collects it from there." >&2 exit 1 fi # Only when one was delivered. Absent means the container verifies the # store's listener against its own trust store, which is what a # deployment with a real CA wants; pointing BAO_CACERT at a file that # is not there would fail the handshake and name the wrong cause. if [ -s "$CREDENTIALS_DIRECTORY/${serverCaCredential}" ]; then export BAO_CACERT="$CREDENTIALS_DIRECTORY/${serverCaCredential}" fi err="$(mktemp)" trap 'rm -f "$err"' EXIT # Cert auth is a login, not a transport setting. The `BAO_CLIENT_*` # variables above only decide which certificate the TLS handshake # presents; without a token `bao` asks its token helper instead, and # that is a `sh` this unit's `path` does not carry. `-token-only` # answers on stdout and skips the helper on both sides. # # No `name=`: a cert role pins both its authority and the common name # it accepts, so this agent's leaf matches its own role and no other. # Naming the role here would be a third copy of a string # `swarm_secret_client::policy::agent_object_name` already owns. if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then echo "this agent's certificate was refused by the swarm secret store at $BAO_ADDR." >&2 if [ -s "$err" ]; then cat "$err" >&2 else echo "bao failed without writing a diagnostic." >&2 fi exit 1 fi export BAO_TOKEN # A token is not yet an answer: the login proves the certificate, this # proves the policy attached to it. Reading this agent's own identity # back is the smallest read its document grants, and it is the same # check `swarm-controller` runs against the leaf before it reports the # creation done — so a policy that drifted apart from the path fails # here rather than in whichever pull needed it first. # # ⚠️ Output discarded, not printed: the field is a certificate and the # object beside it is a private key. Nothing about this check needs a # value, only whether the read succeeded. if ! bao kv get -field=cert ${lib.escapeShellArg identityPath} >/dev/null 2>"$err"; then echo "this agent logged in to the swarm secret store but cannot read ${identityPath}, so its policy does not cover its own path." >&2 if [ -s "$err" ]; then cat "$err" >&2 else echo "bao failed without writing a diagnostic." >&2 fi exit 1 fi echo "authenticated to the swarm secret store at $BAO_ADDR as ${agentName} and read ${identityPath}." ''; }; }; }