Watch
0
0
Fork
You've already forked hyperhive
0

run the root check warn-only on hyperhive's own containers, per unit, not on the container@ template

This commit is contained in:
atlas 2026-10-28 23:37:59 +01:00
commit 24afc89ab3
21 changed files with 707 additions and 172 deletions

View file

@ -1,44 +0,0 @@
# The pre-start check of a container's root, for every `container@` instance.
#
# nixpkgs' start script runs as host root against the container's own root,
# which is persistent and writable by the container, and its `mkdir`, `chmod`,
# `touch` and `cp` follow links the container planted there. A drop-in on the
# `container@` template runs `hive-priv check-rootfs %i` as an `ExecStartPre=`
# before the script (`hive-priv/src/prestart.rs` has the rules). It covers the
# agents' imperative instances and the declarative ones (hive-forge,
# hive-matrix, the swarm services), each of which has a unit file of its own:
# systemd reads the template's drop-in directory for every instance.
#
# Order: systemd parses a drop-in after the unit file, so this line follows the
# unit file's own `ExecStartPre=` (nixpkgs' `preStart`, which touches no path
# under the root), and `ExecStart=` runs only after every `ExecStartPre=`
# succeeded.
#
# The drop-in is a unit-file package rather than `systemd.services."container@"`
# because that attribute renders the template's own unit file, which a
# declarative instance's unit file replaces.
{
config,
lib,
pkgs,
...
}:
let
dropin = pkgs.writeTextFile {
name = "hyperhive-container-rootfs-check";
destination = "/lib/systemd/system/container@.service.d/10-hyperhive-rootfs-check.conf";
text = ''
[Service]
ExecStartPre=${config.services.hyperhive.c0re.package}/bin/hive-priv check-rootfs %i
'';
};
in
{
config = lib.mkIf config.boot.enableContainers {
systemd.packages = [ dropin ];
# The per-agent drop-ins this check replaced rooted a hive-priv build per
# agent here. Boot is the one time no such drop-in is loaded.
systemd.tmpfiles.rules = [ "R! /nix/var/nix/gcroots/hyperhive-check-rootfs - - - - -" ];
};
}

View file

@ -22,7 +22,6 @@
./hive-matrix-service.nix
./hive-matrix.nix
./hive-network.nix
./container-rootfs-check.nix
./hive-priv.nix
./hive-priv-cache.nix
./hive-squid.nix

View file

@ -78,6 +78,8 @@ let
deployCfg.bao.forgePublisherClientCertFile != null
&& deployCfg.bao.forgePublisherClientKeyFile != null;
rootfsCheck = import ../lib/container-rootfs-check.nix { inherit config; };
caTrust = import ../lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
# The container's journal forwarder pushes to its own receiver on the swarm
@ -471,6 +473,7 @@ in
# ../swarm-builder.nix's does.
systemd.services."container@hive-forge" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly "hive-forge")
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -13,6 +13,8 @@ let
# the client is pointed at the server name instead.
hyperhiveDomain = config.services.hyperhive.domain;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# Same runtime→build-time bridge hive-forge and swarm-ci-runner cross:
# binds the hive trust bundle (which folds in the store's root) into the
# container and orders the container after `hive-tls-ca.service`. The
@ -1096,6 +1098,7 @@ in
systemd.services."container@hive-matrix" = lib.mkMerge [
{ after = [ "dnsmasq.service" ]; }
caTrust.containerOrdering
(rootfsCheck.warnOnly "hive-matrix")
];
};
}

View file

@ -0,0 +1,32 @@
# The pre-start check of a container's root, for one of hyperhive's own
# declarative containers.
#
# nixpkgs' start script runs as host root against the container's own root,
# which is persistent and writable by the container, and its `mkdir`, `chmod`,
# `touch` and `cp` follow links the container planted there. `hive-priv
# check-rootfs` looks at the paths the script touches (rules in
# `hive-priv/src/prestart.rs`). With `--warn-only` it logs each violation at
# warning level and lets the start go on; the agent containers run it strict
# from their per-agent drop-in (hive-priv's `WriteResourceLimits`).
#
# Pure function, not a NixOS module. A container's module calls it with its
# own name and folds the result into its unit:
#
# rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# # systemd.services."container@${machine}" = lib.mkMerge [ (rootfsCheck.warnOnly machine) … ];
#
# The name list is the callers', never `config.containers`: that also holds
# containers the operator runs that hyperhive doesn't own, and those get
# nothing. nixpkgs renders each declarative container as its own
# `container@<name>` unit (`nixos-containers.nix`, the `mapAttrsToList` that
# builds `container@${name}`), so a setting on that unit reaches that
# container only.
{ config }:
{
# Attributes for `systemd.services."container@<name>"`.
warnOnly = name: {
serviceConfig.ExecStartPre = [
"${config.services.hyperhive.c0re.package}/bin/hive-priv check-rootfs --warn-only ${name}"
];
};
}

View file

@ -114,6 +114,8 @@ let
# one word.
oneWord = path: builtins.match ".*[[:space:]].*" path == null;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The forwarder reaches the swarm collector and authelia by name over TLS; in
# self-signed mode their leaves chain to the hive CA.
caTrust = import ./lib/hive-ca-trust.nix {
@ -226,6 +228,7 @@ in
# CA ordering keeps the trust bundle's bind source there first.
systemd.services."container@${machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly machine)
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -183,6 +183,8 @@ let
refreshConsumer = import ./lib/refresh-consumer.nix { };
storeSeededSecret = import ./lib/store-seeded-secret.nix { };
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
caTrust = import ./lib/hive-ca-trust.nix {
inherit lib gatewayCfg;
tlsCfg = deployCfg.hive-controller.tls;
@ -970,6 +972,7 @@ in
# not be up.
systemd.services."container@${cfg.machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly cfg.machine)
(lib.mkIf publishes {
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -1689,6 +1689,8 @@ let
forwarderAuthName = "oauth2client/${swarmOtelCfg.storeProducerName}";
autheliaCfg = hyperhiveCfg.swarm.authelia;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# Outbound TLS for that collector: it dials the swarm collector's gateway
# name and authelia's token endpoint, both leaves under the store's runtime
# pki root, which cannot be baked into a derivation. Same helper, same two
@ -4091,6 +4093,7 @@ in
# cannot start is a store that is down.
systemd.services."container@${cfg.machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly cfg.machine)
{ after = [ "systemd-tmpfiles-resetup.service" ]; }
];

View file

@ -52,6 +52,8 @@ let
# containers' forwarders all the same.
forwarderTelemetryPort = 8892;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The daemon reaches authelia, the store and the queue by name over TLS, and
# `git` reaches the forge; in self-signed mode their leaves chain to the hive
# CA.
@ -165,6 +167,7 @@ in
# the unit does not exist.
systemd.services."container@${machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly machine)
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -43,6 +43,8 @@ let
# below compares the two, so both come from here.
forgeUrl = "https://${forgeCfg.domain}";
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The forge's gateway serves a leaf from the store's `pki` mount in
# self-signed mode.
# The runner daemon is Go, whose `SSL_CERT_FILE` replaces the system store,
@ -329,6 +331,7 @@ in
# minting unit in and waits for it, as ./swarm-builder.nix's does.
systemd.services."container@${machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly machine)
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -54,6 +54,8 @@ let
# containers' forwarders all the same.
forwarderTelemetryPort = 8893;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The daemon reaches authelia, the store and the queue by name over TLS, and
# in self-signed mode their leaves chain to the hive CA.
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
@ -146,6 +148,7 @@ in
# missing, so where this host mints the leaf the container waits for it.
systemd.services."container@${machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly machine)
{ after = [ "swarm-bao-pki.service" ]; }
];

View file

@ -24,6 +24,8 @@ let
baoCfg = hyperhiveCfg.swarm.bao;
baoDeploy = deployCfg.bao;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
caTrust = import ./lib/hive-ca-trust.nix {
inherit lib gatewayCfg;
tlsCfg = deployCfg.hive-controller.tls;
@ -686,6 +688,7 @@ in
# ./swarm-builder.nix's does.
systemd.services."container@${cfg.machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly cfg.machine)
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -33,6 +33,8 @@ let
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
gatewayCfg = config.services.hyperhive.gateway;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
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
@ -961,7 +963,10 @@ in
# 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."container@${machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly machine)
];
# Deliver the responder's store identity into the container before it
# starts. A copy rather than a `bindMounts` entry: nixos-container refuses

View file

@ -44,6 +44,8 @@ let
# containers' forwarders all the same.
forwarderTelemetryPort = 8902;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The daemon reaches authelia, the store and the queue by name over TLS, and
# in self-signed mode their leaves chain to the hive CA.
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
@ -134,6 +136,7 @@ in
# missing, so where this host mints the leaf the container waits for it.
systemd.services."container@${machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly machine)
{ after = [ "swarm-bao-pki.service" ]; }
];

View file

@ -173,6 +173,8 @@ let
# that ever names the socket.
hiveSockets = lib.genAttrs (lib.attrNames hyperhiveCfg.swarm.hives) socketOf;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The swarm's authelia is reached by its gateway name, whose leaf is
# issued by the swarm services sub-CA — so this container needs the same
# runtime CA trust every other consumer of a swarm-service name needs.
@ -706,7 +708,10 @@ in
# The CA bind source is written at runtime by a host unit, so the
# container has to start after it — otherwise nspawn sets up a mount
# over a file that does not exist yet.
systemd.services."container@${cfg.machine}" = caTrust.containerOrdering;
systemd.services."container@${cfg.machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly cfg.machine)
];
# A collector that is up but not listening on a socket the gateway names
# answers every producer with a 502 and fails no unit. This one fails when

