mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-06-05 21:03:40 +00:00
nixos/nmtrust: init
This commit is contained in:
@@ -1318,6 +1318,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
|
||||
|
||||
389
nixos/modules/services/networking/nmtrust.nix
Normal file
389
nixos/modules/services/networking/nmtrust.nix
Normal file
@@ -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 ];
|
||||
|
||||
}
|
||||
@@ -1134,6 +1134,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
nixos/tests/nmtrust.nix
Normal file
92
nixos/tests/nmtrust.nix
Normal 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 ];
|
||||
}
|
||||
Reference in New Issue
Block a user