feat(nix): the matrix container gets the swarm-internal trust anchor
`security.pki.certificateFiles` is read when the system is BUILT and the
swarm root is deliberately a runtime file (its key must never enter the
world-readable store), so there was nothing build-time to name and the
container trusted no swarm-internal CA. Federation with a peer whose
cert chains to the swarm root did not validate.
The bridge already existed: `lib/hive-ca-trust.nix`, which `hive-ci` and
`hive-forge` both cross. This wires the matrix container to it -- read-only
bind of the trust bundle, container ordered after `hive-tls-ca.service` --
rather than inventing a second mechanism.
Consumption is the per-runtime half, and it is not what it looks like.
tuwunel links no openssl, which makes `SSL_CERT_FILE` appear inapplicable.
It isn't: the outbound client is `reqwest` with the `rustls` feature,
which builds a `rustls_platform_verifier::Verifier`, and because tuwunel
calls `tls_certs_merge` (additive) rather than `tls_certs_only`, the
platform roots stay alongside its compiled-in webpki set. On Linux that
verifier resolves through `rustls-native-certs` to `openssl-probe`, which
reads `SSL_CERT_FILE`. Established by reading tuwunel 1.8.0 and reqwest
0.13.4 source, not inferred from the feature list -- an earlier reading of
mine concluded the opposite from the absence of a `native-roots` feature
name and was wrong.
The variable therefore names a CONCATENATION (system CAs + the bundle),
never the anchor alone: `openssl-probe` uses it *instead of* the default
store, so pointing it at the bundle would drop every public CA and break
federation with the wider matrix network -- trading a small outage for a
much larger one.
`container@hive-matrix` needed `mkMerge`: it already assigned `after` for
the gateway ordering and the helper contributes its own `after`/`requires`,
so two bare assignments would have conflicted rather than combined.
⚠️ Gate honesty: `state/eval-3093.sh` proves the mount, the ordering, the
bundle service and the env var are RENDERED -- 6 probes, including two
separate absence probes (mount and consumption are wired at different
sites, and the gateway outage happened precisely because two of three
sites tolerated a missing cert and the third did not). It CANNOT prove
tuwunel loads them: a build that ignores the variable evaluates
identically to one that honours it, and the chain above is
version-specific. That half wants an executed check on a real deploy.
This commit is contained in:
parent
2024848251
commit
038d085001
2 changed files with 89 additions and 25 deletions
|
|
@ -148,11 +148,26 @@ Two consumers, and only one of them is fine:
|
|||
`HIVE_TLS_CA_PATH`, and the meta-flake renderer embeds that one file
|
||||
next to every agent's flake. The bundle is the runtime-to-build-time
|
||||
bridge.
|
||||
- **The Matrix container is not.** It has no equivalent bridge, so it
|
||||
trusts no swarm-internal CA and federation with a self-signed peer
|
||||
does not validate. Giving it the root needs a runtime mechanism —
|
||||
bind-mount plus a bundle assembled at unit start, appending to the
|
||||
system bundle rather than replacing it — which is tracked separately.
|
||||
- **The Matrix container crosses the same bridge**, via the shared
|
||||
`lib/hive-ca-trust.nix` helper that `hive-ci` and `hive-forge` already
|
||||
use: the bundle is bind-mounted read-only into the container, and a
|
||||
oneshot concatenates it with the system CAs before tuwunel starts.
|
||||
|
||||
The consumption differs per runtime and is the part worth knowing.
|
||||
tuwunel links no openssl, which makes `SSL_CERT_FILE` look inapplicable
|
||||
— it isn't. Its outbound client is `reqwest` with the `rustls` feature,
|
||||
which builds a `rustls_platform_verifier::Verifier`; because tuwunel
|
||||
calls `tls_certs_merge` (additive) rather than `tls_certs_only`, the
|
||||
platform roots stay alongside its compiled-in webpki set. On Linux that
|
||||
verifier resolves through `rustls-native-certs` → `openssl-probe`,
|
||||
which reads `SSL_CERT_FILE`.
|
||||
|
||||
> ⚠️ **Concatenate; never point `SSL_CERT_FILE` at the anchor alone.**
|
||||
> `openssl-probe` uses it *instead of* the default store, so naming
|
||||
> just the bundle would drop every public CA and break federation with
|
||||
> the wider matrix network — a much bigger outage than the one being
|
||||
> fixed. The same caveat applies to `hive-forge` (Go) for the same
|
||||
> reason.
|
||||
|
||||
To put the root on another host, copy the certificate to the same path
|
||||
there (`scp <stateDir>/root.pem <host>:<stateDir>/root.pem`). One anchor
|
||||
|
|
|
|||
|
|
@ -7,6 +7,19 @@
|
|||
let
|
||||
cfg = config.services.hyperhive.swarm.matrix;
|
||||
networkCfg = config.services.hyperhive.network;
|
||||
tlsCfg = config.services.hyperhive.tls;
|
||||
gatewayCfg = config.services.hyperhive.gateway;
|
||||
|
||||
# Same runtime→build-time bridge hive-ci and hive-forge already cross:
|
||||
# binds the hive trust bundle (which folds in the swarm root) into the
|
||||
# container and orders the container after `hive-tls-ca.service`. The
|
||||
# *consumption* is per-runtime and stays here — see the bundle service
|
||||
# in the container config below.
|
||||
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
||||
useSelfSigned = caTrust.useSelfSigned;
|
||||
# tuwunel's own combined bundle, assembled at start. /run is tmpfs, so
|
||||
# it is rebuilt from the current CA every boot rather than going stale.
|
||||
matrixCaBundle = "/run/hive-matrix-ca/ca-bundle.crt";
|
||||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||||
# Falls back to the SWARM domain: a swarm runs one homeserver, so its
|
||||
# identifier belongs to the swarm rather than to whichever hive happens
|
||||
|
|
@ -426,10 +439,13 @@ in
|
|||
privateNetwork = false;
|
||||
# Read-only bind of the host-managed registration token; tuwunel
|
||||
# reads it via systemd LoadCredential below (not directly).
|
||||
bindMounts.${cfg.registrationTokenFile} = {
|
||||
hostPath = cfg.registrationTokenFile;
|
||||
isReadOnly = true;
|
||||
};
|
||||
bindMounts = {
|
||||
${cfg.registrationTokenFile} = {
|
||||
hostPath = cfg.registrationTokenFile;
|
||||
isReadOnly = true;
|
||||
};
|
||||
}
|
||||
// caTrust.bindMount;
|
||||
config =
|
||||
{ ... }:
|
||||
{
|
||||
|
|
@ -442,20 +458,12 @@ in
|
|||
# all filtering; never run one in here.
|
||||
networking.firewall.enable = false;
|
||||
|
||||
# ⚠️ This container trusts no swarm-internal CA. It used to take
|
||||
# per-hive root CAs, hand-pinned as nix paths, so tuwunel could
|
||||
# validate *federation* TLS from a self-signed peer hive; that
|
||||
# field is gone, and the swarm root that replaces it cannot be
|
||||
# substituted here. `security.pki.certificateFiles` is read when
|
||||
# the system is BUILT, and the swarm root is a runtime file
|
||||
# (`swarm.ca.stateDir`) precisely because its key must never
|
||||
# reach the store — so there is nothing build-time to name.
|
||||
#
|
||||
# Giving the container the swarm root therefore needs a runtime
|
||||
# mechanism (bind-mount + a bundle assembled at start), which is
|
||||
# a different shape than this line and is tracked as its own
|
||||
# issue. Federation with a peer whose cert chains to the swarm
|
||||
# root does not validate until then.
|
||||
# Swarm-internal trust reaches tuwunel at RUNTIME, not via
|
||||
# `security.pki.certificateFiles`. That option is read when the
|
||||
# system is BUILT, and the swarm root is deliberately a runtime
|
||||
# file (`swarm.ca.stateDir`) because its key must never enter the
|
||||
# store — so there is nothing build-time to name. The bind-mount
|
||||
# above plus the bundle service below are what replaced it.
|
||||
|
||||
# tuwunel hard-fails to boot if `/etc/resolv.conf` has no
|
||||
# `nameserver` line (`Failed to configure DNS resolver ... no
|
||||
|
|
@ -527,6 +535,42 @@ in
|
|||
systemd.services.tuwunel.serviceConfig.LoadCredential = [
|
||||
"registration_token:${toString cfg.registrationTokenFile}"
|
||||
];
|
||||
|
||||
# Federation TLS against a peer whose cert chains to the swarm
|
||||
# root: tuwunel's outbound client is reqwest with the `rustls`
|
||||
# feature, which builds a `rustls_platform_verifier::Verifier`
|
||||
# and — because tuwunel calls `tls_certs_merge` rather than
|
||||
# `tls_certs_only` — keeps the platform roots alongside its
|
||||
# compiled-in webpki set. On Linux that verifier resolves through
|
||||
# `rustls-native-certs` → `openssl-probe`, which reads
|
||||
# `SSL_CERT_FILE`. So the openssl-shaped variable IS the lever
|
||||
# here, despite tuwunel linking no openssl.
|
||||
#
|
||||
# ⚠️ CONCATENATE, never point at the anchor alone. `openssl-probe`
|
||||
# uses `SSL_CERT_FILE` *instead of* the default location, so
|
||||
# naming just the hive bundle would drop every public CA and
|
||||
# break federation with the wider matrix network — trading a
|
||||
# small outage for a much larger one.
|
||||
systemd.services.hive-matrix-ca-bundle = lib.mkIf useSelfSigned {
|
||||
description = "assemble tuwunel TLS trust bundle (system CAs + hive CA)";
|
||||
wantedBy = [ "tuwunel.service" ];
|
||||
before = [ "tuwunel.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = "hive-matrix-ca-bundle";
|
||||
};
|
||||
path = [ pkgs.coreutils ];
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
install -d -m 0755 /run/hive-matrix-ca
|
||||
cat /etc/ssl/certs/ca-certificates.crt ${caTrust.caContainerPath} \
|
||||
> ${matrixCaBundle}
|
||||
chmod 0644 ${matrixCaBundle}
|
||||
'';
|
||||
};
|
||||
systemd.services.tuwunel.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned matrixCaBundle;
|
||||
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
};
|
||||
};
|
||||
|
|
@ -550,8 +594,13 @@ in
|
|||
# gateway always runs alongside hyperhive, so the gateway container
|
||||
# unit always exists here. (Declarative `containers.<n>` →
|
||||
# `container@<n>.service` — the nspawn template NixOS generates.)
|
||||
systemd.services."container@hive-matrix".after = [
|
||||
"container@hive-gateway.service"
|
||||
# `mkMerge`, not a bare assignment: `caTrust.containerOrdering` also
|
||||
# sets `after`/`requires` (so the bound trust bundle exists before
|
||||
# nspawn wires the mount up), and two plain assignments to the same
|
||||
# unit would conflict rather than combine.
|
||||
systemd.services."container@hive-matrix" = lib.mkMerge [
|
||||
{ after = [ "container@hive-gateway.service" ]; }
|
||||
caTrust.containerOrdering
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue