# `checks.module-eval-agent-queue-bao` — see ./lib.nix for the shared # rationale (why this suite exists, naming convention, "evaluates # not executes"). { pkgs, lib, self, nixosSystem, }: let inherit (import ./lib.nix { inherit pkgs lib self nixosSystem ; }) agent agentWith runGroup agentHarness ; # The agent side of the swarm queue. Both coordinates set is the only state # in which the harness unit declares a credential at all, so the pair and # the empty fixture beside it are the two arms worth having. agentQueue = agent { queue.natsUrl = "nats://10.42.0.1:4222"; queue.tokenEndpoint = "https://auth.t.local/api/oidc/token"; }; agentNoQueue = agent { }; # The agent side of the swarm secret store. The address is the whole switch — # it is both what generates the login check and what that check points at — # so it and the empty fixture beside it are the two arms worth having. agentBao = agentWith { services.hyperhive.agent.bao.addr = "https://bao.t.local:8200"; }; agentNoBao = agentWith { }; agentBaoIdentity = machine: machine.systemd.services.hive-agent-bao-identity; agentQueueCredential = machine: machine.systemd.services.hive-agent-queue-credential; cases = [ { # Both ids or neither: the secret authenticates nobody without the id it # belongs to, and the harness refuses to treat one of the two as a queue. name = "an agent with queue coordinates imports both halves of its credential"; ok = let c = (agentHarness agentQueue).serviceConfig.LoadCredential; in builtins.elem "hive-queue-agent-secret" c && builtins.elem "hive-queue-agent-client-id" c; } { # `%d` and not a path under the state dir: the host file is `0600` # root-owned, so the only copy this unprivileged unit can open is the # one systemd puts in its own credentials directory. name = "the harness reads its queue credential out of the credentials directory"; ok = let e = (agentHarness agentQueue).environment; in e.HIVE_AGENT_OIDC_CLIENT_SECRET_FILE == "%d/hive-queue-agent-secret" && e.HIVE_AGENT_OIDC_CLIENT_ID_FILE == "%d/hive-queue-agent-client-id"; } { # An agent built before its hive was handed the queue's address. It # must declare nothing rather than name a credential that never # arrives — and the harness then reports "no queue coordinates" # instead of a half-set environment. name = "an agent with no queue coordinates declares no credential"; ok = let u = agentHarness agentNoQueue; in !(u.serviceConfig ? LoadCredential) && !(u.environment ? HIVE_AGENT_OIDC_CLIENT_SECRET_FILE) && !(u.environment ? HIVE_AGENT_OIDC_CLIENT_ID_FILE); } { # The three ids `hive_c0re::lifecycle::agent_identity` forwards under. # Neither end can discover the other's spelling, and a mismatch is a # credential that is simply not there — which this unit then reports as # a hive that delivered nothing. name = "an agent with the store enabled imports every half of its identity"; ok = let c = (agentBaoIdentity agentBao).serviceConfig.LoadCredential; in builtins.elem "hive-agent-bao-cert" c && builtins.elem "hive-agent-bao-key" c && builtins.elem "hive-agent-bao-server-ca" c; } { # `%d` and not a path under the agent's state dir, for the reason the # queue arm above gives: the host file is `0600` to the hive daemon, so # the only copy this unprivileged unit can open is the one systemd puts # in its own credentials directory. The address is the option's value # rather than a literal that agrees with it today. name = "the identity check presents its certificate out of the credentials directory"; ok = let e = (agentBaoIdentity agentBao).environment; in e.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert" && e.BAO_CLIENT_KEY == "%d/hive-agent-bao-key" && e.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr; } { # Same 403-not-a-miss reason as the hive-side readers: the path # `swarm_secret_client::mtls::identity_path` builds is the one this # agent's own policy stanza covers, and a path outside it is refused # however correct it looks. Built from the agent's own name rather than # from a literal, because the name is what makes it this agent's path # and not some other agent's. name = "the identity check reads the agent's own path"; ok = let m = agentBao; name = m.services.hyperhive.agent.user.name; in lib.hasInfix "secret/swarm/agents/${name}/bao-mtls" (agentBaoIdentity m).script; } { # The whole point of the unit, and the thing a quieter default would # undo: every arm of the check ends the unit non-zero, so an agent that # cannot authenticate as itself says so at boot instead of at whichever # pull needed the store first. name = "the identity check fails the unit rather than degrading"; ok = let u = agentBaoIdentity agentBao; in lib.hasInfix "exit 1" u.script && !(lib.hasInfix "exit 0" u.script) && u.serviceConfig.Restart == "on-failure"; } { # Nothing about the identity may be printed, and the read-back is where # that could slip: `bao kv get` on this path answers with certificate # material, and the object beside it is a private key. The check needs # only whether the read succeeded. # # The path goes through `lib.escapeShellArg` here for the same reason the # module passes it through one — that helper decides whether an argument # needs quotes at all, and this one (only `[a-z0-9/-]`) comes back bare. # Spelling the quotes in by hand asserts a rendering nixpkgs chooses # rather than the redirect this property is about. name = "the identity check discards what it reads back"; ok = let m = agentBao; name = m.services.hyperhive.agent.user.name; arg = lib.escapeShellArg "secret/swarm/agents/${name}/bao-mtls"; in lib.hasInfix "bao kv get -field=cert ${arg} >/dev/null" (agentBaoIdentity m).script; } { # The absence arm, and what makes the four above able to fail. An agent # whose swarm never minted an identity has nothing to log in with, and a # failed unit at every boot would be the loudest possible statement # about a deployment that never asked for one. name = "an agent told no store address runs no identity check"; ok = !(agentNoBao.systemd.services ? hive-agent-bao-identity); } { # The per-agent credential's fetch rides on the same store identity the # check above proves, because there is one identity per agent. A fetch # unit loading different ids would be a second certificate nothing # mints. name = "the queue-credential fetch presents the agent's own store identity"; ok = let u = agentQueueCredential agentBao; in builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential && u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert" && u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"; } { # Same 403-not-a-miss reason as every other reader here: the path # `swarm_secret_client::queue::agent_queue_path` builds is the one this # agent's own policy stanza covers. Built from the agent's own name, # because the name is what makes it this agent's credential and not a # neighbour's — which is the entire point of minting one per agent. name = "the queue-credential fetch reads the agent's own per-agent path"; ok = let m = agentBao; name = m.services.hyperhive.agent.user.name; in lib.hasInfix "secret/swarm/agents/${name}/queue" (agentQueueCredential m).script; } { # ⛔ The store certificate authenticates against the store and nothing # else. It must never reach the queue, so nothing in this unit may hand # a `BAO_CLIENT_*` path to anything queue-shaped. name = "the queue-credential fetch never points the queue at the store certificate"; ok = let u = agentQueueCredential agentBao; harness = agentHarness agentBao; in !(lib.hasInfix "nats" u.script) && !(lib.any (lib.hasPrefix "BAO_") (builtins.attrNames harness.environment)); } { # A secret fetched at boot has no business surviving one, and the # directory has to be the unit's own so the file is owned by the agent # rather than needing a mode change. `RemainAfterExit` is what keeps # systemd from removing it out from under the harness. name = "the fetched credential lands in a runtime directory the unit keeps alive"; ok = let u = agentQueueCredential agentBao; c = u.serviceConfig; in c.RuntimeDirectory == "hive-agent-queue-credential" && c.RemainAfterExit && lib.hasInfix "/run/hive-agent-queue-credential/secret" u.script; } { # 🩸 Degrades where the identity check fails, and the reason is that # both reads are governed by one policy stanza: a refusal this unit # sees and that check did not is an object not yet minted, not a policy # that drifted. An agent created before its swarm minted one is a # deployment doing nothing wrong. name = "a per-agent credential that was never minted does not fail the unit"; ok = lib.hasInfix "exit 0" (agentQueueCredential agentBao).script; } { # The harness reads a path and never a value — the same discipline # ../agent-modules/queue.nix keeps for the hive-scoped secret. Not # `%d`: this one is not a systemd credential, it is a file the # container fetched for itself. name = "the harness is handed the fetched credential as a path"; ok = let m = agentBao; in (agentHarness m).environment.HIVE_AGENT_QUEUE_AGENT_SECRET_FILE == m.services.hyperhive.agent.queue.agentSecretFile; } { # The absence arm. An agent whose swarm gave it no store has nothing to # log in with, so there is nothing to fetch with either — and the # harness is then told no path rather than one that never fills. name = "an agent told no store address fetches no queue credential"; ok = !(agentNoBao.systemd.services ? hive-agent-queue-credential) && !((agentHarness agentNoBao).environment ? HIVE_AGENT_QUEUE_AGENT_SECRET_FILE); } ]; in runGroup "agent-queue-bao" cases