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 }