Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7a5e93bbc | ||
|
|
cb650f2dbb | ||
|
|
850cc2c1d3 |
3 changed files with 116 additions and 0 deletions
|
|
@ -62,4 +62,75 @@ in
|
||||||
after = [ "hive-tls-ca.service" ];
|
after = [ "hive-tls-ca.service" ];
|
||||||
requires = [ "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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,18 @@ let
|
||||||
unitName = "authelia-${instance}";
|
unitName = "authelia-${instance}";
|
||||||
stateDir = "/var/lib/${unitName}";
|
stateDir = "/var/lib/${unitName}";
|
||||||
|
|
||||||
|
tlsCfg = hyperhiveCfg.tls;
|
||||||
|
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
||||||
|
# `swarm-authelia-bridge` verifies the gateway when it introspects by name.
|
||||||
|
# Nothing in this container trusted the swarm CA, which is a runtime file no
|
||||||
|
# build-time option can name — so an https call out of here could only ever
|
||||||
|
# fail `UnknownIssuer`. Same defect the queue's responder hit.
|
||||||
|
caBundleModule = caTrust.trustBundle {
|
||||||
|
inherit pkgs;
|
||||||
|
name = cfg.machine;
|
||||||
|
consumers = [ "swarm-authelia-bridge" ];
|
||||||
|
};
|
||||||
|
|
||||||
# The SWARM's domain, because that is where the protected apps now live
|
# The SWARM's domain, because that is where the protected apps now live
|
||||||
# (`forge.<swarm>`, `chat.<swarm>`, `auth.<swarm>`). It moves in the
|
# (`forge.<swarm>`, `chat.<swarm>`, `auth.<swarm>`). It moves in the
|
||||||
# same commit as `domain` below and cannot lag it: authelia validates
|
# same commit as `domain` below and cannot lag it: authelia validates
|
||||||
|
|
@ -751,12 +763,19 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Order the container after the host CA generator, so the bind source
|
||||||
|
# exists before nspawn sets the mount up.
|
||||||
|
systemd.services."container@${cfg.machine}" = caTrust.containerOrdering;
|
||||||
|
|
||||||
containers.${cfg.machine} = {
|
containers.${cfg.machine} = {
|
||||||
autoStart = true;
|
autoStart = true;
|
||||||
ephemeral = false;
|
ephemeral = false;
|
||||||
# Shared host netns, like the forge and matrix containers: the
|
# Shared host netns, like the forge and matrix containers: the
|
||||||
# gateway reaches authelia at 127.0.0.1:<port>.
|
# gateway reaches authelia at 127.0.0.1:<port>.
|
||||||
privateNetwork = false;
|
privateNetwork = false;
|
||||||
|
# Public trust bundle only, read-only. Empty when the gateway is not
|
||||||
|
# self-signed, so the whole trust path drops out cleanly.
|
||||||
|
bindMounts = caTrust.bindMount;
|
||||||
|
|
||||||
config =
|
config =
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
@ -766,6 +785,7 @@ in
|
||||||
inherit (networkCfg) bridgeIp;
|
inherit (networkCfg) bridgeIp;
|
||||||
dnsConsumers = [ "authelia-${instance}.service" ];
|
dnsConsumers = [ "authelia-${instance}.service" ];
|
||||||
})
|
})
|
||||||
|
caBundleModule
|
||||||
];
|
];
|
||||||
|
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = "26.05";
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,22 @@ let
|
||||||
clientAccount = "APP";
|
clientAccount = "APP";
|
||||||
|
|
||||||
machine = "swarm-nats";
|
machine = "swarm-nats";
|
||||||
|
|
||||||
|
tlsCfg = config.services.hyperhive.tls;
|
||||||
|
gatewayCfg = config.services.hyperhive.gateway;
|
||||||
|
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
||||||
|
# The responder introspects authelia over https BY NAME. Its HTTP client is
|
||||||
|
# reqwest/rustls, and `rustls-platform-verifier` resolves roots through
|
||||||
|
# `rustls-native-certs`, which reads `SSL_CERT_FILE` — so the same assembled
|
||||||
|
# bundle the Go containers use applies here. Without it the handshake fails
|
||||||
|
# `UnknownIssuer`, introspection fails, and the responder denies *every*
|
||||||
|
# client: one missing trust anchor surfacing as `authorization violation` at
|
||||||
|
# every would-be queue user.
|
||||||
|
caBundleModule = caTrust.trustBundle {
|
||||||
|
inherit pkgs;
|
||||||
|
name = machine;
|
||||||
|
consumers = [ "swarm-nats-auth" ];
|
||||||
|
};
|
||||||
# Where the responder's credentials live *inside* the container, and the
|
# Where the responder's credentials live *inside* the container, and the
|
||||||
# host path that resolves to. Two names for one location, because the
|
# host path that resolves to. Two names for one location, because the
|
||||||
# host is the only place both filesystems are addressable.
|
# host is the only place both filesystems are addressable.
|
||||||
|
|
@ -438,6 +454,9 @@ in
|
||||||
# unauthenticated interim state would be a hole rather than a
|
# unauthenticated interim state would be a hole rather than a
|
||||||
# rough edge.
|
# rough edge.
|
||||||
privateNetwork = false;
|
privateNetwork = false;
|
||||||
|
# Binds only the public trust bundle, read-only. Empty when the gateway
|
||||||
|
# is not self-signed, so the whole trust path drops out cleanly.
|
||||||
|
bindMounts = caTrust.bindMount;
|
||||||
config =
|
config =
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
|
|
@ -450,6 +469,7 @@ in
|
||||||
# file exists.
|
# file exists.
|
||||||
dnsConsumers = [ "swarm-nats-auth.service" ];
|
dnsConsumers = [ "swarm-nats-auth.service" ];
|
||||||
})
|
})
|
||||||
|
caBundleModule
|
||||||
];
|
];
|
||||||
|
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = "26.05";
|
||||||
|
|
@ -575,6 +595,11 @@ in
|
||||||
# refuses to start when a bind source is missing, so one absent seed
|
# refuses to start when a bind source is missing, so one absent seed
|
||||||
# would take down the **whole container including the queue**, not
|
# would take down the **whole container including the queue**, not
|
||||||
# merely the responder. A far larger blast radius than the fault.
|
# merely the responder. A far larger blast radius than the fault.
|
||||||
|
# Order the container after the host CA generator, so the bind source
|
||||||
|
# exists before nspawn sets the mount up. Without it a late CA fails the
|
||||||
|
# container start outright rather than degrading.
|
||||||
|
systemd.services."container@${machine}" = caTrust.containerOrdering;
|
||||||
|
|
||||||
systemd.services.swarm-nats-auth-secrets = lib.mkIf responderConfigured {
|
systemd.services.swarm-nats-auth-secrets = lib.mkIf responderConfigured {
|
||||||
description = "deliver the swarm queue responder's credentials";
|
description = "deliver the swarm queue responder's credentials";
|
||||||
before = [ "container@swarm-nats.service" ];
|
before = [ "container@swarm-nats.service" ];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue