commit 26c93442450372625e67ee59f73122ebeba79732
parent 9c28bdb164296a1c3ae5e9d7c3e3b45282506cb7
Author: triesap <tyson@radroots.org>
Date: Thu, 20 Aug 2026 21:30:45 +0000
build: add service nixos module
Diffstat:
4 files changed, 645 insertions(+), 1 deletion(-)
diff --git a/build/nix/service/default.nix b/build/nix/service/default.nix
@@ -12,5 +12,6 @@
mkServiceApps = import ./apps.nix { inherit lib pkgs; };
mkServiceDevShell = import ./devshell.nix { inherit lib pkgs; };
mkServiceOciImage = import ./oci.nix { inherit lib pkgs; };
+ mkServiceNixosModule = import ./nixos-module.nix { inherit lib; };
mkServiceOutputs = import ./compose.nix { inherit lib; };
}
diff --git a/build/nix/service/fixture.nix b/build/nix/service/fixture.nix
@@ -1,5 +1,6 @@
{
lib,
+ nixosSystem,
pkgs,
service,
toolchain,
@@ -132,6 +133,117 @@ let
''
else
null;
+ nixosModule = service.mkServiceNixosModule {
+ serviceName = "fixture_service";
+ binaryName = "fixture-service";
+ packageFor = _: package;
+ commandForInstance = instanceName: [
+ "--instance"
+ instanceName
+ "--config"
+ "/etc/radroots/services/fixture_service/${instanceName}/config.toml"
+ ];
+ };
+ nixosConfiguration =
+ if pkgs.stdenv.isLinux then
+ nixosSystem {
+ system = pkgs.stdenv.hostPlatform.system;
+ modules = [
+ nixosModule
+ {
+ system.stateVersion = "25.11";
+ users.groups.fixture-admin = { };
+ services.radroots.fixture_service = {
+ enable = true;
+ adminGroup = "fixture-admin";
+ instances.primary = {
+ configurationFile = pkgs.writeText "fixture-service-config.toml" ''
+ contract_version = 1
+ '';
+ credentials.api-token = "/run/operator/fixture-api-token";
+ };
+ };
+ }
+ ];
+ }
+ else
+ null;
+ nixosUnitName = "radroots-fixture_service-primary";
+ nixosService =
+ if pkgs.stdenv.isLinux then nixosConfiguration.config.systemd.services.${nixosUnitName} else null;
+ nixosUnitText =
+ if pkgs.stdenv.isLinux then
+ nixosConfiguration.config.system.build.units."${nixosUnitName}.service".text
+ else
+ null;
+ nixosModuleCheck =
+ if pkgs.stdenv.isLinux then
+ pkgs.runCommand "fixture-service-nixos-module"
+ {
+ nativeBuildInputs = [
+ pkgs.gnugrep
+ ];
+ unit = pkgs.writeText "${nixosUnitName}.service" nixosUnitText;
+ }
+ ''
+ grep -Fx 'Type=simple' "$unit"
+ grep -Fx 'DynamicUser=true' "$unit"
+ grep -Fx 'User=radroots-fixture_service' "$unit"
+ grep -Fx 'Group=fixture-admin' "$unit"
+ grep -Fx 'UMask=0077' "$unit"
+ grep -Fx 'ConfigurationDirectory=radroots/services/fixture_service/primary' "$unit"
+ grep -Fx 'ConfigurationDirectoryMode=0500' "$unit"
+ grep -Fx 'StateDirectory=radroots/services/fixture_service/primary' "$unit"
+ grep -Fx 'StateDirectoryMode=0700' "$unit"
+ grep -Fx 'CacheDirectory=radroots/services/fixture_service/primary' "$unit"
+ grep -Fx 'CacheDirectoryMode=0700' "$unit"
+ grep -Fx 'RuntimeDirectory=radroots/services/fixture_service/primary' "$unit"
+ grep -Fx 'RuntimeDirectoryMode=0750' "$unit"
+ grep -Fx 'LoadCredential=api-token:/run/operator/fixture-api-token' "$unit"
+ grep -Fx 'NoNewPrivileges=true' "$unit"
+ grep -Fx 'PrivateTmp=true' "$unit"
+ grep -Fx 'PrivateDevices=true' "$unit"
+ grep -Fx 'ProtectSystem=strict' "$unit"
+ grep -Fx 'ProtectHome=true' "$unit"
+ grep -Fx 'ProtectKernelTunables=true' "$unit"
+ grep -Fx 'ProtectKernelModules=true' "$unit"
+ grep -Fx 'ProtectKernelLogs=true' "$unit"
+ grep -Fx 'ProtectControlGroups=true' "$unit"
+ grep -Fx 'ProtectHostname=true' "$unit"
+ grep -Fx 'ProtectClock=true' "$unit"
+ grep -Fx 'RestrictSUIDSGID=true' "$unit"
+ grep -Fx 'LockPersonality=true' "$unit"
+ grep -Fx 'CapabilityBoundingSet=' "$unit"
+ grep -Fx 'AmbientCapabilities=' "$unit"
+ test "$(grep -c '^RestrictAddressFamilies=' "$unit")" = 3
+ grep -Fx 'RestrictAddressFamilies=AF_UNIX' "$unit"
+ grep -Fx 'RestrictAddressFamilies=AF_INET' "$unit"
+ grep -Fx 'RestrictAddressFamilies=AF_INET6' "$unit"
+ grep -Fx 'RestrictNamespaces=true' "$unit"
+ grep -Fx 'RestrictRealtime=true' "$unit"
+ grep -Fx 'RemoveIPC=true' "$unit"
+ grep -Fx 'DevicePolicy=closed' "$unit"
+ grep -Fx 'KeyringMode=private' "$unit"
+ grep -Fx 'ProcSubset=pid' "$unit"
+ grep -Fx 'ProtectProc=invisible' "$unit"
+ grep -Fx 'SystemCallArchitectures=native' "$unit"
+ grep -Fx 'TimeoutStopSec=30s' "$unit"
+ grep -Fx 'KillSignal=SIGTERM' "$unit"
+ grep -Fx 'KillMode=control-group' "$unit"
+ grep -Fx 'Restart=on-failure' "$unit"
+ grep -Fx 'RestartSec=5s' "$unit"
+ if grep -E '^(MemoryDenyWriteExecute|SystemCallFilter)=' "$unit"; then
+ echo "fixture unit enabled an unqualified hardening control" >&2
+ exit 1
+ fi
+ if grep -E '^Environment=.*(api-token|fixture-api-token)' "$unit"; then
+ echo "fixture unit exposed credential material through its environment" >&2
+ exit 1
+ fi
+ touch "$out"
+ ''
+ else
+ null;
appSmoke = pkgs.runCommand "fixture-service-app-smoke" { } ''
test "$(${apps.default.program} --help)" = "fixture-service"
test "$(${apps.release-acceptance.program})" = "fixture-service release acceptance"
@@ -199,6 +311,7 @@ let
inherit smoke;
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
+ nixos-module = nixosModuleCheck;
oci-image = ociImageCheck;
};
};
@@ -621,6 +734,219 @@ let
)).outPath
))
];
+ nixosServiceConfig = if pkgs.stdenv.isLinux then nixosService.serviceConfig else null;
+ nixosModuleArguments = {
+ serviceName = "fixture_service";
+ binaryName = "fixture-service";
+ packageFor = _: package;
+ commandForInstance = _: [ "--help" ];
+ };
+ invalidNixosModuleConstructors = [
+ (nixosModuleArguments // { serviceName = "../fixture"; })
+ (nixosModuleArguments // { serviceName = "fixture-service"; })
+ (nixosModuleArguments // { serviceName = lib.concatStrings (lib.replicate 129 "a"); })
+ (nixosModuleArguments // { binaryName = "fixture service"; })
+ (nixosModuleArguments // { binaryName = lib.concatStrings (lib.replicate 129 "b"); })
+ (nixosModuleArguments // { packageFor = "not-a-function"; })
+ (nixosModuleArguments // { commandForInstance = "not-a-function"; })
+ (nixosModuleArguments // { stopTimeoutSeconds = 0; })
+ (nixosModuleArguments // { stopTimeoutSeconds = 86401; })
+ (nixosModuleArguments // { addressFamilies = [ ]; })
+ (
+ nixosModuleArguments
+ // {
+ addressFamilies = [
+ "AF_UNIX"
+ "AF_UNIX"
+ ];
+ }
+ )
+ (nixosModuleArguments // { addressFamilies = [ "AF_PACKET" ]; })
+ ];
+ invalidNixosModuleConstructorResults = map (
+ arguments: builtins.tryEval ((service.mkServiceNixosModule arguments) { })
+ ) invalidNixosModuleConstructors;
+ baseNixosModuleConfiguration = {
+ enable = true;
+ inherit package;
+ adminGroup = null;
+ instances.primary = {
+ configurationFile = pkgs.writeText "fixture-service-assertion-config.toml" "";
+ credentials = { };
+ };
+ };
+ nixosModuleAssertionsPass =
+ module: moduleServiceName: moduleConfiguration:
+ let
+ evaluated = module {
+ config = lib.setAttrByPath [
+ "services"
+ "radroots"
+ moduleServiceName
+ ] moduleConfiguration;
+ inherit pkgs;
+ };
+ in
+ lib.all (entry: entry.assertion) evaluated.config.assertions;
+ invalidNixosModuleConfigurations = [
+ (baseNixosModuleConfiguration // { enable = false; })
+ (baseNixosModuleConfiguration // { instances = { }; })
+ (baseNixosModuleConfiguration // { adminGroup = "INVALID GROUP"; })
+ (
+ baseNixosModuleConfiguration
+ // {
+ adminGroup = lib.concatStrings (lib.replicate 129 "a");
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances = {
+ "../primary" = baseNixosModuleConfiguration.instances.primary;
+ };
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances = {
+ ${lib.concatStrings (lib.replicate 129 "a")} = baseNixosModuleConfiguration.instances.primary;
+ };
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials."bad:name" = "/run/operator/token";
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials.token = "relative/token";
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials.token = "/nix/store";
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials.token = "/nix/store/secret";
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials.token = "/run/operator/token:alias";
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials.token = "/run/operator/token\nvalue";
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials.token = "/${lib.concatStrings (lib.replicate 4096 "a")}";
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances.primary.credentials = lib.genAttrs (lib.genList (
+ index: "credential-${toString index}"
+ ) 33) (_: "/run/operator/token");
+ }
+ )
+ (
+ baseNixosModuleConfiguration
+ // {
+ instances = lib.genAttrs (lib.genList (index: "instance-${toString index}") 65) (
+ _: baseNixosModuleConfiguration.instances.primary
+ );
+ }
+ )
+ ];
+ invalidNixosModuleConfigurationResults = map (
+ moduleConfiguration:
+ builtins.tryEval (nixosModuleAssertionsPass nixosModule "fixture_service" moduleConfiguration)
+ ) invalidNixosModuleConfigurations;
+ commandNixosModule =
+ command:
+ service.mkServiceNixosModule (
+ nixosModuleArguments
+ // {
+ commandForInstance = _: command;
+ }
+ );
+ invalidNixosCommandResults =
+ map
+ (
+ command:
+ builtins.tryEval (
+ nixosModuleAssertionsPass (commandNixosModule command) "fixture_service"
+ baseNixosModuleConfiguration
+ )
+ )
+ [
+ (lib.replicate 65 "argument")
+ [ (lib.concatStrings (lib.replicate 4097 "a")) ]
+ [ "argument\nvalue" ]
+ ];
+ maximumNixosModule = service.mkServiceNixosModule (
+ nixosModuleArguments
+ // {
+ serviceName = lib.concatStrings (lib.replicate 128 "a");
+ binaryName = lib.concatStrings (lib.replicate 128 "b");
+ stopTimeoutSeconds = 86400;
+ addressFamilies = [ "AF_UNIX" ];
+ }
+ );
+ maximumNixosModuleConfiguration = {
+ enable = true;
+ inherit package;
+ adminGroup = lib.concatStrings (lib.replicate 128 "a");
+ instances.${lib.concatStrings (lib.replicate 109 "b")} = {
+ configurationFile = pkgs.writeText "fixture-service-maximum-config.toml" "";
+ credentials = lib.genAttrs (lib.genList (index: "credential-${toString index}") 32) (
+ _: "/${lib.concatStrings (lib.replicate 4095 "c")}"
+ );
+ };
+ };
+ overlongNixosUnitConfiguration = maximumNixosModuleConfiguration // {
+ instances = {
+ ${lib.concatStrings (lib.replicate 110 "b")} = baseNixosModuleConfiguration.instances.primary;
+ };
+ };
+ overlongNixosUnitResult = builtins.tryEval (
+ nixosModuleAssertionsPass maximumNixosModule (lib.concatStrings (
+ lib.replicate 128 "a"
+ )) overlongNixosUnitConfiguration
+ );
+ maximumNixosInstanceCountConfiguration = baseNixosModuleConfiguration // {
+ instances = lib.genAttrs (lib.genList (index: "instance-${toString index}") 64) (
+ _: baseNixosModuleConfiguration.instances.primary
+ );
+ };
+ maximumNixosCommandModule = commandNixosModule (
+ lib.replicate 64 (lib.concatStrings (lib.replicate 4096 "a"))
+ );
+ noAdminNixosEvaluation = nixosModule {
+ config = lib.setAttrByPath [
+ "services"
+ "radroots"
+ "fixture_service"
+ ] baseNixosModuleConfiguration;
+ inherit pkgs;
+ };
+ noAdminNixosServiceConfig =
+ noAdminNixosEvaluation.config.systemd.services.content.${nixosUnitName}.serviceConfig;
in
assert
service.supportedSystems == [
@@ -656,6 +982,7 @@ assert
"integration"
]
++ lib.optionals pkgs.stdenv.isLinux [
+ "nixos-module"
"oci-image"
]
++ [
@@ -701,6 +1028,55 @@ assert lib.all (result: result.success == false) invalidDevShellResults;
assert maximumOciResult.success;
assert lib.all (result: result.success == false) invalidOciResults;
assert (
+ pkgs.stdenv.isLinux
+ ->
+ builtins.attrNames (
+ lib.filterAttrs (name: _: lib.hasPrefix "radroots-" name) nixosConfiguration.config.systemd.services
+ ) == [ nixosUnitName ]
+);
+assert (pkgs.stdenv.isLinux -> nixosService.wantedBy == [ "multi-user.target" ]);
+assert (pkgs.stdenv.isLinux -> nixosService.wants == [ "network-online.target" ]);
+assert (pkgs.stdenv.isLinux -> nixosService.after == [ "network-online.target" ]);
+assert (pkgs.stdenv.isLinux -> nixosServiceConfig.Type == "simple");
+assert (pkgs.stdenv.isLinux -> nixosServiceConfig.DynamicUser);
+assert (pkgs.stdenv.isLinux -> nixosServiceConfig.RuntimeDirectoryMode == "0750");
+assert (
+ pkgs.stdenv.isLinux
+ ->
+ nixosServiceConfig.BindReadOnlyPaths == [
+ "${nixosConfiguration.config.services.radroots.fixture_service.instances.primary.configurationFile}:/etc/radroots/services/fixture_service/primary/config.toml"
+ ]
+);
+assert (
+ pkgs.stdenv.isLinux
+ ->
+ nixosServiceConfig.LoadCredential == [
+ "api-token:/run/operator/fixture-api-token"
+ ]
+);
+assert (pkgs.stdenv.isLinux -> !(nixosServiceConfig ? MemoryDenyWriteExecute));
+assert (pkgs.stdenv.isLinux -> !(nixosServiceConfig ? SystemCallFilter));
+assert lib.all (result: result.success == false) invalidNixosModuleConstructorResults;
+assert lib.all (
+ result: result.success && result.value == false
+) invalidNixosModuleConfigurationResults;
+assert lib.all (result: result.success && result.value == false) invalidNixosCommandResults;
+assert (
+ nixosModuleAssertionsPass maximumNixosModule (lib.concatStrings (
+ lib.replicate 128 "a"
+ )) maximumNixosModuleConfiguration
+);
+assert (overlongNixosUnitResult.success && overlongNixosUnitResult.value == false);
+assert (
+ nixosModuleAssertionsPass nixosModule "fixture_service" maximumNixosInstanceCountConfiguration
+);
+assert (
+ nixosModuleAssertionsPass maximumNixosCommandModule "fixture_service" baseNixosModuleConfiguration
+);
+assert noAdminNixosServiceConfig.Group == "radroots-fixture_service";
+assert noAdminNixosServiceConfig.RuntimeDirectoryMode == "0700";
+assert !(noAdminNixosServiceConfig ? SupplementaryGroups);
+assert (
!pkgs.stdenv.isLinux
-> (builtins.tryEval ((service.mkServiceOciImage ociArgs).outPath)).success == false
);
diff --git a/build/nix/service/nixos-module.nix b/build/nix/service/nixos-module.nix
@@ -0,0 +1,253 @@
+{ lib }:
+{
+ serviceName,
+ binaryName,
+ packageFor,
+ commandForInstance,
+ stopTimeoutSeconds ? 30,
+ addressFamilies ? [
+ "AF_UNIX"
+ "AF_INET"
+ "AF_INET6"
+ ],
+}:
+assert lib.assertMsg (
+ builtins.isString serviceName
+ && builtins.stringLength serviceName <= 128
+ && builtins.match "^[a-z][a-z0-9_]*$" serviceName != null
+) "serviceName must be a bounded lowercase snake-case identifier";
+assert lib.assertMsg (
+ builtins.isString binaryName
+ && builtins.stringLength binaryName <= 128
+ && builtins.match "^[a-z][a-z0-9_-]*$" binaryName != null
+) "binaryName must be a lowercase Cargo binary identifier";
+assert lib.assertMsg (builtins.isFunction packageFor) "packageFor must be a function";
+assert lib.assertMsg (builtins.isFunction commandForInstance)
+ "commandForInstance must be a function";
+assert lib.assertMsg (
+ builtins.isInt stopTimeoutSeconds && stopTimeoutSeconds > 0 && stopTimeoutSeconds <= 86400
+) "stopTimeoutSeconds must be between 1 and 86400";
+assert lib.assertMsg (
+ builtins.isList addressFamilies
+ && addressFamilies != [ ]
+ && builtins.length addressFamilies <= 3
+ && lib.all (
+ family:
+ builtins.elem family [
+ "AF_UNIX"
+ "AF_INET"
+ "AF_INET6"
+ ]
+ ) addressFamilies
+ && builtins.length (lib.unique addressFamilies) == builtins.length addressFamilies
+) "addressFamilies must be a unique nonempty subset of the governed families";
+{
+ config,
+ pkgs,
+ ...
+}:
+let
+ optionPath = [
+ "services"
+ "radroots"
+ serviceName
+ ];
+ cfg = lib.getAttrFromPath optionPath config;
+ instanceNames = builtins.attrNames cfg.instances;
+ systemUser = "radroots-${serviceName}";
+ validInstanceName =
+ name:
+ builtins.isString name
+ && builtins.stringLength name <= 128
+ && builtins.match "^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$" name != null;
+ unitName = instanceName: "radroots-${serviceName}-${instanceName}";
+ validUnitName = instanceName: builtins.stringLength (unitName instanceName) <= 247;
+ validCredentialName =
+ name:
+ builtins.stringLength name <= 128 && builtins.match "^[A-Za-z0-9][A-Za-z0-9_.-]*$" name != null;
+ validCredentialPath =
+ path:
+ builtins.isString path
+ && builtins.stringLength path <= 4096
+ && lib.hasPrefix "/" path
+ && path != "/nix/store"
+ && !(lib.hasPrefix "/nix/store/" path)
+ && builtins.match "^[^:\n]+$" path != null;
+ validCredentials =
+ instance:
+ builtins.length (builtins.attrNames instance.credentials) <= 32
+ && lib.all (name: validCredentialName name && validCredentialPath instance.credentials.${name}) (
+ builtins.attrNames instance.credentials
+ );
+ commandFor = instanceName: commandForInstance instanceName;
+ validCommand =
+ instanceName:
+ let
+ command = commandFor instanceName;
+ in
+ builtins.isList command
+ && builtins.length command <= 64
+ && lib.all (
+ argument:
+ builtins.isString argument
+ && builtins.stringLength argument <= 4096
+ && builtins.match "^[^\n]*$" argument != null
+ ) command;
+ package = cfg.package;
+ assertions = [
+ {
+ assertion = cfg.enable == (instanceNames != [ ]);
+ message = "the service must be enabled with at least one instance and disabled without instances";
+ }
+ {
+ assertion = builtins.length instanceNames <= 64;
+ message = "a service may define at most 64 instances";
+ }
+ {
+ assertion = lib.isDerivation package;
+ message = "the service package must be a derivation";
+ }
+ {
+ assertion =
+ cfg.adminGroup == null
+ || (
+ builtins.stringLength cfg.adminGroup <= 128
+ && builtins.match "^[a-z_][a-z0-9_-]*$" cfg.adminGroup != null
+ );
+ message = "adminGroup must be a bounded canonical system group name";
+ }
+ ]
+ ++ lib.concatMap (
+ instanceName:
+ let
+ instance = cfg.instances.${instanceName};
+ in
+ [
+ {
+ assertion = validInstanceName instanceName;
+ message = "every service instance must use a bounded canonical identifier";
+ }
+ {
+ assertion = validUnitName instanceName;
+ message = "the derived systemd unit name exceeds its 255-byte limit";
+ }
+ {
+ assertion = validCredentials instance;
+ message = "credentials must use bounded names and external absolute source paths";
+ }
+ {
+ assertion = validCommand instanceName;
+ message = "the service command must contain at most 64 bounded single-line arguments";
+ }
+ ]
+ ) instanceNames;
+ mkService =
+ instanceName: instance:
+ let
+ serviceInstance = "radroots/services/${serviceName}/${instanceName}";
+ configurationPath = "/etc/${serviceInstance}/config.toml";
+ credentialBindings = lib.mapAttrsToList (name: path: "${name}:${path}") instance.credentials;
+ serviceConfig = {
+ Type = "simple";
+ User = systemUser;
+ Group = if cfg.adminGroup == null then systemUser else cfg.adminGroup;
+ DynamicUser = true;
+ ExecStart = lib.escapeShellArgs ([ "${package}/bin/${binaryName}" ] ++ commandFor instanceName);
+ WorkingDirectory = "/";
+ Restart = "on-failure";
+ RestartSec = "5s";
+ TimeoutStopSec = "${toString stopTimeoutSeconds}s";
+ KillSignal = "SIGTERM";
+ KillMode = "control-group";
+ SendSIGKILL = true;
+ UMask = "0077";
+ ConfigurationDirectory = serviceInstance;
+ ConfigurationDirectoryMode = "0500";
+ StateDirectory = serviceInstance;
+ StateDirectoryMode = "0700";
+ CacheDirectory = serviceInstance;
+ CacheDirectoryMode = "0700";
+ RuntimeDirectory = serviceInstance;
+ RuntimeDirectoryMode = if cfg.adminGroup == null then "0700" else "0750";
+ RuntimeDirectoryPreserve = false;
+ BindReadOnlyPaths = [
+ "${instance.configurationFile}:${configurationPath}"
+ ];
+ NoNewPrivileges = true;
+ PrivateTmp = true;
+ PrivateDevices = true;
+ ProtectSystem = "strict";
+ ProtectHome = true;
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectKernelLogs = true;
+ ProtectControlGroups = true;
+ ProtectHostname = true;
+ ProtectClock = true;
+ RestrictSUIDSGID = true;
+ LockPersonality = true;
+ CapabilityBoundingSet = "";
+ AmbientCapabilities = "";
+ RestrictAddressFamilies = addressFamilies;
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RemoveIPC = true;
+ DevicePolicy = "closed";
+ KeyringMode = "private";
+ ProcSubset = "pid";
+ ProtectProc = "invisible";
+ SystemCallArchitectures = "native";
+ }
+ // lib.optionalAttrs (credentialBindings != [ ]) {
+ LoadCredential = credentialBindings;
+ };
+ in
+ lib.nameValuePair (unitName instanceName) {
+ description = "Hardened ${serviceName} service instance ${instanceName}";
+ wantedBy = [ "multi-user.target" ];
+ wants = [ "network-online.target" ];
+ after = [ "network-online.target" ];
+ restartIfChanged = true;
+ stopIfChanged = true;
+ inherit serviceConfig;
+ };
+in
+{
+ options = lib.setAttrByPath optionPath {
+ enable = lib.mkEnableOption "the hardened ${serviceName} service";
+ package = lib.mkOption {
+ type = lib.types.package;
+ default = packageFor pkgs;
+ description = "The exact package containing the ${binaryName} service binary.";
+ };
+ adminGroup = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Optional existing group admitted by the service's local admin policy.";
+ };
+ instances = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.submodule {
+ options = {
+ configurationFile = lib.mkOption {
+ type = lib.types.path;
+ description = "A non-secret configuration file mounted read-only at the canonical path.";
+ };
+ credentials = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ description = "External absolute credential source paths passed through LoadCredential.";
+ };
+ };
+ }
+ );
+ default = { };
+ description = "Explicit service instances; every declared instance is active when enabled.";
+ };
+ };
+
+ config = {
+ inherit assertions;
+ systemd.services = lib.mkIf cfg.enable (lib.mapAttrs' mkService cfg.instances);
+ };
+}
diff --git a/flake.nix b/flake.nix
@@ -16,11 +16,24 @@
};
outputs =
- inputs@{ flake-parts, ... }:
+ inputs@{
+ self,
+ flake-parts,
+ ...
+ }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.treefmt-nix.flakeModule ];
systems = import ./build/nix/service/systems.nix;
+ flake.nixosModules.default =
+ (import ./build/nix/service/nixos-module.nix { lib = inputs.nixpkgs.lib; })
+ {
+ serviceName = "fixture_service";
+ binaryName = "fixture-service";
+ packageFor = pkgs: self.packages.${pkgs.stdenv.hostPlatform.system}.default;
+ commandForInstance = _: [ "--help" ];
+ };
+
perSystem =
{
config,
@@ -51,6 +64,7 @@
};
serviceFixture = import ./build/nix/service/fixture.nix {
inherit lib pkgs service;
+ nixosSystem = inputs.nixpkgs.lib.nixosSystem;
toolchain = toolchains.stable;
};
in