drop the shared hive agent login
Every agent now connects to the queue and reads logs with its own secret from the store (#5181), so the per-hive `hive-<h>-agent` OIDC client has no reader left. Remove it end to end: - hive-c0re no longer forwards `hive-queue-agent-{secret,client-id}` into containers, and no longer forwards HIVE_AGENT_OIDC_TOKEN_ENDPOINT; the agent module that loaded them (nix/agent-modules/queue.nix) is gone, its options refused with a removal message. - The host reader (glue-queue-agent-credential.nix, swarm-bao-queue-agent), its store role and policy unit (swarm-bao-queue-agent-policy), its PKI leaf and the deploy.bao.queueAgent* / queue.agentCredentialDir options are gone; the options are refused with a removal message. - authelia mints no `hive-<h>-agent` client, and swarm-secret-publish no longer writes swarm/hives/<h>/queue/agent. - swarm-secret-client drops queue::agent_client_path / queue::Credential. - HIVE_AGENT_NATS_URL is now gated on the agents' queue address alone. The log store's authelia check stays for operators. swarm-nats-auth still refuses a `-agent` client id; that refusal and `agentClientSuffix` are left for a follow-up. Refs #4459 Refs #4765
This commit is contained in:
parent
d19bd83aa5
commit
868c6cf96c
41 changed files with 286 additions and 1410 deletions
|
|
@ -24,16 +24,13 @@ let
|
|||
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.
|
||||
# An `agent.nix` still setting the queue coordinates the agent module no
|
||||
# longer declares.
|
||||
agentQueue = agent {
|
||||
queue.natsUrl = "nats://10.42.0.1:4222";
|
||||
queue.tokenEndpoint = "https://auth.t.local/api/oidc/token";
|
||||
};
|
||||
|
||||
agentNoQueue = shared.agentBare;
|
||||
|
||||
# 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.
|
||||
|
|
@ -106,21 +103,24 @@ let
|
|||
ok = swarmMetricsIn agentLogsNoBao == null && swarmMetricsIn agentLogs != null;
|
||||
}
|
||||
{
|
||||
# Both ids or neither: the secret authenticates nobody without the id it
|
||||
# belongs to, and `swarm-logs` reads both.
|
||||
name = "an agent with queue coordinates imports both halves of its credential";
|
||||
# The hive forwards no shared queue credential, so the harness unit
|
||||
# names none, whatever the agent's config says.
|
||||
name = "an agent imports no hive-shared queue credential";
|
||||
ok =
|
||||
let
|
||||
c = (agentHarness agentQueue).serviceConfig.LoadCredential;
|
||||
credentials = m: (agentHarness m).serviceConfig.LoadCredential or [ ];
|
||||
in
|
||||
builtins.elem "hive-queue-agent-secret" c && builtins.elem "hive-queue-agent-client-id" c;
|
||||
!lib.any (lib.hasPrefix "hive-queue-agent-") (credentials agentQueue ++ credentials agentBao)
|
||||
# Control: the lookup finds the store identity's credential.
|
||||
&& lib.elem "hive-agent-bao-server-ca" (credentials agentBao);
|
||||
}
|
||||
{
|
||||
# An agent built before its hive was handed the queue's address. It
|
||||
# must declare nothing rather than name a credential that never
|
||||
# arrives.
|
||||
name = "an agent with no queue coordinates declares no credential";
|
||||
ok = !((agentHarness agentNoQueue).serviceConfig ? LoadCredential);
|
||||
# Both options are refused, naming where the address
|
||||
# comes from now, rather than dropped silently.
|
||||
name = "an agent setting the removed queue options is refused";
|
||||
ok =
|
||||
lib.count (a: !a.assertion && lib.hasInfix "HIVE_AGENT_NATS_URL" a.message) agentQueue.assertions
|
||||
== 2;
|
||||
}
|
||||
{
|
||||
# The harness exits when it cannot reach the queue. It has to come back
|
||||
|
|
|
|||
|
|
@ -106,14 +106,11 @@ let
|
|||
deploy.matrix.enable = true;
|
||||
};
|
||||
|
||||
# The store with none of the readers beside it. Grafana and the collector are
|
||||
# simply off; the queue reader renders on any host holding
|
||||
# its leaf, which ./glue-bao-tls.nix mints here, so that leaf is taken away.
|
||||
# The store with none of the readers beside it: Grafana and the collector are
|
||||
# off.
|
||||
baoGrantNoReaders = hiveWithController {
|
||||
deploy.bao.enable = true;
|
||||
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
|
||||
deploy.bao.queueAgentClientCertFile = lib.mkForce null;
|
||||
deploy.bao.queueAgentClientKeyFile = lib.mkForce null;
|
||||
};
|
||||
|
||||
# The store with the forwarder's own pair taken away. The forwarder renders
|
||||
|
|
@ -131,8 +128,6 @@ let
|
|||
deploy.swarm-otel.enable = true;
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
deploy.bao.queueAgentClientCertFile = "/etc/pki/bao-queue-agent.pem";
|
||||
deploy.bao.queueAgentClientKeyFile = "/etc/pki/bao-queue-agent-key.pem";
|
||||
deploy.bao.grafanaOidcClientCertFile = "/etc/pki/bao-grafana-oidc.pem";
|
||||
deploy.bao.grafanaOidcClientKeyFile = "/etc/pki/bao-grafana-oidc-key.pem";
|
||||
deploy.bao.otelOidcClientCertFile = "/etc/pki/bao-otel-oidc.pem";
|
||||
|
|
@ -142,7 +137,6 @@ let
|
|||
# The readers ./glue-bao-readers-policy-order.nix orders after their policy
|
||||
# units.
|
||||
policyReaders = [
|
||||
"swarm-bao-queue-agent"
|
||||
"swarm-bao-grafana-oidc"
|
||||
"swarm-bao-otel-oidc"
|
||||
];
|
||||
|
|
@ -175,7 +169,6 @@ let
|
|||
"swarm-bao-controller-policy"
|
||||
"swarm-bao-secret-publisher-policy"
|
||||
"swarm-bao-matrix-ctl-policy"
|
||||
"swarm-bao-queue-agent-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
"swarm-bao-forwarder-oidc-policy"
|
||||
|
|
@ -599,19 +592,18 @@ let
|
|||
# arms pin what each principal must NOT reach, so a later widening fails
|
||||
# here instead of being noticed in a store.
|
||||
{
|
||||
name = "the queue-credential reader's grant is one hive's queue credential and nothing else";
|
||||
# A hive holds no shared queue credential for its agents: no unit writes
|
||||
# a role or policy for one, none reads one, and no granting unit names
|
||||
# its path. The control: the same scan finds a reader's grant.
|
||||
name = "no unit grants, reads or writes a hive-shared agent queue credential";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-queue-agent-policy.script;
|
||||
scripts = lib.mapAttrsToList (_: u: u.script or "") baoGrantWithConsumers.systemd.services;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/hives/h1/queue/agent\" {" s
|
||||
&& lib.hasInfix "capabilities = [\"read\"]" s
|
||||
&& !(lib.hasInfix "secret/data/swarm/agents" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/services" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives/h1/*" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives/h1/matrix" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives/*" s)
|
||||
&& !(lib.hasInfix "sys/policies/acl" s);
|
||||
!(baoGrantWithConsumers.systemd.services ? swarm-bao-queue-agent-policy)
|
||||
&& !(baoGrantWithConsumers.systemd.services ? swarm-bao-queue-agent)
|
||||
&& !(lib.any (lib.hasInfix "/queue/agent") scripts)
|
||||
&& lib.any (lib.hasInfix "/oidc/client") scripts;
|
||||
}
|
||||
{
|
||||
# ⚠️ The client id is the path segment, so the negative arm that matters
|
||||
|
|
@ -856,11 +848,7 @@ let
|
|||
# through a token that carries it, and a token is minted by a cert-auth
|
||||
# role matching a CN. Distinct subjects is the whole mechanism — one
|
||||
# subject for every reader is one principal however the policies read.
|
||||
#
|
||||
# The per-hive subject carries the hive name because its path does; the
|
||||
# service subjects do not, because an OIDC client is registered once
|
||||
# per swarm. Pinned so neither shape is tidied into the other.
|
||||
name = "each of the four readers logs in under a subject of its own";
|
||||
name = "each of the three readers logs in under a subject of its own";
|
||||
ok =
|
||||
let
|
||||
subjectOf =
|
||||
|
|
@ -875,8 +863,7 @@ let
|
|||
# the reason the three service principals above state.
|
||||
&& !(lib.hasInfix "auth/cert/certs/hive-" s);
|
||||
in
|
||||
subjectOf "swarm-bao-queue-agent-policy" "swarm-queue-agent-h1" "swarm-bao-queue-agent-h1"
|
||||
&& subjectOf "swarm-bao-grafana-oidc-policy" "swarm-grafana-oidc" "swarm-bao-grafana-oidc"
|
||||
subjectOf "swarm-bao-grafana-oidc-policy" "swarm-grafana-oidc" "swarm-bao-grafana-oidc"
|
||||
&& subjectOf "swarm-bao-otel-oidc-policy" "swarm-otel-oidc" "swarm-bao-otel-oidc"
|
||||
&& subjectOf "swarm-bao-forwarder-oidc-policy" "swarm-forwarder-oidc" "swarm-bao-forwarder-oidc";
|
||||
}
|
||||
|
|
@ -889,7 +876,7 @@ let
|
|||
#
|
||||
# Each pair is asserted whole: a certificate with no key authenticates
|
||||
# nothing, so a half-set pair is a reader that does not render.
|
||||
name = "each of the four readers presents its own leaf, never the hive's";
|
||||
name = "each of the three readers presents its own leaf, never the hive's";
|
||||
ok =
|
||||
let
|
||||
b = baoGrantWithConsumers.services.hyperhive.deploy.bao;
|
||||
|
|
@ -898,8 +885,6 @@ let
|
|||
b.clientKeyFile
|
||||
];
|
||||
own = [
|
||||
b.queueAgentClientCertFile
|
||||
b.queueAgentClientKeyFile
|
||||
b.grafanaOidcClientCertFile
|
||||
b.grafanaOidcClientKeyFile
|
||||
b.otelOidcClientCertFile
|
||||
|
|
@ -915,7 +900,6 @@ let
|
|||
lib.all (p: p != null) own
|
||||
&& !(lib.any (p: lib.elem p hiveLeaf) own)
|
||||
&& lib.length (lib.unique own) == lib.length own
|
||||
&& presents "swarm-bao-queue-agent" b.queueAgentClientCertFile b.queueAgentClientKeyFile
|
||||
&& presents "swarm-bao-grafana-oidc" b.grafanaOidcClientCertFile b.grafanaOidcClientKeyFile
|
||||
&& presents "swarm-bao-otel-oidc" b.otelOidcClientCertFile b.otelOidcClientKeyFile
|
||||
&& presents "swarm-bao-forwarder-oidc" b.forwarderOidcClientCertFile b.forwarderOidcClientKeyFile;
|
||||
|
|
@ -923,9 +907,7 @@ let
|
|||
{
|
||||
# The minting side of the same claim. A role matching a subject nothing
|
||||
# signs is a reader that cannot log in, so the leaves and the roles have
|
||||
# to be asserted against each other — and the per-hive leaf carries
|
||||
# THIS host's hive name, which is what makes one hive's leaf useless
|
||||
# against another hive's role.
|
||||
# to be asserted against each other.
|
||||
name = "the PKI unit signs a leaf per reader, each under that reader's own subject";
|
||||
ok =
|
||||
let
|
||||
|
|
@ -936,8 +918,6 @@ let
|
|||
# literal spanning both would pin this file's line wrapping rather than
|
||||
# the pairing it means to.
|
||||
lib.all (lib.flip lib.hasInfix s) [
|
||||
"/queue-agent.pem ]"
|
||||
"swarm-bao-queue-agent-h1 \"\" clientAuth"
|
||||
"/grafana-oidc.pem ]"
|
||||
"swarm-bao-grafana-oidc \"\" clientAuth"
|
||||
"/otel-oidc.pem ]"
|
||||
|
|
@ -965,11 +945,10 @@ let
|
|||
# client certificate and the host is the side that has one, so a unit
|
||||
# rendered inside the store's container would have neither an identity
|
||||
# nor a route. Plus the ordering that makes the mounts exist first.
|
||||
name = "the four readers' granting units are ordered after the mounts and rendered on the host";
|
||||
name = "the three readers' granting units are ordered after the mounts and rendered on the host";
|
||||
ok =
|
||||
let
|
||||
units = [
|
||||
"swarm-bao-queue-agent-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
"swarm-bao-forwarder-oidc-policy"
|
||||
|
|
@ -990,7 +969,7 @@ let
|
|||
# The forwarder is listed apart from `policyReaders`: it renders wherever
|
||||
# the store does, so it is never absent on a store host and never present
|
||||
# on a remote one, and the two cases below would fail on it for that.
|
||||
name = "each of the four readers is ordered after the unit writing its role";
|
||||
name = "each of the three readers is ordered after the unit writing its role";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantWithConsumers.systemd.services;
|
||||
|
|
@ -1483,9 +1462,9 @@ let
|
|||
}
|
||||
{
|
||||
# The other hives' side: every hive in the directory but this one gets
|
||||
# the same three leaves, under its own name, in a directory of its own
|
||||
# the same two leaves, under its own name, in a directory of its own
|
||||
# that the operator copies to its host.
|
||||
name = "the PKI unit signs every other hive's reader, queue-agent and gateway-leaf identities";
|
||||
name = "the PKI unit signs every other hive's reader and gateway-leaf identities";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantTwoHives.systemd.services.swarm-bao-pki.script;
|
||||
|
|
@ -1494,7 +1473,6 @@ let
|
|||
file: cn: lib.hasInfix "/hives/h2/${file}.pem ]" s && lib.hasInfix " ${cn} \"\" clientAuth" s;
|
||||
in
|
||||
signs "client" "h2"
|
||||
&& signs "queue-agent" "${b.queueAgentCommonNamePrefix}-h2"
|
||||
&& signs "hive-leaf" "${b.hiveLeafCommonNamePrefix}-h2"
|
||||
# This hive's own leaves stay at the top of the directory.
|
||||
&& !(lib.hasInfix "/hives/h1/" s);
|
||||
|
|
|
|||
|
|
@ -31,18 +31,10 @@ let
|
|||
# nothing here mints a leaf and the operator names one placed by hand. The
|
||||
# deployment this pairing exists to serve, and the one that was previously
|
||||
# inexpressible — the gate asked whether the store was a neighbour.
|
||||
#
|
||||
# 🩸 One leaf PER READER, and this fixture is where that cost is visible: the
|
||||
# queue-credential unit identifies itself to the store separately from the
|
||||
# hive, so an operator placing leaves by hand places one for it as well as
|
||||
# the hive's own. Naming only `clientCertFile` here would leave the unit
|
||||
# unrendered — which is what the arms below would then be asserting about.
|
||||
baoRemoteReader = hiveWithController {
|
||||
deploy.matrix.enable = true;
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
deploy.bao.queueAgentClientCertFile = "/etc/pki/bao-queue-agent.pem";
|
||||
deploy.bao.queueAgentClientKeyFile = "/etc/pki/bao-queue-agent-key.pem";
|
||||
};
|
||||
|
||||
# A homeserver on a hive with NO store identity at all — neither a local
|
||||
|
|
@ -51,24 +43,15 @@ let
|
|||
# fixture set its own, as ./lib.nix asks.
|
||||
inherit (shared) matrixNoBaoIdentity;
|
||||
|
||||
# 🩸 The hand-configured remote reader that named the hive's leaf and stopped.
|
||||
# `baoRemoteReader` above is the same deployment done right; this one holds
|
||||
# the hive's own leaf, so it demonstrably reads the store, and is missing the
|
||||
# queue reader's pair. Without the refusal it would render no queue reader
|
||||
# and say nothing — which is what the refusal arms below are about.
|
||||
# 🩸 The hand-configured remote reader that named the hive's leaf and stopped:
|
||||
# it holds the hive's own leaf, so it demonstrably reads the store, and is
|
||||
# missing matrix-ctl's pair.
|
||||
baoRemoteReaderMissingLeaves = hiveWithController {
|
||||
deploy.matrix.enable = true;
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
};
|
||||
|
||||
# The same omission on a hive that does NOT run a homeserver: the queue
|
||||
# refusal checks only the hive's own toggle.
|
||||
remoteReaderNoMatrixMissingLeaves = hiveWithController {
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
};
|
||||
|
||||
# The store's host with the hive's own reader pointed somewhere else.
|
||||
matrixOnStoreHostReaderMoved = hiveWithController {
|
||||
deploy.bao.enable = true;
|
||||
|
|
@ -77,122 +60,15 @@ let
|
|||
deploy.bao.clientKeyFile = "/etc/pki/h1-key.pem";
|
||||
};
|
||||
|
||||
# Did a module refuse this host, and over which option. An assertion is a
|
||||
# config VALUE until something forces it — `.config` never throws — so a
|
||||
# fixture in a state the module refuses stays evaluable and the refusal reads
|
||||
# back as data. Matched on the option name the message names rather than on
|
||||
# its prose, because the option name is the part an operator has to act on
|
||||
# and a message that stopped naming it would be the actual defect. Same shape
|
||||
# as ./grafana.nix's `grafanaRefusedFor`, which reads `swarm-grafana.nix`'s
|
||||
# own refusal.
|
||||
refusedOver = m: option: lib.any (a: !a.assertion && lib.hasInfix option a.message) m.assertions;
|
||||
cases = [
|
||||
{
|
||||
# A login failure is the store being unreachable, sealed, or not yet
|
||||
# holding this host's role — all of which a retry fixes. A read that
|
||||
# answers "nothing there" is not, so only the first is allowed to fail
|
||||
# the unit.
|
||||
name = "the queue credential reader retries a failed login and still degrades on an empty read";
|
||||
ok =
|
||||
let
|
||||
u = baoWithMatrix.systemd.services.swarm-bao-queue-agent;
|
||||
# Everything between the login's failure branch and the read's, which
|
||||
# is where the exit that decides "retry or give up" lives.
|
||||
afterLogin = lib.last (lib.splitString "bao login" u.script);
|
||||
loginBranch = lib.head (lib.splitString "bao kv get" afterLogin);
|
||||
in
|
||||
u.serviceConfig.Restart or null == "on-failure"
|
||||
&& u.startLimitBurst or 0 > 0
|
||||
# The window has to outlast every attempt, or the burst is unreachable.
|
||||
&& u.startLimitIntervalSec or 0 > (u.serviceConfig.RestartSec or 0) * (u.startLimitBurst or 0)
|
||||
&& lib.hasInfix "exit 1" loginBranch
|
||||
&& lib.hasInfix "exit 0" (lib.last (lib.splitString "bao kv get" u.script));
|
||||
}
|
||||
{
|
||||
# The reader's own grant covers `swarm/hives/<this hive>/*`; a path
|
||||
# outside it answers 403, not "no such key". This is the path
|
||||
# `swarm_secret_client::queue::agent_client_path` builds from the same
|
||||
# pieces. The negative arm is the rename this one is exposed to: a
|
||||
# credential named for the queue rather than for the hive that presents
|
||||
# it reads as correct and is refused on every boot.
|
||||
name = "the agent queue credential path sits inside the prefix the reader is granted";
|
||||
ok =
|
||||
let
|
||||
s = baoRemoteReader.systemd.services.swarm-bao-queue-agent.script;
|
||||
in
|
||||
lib.hasInfix "secret/swarm/hives/h1/queue/agent" s && !(lib.hasInfix "secret/swarm/queue/" s);
|
||||
}
|
||||
{
|
||||
# The unit's output is the option's value, not a literal that agrees with
|
||||
# it today: an operator moving the directory has to move both files. The
|
||||
# prefix is asserted too because `hasInfix ""` is true — an option
|
||||
# renamed out from under this arm would otherwise read empty and pass.
|
||||
name = "the queue credential reader writes both files under the directory its option names";
|
||||
ok =
|
||||
let
|
||||
m = baoRemoteReader;
|
||||
dir = toString m.services.hyperhive.deploy.hive-controller.queue.agentCredentialDir;
|
||||
s = m.systemd.services.swarm-bao-queue-agent.script;
|
||||
in
|
||||
lib.hasPrefix "/var/lib/" dir
|
||||
&& lib.hasInfix "${dir}/secret" s
|
||||
&& lib.hasInfix "${dir}/client_id" s;
|
||||
}
|
||||
{
|
||||
# No agent container may render before this unit has had its attempts,
|
||||
# and the edge that guarantees it must delay hive-c0re rather than sink
|
||||
# it: an unreachable store is this unit's `Restart=on-failure` window,
|
||||
# not a reason for the daemon that renders every agent to fail its own
|
||||
# start.
|
||||
name = "the queue credential reader orders before hive-c0re and is wanted, not required, by it";
|
||||
ok =
|
||||
let
|
||||
u = baoRemoteReader.systemd.services.swarm-bao-queue-agent;
|
||||
in
|
||||
builtins.elem "hive-c0re.service" (u.before or [ ])
|
||||
&& builtins.elem "hive-c0re.service" (u.wantedBy or [ ])
|
||||
&& !(builtins.elem "hive-c0re.service" (u.requiredBy or [ ]))
|
||||
&& !(builtins.elem "hive-c0re.service" (u.requires or [ ]));
|
||||
}
|
||||
{
|
||||
# Nothing local mints certificates off-host, so this orders against
|
||||
# nothing there.
|
||||
name = "an off-host reader names no unit the store's host would have provided";
|
||||
# Membership first, then the value: indexing a missing unit throws, and a
|
||||
# table that reports which property broke must not be the thing that dies.
|
||||
ok =
|
||||
let
|
||||
s = baoRemoteReader.systemd.services;
|
||||
u = s.swarm-bao-queue-agent;
|
||||
in
|
||||
s ? swarm-bao-queue-agent && !(lib.elem "swarm-bao-pki.service" (u.after ++ u.wants ++ u.requires));
|
||||
}
|
||||
{
|
||||
# Presence control for the case above: the list is conditional, not gone.
|
||||
# Wanted, never required: a refused pki start fails a requirer's job with
|
||||
# `dependency`, which its `Restart=on-failure` never retries.
|
||||
name = "a co-located reader still orders after and wants the local pki unit, and does not require it";
|
||||
ok =
|
||||
let
|
||||
s = baoWithMatrix.systemd.services;
|
||||
u = s.swarm-bao-queue-agent;
|
||||
in
|
||||
s ? swarm-bao-queue-agent
|
||||
&& lib.elem "swarm-bao-pki.service" u.after
|
||||
&& lib.elem "swarm-bao-pki.service" u.wants
|
||||
&& u.requires == [ ];
|
||||
}
|
||||
{
|
||||
# Nothing asserted this script before, which is how it kept a branch that
|
||||
# named three states and threw away the only thing telling them apart. A
|
||||
# missing value, a refused identity and an unreachable host all end in the
|
||||
# same degraded mode here, correctly — what must survive is which one.
|
||||
name = "the queue credential reader carries the store's own diagnostic into the journal";
|
||||
ok =
|
||||
let
|
||||
s = baoWithMatrix.systemd.services.swarm-bao-queue-agent.script;
|
||||
in
|
||||
!(lib.hasInfix "2>/dev/null" s) && lib.hasInfix ''cat "''$err"'' s;
|
||||
# A hive holds no shared queue credential for its agents, wherever the
|
||||
# store runs. The control: hive-c0re renders on both hosts.
|
||||
name = "no hive renders a reader for a hive-shared agent queue credential";
|
||||
ok = lib.all (m: !(m.systemd.services ? swarm-bao-queue-agent) && m.systemd.services ? hive-c0re) [
|
||||
baoWithMatrix
|
||||
baoRemoteReader
|
||||
];
|
||||
}
|
||||
{
|
||||
# hive-c0re runs as hive-core and the client key is `0600` root-owned
|
||||
|
|
@ -404,46 +280,6 @@ let
|
|||
&& !(c.systemd.services ? swarm-matrix-appservice-publish)
|
||||
&& !(lib.any (lib.hasPrefix "swarm-appservice.yaml") c.systemd.services.tuwunel.serviceConfig.LoadCredential);
|
||||
}
|
||||
{
|
||||
# 🩸 The arm the whole refusal exists for. The reader is gated on its own
|
||||
# leaf, so the way this regresses is the build going green and the unit
|
||||
# not rendering — which no presence check on a rendered unit can see,
|
||||
# because the unit that is missing is the evidence. Read as a refusal
|
||||
# naming each option, so an operator acts on the message without opening
|
||||
# the nix.
|
||||
name = "a remote reader missing the queue reader's leaf is refused, naming both options";
|
||||
ok =
|
||||
refusedOver baoRemoteReaderMissingLeaves "queueAgentClientCertFile"
|
||||
&& refusedOver baoRemoteReaderMissingLeaves "queueAgentClientKeyFile";
|
||||
}
|
||||
{
|
||||
name = "the queue refusal needs no homeserver";
|
||||
ok = refusedOver remoteReaderNoMatrixMissingLeaves "queueAgentClientCertFile";
|
||||
}
|
||||
{
|
||||
# ⚠️ The arm that keeps the refusal from being worse than the silence it
|
||||
# replaced. A hive with NO store identity is the supported no-store
|
||||
# deployment and also the state an operator passes through bringing a hive
|
||||
# up — neither may fail to evaluate. Asserted as "no refusal names either
|
||||
# option", not as "this one fixture is fine", because the way this breaks
|
||||
# is a gate widened to the principal's leaf alone.
|
||||
name = "a hive with no store identity at all is refused over none of the per-principal leaves";
|
||||
ok = lib.all (option: !(refusedOver matrixNoBaoIdentity option)) [
|
||||
"queueAgentClientCertFile"
|
||||
"queueAgentClientKeyFile"
|
||||
];
|
||||
}
|
||||
{
|
||||
# The other half of the same guard, and the one an operator meets far more
|
||||
# often: on the store's own host ./host-modules/glue-bao-tls.nix mkDefaults
|
||||
# every pair, so there is nothing to name and nothing to refuse. Paired with
|
||||
# the fully-named remote reader, which is the same deployment done by hand.
|
||||
name = "neither a store host nor a correctly-named remote reader is refused";
|
||||
ok = lib.all (m: !(refusedOver m "queueAgentClientCertFile")) [
|
||||
baoWithMatrix
|
||||
baoRemoteReader
|
||||
];
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "bao-matrix-reader" cases
|
||||
|
|
|
|||
|
|
@ -216,13 +216,17 @@ let
|
|||
&& env shared.controllerAlone == { };
|
||||
}
|
||||
{
|
||||
# Where the reader puts the files and where the daemon looks for them is
|
||||
# one agreement spanning two modules. Asserted against the option rather
|
||||
# than the literal so moving the directory moves both ends.
|
||||
name = "hive-c0re is told where the agents' queue credential lands";
|
||||
# An agent connects to the queue with its own secret, so the hive hands
|
||||
# its agents no shared credential and no token endpoint to mint one at.
|
||||
# The control: the same environment carries the agents' queue address.
|
||||
name = "hive-c0re forwards no shared agent queue credential";
|
||||
ok =
|
||||
allLocal.systemd.services.hive-c0re.environment.HIVE_C0RE_AGENT_QUEUE_CREDENTIAL_DIR
|
||||
== toString allLocal.services.hyperhive.deploy.hive-controller.queue.agentCredentialDir;
|
||||
let
|
||||
e = allLocal.systemd.services.hive-c0re.environment;
|
||||
in
|
||||
!(e ? HIVE_C0RE_AGENT_QUEUE_CREDENTIAL_DIR)
|
||||
&& !(e ? HIVE_AGENT_OIDC_TOKEN_ENDPOINT)
|
||||
&& e ? HIVE_AGENT_NATS_URL;
|
||||
}
|
||||
{
|
||||
# Inside an agent container loopback is the agent itself, so the agents'
|
||||
|
|
@ -256,28 +260,10 @@ let
|
|||
&& lib.elem "/hive-collector.hive/${bridge}" allLocal.services.dnsmasq.settings.address;
|
||||
}
|
||||
{
|
||||
# The agents mint against the swarm's IdP, the same endpoint the hive's
|
||||
# own client uses — a hive-local guess would produce a token the queue
|
||||
# would not accept.
|
||||
name = "the agents' token endpoint is the swarm IdP's";
|
||||
ok =
|
||||
let
|
||||
e = allLocal.systemd.services.hive-c0re.environment;
|
||||
in
|
||||
lib.hasSuffix "/api/oidc/token" e.HIVE_AGENT_OIDC_TOKEN_ENDPOINT
|
||||
&& e.HIVE_AGENT_OIDC_TOKEN_ENDPOINT == e.HIVE_C0RE_OIDC_TOKEN_ENDPOINT;
|
||||
}
|
||||
{
|
||||
# The absence arm, and what makes the two above able to fail: a hive
|
||||
# with no queue address for its agents must forward neither coordinate,
|
||||
# because half a pair reaches the harness as a partial configuration
|
||||
# rather than as none.
|
||||
name = "a hive with no agent queue address forwards no agent queue coordinates";
|
||||
ok =
|
||||
let
|
||||
e = noAgentQueue.systemd.services.hive-c0re.environment;
|
||||
in
|
||||
!(e ? HIVE_AGENT_NATS_URL) && !(e ? HIVE_AGENT_OIDC_TOKEN_ENDPOINT);
|
||||
# The absence arm, and what makes the agents' queue address cases above able
|
||||
# to fail.
|
||||
name = "a hive with no agent queue address forwards none";
|
||||
ok = !(noAgentQueue.systemd.services.hive-c0re.environment ? HIVE_AGENT_NATS_URL);
|
||||
}
|
||||
{
|
||||
# Control: `bare` sets none of them, and its serve.json carries the
|
||||
|
|
|
|||
|
|
@ -37,13 +37,6 @@ let
|
|||
# homeserver across the network.
|
||||
matrixElsewhere = shared.bareWithController;
|
||||
cases = [
|
||||
{
|
||||
# Absence arm, and what makes the one above able to fail: with no leaf
|
||||
# this unit would fail a TLS handshake on every boot, so it must not
|
||||
# exist at all rather than retry its way through the start limit.
|
||||
name = "a hive with no store identity renders no queue credential reader";
|
||||
ok = !(matrixNoBaoIdentity.systemd.services ? swarm-bao-queue-agent);
|
||||
}
|
||||
{
|
||||
# Two limits governed a matrix upload and nothing kept them in agreement,
|
||||
# so raising the documented one past the gateway's hardcoded cap changed
|
||||
|
|
|
|||
|
|
@ -43,14 +43,12 @@ let
|
|||
# leaves, not a whole prefix, so a hive that inherited a dead entry there
|
||||
# would silently gain the ability to overwrite the swarm's matrix credential.
|
||||
#
|
||||
# 🩸 The two per-hive subjects have a different shape: those roles are
|
||||
# written PER HIVE, so the subject a hive must not be is
|
||||
# `<prefix>-<some hive's name>` rather than the prefix itself. Reserving only
|
||||
# the prefix would leave the composed spelling free, and a hive taking it
|
||||
# would present a leaf the other hive's role accepts — a hive reading another
|
||||
# hive's queue credential, the exact widening the split exists to avoid. The
|
||||
# collision is with the OTHER hive's role, hence `other` beside `qr-other`
|
||||
# and `hl-other`.
|
||||
# 🩸 The per-hive subject has a different shape: that role is written PER
|
||||
# HIVE, so the subject a hive must not be is `<prefix>-<some hive's name>`
|
||||
# rather than the prefix itself. Reserving only the prefix would leave the
|
||||
# composed spelling free, and a hive taking it would present a leaf the other
|
||||
# hive's role accepts. The collision is with the OTHER hive's role, hence
|
||||
# `other` beside `hl-other`.
|
||||
hiveNamedAfterEverySubject = hive {
|
||||
deploy.swarm-otel.enable = false;
|
||||
deploy.bao = {
|
||||
|
|
@ -62,7 +60,6 @@ let
|
|||
forwarderOidcCommonName = "fwctl";
|
||||
forgePublisherCommonName = "fpctl";
|
||||
granterCommonName = "grctl";
|
||||
queueAgentCommonNamePrefix = "qr";
|
||||
hiveLeafCommonNamePrefix = "hl";
|
||||
};
|
||||
swarm.hives = {
|
||||
|
|
@ -75,7 +72,6 @@ let
|
|||
fpctl.domain = "fp.t.local";
|
||||
grctl.domain = "gr.t.local";
|
||||
other.domain = "other.t.local";
|
||||
qr-other.domain = "q.t.local";
|
||||
hl-other.domain = "hl.t.local";
|
||||
};
|
||||
};
|
||||
|
|
@ -216,16 +212,9 @@ let
|
|||
{
|
||||
# 🩸 The per-hive half, and the one a prefix-only reservation would miss:
|
||||
# the role is `<prefix>-<hive>`, so the reserved string has to be composed
|
||||
# against every declared hive. Here hive `qr-other` collides with the role
|
||||
# written for hive `other` — a leaf that reads a credential belonging to a
|
||||
# hive that is not it.
|
||||
name = "a hive named after another hive's per-hive reader subject is refused";
|
||||
ok = equalityGuardRefuses hiveNamedAfterEverySubject "qr-other";
|
||||
}
|
||||
{
|
||||
# A hive taking this spelling would present a leaf hive `other`'s
|
||||
# gateway-leaf role accepts, and could ask the store for that hive's
|
||||
# certificate. The control is the legal roster below.
|
||||
# against every declared hive. A hive taking this spelling would present
|
||||
# a leaf hive `other`'s gateway-leaf role accepts, and could ask the store
|
||||
# for that hive's certificate. The control is the legal roster below.
|
||||
name = "a hive named after another hive's gateway-leaf subject is refused";
|
||||
ok = equalityGuardRefuses hiveNamedAfterEverySubject "hl-other";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,13 +77,10 @@ let
|
|||
|
||||
# The same hive given its own store leaf by hand, which is what turns
|
||||
# publishing on away from the IdP's host: hive-c0re reads the status secret
|
||||
# from the store under it. The queue-credential reader's pair comes with it,
|
||||
# because a hive that reads the store is refused at eval without one.
|
||||
# from the store under it.
|
||||
hiveLeaf = {
|
||||
deploy.bao.clientCertFile = "/etc/pki/h1.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/h1-key.pem";
|
||||
deploy.bao.queueAgentClientCertFile = "/etc/pki/h1-queue-agent.pem";
|
||||
deploy.bao.queueAgentClientKeyFile = "/etc/pki/h1-queue-agent-key.pem";
|
||||
};
|
||||
remotePublishing = hiveWithController (lib.recursiveUpdate remoteSecrets hiveLeaf);
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ let
|
|||
# Each hive's hive-c0re reads this exact path
|
||||
# (`swarm_secret_client::queue::hive_client_path`, pinned by that crate's
|
||||
# own test), for every hive in the roster, not only the one this host is.
|
||||
# The agents' client beside it stays a separate path.
|
||||
name = "the publisher writes every hive's own client under that hive's prefix";
|
||||
ok =
|
||||
let
|
||||
|
|
@ -91,8 +90,7 @@ let
|
|||
[
|
||||
"h1"
|
||||
"h2"
|
||||
]
|
||||
&& lib.hasInfix "secret/swarm/hives/h1/queue/agent" s;
|
||||
];
|
||||
}
|
||||
{
|
||||
# The controller reads this exact path
|
||||
|
|
@ -333,17 +331,17 @@ let
|
|||
lib.hasInfix "value=@" code && !(lib.hasInfix "$(cat" code);
|
||||
}
|
||||
{
|
||||
# Two ends of a wire nothing at eval time carries end to end: this is the
|
||||
# path `swarm_secret_client::queue` resolves for the reader. Both hives
|
||||
# are asserted, so a publisher that knew one name rather than the roster
|
||||
# fails here rather than on the second hive ever added to a swarm.
|
||||
name = "the publisher writes every hive in the roster to that hive's own queue path";
|
||||
# No hive holds a shared queue credential for its agents, so authelia
|
||||
# mints no agent client and the publisher writes no such path. The
|
||||
# control: the same script writes each hive's own client.
|
||||
name = "the publisher writes no hive-shared agent queue credential";
|
||||
ok =
|
||||
let
|
||||
s = (inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script;
|
||||
in
|
||||
lib.hasInfix "secret/swarm/hives/h1/queue/agent" s
|
||||
&& lib.hasInfix "secret/swarm/hives/h2/queue/agent" s;
|
||||
!(lib.hasInfix "/queue/agent" s)
|
||||
&& !(lib.hasInfix "-agent.secret" s)
|
||||
&& lib.hasInfix "secret/swarm/hives/h1/oidc/client" s;
|
||||
}
|
||||
{
|
||||
# A property of the SET, not of one unit: both of these authenticate by
|
||||
|
|
@ -373,7 +371,7 @@ let
|
|||
&& !(lib.hasInfix "bao kv" (lib.head (lib.splitString "bao login" c)));
|
||||
in
|
||||
holdsTokenFirst (inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script
|
||||
&& holdsTokenFirst baoWithMatrix.systemd.services.swarm-bao-queue-agent.script
|
||||
&& holdsTokenFirst baoWithMatrix.systemd.services.swarm-bao-matrix-oidc.script
|
||||
# Controls, so a clean verdict above means something. In order: a bare
|
||||
# read is refused, a read placed before the login is refused, and a
|
||||
# login that exists only in a comment is refused — that last one is the
|
||||
|
|
|
|||
Loading…
Reference in a new issue