refactor: nix/host-modules + nix/agent-modules layout, update doc paths
This commit is contained in:
parent
cb755b677c
commit
4a48ce5024
52 changed files with 48 additions and 44 deletions
|
|
@ -1,274 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.tls;
|
||||
hyperhiveCfg = config.services.hyperhive;
|
||||
gatewayCfg = config.services.hyperhive.gateway;
|
||||
domain = hyperhiveCfg.domain;
|
||||
|
||||
# The host-managed hive CA is the trust anchor for self-signed mode.
|
||||
# It is only stood up when the gateway actually serves a self-signed
|
||||
# cert: the gateway must be in self-signed mode. `domain` is required
|
||||
# (asserted in hive-network.nix), so the leaf SANs always have a
|
||||
# domain to derive from. The self-signed condition is the gateway
|
||||
# module's single source of truth (`gateway.useSelfSigned`): true when
|
||||
# neither an operator cert (`tls.certDir`) nor ACME is set.
|
||||
active = hyperhiveCfg.enable && gatewayCfg.useSelfSigned;
|
||||
in
|
||||
{
|
||||
# Host-side TLS trust root for the self-signed gateway mode.
|
||||
#
|
||||
# A bare self-signed leaf would be its own trust anchor, so every
|
||||
# regeneration would be a new anchor and every consumer (agents,
|
||||
# federation peers) would have to re-trust on each rotation — and a
|
||||
# runtime-generated, in-container cert can't be wired into an agent's
|
||||
# build-time trust store at all.
|
||||
#
|
||||
# So the anchor is a long-lived **hive CA** held on the host. The
|
||||
# gateway serves a **leaf** signed by that CA (via the `tls.certDir`
|
||||
# bind-mount path); agents and federation peers trust the *CA* once,
|
||||
# and leaf rotation never re-breaks them. See `docs/gateway.md`
|
||||
# ("Self-signed TLS").
|
||||
|
||||
options.services.hyperhive.tls = {
|
||||
stateDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/var/lib/hive-tls";
|
||||
description = ''
|
||||
Host directory holding the hive CA + gateway leaf cert for the
|
||||
self-signed gateway mode. `ca.pem` (the anchor agents and
|
||||
federation peers trust), `ca-key.pem` (0600, never leaves the
|
||||
host), `gateway.pem` / `gateway-key.pem` (the leaf the gateway
|
||||
container bind-mounts and nginx serves). Persistent so the CA
|
||||
survives reboots — re-deriving it would re-break every consumer.
|
||||
'';
|
||||
};
|
||||
|
||||
caValidityDays = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 7300;
|
||||
description = ''
|
||||
Validity window of the hive CA in days (default ~20y). Kept long
|
||||
and well beyond `leafValidityDays` so the CA outlives many leaf
|
||||
rotations — the whole point of the CA is to be a stable anchor
|
||||
that consumers trust once. The CA is regenerated only if missing
|
||||
or already expired.
|
||||
'';
|
||||
};
|
||||
|
||||
leafValidityDays = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 30;
|
||||
description = ''
|
||||
Validity window of the gateway leaf cert in days (default 30).
|
||||
Short-lived by design — ahead of the CA/Browser-Forum's move
|
||||
toward ~47-day max lifetimes — which bounds the blast radius of a
|
||||
leaf-key compromise. The leaf is re-signed by the (stable) CA
|
||||
when it is missing or near expiry; because it shares the CA
|
||||
anchor, a rotation does not disturb consumer trust. Agents and
|
||||
federation peers validate against the CA, not browser CA/B-forum
|
||||
limits. The weekly `hive-tls-resign` timer re-signs the leaf once
|
||||
it is within half its validity of expiry and propagates the new
|
||||
leaf into the running gateway, so a long-uptime host renews
|
||||
automatically without a reboot.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf active {
|
||||
# Generate (and rotate) the hive CA + gateway leaf before the gateway
|
||||
# container starts. Idempotent: the CA is created once and reused; the
|
||||
# leaf is re-signed on expiry under the same CA so the anchor is stable.
|
||||
systemd.services.hive-tls-ca = {
|
||||
description = "Generate hive CA + gateway leaf TLS cert (self-signed mode)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
# Gateway nginx reads the leaf from the bind-mount, so the cert must
|
||||
# exist before the container starts. Declarative nixos-containers are
|
||||
# instances of the `container@.service` template.
|
||||
before = [ "container@hive-gateway.service" ];
|
||||
requiredBy = [ "container@hive-gateway.service" ];
|
||||
path = [ pkgs.openssl ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
UMask = "0077";
|
||||
# Pin the journal identity (else it's the `script` store-path wrapper).
|
||||
SyslogIdentifier = "hive-tls-ca";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
d=${lib.escapeShellArg cfg.stateDir}
|
||||
install -d -m 0755 "$d"
|
||||
|
||||
ca="$d/ca.pem"
|
||||
cak="$d/ca-key.pem"
|
||||
leaf="$d/gateway.pem"
|
||||
leafk="$d/gateway-key.pem"
|
||||
|
||||
# --- CA: generate once, reuse across leaf rotations. Regenerate
|
||||
# only if missing or already expired (checkend 0). A new CA means
|
||||
# every consumer must re-trust, so the leaf is dropped to force a
|
||||
# re-sign under the fresh CA.
|
||||
if [ ! -s "$ca" ] || [ ! -s "$cak" ] \
|
||||
|| ! openssl x509 -in "$ca" -noout -checkend 0 >/dev/null 2>&1; then
|
||||
echo "generating fresh hive CA at $ca"
|
||||
openssl req -x509 -newkey rsa:4096 -nodes -sha256 \
|
||||
-days ${toString cfg.caValidityDays} \
|
||||
-keyout "$cak" -out "$ca" \
|
||||
-subj "/CN=hive-ca ${domain}" \
|
||||
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
|
||||
-addext "keyUsage=critical,keyCertSign,cRLSign"
|
||||
chmod 0600 "$cak"
|
||||
chmod 0644 "$ca"
|
||||
rm -f "$leaf" "$leafk"
|
||||
fi
|
||||
|
||||
# --- Leaf: (re)sign when missing or within 30 days of expiry,
|
||||
# always under the current (stable) CA.
|
||||
if [ ! -s "$leaf" ] || [ ! -s "$leafk" ] \
|
||||
|| ! openssl x509 -in "$leaf" -noout -checkend 2592000 >/dev/null 2>&1; then
|
||||
echo "signing fresh gateway leaf at $leaf"
|
||||
csr="$(mktemp "$d/gateway.csr.XXXXXX")"
|
||||
ext="$(mktemp "$d/leaf.ext.XXXXXX")"
|
||||
trap 'rm -f "$csr" "$ext"' EXIT
|
||||
|
||||
openssl req -newkey rsa:4096 -nodes -sha256 \
|
||||
-keyout "$leafk" -out "$csr" \
|
||||
-subj "/CN=${domain}"
|
||||
|
||||
# printf (not a heredoc) so the ext-file lines carry no leading
|
||||
# whitespace once nix has stripped the indented-string indent.
|
||||
{
|
||||
printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \
|
||||
${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \
|
||||
${lib.escapeShellArg domain} ${lib.escapeShellArg domain}
|
||||
printf 'basicConstraints=critical,CA:FALSE\n'
|
||||
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
|
||||
printf 'extendedKeyUsage=serverAuth\n'
|
||||
} > "$ext"
|
||||
|
||||
openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \
|
||||
-CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \
|
||||
-extfile "$ext" -out "$leaf"
|
||||
chmod 0600 "$leafk"
|
||||
chmod 0644 "$leaf"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# Weekly re-sign of the gateway leaf so short-lived leaves renew
|
||||
# without depending on a reboot.
|
||||
#
|
||||
# `hive-tls-ca` only re-signs at service activation (boot/rebuild); a
|
||||
# long-uptime host would otherwise let a 30-day leaf lapse silently.
|
||||
# This service re-signs the leaf directly (not by bouncing hive-tls-ca)
|
||||
# and propagates the new leaf into the running gateway container when
|
||||
# the file actually changed.
|
||||
#
|
||||
# Propagation mechanism: nginx in the gateway container serves a *copy*
|
||||
# of the leaf written by `hive-gateway-self-signed-cert` (which runs at
|
||||
# container start). A host-side `systemctl -M hive-gateway` call
|
||||
# triggers the re-import + reload, mirroring how hive-c0re reloads the
|
||||
# gateway after each agents.conf write. A path unit *inside* the
|
||||
# container was tried first but does not work: IN_MOVED_TO from an
|
||||
# atomic rename on the host does not propagate across the nspawn
|
||||
# mount-namespace boundary.
|
||||
#
|
||||
# `|| true` on propagation so a stopped gateway never fails the unit —
|
||||
# its next boot will import the already-rotated leaf anyway.
|
||||
systemd.services.hive-tls-resign = {
|
||||
description = "Re-sign the gateway TLS leaf and propagate it into the gateway container";
|
||||
# hive-tls-ca must have run first so the CA key exists before we try
|
||||
# to re-sign under it. On first boot `Persistent=true` on the weekly
|
||||
# timer fires immediately; without this ordering the resign could race
|
||||
# the CA initialisation and fail with "no such file" on the CA key.
|
||||
after = [ "hive-tls-ca.service" ];
|
||||
path = [
|
||||
pkgs.openssl
|
||||
pkgs.coreutils
|
||||
pkgs.systemd
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
UMask = "0077";
|
||||
SyslogIdentifier = "hive-tls-resign";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
d=${lib.escapeShellArg cfg.stateDir}
|
||||
ca="$d/ca.pem"
|
||||
cak="$d/ca-key.pem"
|
||||
leaf="$d/gateway.pem"
|
||||
leafk="$d/gateway-key.pem"
|
||||
|
||||
# Re-sign only when the leaf is within half its validity of expiry.
|
||||
# The weekly cadence catches this window well before the leaf lapses.
|
||||
halflife=$(( ${toString cfg.leafValidityDays} * 86400 / 2 ))
|
||||
if [ -s "$leaf" ] && \
|
||||
openssl x509 -in "$leaf" -noout -checkend "$halflife" >/dev/null 2>&1; then
|
||||
echo "gateway leaf valid for more than half its lifetime — no resign needed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "gateway leaf missing or near expiry — re-signing under current CA"
|
||||
before="$(sha256sum "$leaf" 2>/dev/null || true)"
|
||||
|
||||
csr="$(mktemp "$d/gateway.csr.XXXXXX")"
|
||||
ext="$(mktemp "$d/leaf.ext.XXXXXX")"
|
||||
trap 'rm -f "$csr" "$ext"' EXIT
|
||||
|
||||
openssl req -newkey rsa:4096 -nodes -sha256 \
|
||||
-keyout "$leafk" -out "$csr" \
|
||||
-subj "/CN=${domain}"
|
||||
|
||||
{
|
||||
printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \
|
||||
${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \
|
||||
${lib.escapeShellArg domain} ${lib.escapeShellArg domain}
|
||||
printf 'basicConstraints=critical,CA:FALSE\n'
|
||||
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
|
||||
printf 'extendedKeyUsage=serverAuth\n'
|
||||
} > "$ext"
|
||||
|
||||
openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \
|
||||
-CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \
|
||||
-extfile "$ext" -out "$leaf"
|
||||
chmod 0600 "$leafk"
|
||||
chmod 0644 "$leaf"
|
||||
|
||||
after="$(sha256sum "$leaf" 2>/dev/null || true)"
|
||||
if [ "$before" != "$after" ]; then
|
||||
echo "gateway leaf rotated — propagating into hive-gateway"
|
||||
systemctl -M hive-gateway restart hive-gateway-self-signed-cert.service || true
|
||||
systemctl -M hive-gateway reload nginx.service || true
|
||||
else
|
||||
echo "gateway leaf unchanged (already up to date)"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.hive-tls-resign = {
|
||||
description = "Weekly gateway-leaf re-sign and propagation";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
# Run weekly; Persistent=true fires a missed run on next boot if
|
||||
# the timer was not active (e.g. the host was off on the scheduled
|
||||
# day), preventing a dormant timer from letting the leaf lapse.
|
||||
OnCalendar = "weekly";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Signal the hive-c0re lifecycle that a hive CA exists: it bind-mounts
|
||||
# this file (read-only, the CA cert ONLY — never the key) into each
|
||||
# agent container so agents + their tools can trust the gateway's
|
||||
# self-signed leaf, and the meta flake wires the per-agent trust
|
||||
# bundle. Only the `ca.pem` path is exposed; `ca-key.pem` stays on the
|
||||
# host (an agent that could read it could mint trusted certs).
|
||||
systemd.services.hive-c0re.environment.HIVE_TLS_CA_PATH = "${cfg.stateDir}/ca.pem";
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue