nmtrust: init at 0.1.0 (#510650)

This commit is contained in:
Guillaume Girol
2026-04-29 19:03:50 +00:00
committed by GitHub
6 changed files with 559 additions and 0 deletions
+6
View File
@@ -3964,6 +3964,12 @@
name = "Joseph Madden";
keys = [ { fingerprint = "3CF8 E983 2219 AB4B 0E19 158E 6112 1921 C9F8 117C"; } ];
};
brett = {
email = "brett@librum.org";
github = "brett";
githubId = 523;
name = "Brett Eisenberg";
};
brettlyons = {
email = "blyons@fastmail.com";
github = "brettlyons";
+1
View File
@@ -1322,6 +1322,7 @@
./services/networking/nix-store-gcs-proxy.nix
./services/networking/nixops-dns.nix
./services/networking/nm-file-secret-agent.nix
./services/networking/nmtrust.nix
./services/networking/nncp.nix
./services/networking/nntp-proxy.nix
./services/networking/nomad.nix
@@ -0,0 +1,389 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.nmtrust;
# Resolve trusted UUIDs from ensureProfiles + extra
profileUUIDs = map (
name: config.networking.networkmanager.ensureProfiles.profiles.${name}.connection.uuid
) cfg.trustedConnections;
trustedUUIDs = profileUUIDs ++ cfg.trustedUUIDsExtra;
userNames = builtins.attrNames cfg.userUnits;
# The package reads config from /etc/nmtrust/config at runtime
trustHelper = pkgs.nmtrust;
# Trust target names
trustTargets = [
"nmtrust-trusted"
"nmtrust-untrusted"
"nmtrust-offline"
];
# Generate Conflicts= for a target (all other trust targets)
conflictsFor = target: map (t: "${t}.target") (builtins.filter (t: t != target) trustTargets);
# Generate systemd unit overrides for a system unit.
# Uses StopWhenUnneeded instead of PartOf to avoid same-transaction
# issues: when transitioning between targets that both want a unit
# (e.g. offline -> trusted for allowOffline units), PartOf on the
# old target would stop the unit before WantedBy on the new target
# can restart it. StopWhenUnneeded only stops the unit when NO
# active target wants it.
mkSystemUnitOverrides =
unitName: unitCfg:
let
targets = [
"nmtrust-trusted.target"
]
++ lib.optional unitCfg.allowOffline "nmtrust-offline.target";
in
{
unitConfig.StopWhenUnneeded = true;
wantedBy = targets;
};
# Generate user unit overrides
mkUserUnitOverrides =
unitName: unitCfg:
let
targets = [
"nmtrust-trusted.target"
]
++ lib.optional unitCfg.allowOffline "nmtrust-offline.target";
in
{
unitConfig.StopWhenUnneeded = true;
wantedBy = targets;
};
# NM dispatcher script
dispatcherScript = pkgs.writeShellScript "nmtrust-dispatcher" ''
case "$2" in
up|down|vpn-up|vpn-down|connectivity-change)
${config.systemd.package}/bin/systemd-run \
--no-block \
--on-active=1s \
--unit=nmtrust-apply-debounce \
${config.systemd.package}/bin/systemctl start nmtrust-apply.service \
2>/dev/null || true
;;
esac
'';
in
{
#
# Options
#
options.services.nmtrust = {
enable = lib.mkEnableOption "network trust management";
trustedConnections = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
List of NetworkManager profile names from
{option}`networking.networkmanager.ensureProfiles`.
UUIDs are resolved at evaluation time.
'';
};
trustedUUIDsExtra = lib.mkOption {
type = lib.types.listOf (
lib.types.strMatching "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
);
default = [ ];
description = ''
Additional trusted connection UUIDs not managed via
{option}`networking.networkmanager.ensureProfiles`.
Must be valid UUID format.
'';
};
excludedConnectionPatterns = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Glob patterns matched against connection names at runtime using
fnmatch(3) with FNM_NOESCAPE. Connection names are treated as
literal strings (no backslash interpretation).
Matching connections are ignored when computing trust state.
'';
};
mixedPolicy = lib.mkOption {
type = lib.types.enum [
"trusted"
"untrusted"
];
default = "untrusted";
description = ''
How to treat mixed trust state (some connections trusted,
some untrusted).
'';
};
evalFailurePolicy = lib.mkOption {
type = lib.types.enum [
"untrusted"
"offline"
];
default = "untrusted";
description = ''
How to handle trust evaluation failures (D-Bus errors, NM
unavailable). `"untrusted"` (default) is fail-closed: trusted-only
units stop. `"offline"` allows units with
{option}`allowOffline` to run.
'';
};
systemUnits = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options.allowOffline = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether this unit should also run when offline.";
};
}
);
default = { };
description = ''
System units to bind to the trusted network target.
Keys are systemd unit names.
'';
};
userUnits = lib.mkOption {
type = lib.types.attrsOf (
lib.types.attrsOf (
lib.types.submodule {
options.allowOffline = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether this unit should also run when offline.";
};
}
)
);
default = { };
example = lib.literalExpression ''
{
alice = {
"etesync-dav.service" = { };
"syncthing.service" = { allowOffline = true; };
};
}
'';
description = ''
Per-user units to bind to the trusted network target.
Outer keys are usernames, inner keys are systemd unit names.
Users must have linger enabled
({option}`users.users.<name>.linger`).
'';
};
};
#
# Config
#
config = lib.mkIf cfg.enable {
# --- Assertions ---
assertions =
# NetworkManager is required
[
{
assertion = config.networking.networkmanager.enable;
message = "services.nmtrust requires networking.networkmanager.enable = true.";
}
]
++
# trustedConnections -> ensureProfiles UUID resolution
(map (name: {
assertion =
config.networking.networkmanager.ensureProfiles.profiles ? ${name}
&& config.networking.networkmanager.ensureProfiles.profiles.${name}.connection ? uuid;
message =
"services.nmtrust.trustedConnections references '${name}' "
+ "but no matching networking.networkmanager.ensureProfiles entry with a UUID exists.";
}) cfg.trustedConnections)
++
# userUnits -> user existence
(map (username: {
assertion = config.users.users ? ${username};
message =
"services.nmtrust.userUnits references user '${username}' "
+ "but no matching users.users entry exists.";
}) userNames)
++
# userUnits -> linger enabled
(map (username: {
assertion =
let
l = config.users.users.${username}.linger;
in
l != null && l;
message =
"services.nmtrust.userUnits references user '${username}' but "
+ "linger is not enabled. Set users.users.${username}.linger = true to "
+ "ensure the user's systemd instance is running for trust-based unit management. "
+ "Note: enabling linger causes ALL of this user's enabled user services to run "
+ "persistently, not just trust-managed units.";
}) (builtins.filter (u: config.users.users ? ${u}) userNames));
# --- Helper package on PATH ---
environment.systemPackages = [ trustHelper ];
# --- Runtime config file ---
environment.etc."nmtrust/config" = {
text =
let
toBashArray = xs: "(" + lib.concatMapStringsSep " " (x: lib.escapeShellArg x) xs + ")";
in
''
# Generated by NixOS module do not edit
TRUSTED_UUIDS=${toBashArray trustedUUIDs}
EXCLUDED_PATTERNS=${toBashArray (cfg.excludedConnectionPatterns)}
MIXED_POLICY=${lib.escapeShellArg cfg.mixedPolicy}
EVAL_FAILURE_POLICY=${lib.escapeShellArg cfg.evalFailurePolicy}
MANAGED_USERS=${toBashArray userNames}
'';
};
# --- tmpfiles.d ---
systemd.tmpfiles.rules = [
"d /run/nmtrust 0700 root root -"
];
# --- System trust targets ---
systemd.targets = lib.listToAttrs (
map (target: {
name = target;
value = {
description = "Network Trust State: ${
if target == "nmtrust-trusted" then
"Trusted"
else if target == "nmtrust-untrusted" then
"Untrusted"
else
"Offline"
}";
unitConfig.Conflicts = conflictsFor target;
};
}) trustTargets
);
# --- User trust targets ---
systemd.user.targets = lib.listToAttrs (
map (target: {
name = target;
value = {
description = "Network Trust State: ${
if target == "nmtrust-trusted" then
"Trusted (User)"
else if target == "nmtrust-untrusted" then
"Untrusted (User)"
else
"Offline (User)"
}";
unitConfig.Conflicts = conflictsFor target;
};
}) trustTargets
);
# --- System unit overrides + services ---
# Strip .service/.timer/.socket suffixes — NixOS appends them automatically
systemd.services =
lib.mapAttrs' (name: value: {
name = lib.removeSuffix ".service" (lib.removeSuffix ".timer" (lib.removeSuffix ".socket" name));
value = mkSystemUnitOverrides name value;
}) cfg.systemUnits
// {
nmtrust-apply = {
description = "Evaluate and apply network trust state";
after = [ "NetworkManager.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${trustHelper}/bin/nmtrust apply";
ProtectSystem = "strict";
ReadWritePaths = [ "/run/nmtrust" ];
ProtectHome = true;
NoNewPrivileges = true;
PrivateTmp = true;
};
};
nmtrust-eval = {
description = "Evaluate network trust state on boot";
wantedBy = [ "network-online.target" ];
wants = [ "network-online.target" ];
after = [
"NetworkManager.service"
"network-online.target"
];
restartTriggers = [
config.environment.etc."nmtrust/config".source
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${trustHelper}/bin/nmtrust apply";
ProtectSystem = "strict";
ReadWritePaths = [ "/run/nmtrust" ];
ProtectHome = true;
NoNewPrivileges = true;
PrivateTmp = true;
};
};
};
# --- User unit overrides ---
systemd.user.services = lib.foldl' (
acc: username:
lib.foldl' (
acc': unitName:
let
strippedName = lib.removeSuffix ".service" (
lib.removeSuffix ".timer" (lib.removeSuffix ".socket" unitName)
);
in
acc'
// {
${strippedName} = mkUserUnitOverrides unitName cfg.userUnits.${username}.${unitName};
}
) acc (builtins.attrNames cfg.userUnits.${username})
) { } userNames;
# --- NM dispatcher ---
networking.networkmanager.dispatcherScripts = [
{
source = dispatcherScript;
type = "basic";
}
];
};
meta.maintainers = [ lib.maintainers.brett ];
}
+1
View File
@@ -1141,6 +1141,7 @@ in
pkgs.callPackage ../../pkgs/stdenv/generic/check-meta-test.nix
{ };
nixseparatedebuginfod2 = runTest ./nixseparatedebuginfod2.nix;
nmtrust = runTest ./nmtrust.nix;
node-red = runTest ./node-red.nix;
nohang = runTest ./nohang.nix;
nomad = runTest ./nomad.nix;
+92
View File
@@ -0,0 +1,92 @@
{ lib, pkgs, ... }:
{
name = "nmtrust";
nodes.machine =
{ pkgs, ... }:
{
networking.networkmanager.enable = true;
# Prevent the VM's built-in interfaces from polluting trust state.
networking.networkmanager.unmanaged = [
"eth0"
"eth1"
"lo"
];
networking.networkmanager.ensureProfiles.profiles = {
trusted-net = {
connection = {
id = "trusted-net";
uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
type = "dummy";
interface-name = "dummy-trusted";
autoconnect = "false";
};
ipv4.method = "manual";
ipv4.addresses = "10.99.1.1/24";
};
untrusted-net = {
connection = {
id = "untrusted-net";
uuid = "11111111-2222-3333-4444-555555555555";
type = "dummy";
interface-name = "dummy-untrusted";
autoconnect = "false";
};
ipv4.method = "manual";
ipv4.addresses = "10.99.2.1/24";
};
};
services.nmtrust = {
enable = true;
trustedConnections = [ "trusted-net" ];
systemUnits."trust-canary.service" = { };
};
# Canary service: runs only while the trusted target is active.
systemd.services.trust-canary = {
description = "nmtrust test canary";
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.coreutils}/bin/sleep infinity";
};
};
};
testScript = ''
import time
def apply(machine):
"""Trigger nmtrust-apply and wait for it to finish."""
time.sleep(1)
machine.succeed("systemctl start nmtrust-apply.service")
machine.wait_until_succeeds(
"systemctl show nmtrust-apply.service -p ActiveState --value | grep -q inactive",
timeout=10,
)
machine.wait_for_unit("multi-user.target")
with subtest("offline on boot with no connections active"):
apply(machine)
machine.succeed("systemctl is-active nmtrust-offline.target")
machine.fail("systemctl is-active trust-canary.service")
with subtest("trusted when trusted connection is up"):
machine.succeed("nmcli connection up trusted-net")
apply(machine)
machine.succeed("systemctl is-active nmtrust-trusted.target")
machine.succeed("systemctl is-active trust-canary.service")
with subtest("untrusted when untrusted connection replaces trusted"):
machine.succeed("nmcli connection down trusted-net")
machine.succeed("nmcli connection up untrusted-net")
apply(machine)
machine.succeed("systemctl is-active nmtrust-untrusted.target")
machine.fail("systemctl is-active trust-canary.service")
'';
meta.maintainers = with lib.maintainers; [ brett ];
}
+70
View File
@@ -0,0 +1,70 @@
{
lib,
stdenv,
fetchFromGitHub,
makeWrapper,
shellcheck,
bash,
systemd,
coreutils,
gawk,
util-linux,
}:
stdenv.mkDerivation {
pname = "nmtrust";
version = "0.1.0";
strictDeps = true;
__structuredAttrs = true;
src = fetchFromGitHub {
owner = "brett";
repo = "nmtrust-nix";
rev = "v0.1.0";
hash = "sha256-7Cs00mCzByTKe7w5pnkkgqtZyUSaPa2r/5Uv133eZy0=";
};
nativeBuildInputs = [ makeWrapper ];
nativeCheckInputs = [ shellcheck ];
doCheck = true;
checkPhase = ''
runHook preCheck
shellcheck nmtrust.sh
runHook postCheck
'';
installPhase = ''
runHook preInstall
install -Dm755 nmtrust.sh $out/bin/nmtrust
wrapProgram $out/bin/nmtrust \
--prefix PATH : ${
lib.makeBinPath [
bash
systemd
coreutils
gawk
util-linux
]
}
runHook postInstall
'';
meta = {
description = "Declarative network trust management for NixOS";
longDescription = ''
nmtrust evaluates the trust state of active NetworkManager connections
and activates corresponding systemd targets. Services bound to these
targets start and stop automatically as you move between trusted,
untrusted, and offline networks.
A NixOS-native reimplementation of nmtrust by Peter Hogg (pigmonkey).
'';
homepage = "https://github.com/brett/nmtrust-nix";
license = lib.licenses.unlicense;
maintainers = [ lib.maintainers.brett ];
mainProgram = "nmtrust";
platforms = lib.platforms.linux;
};
}