Unverified Commit b24161b0 authored by Guillaume Girol's avatar Guillaume Girol Committed by GitHub
Browse files

nmtrust: init at 0.1.0 (#510650)

parents bb14bda2 e270fda6
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
@@ -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
+389 −0
Original line number Diff line number Diff line
{
  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 −0
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
{ 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 ];
}
Loading