Last of the three modules that hand-rolled the same concat with wantedBy + before and no requires, so a failed assembly left the consumer running against a missing file and trusting nothing -- every outbound TLS call fails while the unit looks healthy. The forge is the one with two consumers: forgejo-sso-source fetches the issuer's discovery document over the swarm CA and once shipped without the trust its sibling had. It only exists when SSO is on, so the consumer list is conditional -- naming an absent unit would define a serviceless one and order nothing. Removes the now-dead useSelfSigned and caContainerPath bindings (nix does not warn) and retargets three comments the deletion orphaned, including the helper header that still named this module as the per-call-site concat.
137 lines
5.7 KiB
Nix
137 lines
5.7 KiB
Nix
# Shared hive-CA trust plumbing for containers that must trust the
|
|
# self-signed gateway/forge leaf for *outbound* TLS (webhook delivery,
|
|
# CI artifact upload, …). The hive CA is generated at runtime by the host
|
|
# `hive-tls-ca.service` (see `hive-tls.nix`) — it can't be baked into a
|
|
# derivation — so each such container binds the public `ca.pem` read-only
|
|
# and orders its `container@<name>` unit after `hive-tls-ca.service` so the
|
|
# bind source exists before nspawn sets the mount up.
|
|
#
|
|
# `bindMount` + `containerOrdering` are the language-agnostic half. The
|
|
# *consumption* differs per runtime: an additive variable (Node's
|
|
# `NODE_EXTRA_CA_CERTS`, hive-ci) points straight at `caContainerPath` from
|
|
# the call site, while a *replacing* one (Go's `SSL_CERT_FILE`, rustls) needs
|
|
# the system-CAs+hive-CA concat that `trustBundle` below does for it.
|
|
#
|
|
# Pure function — NOT a NixOS module (don't add it to the host-modules
|
|
# aggregator). Call it from a module's `let`:
|
|
#
|
|
# caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
|
# # then, in the container:
|
|
# # bindMounts = { … } // caTrust.bindMount;
|
|
# # systemd.services."container@hive-ci" = lib.mkMerge [ caTrust.containerOrdering … ];
|
|
# # environment.NODE_EXTRA_CA_CERTS = caTrust.caContainerPath; # consumption, per-caller
|
|
#
|
|
# `tlsCfg` = config.services.hyperhive.tls
|
|
# `gatewayCfg` = config.services.hyperhive.gateway
|
|
{
|
|
lib,
|
|
tlsCfg,
|
|
gatewayCfg,
|
|
}:
|
|
let
|
|
# `gateway.useSelfSigned` is the single source of truth for the
|
|
# self-signed condition — no duplicated derivation.
|
|
useSelfSigned = gatewayCfg.useSelfSigned;
|
|
# The bundle, not `ca.pem`: the hive CA is an intermediate under the
|
|
# swarm root, and openssl (which is what both consumers below sit on —
|
|
# Node's `NODE_EXTRA_CA_CERTS`, Go's `SSL_CERT_FILE`) will not end a
|
|
# chain at a trusted cert that isn't self-signed. `hive-tls.nix` writes
|
|
# the bundle next to the CA and explains the split.
|
|
caHostPath = "${tlsCfg.stateDir}/trust-bundle.pem";
|
|
caContainerPath = "/run/hive-ca/trust-bundle.pem";
|
|
in
|
|
{
|
|
inherit useSelfSigned caContainerPath;
|
|
|
|
# Fold into the container's `bindMounts` via `//`. Binds ONLY the public
|
|
# CA cert (never the `hive-tls` state dir — it holds the CA + leaf private
|
|
# keys), read-only. Empty when not self-signed, so the whole trust path
|
|
# drops out cleanly.
|
|
bindMount = lib.optionalAttrs useSelfSigned {
|
|
${caContainerPath} = {
|
|
hostPath = caHostPath;
|
|
isReadOnly = true;
|
|
};
|
|
};
|
|
|
|
# Fold into the caller's `container@<name>` unit (via `lib.mkMerge` if the
|
|
# caller adds its own keys, e.g. hive-ci's `TimeoutStartSec`). Orders the
|
|
# container after the host `hive-tls-ca.service` so the bind source exists
|
|
# before nspawn sets the mount up — a condition-skipped/late CA would
|
|
# otherwise fail the container start.
|
|
containerOrdering = lib.mkIf useSelfSigned {
|
|
after = [ "hive-tls-ca.service" ];
|
|
requires = [ "hive-tls-ca.service" ];
|
|
};
|
|
|
|
# System CAs + hive CA in one bundle, with `SSL_CERT_FILE` set on each
|
|
# consumer — for runtimes whose trust variable *replaces* the store (Go,
|
|
# rustls-native-certs). An additive one (Node's `NODE_EXTRA_CA_CERTS`,
|
|
# hive-ci) needs no bundle and should not use this.
|
|
#
|
|
# imports = [ (caTrust.trustBundle { inherit pkgs; name = "swarm-nats";
|
|
# consumers = [ "swarm-nats-auth" ]; }) ];
|
|
#
|
|
# Three constraints, each earned:
|
|
# - `requires` on the CONSUMER: `before` orders but does not gate, so a
|
|
# failed assembly otherwise leaves it running and trusting *nothing*.
|
|
# - assemble to a temp path, verify, then move: `cat` of an empty bind
|
|
# exits 0, and a partial bundle must never appear under the final name.
|
|
# - `consumers` are BARE unit names — they are `systemd.services` keys
|
|
# (no suffix) *and* go in `before`/`requires` (suffixed). Reversed, the
|
|
# edge names a unit that does not exist and systemd orders nothing.
|
|
#
|
|
# Returns a module, not bare services: a caller already writing
|
|
# `systemd.services.<consumer>` cannot also write `systemd.services`.
|
|
trustBundle =
|
|
{
|
|
name,
|
|
consumers,
|
|
pkgs,
|
|
}:
|
|
let
|
|
dir = "/run/${name}-ca";
|
|
bundlePath = "${dir}/trust-bundle.pem";
|
|
unit = "${name}-ca-bundle";
|
|
in
|
|
{
|
|
_file = "hive-ca-trust.nix#trustBundle:${name}";
|
|
config.systemd.services = lib.optionalAttrs useSelfSigned (
|
|
{
|
|
${unit} = {
|
|
description = "assemble ${name} TLS trust bundle (system CAs + hive CA)";
|
|
wantedBy = [ "multi-user.target" ];
|
|
before = map (c: "${c}.service") consumers;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
SyslogIdentifier = unit;
|
|
};
|
|
path = [
|
|
pkgs.coreutils
|
|
pkgs.gnugrep
|
|
];
|
|
script = ''
|
|
set -euo pipefail
|
|
install -d -m 0755 ${dir}
|
|
tmp=${bundlePath}.tmp
|
|
cat /etc/ssl/certs/ca-certificates.crt ${caContainerPath} > "$tmp"
|
|
# `cat` of an empty or missing-but-mounted source exits 0, so the
|
|
# result has to be inspected rather than the command trusted.
|
|
if ! grep -q 'BEGIN CERTIFICATE' "$tmp"; then
|
|
echo "${unit}: assembled bundle contains no certificate" >&2
|
|
exit 1
|
|
fi
|
|
chmod 0644 "$tmp"
|
|
mv "$tmp" ${bundlePath}
|
|
'';
|
|
};
|
|
}
|
|
// lib.genAttrs consumers (_: {
|
|
requires = [ "${unit}.service" ];
|
|
after = [ "${unit}.service" ];
|
|
environment.SSL_CERT_FILE = bundlePath;
|
|
})
|
|
);
|
|
};
|
|
}
|