The store declared no journald units and served no metrics: nothing in `swarm-bao.nix` mentioned either, while every sibling swarm service declares both. Metrics get their own loopback listener rather than a flag on the API one, and that follows from what a scraper can express rather than from taste: `swarm.otel.scrapeTargets` is `host:port`, plaintext and with no credential, while the API listener is TLS and demands a client certificate once a client CA is set. `metrics_only` narrows the new listener to the metrics path; `prometheus_retention_time` is what serves the endpoint at all. Measured against openbao 2.6.2 before writing any of it: the metrics path answers 200 on such a listener *while the node is sealed and uninitialised*, 503 on the API listener, and 404 for a non-metrics path on the metrics listener. The listener exists only where a collector does — it is unauthenticated by design for now, and an endpoint with no reader would be exposure bought for nothing. The port cannot be the API port + 1: openbao derives every listener's cluster address as its own port plus one, so that number is already taken. An assertion says so, since the failure is otherwise a race with no log line. Journald units are declared by the module that defines each unit, not gathered here, matching the option's own rule — a name nothing defines is silently ignored, so a central list would read as coverage on hives that have neither glue module. Refs #3849
123 lines
5.7 KiB
Nix
123 lines
5.7 KiB
Nix
# Glue: the matrix registration token comes from the secret store.
|
|
#
|
|
# The store's first reader, and deliberately a small one. It fetches an opaque
|
|
# 32-byte value and writes it where ./hive-matrix.nix already looks — the
|
|
# homeserver never learns the store exists, and its config is unchanged.
|
|
#
|
|
# ⚠️ Why this credential first. It has no second file and no format: authelia's
|
|
# OIDC secret needs a `.secret` *and* a matching `.digest`, so shipping that
|
|
# one first would debug "can a reader authenticate and get bytes back" and
|
|
# "did we write authelia's file format right" at the same time, with an SSO
|
|
# outage as the failure mode. Here the failure is narrow — new agent accounts
|
|
# cannot be provisioned, existing ones are untouched, nothing crash-loops.
|
|
#
|
|
# ⚠️ The fallback is today's behaviour, not a new one. `hive-matrix.nix`'s
|
|
# activation script still mints a token when the file is absent; this unit
|
|
# overwrites it with the swarm's copy when the store has one. A store that is
|
|
# empty or unreachable leaves a working hive with a local token.
|
|
#
|
|
# 📌 This runs wherever a client identity is configured, NOT only where the
|
|
# store is. `deploy.bao.enable` would have been the co-location assumption
|
|
# itself; the reader needs a certificate, not a neighbour. On the store's own
|
|
# host ./glue-bao-tls.nix supplies one as a `mkDefault` and nothing changes;
|
|
# elsewhere an operator places the leaf and names it, and the same unit works.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
hyperhiveCfg = config.services.hyperhive;
|
|
deployCfg = hyperhiveCfg.deploy;
|
|
baoCfg = hyperhiveCfg.swarm.bao;
|
|
matrixCfg = hyperhiveCfg.swarm.matrix;
|
|
|
|
baoDeploy = deployCfg.bao;
|
|
|
|
# What decides whether this unit exists at all. A reader is defined by holding
|
|
# a certificate the store accepts, and that is true on the store's own host
|
|
# and on a hive three networks away for exactly the same reason.
|
|
haveClientIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null;
|
|
|
|
# Where the token lives in the store. A path, not a convention to guess at:
|
|
# whoever writes it and whoever reads it must agree, and the agreement
|
|
# belongs in one visible place.
|
|
tokenPath = "secret/swarm/matrix/registration-token";
|
|
|
|
# A literal, not an option — ./hive-matrix.nix names its container
|
|
# `containers.hive-matrix` directly and declares no `machine` to derive it
|
|
# from, which the trust-bundle call in that file already says out loud.
|
|
# ⚠️ `matrixCfg.machine` parses fine and fails at module-system resolution,
|
|
# so this is the kind of mistake only reading the target module catches.
|
|
matrixMachine = "hive-matrix";
|
|
in
|
|
{
|
|
config = lib.mkIf (hyperhiveCfg.enable && haveClientIdentity && deployCfg.matrix.enable) {
|
|
# Same rule as the unit's own gate: this reader exists on a host that has a
|
|
# client identity and a homeserver, which is not every host that runs the
|
|
# store, so the store's module cannot name it.
|
|
services.hyperhive.swarm.otel.journaldUnits = [ "swarm-bao-matrix-token" ];
|
|
|
|
systemd.services.swarm-bao-matrix-token = {
|
|
description = "fetch the matrix registration token from the swarm secret store";
|
|
# Every one of these names a unit that exists only where the store runs.
|
|
# `Requires=` on an absent unit fails the job outright, so the ordering is
|
|
# conditional even though the read is not: off-host there is nothing local
|
|
# to wait for, and the timeout below is what bounds the attempt instead.
|
|
after = lib.optionals baoDeploy.enable [
|
|
"swarm-bao-pki.service"
|
|
"container@${baoCfg.machine}.service"
|
|
];
|
|
wants = lib.optionals baoDeploy.enable [ "container@${baoCfg.machine}.service" ];
|
|
requires = lib.optionals baoDeploy.enable [ "swarm-bao-pki.service" ];
|
|
before = [ "container@${matrixMachine}.service" ];
|
|
wantedBy = [ "container@${matrixMachine}.service" ];
|
|
path = [
|
|
deployCfg.bao.package
|
|
pkgs.coreutils
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
# What actually bounds the read below. Stated here rather than
|
|
# left to systemd's default, so the number a boot waits on is in
|
|
# the file that waits.
|
|
TimeoutStartSec = 30;
|
|
};
|
|
environment = {
|
|
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
|
BAO_CLIENT_CERT = baoDeploy.clientCertFile;
|
|
BAO_CLIENT_KEY = baoDeploy.clientKeyFile;
|
|
}
|
|
# Absent means the system trust store, which is what a deployment with a
|
|
# real CA wants and what a self-signed one must not be left with.
|
|
// lib.optionalAttrs (baoDeploy.serverCaFile != null) {
|
|
BAO_CACERT = baoDeploy.serverCaFile;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
# A sealed or uninitialised store answers on the port and never
|
|
# answers the read, so "the store is up" is not the same as "the
|
|
# store can answer". `TimeoutStartSec` above is the bound; the
|
|
# homeserver only `Wants=` this unit, so hitting it degrades to
|
|
# keeping the local token rather than holding up the container.
|
|
if ! token="$(bao kv get -field=value ${lib.escapeShellArg tokenPath} 2>/dev/null)"; then
|
|
echo "swarm-bao holds no ${tokenPath}, or is sealed/unreachable." >&2
|
|
echo "Keeping the token hive-matrix already has." >&2
|
|
exit 0
|
|
fi
|
|
|
|
if [ -z "$token" ]; then
|
|
echo "swarm-bao returned an empty ${tokenPath}; keeping the local token." >&2
|
|
exit 0
|
|
fi
|
|
|
|
umask 077
|
|
printf '%s\n' "$token" > ${lib.escapeShellArg (toString matrixCfg.registrationTokenFile)}
|
|
chmod 0600 ${lib.escapeShellArg (toString matrixCfg.registrationTokenFile)}
|
|
'';
|
|
};
|
|
};
|
|
}
|