matrix: drop the per-hive appservice and its unused token
Every hive's homeserver loaded a per-hive `hyperhive` appservice
registration, and the swarm minted and delivered an as_token for it
through bao. No client presented that token: swarm-controller creates
every `@hive-<hive>` sender and every agent account with the swarm
appservice token (hive_sender.rs, agent_token.rs), and no Rust code read
the per-hive one.
Removed, with everything that existed only to serve it:
- hive-matrix.nix: the `hyperhive` registration, its activation-time
render, the host bind mount of /var/lib/hyperhive/matrix-appservice,
its LoadCredential entry, and the internal options
`deploy.matrix.appserviceTokenFile` / `appserviceRegistrationScript`.
tuwunel now loads only the swarm registration.
- glue-matrix-bao-token.nix and its `swarm-bao-matrix-token` unit.
- glue-bao-tls.nix: the matrix-token leaf and its defaults.
- swarm-bao.nix: the `swarm-matrix-token-<hive>` roles and policies
(`swarm-bao-matrix-token-policy`), and the options
`deploy.bao.matrixTokenClient{Cert,Key}File` and
`matrixTokenCommonNamePrefix`, now mkRemovedOptionModule.
- swarm-secret-publisher.nix: the per-hive token mint and its state dir.
- swarm.nix: the reserved `<matrixTokenCommonNamePrefix>-<hive>` CN.
- swarm-secret-client: `matrix::appservice_token_path`.
The swarm registration (swarm-matrix-ctl, swarm-controller) is untouched.
Comments and docs that pointed at the removed glue file now point at
glue-queue-agent-credential.nix, which has the same reader shape.
Refs #4428
This commit is contained in:
parent
e4b170f8c6
commit
48067d07e8
31 changed files with 362 additions and 1233 deletions
|
|
@ -162,13 +162,6 @@ let
|
|||
name = "an operator's own certificate path beats the glue's default";
|
||||
ok = baoExplicitCerts.services.hyperhive.deploy.bao.serverCertFile == "/etc/pki/bao.pem";
|
||||
}
|
||||
{
|
||||
# Absence arm. A store with nothing to serve renders no reader, so the
|
||||
# unit is a function of the PAIRING rather than of the store — which is
|
||||
# the property that makes it glue instead of a feature of either side.
|
||||
name = "a store with no homeserver beside it renders no token reader";
|
||||
ok = !(baoPkcs11.systemd.services ? swarm-bao-matrix-token);
|
||||
}
|
||||
{
|
||||
# The name a reader dials has to resolve where the store runs; a
|
||||
# multi-host swarm resolves it upstream instead.
|
||||
|
|
|
|||
|
|
@ -87,20 +87,19 @@ let
|
|||
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
|
||||
deploy.bao.clientCaFile = lib.mkForce null;
|
||||
};
|
||||
# The store plus every one of the four readers that used to log in as the
|
||||
# hive. One fixture rather than four: the claim they are four *separate*
|
||||
# principals is only testable where all four render at once — that is the
|
||||
# deployment in which two of them sharing a leaf would be invisible.
|
||||
# The store plus every reader that used to log in as the hive. One fixture
|
||||
# rather than one per reader: the claim they are *separate* principals is
|
||||
# only testable where all of them render at once — that is the deployment in
|
||||
# which two of them sharing a leaf would be invisible.
|
||||
baoGrantWithConsumers = hive {
|
||||
deploy.bao.enable = true;
|
||||
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
|
||||
deploy.matrix.enable = true;
|
||||
deploy.grafana.enable = true;
|
||||
deploy.swarm-otel.enable = true;
|
||||
};
|
||||
|
||||
# The store with none of the four readers beside it. Grafana, the collector and
|
||||
# the homeserver are simply off; the queue reader renders on any host holding
|
||||
# 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.
|
||||
baoGrantNoReaders = hive {
|
||||
deploy.bao.enable = true;
|
||||
|
|
@ -117,16 +116,13 @@ let
|
|||
deploy.bao.forwarderOidcClientKeyFile = lib.mkForce null;
|
||||
};
|
||||
|
||||
# All four readers against a store they do not run, each with a leaf placed
|
||||
# by hand. The deployment in which there is no local policy unit to wait for.
|
||||
# Every reader against a store it does not run, each with a leaf placed by
|
||||
# hand. The deployment in which there is no local policy unit to wait for.
|
||||
baoRemoteReaders = hive {
|
||||
deploy.matrix.enable = true;
|
||||
deploy.grafana.enable = true;
|
||||
deploy.swarm-otel.enable = true;
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
deploy.bao.matrixTokenClientCertFile = "/etc/pki/bao-matrix-token.pem";
|
||||
deploy.bao.matrixTokenClientKeyFile = "/etc/pki/bao-matrix-token-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";
|
||||
|
|
@ -135,10 +131,9 @@ let
|
|||
deploy.bao.otelOidcClientKeyFile = "/etc/pki/bao-otel-oidc-key.pem";
|
||||
};
|
||||
|
||||
# The four readers ./glue-bao-readers-policy-order.nix orders after their
|
||||
# policy units.
|
||||
# The readers ./glue-bao-readers-policy-order.nix orders after their policy
|
||||
# units.
|
||||
policyReaders = [
|
||||
"swarm-bao-matrix-token"
|
||||
"swarm-bao-queue-agent"
|
||||
"swarm-bao-grafana-oidc"
|
||||
"swarm-bao-otel-oidc"
|
||||
|
|
@ -166,13 +161,12 @@ let
|
|||
_: u: (u.environment.BAO_CLIENT_CERT or null) == granterCertFile
|
||||
) baoGrantWithConsumers.systemd.services;
|
||||
|
||||
# The fourteen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# The thirteen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# control below.
|
||||
grantingUnitNames = [
|
||||
"swarm-bao-controller-policy"
|
||||
"swarm-bao-secret-publisher-policy"
|
||||
"swarm-bao-matrix-ctl-policy"
|
||||
"swarm-bao-matrix-token-policy"
|
||||
"swarm-bao-queue-agent-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
|
|
@ -479,11 +473,11 @@ let
|
|||
&& !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-matrix-ctl-policy);
|
||||
}
|
||||
|
||||
# ── the four readers that used to share the hive's own leaf ──────────────
|
||||
# ── the readers that used to share the hive's own leaf ───────────────────
|
||||
#
|
||||
# 🩸 Until this split all four presented `deploy.bao.clientCertFile`, whose
|
||||
# policy granted read on `swarm/agents/*`, `swarm/hives/<hive>/*` AND
|
||||
# `swarm/services/*`. Four principals behind one certificate are one
|
||||
# 🩸 Until this split all of them presented `deploy.bao.clientCertFile`,
|
||||
# whose policy granted read on `swarm/agents/*`, `swarm/hives/<hive>/*` AND
|
||||
# `swarm/services/*`. Principals behind one certificate are one
|
||||
# principal to bao, so the only expressible grant was the union: the unit
|
||||
# fetching Grafana's OIDC secret could fetch every agent credential in the
|
||||
# swarm.
|
||||
|
|
@ -493,30 +487,6 @@ let
|
|||
# just as well when the other two stanzas are still there beside it. The
|
||||
# arms pin what each principal must NOT reach, so a later widening fails
|
||||
# here instead of being noticed in a store.
|
||||
{
|
||||
name = "the matrix-token reader's grant is one hive's appservice token and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-token-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/hives/h1/matrix/appservice-token\" {" s
|
||||
&& lib.hasInfix "capabilities = [\"read\"]" s
|
||||
# The three stanzas the hive's own leaf carried, none of which this
|
||||
# principal needs: every agent's credential, every service's OIDC
|
||||
# client, and the rest of its own hive's tree — including the queue
|
||||
# credential its sibling reader fetches.
|
||||
&& !(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/queue" s)
|
||||
# A `hives/*` wildcard would serve every hive from one role and let any
|
||||
# hive read any other's token — the reach this split exists to remove,
|
||||
# not to create.
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives/*" s)
|
||||
# Nothing may rewrite the policy constraining it, for the reason the
|
||||
# controller's own `hive-*` narrowing above gives.
|
||||
&& !(lib.hasInfix "sys/policies/acl" s);
|
||||
}
|
||||
{
|
||||
name = "the queue-credential reader's grant is one hive's queue credential and nothing else";
|
||||
ok =
|
||||
|
|
@ -616,13 +586,13 @@ let
|
|||
{
|
||||
# 🩸 The half that makes the policies above bind: a policy grants only
|
||||
# through a token that carries it, and a token is minted by a cert-auth
|
||||
# role matching a CN. Four distinct subjects is the whole mechanism — one
|
||||
# subject for four readers is one principal however the policies read.
|
||||
# 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 subjects carry the hive name because their paths do; the
|
||||
# two service subjects do not, because an OIDC client is registered once
|
||||
# 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 five readers logs in under a subject of its own";
|
||||
name = "each of the four readers logs in under a subject of its own";
|
||||
ok =
|
||||
let
|
||||
subjectOf =
|
||||
|
|
@ -637,8 +607,7 @@ let
|
|||
# the reason the three service principals above state.
|
||||
&& !(lib.hasInfix "auth/cert/certs/hive-" s);
|
||||
in
|
||||
subjectOf "swarm-bao-matrix-token-policy" "swarm-matrix-token-h1" "swarm-bao-matrix-token-h1"
|
||||
&& subjectOf "swarm-bao-queue-agent-policy" "swarm-queue-agent-h1" "swarm-bao-queue-agent-h1"
|
||||
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-otel-oidc-policy" "swarm-otel-oidc" "swarm-bao-otel-oidc"
|
||||
&& subjectOf "swarm-bao-forwarder-oidc-policy" "swarm-forwarder-oidc" "swarm-bao-forwarder-oidc";
|
||||
|
|
@ -652,7 +621,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 five readers presents its own leaf, never the hive's";
|
||||
name = "each of the four readers presents its own leaf, never the hive's";
|
||||
ok =
|
||||
let
|
||||
b = baoGrantWithConsumers.services.hyperhive.deploy.bao;
|
||||
|
|
@ -661,8 +630,6 @@ let
|
|||
b.clientKeyFile
|
||||
];
|
||||
own = [
|
||||
b.matrixTokenClientCertFile
|
||||
b.matrixTokenClientKeyFile
|
||||
b.queueAgentClientCertFile
|
||||
b.queueAgentClientKeyFile
|
||||
b.grafanaOidcClientCertFile
|
||||
|
|
@ -680,7 +647,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-matrix-token" b.matrixTokenClientCertFile b.matrixTokenClientKeyFile
|
||||
&& 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
|
||||
|
|
@ -689,7 +655,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 two per-hive leaves carry
|
||||
# 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.
|
||||
name = "the PKI unit signs a leaf per reader, each under that reader's own subject";
|
||||
|
|
@ -702,8 +668,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) [
|
||||
"/matrix-token.pem ]"
|
||||
"swarm-bao-matrix-token-h1 \"\" clientAuth"
|
||||
"/queue-agent.pem ]"
|
||||
"swarm-bao-queue-agent-h1 \"\" clientAuth"
|
||||
"/grafana-oidc.pem ]"
|
||||
|
|
@ -733,11 +697,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 five readers' granting units are ordered after the mounts and rendered on the host";
|
||||
name = "the four readers' granting units are ordered after the mounts and rendered on the host";
|
||||
ok =
|
||||
let
|
||||
units = [
|
||||
"swarm-bao-matrix-token-policy"
|
||||
"swarm-bao-queue-agent-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
|
|
@ -759,7 +722,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 five readers is ordered after the unit writing its role";
|
||||
name = "each of the four readers is ordered after the unit writing its role";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantWithConsumers.systemd.services;
|
||||
|
|
@ -778,7 +741,7 @@ let
|
|||
# The ordering is set apart from each reader's own definition, so it can
|
||||
# define a reader by itself: `after` on a unit nothing else declares is a
|
||||
# unit with no ExecStart. On the store's host without the readers, none
|
||||
# of the four may exist.
|
||||
# of them may exist.
|
||||
name = "a store host without the readers gains no reader unit from their ordering";
|
||||
ok = lib.all (reader: !(baoGrantNoReaders.systemd.services ? ${reader})) policyReaders;
|
||||
}
|
||||
|
|
@ -800,24 +763,24 @@ let
|
|||
}
|
||||
{
|
||||
# A store host without the granter's pair writes its grants some other
|
||||
# way, so none of the fourteen units may exist. Without this arm
|
||||
# way, so none of the thirteen units may exist. Without this arm
|
||||
# `lib.mkIf haveGranter` could be dropped from any of them and every other
|
||||
# case here would still pass.
|
||||
name = "without the granter's pair none of the fourteen granting units render";
|
||||
name = "without the granter's pair none of the thirteen granting units render";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterOptOut.systemd.services;
|
||||
in
|
||||
lib.all (unit: !(s ? ${unit})) (grantingUnitNames ++ [ "swarm-bao-granter-role" ])
|
||||
# The control: the same store with the pair renders all fourteen.
|
||||
# The control: the same store with the pair renders all thirteen.
|
||||
&& lib.all (unit: baoGrantHere.systemd.services ? ${unit}) grantingUnitNames;
|
||||
}
|
||||
{
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the fourteen still
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the thirteen still
|
||||
# render, and a refused granter fails them with the step that fixes it.
|
||||
# A store host that never named a token is told to name one, since the
|
||||
# unit that sets the granter up renders only where it has.
|
||||
name = "a store host without a bootstrap token renders the fourteen, each failing loudly with the one-time step";
|
||||
name = "a store host without a bootstrap token renders the thirteen, each failing loudly with the one-time step";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterNoToken.systemd.services;
|
||||
|
|
@ -1247,8 +1210,8 @@ let
|
|||
}
|
||||
{
|
||||
# `matrix_account::hive_sender` writes every hive's sender token, and
|
||||
# nothing else under `hives/`: a hive's appservice token sits beside
|
||||
# it. `+` is one segment, which keeps this to the one leaf; a `*` is a
|
||||
# nothing else under `hives/`, where each hive's queue credential also
|
||||
# lives. `+` is one segment, which keeps this to the one leaf; a `*` is a
|
||||
# glob only at the end of a path. Pinned as the whole stanza, so an
|
||||
# added capability fails.
|
||||
name = "the controller writes each hive's sender token and nothing else under hives";
|
||||
|
|
@ -1392,8 +1355,8 @@ let
|
|||
}
|
||||
{
|
||||
# What makes the case above mean something: discovery by the granter's
|
||||
# certificate reaches all fourteen units, and each yields calls.
|
||||
name = "the granter-policy check sees all fourteen granting units, and parses calls from each";
|
||||
# certificate reaches all thirteen units, and each yields calls.
|
||||
name = "the granter-policy check sees all thirteen granting units, and parses calls from each";
|
||||
ok =
|
||||
lib.sort lib.lessThan (lib.attrNames granterUnits) == lib.sort lib.lessThan grantingUnitNames
|
||||
&& lib.all (u: baoCalls u.script != [ ]) (lib.attrValues granterUnits)
|
||||
|
|
@ -1723,11 +1686,6 @@ let
|
|||
}
|
||||
)
|
||||
[
|
||||
{
|
||||
fetch = "swarm-bao-matrix-token";
|
||||
machine = "hive-matrix";
|
||||
consumer = "tuwunel.service";
|
||||
}
|
||||
{
|
||||
fetch = "swarm-bao-otel-oidc";
|
||||
machine = baoGrantWithConsumers.services.hyperhive.swarm.otel.machine;
|
||||
|
|
|
|||
|
|
@ -34,16 +34,14 @@ let
|
|||
# inexpressible — the gate asked whether the store was a neighbour.
|
||||
#
|
||||
# 🩸 One leaf PER READER, and this fixture is where that cost is visible: the
|
||||
# two units below identify themselves to the store separately, so an operator
|
||||
# placing leaves by hand places one for each rather than one for both. Naming
|
||||
# only `clientCertFile` here would leave neither unit rendered — which is what
|
||||
# the arms below would then be asserting about.
|
||||
# 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 = hive {
|
||||
deploy.matrix.enable = true;
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
deploy.bao.matrixTokenClientCertFile = "/etc/pki/bao-matrix-token.pem";
|
||||
deploy.bao.matrixTokenClientKeyFile = "/etc/pki/bao-matrix-token-key.pem";
|
||||
deploy.bao.queueAgentClientCertFile = "/etc/pki/bao-queue-agent.pem";
|
||||
deploy.bao.queueAgentClientKeyFile = "/etc/pki/bao-queue-agent-key.pem";
|
||||
};
|
||||
|
|
@ -54,24 +52,19 @@ let
|
|||
# fixture set its own, as ./lib.nix asks.
|
||||
matrixNoBaoIdentity = hive { deploy.matrix.enable = true; };
|
||||
|
||||
# 🩸 The hand-configured remote reader that named seven of the eight options.
|
||||
# 🩸 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 both
|
||||
# per-principal pairs. Before the four-way split this host rendered both units
|
||||
# off `clientCertFile` alone, so what it has now is a silent regression rather
|
||||
# than any shape an operator chose — which is what the refusal arms below are
|
||||
# about. Kept as one fixture rather than two because both refusals fire on it
|
||||
# and each arm names which.
|
||||
# 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.
|
||||
baoRemoteReaderMissingLeaves = hive {
|
||||
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. Separates the
|
||||
# matrix refusal's `deploy.matrix.enable` clause from the queue refusal, which
|
||||
# checks only the hive's own toggle — an arm below reads exactly one refusal
|
||||
# off it.
|
||||
# The same omission on a hive that does NOT run a homeserver: the queue
|
||||
# refusal checks only the hive's own toggle.
|
||||
remoteReaderNoMatrixMissingLeaves = hive {
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
|
|
@ -92,10 +85,10 @@ let
|
|||
# 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 matrix token reader retries a failed login and still degrades on an empty read";
|
||||
name = "the queue credential reader retries a failed login and still degrades on an empty read";
|
||||
ok =
|
||||
let
|
||||
u = baoWithMatrix.systemd.services.swarm-bao-matrix-token;
|
||||
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);
|
||||
|
|
@ -109,30 +102,15 @@ let
|
|||
&& lib.hasInfix "exit 0" (lib.last (lib.splitString "bao kv get" u.script));
|
||||
}
|
||||
{
|
||||
# The reader's own grant covers `swarm/hives/<this hive>/*` and
|
||||
# `swarm/agents/*`; a path outside those answers 403, not "no such key".
|
||||
# So the hive segment is what makes the read reachable, and a rename that
|
||||
# drops it looks correct and fails identically on every boot.
|
||||
name = "the matrix token path sits inside the prefix the reader is granted";
|
||||
ok =
|
||||
let
|
||||
s = baoWithMatrix.systemd.services.swarm-bao-matrix-token.script;
|
||||
in
|
||||
lib.hasInfix "secret/swarm/hives/" s
|
||||
&& lib.hasInfix "/matrix/appservice-token" s
|
||||
# The shape it used to have: `matrix` where a principal kind belongs,
|
||||
# which no grant covers.
|
||||
&& !(lib.hasInfix "secret/swarm/matrix/" s);
|
||||
}
|
||||
{
|
||||
# The store's second reader, and the gate that decides it exists is the
|
||||
# certificate rather than anything about agents: containers are created
|
||||
# The gate that decides this reader exists is the certificate rather
|
||||
# than anything about agents: containers are created
|
||||
# at runtime, so there is no static "this hive runs agents" fact to ask.
|
||||
name = "a hive that names a client identity reads its agent queue credential";
|
||||
ok = baoRemoteReader.systemd.services ? swarm-bao-queue-agent;
|
||||
}
|
||||
{
|
||||
# Same 403-not-a-miss reason as the matrix arm above, against the path
|
||||
# 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
|
||||
|
|
@ -176,17 +154,6 @@ let
|
|||
&& !(builtins.elem "hive-c0re.service" (u.requiredBy or [ ]))
|
||||
&& !(builtins.elem "hive-c0re.service" (u.requires or [ ]));
|
||||
}
|
||||
{
|
||||
# The store's first reader. Its unit belongs to the pairing, not to
|
||||
# either service: matrix must not learn the store exists, and the store
|
||||
# must not know who reads it.
|
||||
name = "a store deployed beside the homeserver fetches its appservice token";
|
||||
ok = baoWithMatrix.systemd.services ? swarm-bao-matrix-token;
|
||||
}
|
||||
{
|
||||
name = "a hive that names a client identity reads from a store it does not run";
|
||||
ok = baoRemoteReader.systemd.services ? swarm-bao-matrix-token;
|
||||
}
|
||||
{
|
||||
# `Requires=` on a unit that does not exist fails the job, and nothing
|
||||
# local mints certificates off-host — so this orders against nothing.
|
||||
|
|
@ -198,7 +165,7 @@ let
|
|||
let
|
||||
s = baoRemoteReader.systemd.services;
|
||||
in
|
||||
s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ ];
|
||||
s ? swarm-bao-queue-agent && s.swarm-bao-queue-agent.requires == [ ];
|
||||
}
|
||||
{
|
||||
# Presence control for the case above: the list is conditional, not gone.
|
||||
|
|
@ -207,17 +174,17 @@ let
|
|||
let
|
||||
s = baoWithMatrix.systemd.services;
|
||||
in
|
||||
s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ "swarm-bao-pki.service" ];
|
||||
s ? swarm-bao-queue-agent && s.swarm-bao-queue-agent.requires == [ "swarm-bao-pki.service" ];
|
||||
}
|
||||
{
|
||||
# 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 matrix token reader carries the store's own diagnostic into the journal";
|
||||
name = "the queue credential reader carries the store's own diagnostic into the journal";
|
||||
ok =
|
||||
let
|
||||
s = baoWithMatrix.systemd.services.swarm-bao-matrix-token.script;
|
||||
s = baoWithMatrix.systemd.services.swarm-bao-queue-agent.script;
|
||||
in
|
||||
!(lib.hasInfix "2>/dev/null" s) && lib.hasInfix ''cat "''$err"'' s;
|
||||
}
|
||||
|
|
@ -295,18 +262,12 @@ let
|
|||
# it the unit names two paths the container does not have, and fails at
|
||||
# the TLS handshake naming no cause. Read off the mount table rather than
|
||||
# the option, so a pairing that stops reaching `bindMounts` still fails.
|
||||
#
|
||||
# The second arm is the shape guard: `bindMounts` is one literal plus two
|
||||
# merges, and a rewrite that dropped the appservice registration would
|
||||
# take the homeserver's own credential with it.
|
||||
name = "the matrix container binds matrix-ctl's PKI read-only, without losing the appservice registration";
|
||||
name = "the matrix container binds matrix-ctl's PKI read-only";
|
||||
ok =
|
||||
let
|
||||
mounts = baoWithMatrix.containers.hive-matrix.bindMounts;
|
||||
in
|
||||
mounts ? "/var/lib/swarm-bao-pki"
|
||||
&& mounts."/var/lib/swarm-bao-pki".isReadOnly
|
||||
&& mounts ? "/var/lib/hyperhive/matrix-appservice";
|
||||
mounts ? "/var/lib/swarm-bao-pki" && mounts."/var/lib/swarm-bao-pki".isReadOnly;
|
||||
}
|
||||
{
|
||||
# The cert role ./host-modules/swarm-bao.nix writes, which is the one
|
||||
|
|
@ -366,12 +327,18 @@ let
|
|||
or false;
|
||||
}
|
||||
{
|
||||
# The swarm registration reaches tuwunel the way the hive's does: a
|
||||
# `.yaml` credential in the directory `appservice_dir` names.
|
||||
name = "tuwunel loads the swarm registration as a yaml credential";
|
||||
ok = lib.elem "swarm-appservice.yaml:/var/lib/swarm-matrix-appservice/swarm.yaml" (
|
||||
baoWithMatrix.containers.hive-matrix.config.systemd.services.tuwunel.serviceConfig.LoadCredential
|
||||
);
|
||||
# The swarm registration reaches tuwunel as a `.yaml` credential in the
|
||||
# directory `appservice_dir` names, and it is the only registration
|
||||
# there: no client presents a per-hive one's token, so loading one would
|
||||
# be a credential nobody reads.
|
||||
name = "tuwunel loads the swarm registration as a yaml credential, and no other";
|
||||
ok =
|
||||
let
|
||||
creds =
|
||||
baoWithMatrix.containers.hive-matrix.config.systemd.services.tuwunel.serviceConfig.LoadCredential;
|
||||
in
|
||||
lib.elem "swarm-appservice.yaml:/var/lib/swarm-matrix-appservice/swarm.yaml" creds
|
||||
&& lib.length (lib.filter (c: lib.hasInfix ".yaml:" c) creds) == 1;
|
||||
}
|
||||
{
|
||||
# A missing `LoadCredential` source fails tuwunel, so the render has to
|
||||
|
|
@ -402,38 +369,30 @@ let
|
|||
&& !(lib.any (lib.hasPrefix "swarm-appservice.yaml") c.systemd.services.tuwunel.serviceConfig.LoadCredential);
|
||||
}
|
||||
{
|
||||
# 🩸 The arm the whole refusal exists for. Both readers are gated on their
|
||||
# own leaf, so the way this regresses is the build going green and three
|
||||
# units rendering where four should — 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 per-principal leaves is refused, naming both options";
|
||||
# 🩸 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 "matrixTokenClientCertFile"
|
||||
&& refusedOver baoRemoteReaderMissingLeaves "matrixTokenClientKeyFile"
|
||||
&& refusedOver baoRemoteReaderMissingLeaves "queueAgentClientCertFile"
|
||||
refusedOver baoRemoteReaderMissingLeaves "queueAgentClientCertFile"
|
||||
&& refusedOver baoRemoteReaderMissingLeaves "queueAgentClientKeyFile";
|
||||
}
|
||||
{
|
||||
# The matrix refusal's own gate, which the queue refusal does not have.
|
||||
# Without this arm the two are indistinguishable on the fixture above.
|
||||
name = "the queue refusal needs no homeserver, and the matrix refusal stays quiet without one";
|
||||
ok =
|
||||
refusedOver remoteReaderNoMatrixMissingLeaves "queueAgentClientCertFile"
|
||||
&& !(refusedOver remoteReaderNoMatrixMissingLeaves "matrixTokenClientCertFile");
|
||||
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 any of
|
||||
# the four options", not as "this one fixture is fine", because the way
|
||||
# this breaks is a gate widened to the principal's leaf alone.
|
||||
# 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)) [
|
||||
"matrixTokenClientCertFile"
|
||||
"matrixTokenClientKeyFile"
|
||||
"queueAgentClientCertFile"
|
||||
"queueAgentClientKeyFile"
|
||||
];
|
||||
|
|
@ -441,22 +400,13 @@ let
|
|||
{
|
||||
# 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
|
||||
# all eight, so there is nothing to name and nothing to refuse. Paired with
|
||||
# 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:
|
||||
lib.all (option: !(refusedOver m option)) [
|
||||
"matrixTokenClientCertFile"
|
||||
"queueAgentClientCertFile"
|
||||
]
|
||||
)
|
||||
[
|
||||
baoWithMatrix
|
||||
baoRemoteReader
|
||||
];
|
||||
ok = lib.all (m: !(refusedOver m "queueAgentClientCertFile")) [
|
||||
baoWithMatrix
|
||||
baoRemoteReader
|
||||
];
|
||||
}
|
||||
];
|
||||
in
|
||||
|
|
|
|||
|
|
@ -59,14 +59,7 @@ let
|
|||
lib.hasInfix "client_max_body_size 100048576;" c && !(lib.hasInfix "client_max_body_size 50M" c);
|
||||
}
|
||||
{
|
||||
# Absence arm for the one above, and the reason the gate is the identity
|
||||
# rather than the homeserver: without it, deploying matrix anywhere would
|
||||
# render a reader that cannot authenticate.
|
||||
name = "a homeserver with no way to authenticate to the store renders no token reader";
|
||||
ok = !(matrixNoBaoIdentity.systemd.services ? swarm-bao-matrix-token);
|
||||
}
|
||||
{
|
||||
# Absence arm for the case above. A hive with no client identity gets no
|
||||
# Absence arm. A hive with no client identity gets no
|
||||
# store environment at all — the daemon reports a queue it cannot serve
|
||||
# rather than a handshake it cannot explain.
|
||||
name = "a hive with no client identity gives hive-c0re no store environment";
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ let
|
|||
swarm.hives.grctl.domain = "gr.t.local";
|
||||
};
|
||||
|
||||
# 🩸 A different shape from every fixture above: the matrix-token and
|
||||
# queue-credential roles are written PER HIVE, so the subject a hive must not
|
||||
# 🩸 A different shape from every fixture above: the queue-credential 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 — which is a hive reading
|
||||
|
|
|
|||
|
|
@ -243,41 +243,6 @@ let
|
|||
lib.hasInfix "secret/swarm/hives/h1/queue/agent" s
|
||||
&& lib.hasInfix "secret/swarm/hives/h2/queue/agent" s;
|
||||
}
|
||||
{
|
||||
# The producer's end of the read `glue-matrix-bao-token.nix` already did.
|
||||
# Both hives are asserted for the reason the queue case above gives: a
|
||||
# publisher that knew one name rather than the roster would pass on a
|
||||
# single-hive fixture and strand the second hive ever added — which is
|
||||
# the two-hives-never-converge shape this slice exists to close.
|
||||
name = "the publisher mints an appservice token for every hive and writes it to that hive's matrix path";
|
||||
ok =
|
||||
let
|
||||
s = secretPublisherHere.systemd.services.swarm-secret-publish.script;
|
||||
in
|
||||
lib.hasInfix "secret/swarm/hives/h1/matrix/appservice-token" s
|
||||
&& lib.hasInfix "secret/swarm/hives/h2/matrix/appservice-token" s
|
||||
&& lib.hasInfix "/dev/urandom" s;
|
||||
}
|
||||
{
|
||||
# What makes a re-publish idempotent. This principal is granted
|
||||
# `create`/`update` and no `read`, so it cannot ask the store whether a
|
||||
# hive already has a token — with nowhere to keep one, every run would
|
||||
# mint a fresh value and rotate the swarm's token. A state directory is
|
||||
# that somewhere, and nothing else in this unit needs one, so its absence
|
||||
# means exactly this.
|
||||
#
|
||||
# The second arm is the mint's own guard: the state file is only written
|
||||
# when it is missing or empty. Dropping that test leaves a unit that
|
||||
# still has a state directory and still rotates on every boot.
|
||||
name = "the publisher keeps the tokens it minted, and mints only when it holds none";
|
||||
ok =
|
||||
let
|
||||
u = secretPublisherHere.systemd.services.swarm-secret-publish;
|
||||
in
|
||||
lib.hasInfix "matrix-appservice-token" (u.serviceConfig.StateDirectory or "")
|
||||
&& u.serviceConfig.StateDirectoryMode or null == "0700"
|
||||
&& lib.hasInfix "if [ ! -s \"$src\" ]" u.script;
|
||||
}
|
||||
{
|
||||
# A property of the SET, not of one unit: both of these authenticate by
|
||||
# certificate, and `BAO_CLIENT_CERT` is transport rather than identity, so
|
||||
|
|
@ -306,7 +271,7 @@ let
|
|||
&& !(lib.hasInfix "bao kv" (lib.head (lib.splitString "bao login" c)));
|
||||
in
|
||||
holdsTokenFirst secretPublisherHere.systemd.services.swarm-secret-publish.script
|
||||
&& holdsTokenFirst baoWithMatrix.systemd.services.swarm-bao-matrix-token.script
|
||||
&& holdsTokenFirst baoWithMatrix.systemd.services.swarm-bao-queue-agent.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