View file

@ -57,6 +57,8 @@ let
# The standard OTLP/HTTP port, on the container's loopback.
forwarderOtlpPort = 4318;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The daemon reaches authelia, the store and the queue by name over TLS, and
# in self-signed mode their leaves chain to the hive CA.
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
@ -172,6 +174,7 @@ in
# the unit does not exist.
systemd.services."container@${machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly machine)
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -99,6 +99,8 @@ let
# one word.
oneWord = path: builtins.match ".*[[:space:]].*" path == null;
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
# The forwarder reaches the swarm collector and authelia by name over TLS; in
# self-signed mode their leaves chain to the hive CA.
caTrust = import ./lib/hive-ca-trust.nix {
@ -290,6 +292,7 @@ in
# ordering keeps the trust bundle's bind source there first.
systemd.services."container@${cfg.machine}" = lib.mkMerge [
caTrust.containerOrdering
(rootfsCheck.warnOnly cfg.machine)
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];

View file

@ -198,6 +198,7 @@ let
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
refreshConsumer = import ./lib/refresh-consumer.nix { };
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
in
{
# What the store IS from any hive's point of view, the name it answers on,
@ -728,6 +729,8 @@ in
systemd.timers.${forwarderFetchUnit} =
storeRefreshTimer "re-read the metrics store forwarder's OIDC client secret from the swarm secret store";
systemd.services."container@${cfg.machine}" = rootfsCheck.warnOnly cfg.machine;
containers.${cfg.machine} = {
autoStart = true;
ephemeral = false;

View file

@ -19,6 +19,7 @@ let
;
})
hive
hiveWith
hiveWithController
runGroup
forceServiceConfigs
@ -73,6 +74,13 @@ let
gateway.openFirewall = true;
};
# A container the operator runs that hyperhive doesn't own, next to one it
# does (the classifier's).
withForeignContainer = hiveWith [ { containers.operator-box.config = { }; } ] {
deploy.bao.enable = true;
deploy.review-classifier.enable = true;
};
# On the store's host, which mints the runner's store identity.
withCi = hive {
deploy.bao.enable = true;
@ -1498,35 +1506,76 @@ let
&& (sc.ProtectKernelModules or false);
}
{
# Every `container@` instance starts through the template's drop-in, the
# agents' and the swarm services' alike, so a host with containers has it
# whether or not it runs a hive. A host with none has no `container@`
# unit and gets no drop-in. Only the rendered line is pinned: eval cannot
# show where systemd reads the file from. The misspelled name is the
# control that the lookup finds nothing when nothing is there.
name = "the container@ template drop-in runs the root check on every host with containers and on no other";
# Hyperhive's own declarative containers each run the root check from
# their own unit, warn-only. Every container in these fixtures is one of
# hyperhive's, so the names are read off the fixture: a container added
# without the check breaks the list, which is the point. The unit's own
# pre-start script is not a `hive-priv` entry, so the count is the check
# alone.
name = "every hyperhive container runs the root check warn-only from its own unit, and no hyperhive container is left out";
ok =
let
dropinText =
name: m:
checks =
m: name:
let
hits = lib.filter (p: (p.name or "") == name) m.systemd.packages;
pre = m.systemd.services."container@${name}".serviceConfig.ExecStartPre or [ ];
in
if builtins.length hits == 1 then (builtins.head hits).text else "";
runsCheck =
name: m:
let
want = "ExecStartPre=${m.services.hyperhive.c0re.package}/bin/hive-priv check-rootfs %i";
lines = lib.splitString "\n" (dropinText name m);
in
lib.elem (builtins.unsafeDiscardStringContext want) (map builtins.unsafeDiscardStringContext lines);
check = "hyperhive-container-rootfs-check";
builtins.filter (lib.hasInfix "hive-priv") (
map (p: builtins.unsafeDiscardStringContext (toString p)) pre
);
carries =
m: name:
checks m name == [
(builtins.unsafeDiscardStringContext "${m.services.hyperhive.c0re.package}/bin/hive-priv check-rootfs --warn-only ${name}")
];
names = lib.attrNames allLocal.containers;
in
runsCheck check allLocal
&& runsCheck check shared.servicesHere
&& !(runsCheck "${check}-misspelled" allLocal)
&& !(bare.systemd.units ? "container@.service")
&& !(runsCheck check bare);
names == [
"hive-forge"
"hive-matrix"
"swarm-attic"
"swarm-authelia"
"swarm-bao"
"swarm-builder"
"swarm-claude-login"
"swarm-grafana"
"swarm-nats"
"swarm-notify-router"
"swarm-otel"
"swarm-review-classifier"
"swarm-victorialogs"
"swarm-victoriametrics"
]
&& lib.all (carries allLocal) names
&& withCi.containers ? swarm-ci-runner
&& lib.all (carries withCi) (lib.attrNames withCi.containers)
# Control: a name with no unit finds nothing.
&& checks allLocal "swarm-nope" == [ ];
}
{
# The operator's other containers are not hyperhive's to touch: a
# `containers.<name>` it declares gets no check, while the classifier
# beside it does. The positive arm is what makes the absence mean
# something: the same lookup finds the classifier's.
name = "a container hyperhive does not declare gets no root check";
ok =
let
checks =
name:
builtins.filter (lib.hasInfix "hive-priv") (
map (p: builtins.unsafeDiscardStringContext (toString p)) (
withForeignContainer.systemd.services."container@${name}".serviceConfig.ExecStartPre or [ ]
)
);
in
lib.attrNames withForeignContainer.containers == [
"operator-box"
"swarm-bao"
"swarm-review-classifier"
]
&& lib.length (checks "swarm-review-classifier") == 1
&& lib.length (checks "swarm-bao") == 1
&& checks "operator-box" == [ ];
}
]
# Starting a unit that stays `active (exited)` is a no-op, so a timer aimed

View file

@ -42,8 +42,8 @@ let
# nixpkgs. `read-only.nix` makes any host module that defines top-level
# `nixpkgs.config`/`overlays`/`hostPlatform` fail the fixture's eval instead
# of being ignored.
hive =
extra:
hiveWith =
modules: extra:
(nixosSystem {
modules = [
(pkgs.path + "/nixos/modules/misc/nixpkgs/read-only.nix")
@ -70,9 +70,13 @@ let
swarm.hives.h1.domain = "h1.t.local";
} extra;
}
];
]
++ modules;
}).config;
# `hiveWith` with no extra modules: the host a fixture normally is.
hive = hiveWith [ ];
# `hive` with the hive-controller tier on (hive-c0re, the agent containers,
# `deploy.hive-controller.*`). Only a fixture whose claim reads that tier
# takes this; every other claim is about the swarm services and evaluates
@ -299,7 +303,7 @@ let
builtins.foldl' (acc: v: builtins.seq v acc) true vals;
in
{
inherit hive;
inherit hive hiveWith;
inherit hiveWithController;
inherit agent;
inherit agentWith;

Some files were not shown because too many files have changed in this diff Show more