lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

nixos-module.nix (8881B)


      1 { lib }:
      2 {
      3   serviceName,
      4   binaryName,
      5   packageFor,
      6   commandForInstance,
      7   stopTimeoutSeconds ? 30,
      8   addressFamilies ? [
      9     "AF_UNIX"
     10     "AF_INET"
     11     "AF_INET6"
     12   ],
     13 }:
     14 assert lib.assertMsg (
     15   builtins.isString serviceName
     16   && builtins.stringLength serviceName <= 128
     17   && builtins.match "^[a-z][a-z0-9_]*$" serviceName != null
     18 ) "serviceName must be a bounded lowercase snake-case identifier";
     19 assert lib.assertMsg (
     20   builtins.isString binaryName
     21   && builtins.stringLength binaryName <= 128
     22   && builtins.match "^[a-z][a-z0-9_-]*$" binaryName != null
     23 ) "binaryName must be a lowercase Cargo binary identifier";
     24 assert lib.assertMsg (builtins.isFunction packageFor) "packageFor must be a function";
     25 assert lib.assertMsg (builtins.isFunction commandForInstance)
     26   "commandForInstance must be a function";
     27 assert lib.assertMsg (
     28   builtins.isInt stopTimeoutSeconds && stopTimeoutSeconds > 0 && stopTimeoutSeconds <= 86400
     29 ) "stopTimeoutSeconds must be between 1 and 86400";
     30 assert lib.assertMsg (
     31   builtins.isList addressFamilies
     32   && addressFamilies != [ ]
     33   && builtins.length addressFamilies <= 3
     34   && lib.all (
     35     family:
     36     builtins.elem family [
     37       "AF_UNIX"
     38       "AF_INET"
     39       "AF_INET6"
     40     ]
     41   ) addressFamilies
     42   && builtins.length (lib.unique addressFamilies) == builtins.length addressFamilies
     43 ) "addressFamilies must be a unique nonempty subset of the governed families";
     44 {
     45   config,
     46   pkgs,
     47   ...
     48 }:
     49 let
     50   optionPath = [
     51     "services"
     52     "radroots"
     53     serviceName
     54   ];
     55   cfg = lib.getAttrFromPath optionPath config;
     56   instanceNames = builtins.attrNames cfg.instances;
     57   systemUser = "radroots-${serviceName}";
     58   validInstanceName =
     59     name:
     60     builtins.isString name
     61     && builtins.stringLength name <= 128
     62     && builtins.match "^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$" name != null;
     63   unitName = instanceName: "radroots-${serviceName}-${instanceName}";
     64   validUnitName = instanceName: builtins.stringLength (unitName instanceName) <= 247;
     65   validCredentialName =
     66     name:
     67     builtins.stringLength name <= 128 && builtins.match "^[A-Za-z0-9][A-Za-z0-9_.-]*$" name != null;
     68   validCredentialPath =
     69     path:
     70     let
     71       segments = if builtins.isString path then lib.splitString "/" path else [ ];
     72     in
     73     builtins.isString path
     74     && builtins.stringLength path > 1
     75     && builtins.stringLength path <= 4096
     76     && lib.hasPrefix "/" path
     77     && lib.last segments != ""
     78     && lib.all (segment: segment != "" && segment != "." && segment != "..") (lib.drop 1 segments)
     79     && path != "/nix/store"
     80     && !(lib.hasPrefix "/nix/store/" path)
     81     && !(lib.hasInfix ":" path)
     82     && !(lib.hasInfix "\r" path)
     83     && !(lib.hasInfix "\n" path);
     84   validCredentials =
     85     instance:
     86     builtins.length (builtins.attrNames instance.credentials) <= 32
     87     && lib.all (name: validCredentialName name && validCredentialPath instance.credentials.${name}) (
     88       builtins.attrNames instance.credentials
     89     );
     90   commandFor = instanceName: commandForInstance instanceName;
     91   validCommand =
     92     instanceName:
     93     let
     94       command = commandFor instanceName;
     95     in
     96     builtins.isList command
     97     && builtins.length command <= 64
     98     && lib.all (
     99       argument:
    100       builtins.isString argument
    101       && builtins.stringLength argument <= 4096
    102       && !(lib.hasInfix "\r" argument)
    103       && !(lib.hasInfix "\n" argument)
    104     ) command;
    105   package = cfg.package;
    106   assertions = [
    107     {
    108       assertion = cfg.enable == (instanceNames != [ ]);
    109       message = "the service must be enabled with at least one instance and disabled without instances";
    110     }
    111     {
    112       assertion = builtins.length instanceNames <= 64;
    113       message = "a service may define at most 64 instances";
    114     }
    115     {
    116       assertion = lib.isDerivation package;
    117       message = "the service package must be a derivation";
    118     }
    119     {
    120       assertion =
    121         cfg.adminGroup == null
    122         || (
    123           builtins.stringLength cfg.adminGroup <= 128
    124           && builtins.match "^[a-z_][a-z0-9_-]*$" cfg.adminGroup != null
    125         );
    126       message = "adminGroup must be a bounded canonical system group name";
    127     }
    128   ]
    129   ++ lib.concatMap (
    130     instanceName:
    131     let
    132       instance = cfg.instances.${instanceName};
    133     in
    134     [
    135       {
    136         assertion = validInstanceName instanceName;
    137         message = "every service instance must use a bounded canonical identifier";
    138       }
    139       {
    140         assertion = validUnitName instanceName;
    141         message = "the derived systemd unit name exceeds its 255-byte limit";
    142       }
    143       {
    144         assertion = validCredentials instance;
    145         message = "credentials must use bounded names and external absolute source paths";
    146       }
    147       {
    148         assertion = validCommand instanceName;
    149         message = "the service command must contain at most 64 bounded single-line arguments";
    150       }
    151     ]
    152   ) instanceNames;
    153   mkService =
    154     instanceName: instance:
    155     let
    156       serviceInstance = "radroots/services/${serviceName}/${instanceName}";
    157       configurationPath = "/etc/${serviceInstance}/config.toml";
    158       credentialBindings = lib.mapAttrsToList (name: path: "${name}:${path}") instance.credentials;
    159       serviceConfig = {
    160         Type = "simple";
    161         User = systemUser;
    162         Group = if cfg.adminGroup == null then systemUser else cfg.adminGroup;
    163         DynamicUser = true;
    164         ExecStart = lib.escapeShellArgs ([ "${package}/bin/${binaryName}" ] ++ commandFor instanceName);
    165         WorkingDirectory = "/";
    166         Restart = "on-failure";
    167         RestartSec = "5s";
    168         TimeoutStopSec = "${toString stopTimeoutSeconds}s";
    169         KillSignal = "SIGTERM";
    170         KillMode = "control-group";
    171         SendSIGKILL = true;
    172         UMask = "0077";
    173         ConfigurationDirectory = serviceInstance;
    174         ConfigurationDirectoryMode = "0500";
    175         StateDirectory = serviceInstance;
    176         StateDirectoryMode = "0700";
    177         CacheDirectory = serviceInstance;
    178         CacheDirectoryMode = "0700";
    179         RuntimeDirectory = serviceInstance;
    180         RuntimeDirectoryMode = if cfg.adminGroup == null then "0700" else "0750";
    181         RuntimeDirectoryPreserve = false;
    182         BindReadOnlyPaths = [
    183           "${instance.configurationFile}:${configurationPath}"
    184         ];
    185         NoNewPrivileges = true;
    186         PrivateTmp = true;
    187         PrivateDevices = true;
    188         ProtectSystem = "strict";
    189         ProtectHome = true;
    190         ProtectKernelTunables = true;
    191         ProtectKernelModules = true;
    192         ProtectKernelLogs = true;
    193         ProtectControlGroups = true;
    194         ProtectHostname = true;
    195         ProtectClock = true;
    196         RestrictSUIDSGID = true;
    197         LockPersonality = true;
    198         CapabilityBoundingSet = "";
    199         AmbientCapabilities = "";
    200         RestrictAddressFamilies = addressFamilies;
    201         RestrictNamespaces = true;
    202         RestrictRealtime = true;
    203         RemoveIPC = true;
    204         DevicePolicy = "closed";
    205         KeyringMode = "private";
    206         ProcSubset = "pid";
    207         ProtectProc = "invisible";
    208         SystemCallArchitectures = "native";
    209       }
    210       // lib.optionalAttrs (credentialBindings != [ ]) {
    211         LoadCredential = credentialBindings;
    212       };
    213     in
    214     lib.nameValuePair (unitName instanceName) {
    215       description = "Hardened ${serviceName} service instance ${instanceName}";
    216       wantedBy = [ "multi-user.target" ];
    217       wants = [ "network-online.target" ];
    218       after = [ "network-online.target" ];
    219       restartIfChanged = true;
    220       stopIfChanged = true;
    221       inherit serviceConfig;
    222     };
    223 in
    224 {
    225   options = lib.setAttrByPath optionPath {
    226     enable = lib.mkEnableOption "the hardened ${serviceName} service";
    227     package = lib.mkOption {
    228       type = lib.types.package;
    229       default = packageFor pkgs;
    230       description = "The exact package containing the ${binaryName} service binary.";
    231     };
    232     adminGroup = lib.mkOption {
    233       type = lib.types.nullOr lib.types.str;
    234       default = null;
    235       description = "Optional existing group admitted by the service's local admin policy.";
    236     };
    237     instances = lib.mkOption {
    238       type = lib.types.attrsOf (
    239         lib.types.submodule {
    240           options = {
    241             configurationFile = lib.mkOption {
    242               type = lib.types.path;
    243               description = "A non-secret configuration file mounted read-only at the canonical path.";
    244             };
    245             credentials = lib.mkOption {
    246               type = lib.types.attrsOf lib.types.str;
    247               default = { };
    248               description = "External absolute credential source paths passed through LoadCredential.";
    249             };
    250           };
    251         }
    252       );
    253       default = { };
    254       description = "Explicit service instances; every declared instance is active when enabled.";
    255     };
    256   };
    257 
    258   config = {
    259     inherit assertions;
    260     systemd.services = lib.mkIf cfg.enable (lib.mapAttrs' mkService cfg.instances);
    261   };
    262 }