at 23.11-pre 18 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.taskserver; 7 8 taskd = "${pkgs.taskserver}/bin/taskd"; 9 10 mkManualPkiOption = desc: mkOption { 11 type = types.nullOr types.path; 12 default = null; 13 description = lib.mdDoc '' 14 ${desc} 15 16 ::: {.note} 17 Setting this option will prevent automatic CA creation and handling. 18 ::: 19 ''; 20 }; 21 22 manualPkiOptions = { 23 ca.cert = mkManualPkiOption '' 24 Fully qualified path to the CA certificate. 25 ''; 26 27 server.cert = mkManualPkiOption '' 28 Fully qualified path to the server certificate. 29 ''; 30 31 server.crl = mkManualPkiOption '' 32 Fully qualified path to the server certificate revocation list. 33 ''; 34 35 server.key = mkManualPkiOption '' 36 Fully qualified path to the server key. 37 ''; 38 }; 39 40 mkAutoDesc = preamble: lib.mdDoc '' 41 ${preamble} 42 43 ::: {.note} 44 This option is for the automatically handled CA and will be ignored if any 45 of the {option}`services.taskserver.pki.manual.*` options are set. 46 ::: 47 ''; 48 49 mkExpireOption = desc: mkOption { 50 type = types.nullOr types.int; 51 default = null; 52 example = 365; 53 apply = val: if val == null then -1 else val; 54 description = mkAutoDesc '' 55 The expiration time of ${desc} in days or `null` for no 56 expiration time. 57 ''; 58 }; 59 60 autoPkiOptions = { 61 bits = mkOption { 62 type = types.int; 63 default = 4096; 64 example = 2048; 65 description = mkAutoDesc "The bit size for generated keys."; 66 }; 67 68 expiration = { 69 ca = mkExpireOption "the CA certificate"; 70 server = mkExpireOption "the server certificate"; 71 client = mkExpireOption "client certificates"; 72 crl = mkExpireOption "the certificate revocation list (CRL)"; 73 }; 74 }; 75 76 needToCreateCA = let 77 notFound = path: let 78 dotted = concatStringsSep "." path; 79 in throw "Can't find option definitions for path `${dotted}'."; 80 findPkiDefinitions = path: attrs: let 81 mkSublist = key: val: let 82 newPath = path ++ singleton key; 83 in if isOption val 84 then attrByPath newPath (notFound newPath) cfg.pki.manual 85 else findPkiDefinitions newPath val; 86 in flatten (mapAttrsToList mkSublist attrs); 87 in all (x: x == null) (findPkiDefinitions [] manualPkiOptions); 88 89 orgOptions = { ... }: { 90 options.users = mkOption { 91 type = types.uniq (types.listOf types.str); 92 default = []; 93 example = [ "alice" "bob" ]; 94 description = lib.mdDoc '' 95 A list of user names that belong to the organization. 96 ''; 97 }; 98 99 options.groups = mkOption { 100 type = types.listOf types.str; 101 default = []; 102 example = [ "workers" "slackers" ]; 103 description = lib.mdDoc '' 104 A list of group names that belong to the organization. 105 ''; 106 }; 107 }; 108 109 certtool = "${pkgs.gnutls.bin}/bin/certtool"; 110 111 nixos-taskserver = with pkgs.python3.pkgs; buildPythonApplication { 112 name = "nixos-taskserver"; 113 114 src = pkgs.runCommand "nixos-taskserver-src" { preferLocalBuild = true; } '' 115 mkdir -p "$out" 116 cat "${pkgs.substituteAll { 117 src = ./helper-tool.py; 118 inherit taskd certtool; 119 inherit (cfg) dataDir user group fqdn; 120 certBits = cfg.pki.auto.bits; 121 clientExpiration = cfg.pki.auto.expiration.client; 122 crlExpiration = cfg.pki.auto.expiration.crl; 123 isAutoConfig = if needToCreateCA then "True" else "False"; 124 }}" > "$out/main.py" 125 cat > "$out/setup.py" <<EOF 126 from setuptools import setup 127 setup(name="nixos-taskserver", 128 py_modules=["main"], 129 install_requires=["Click"], 130 entry_points="[console_scripts]\\nnixos-taskserver=main:cli") 131 EOF 132 ''; 133 134 propagatedBuildInputs = [ click ]; 135 }; 136 137in { 138 options = { 139 services.taskserver = { 140 enable = mkOption { 141 type = types.bool; 142 default = false; 143 description = let 144 url = "https://nixos.org/manual/nixos/stable/index.html#module-services-taskserver"; 145 in lib.mdDoc '' 146 Whether to enable the Taskwarrior server. 147 148 More instructions about NixOS in conjunction with Taskserver can be 149 found [in the NixOS manual](${url}). 150 ''; 151 }; 152 153 user = mkOption { 154 type = types.str; 155 default = "taskd"; 156 description = lib.mdDoc "User for Taskserver."; 157 }; 158 159 group = mkOption { 160 type = types.str; 161 default = "taskd"; 162 description = lib.mdDoc "Group for Taskserver."; 163 }; 164 165 dataDir = mkOption { 166 type = types.path; 167 default = "/var/lib/taskserver"; 168 description = lib.mdDoc "Data directory for Taskserver."; 169 }; 170 171 ciphers = mkOption { 172 type = types.nullOr (types.separatedString ":"); 173 default = null; 174 example = "NORMAL:-VERS-SSL3.0"; 175 description = let 176 url = "https://gnutls.org/manual/html_node/Priority-Strings.html"; 177 in lib.mdDoc '' 178 List of GnuTLS ciphers to use. See the GnuTLS documentation about 179 priority strings at <${url}> for full details. 180 ''; 181 }; 182 183 organisations = mkOption { 184 type = types.attrsOf (types.submodule orgOptions); 185 default = {}; 186 example.myShinyOrganisation.users = [ "alice" "bob" ]; 187 example.myShinyOrganisation.groups = [ "staff" "outsiders" ]; 188 example.yetAnotherOrganisation.users = [ "foo" "bar" ]; 189 description = lib.mdDoc '' 190 An attribute set where the keys name the organisation and the values 191 are a set of lists of {option}`users` and 192 {option}`groups`. 193 ''; 194 }; 195 196 confirmation = mkOption { 197 type = types.bool; 198 default = true; 199 description = lib.mdDoc '' 200 Determines whether certain commands are confirmed. 201 ''; 202 }; 203 204 debug = mkOption { 205 type = types.bool; 206 default = false; 207 description = lib.mdDoc '' 208 Logs debugging information. 209 ''; 210 }; 211 212 extensions = mkOption { 213 type = types.nullOr types.path; 214 default = null; 215 description = lib.mdDoc '' 216 Fully qualified path of the Taskserver extension scripts. 217 Currently there are none. 218 ''; 219 }; 220 221 ipLog = mkOption { 222 type = types.bool; 223 default = false; 224 description = lib.mdDoc '' 225 Logs the IP addresses of incoming requests. 226 ''; 227 }; 228 229 queueSize = mkOption { 230 type = types.int; 231 default = 10; 232 description = lib.mdDoc '' 233 Size of the connection backlog, see {manpage}`listen(2)`. 234 ''; 235 }; 236 237 requestLimit = mkOption { 238 type = types.int; 239 default = 1048576; 240 description = lib.mdDoc '' 241 Size limit of incoming requests, in bytes. 242 ''; 243 }; 244 245 allowedClientIDs = mkOption { 246 type = with types; either str (listOf str); 247 default = []; 248 example = [ "[Tt]ask [2-9]+" ]; 249 description = lib.mdDoc '' 250 A list of regular expressions that are matched against the reported 251 client id (such as `task 2.3.0`). 252 253 The values `all` or `none` have 254 special meaning. Overridden by any entry in the option 255 {option}`services.taskserver.disallowedClientIDs`. 256 ''; 257 }; 258 259 disallowedClientIDs = mkOption { 260 type = with types; either str (listOf str); 261 default = []; 262 example = [ "[Tt]ask [2-9]+" ]; 263 description = lib.mdDoc '' 264 A list of regular expressions that are matched against the reported 265 client id (such as `task 2.3.0`). 266 267 The values `all` or `none` have 268 special meaning. Any entry here overrides those in 269 {option}`services.taskserver.allowedClientIDs`. 270 ''; 271 }; 272 273 listenHost = mkOption { 274 type = types.str; 275 default = "localhost"; 276 example = "::"; 277 description = lib.mdDoc '' 278 The address (IPv4, IPv6 or DNS) to listen on. 279 ''; 280 }; 281 282 listenPort = mkOption { 283 type = types.int; 284 default = 53589; 285 description = lib.mdDoc '' 286 Port number of the Taskserver. 287 ''; 288 }; 289 290 openFirewall = mkOption { 291 type = types.bool; 292 default = false; 293 description = lib.mdDoc '' 294 Whether to open the firewall for the specified Taskserver port. 295 ''; 296 }; 297 298 fqdn = mkOption { 299 type = types.str; 300 default = "localhost"; 301 description = lib.mdDoc '' 302 The fully qualified domain name of this server, which is also used 303 as the common name in the certificates. 304 ''; 305 }; 306 307 trust = mkOption { 308 type = types.enum [ "allow all" "strict" ]; 309 default = "strict"; 310 description = lib.mdDoc '' 311 Determines how client certificates are validated. 312 313 The value `allow all` performs no client 314 certificate validation. This is not recommended. The value 315 `strict` causes the client certificate to be 316 validated against a CA. 317 ''; 318 }; 319 320 pki.manual = manualPkiOptions; 321 pki.auto = autoPkiOptions; 322 323 config = mkOption { 324 type = types.attrs; 325 example.client.cert = "/tmp/debugging.cert"; 326 description = lib.mdDoc '' 327 Configuration options to pass to Taskserver. 328 329 The options here are the same as described in 330 {manpage}`taskdrc(5)`, but with one difference: 331 332 The `server` option is 333 `server.listen` here, because the 334 `server` option would collide with other options 335 like `server.cert` and we would run in a type error 336 (attribute set versus string). 337 338 Nix types like integers or booleans are automatically converted to 339 the right values Taskserver would expect. 340 ''; 341 apply = let 342 mkKey = path: if path == ["server" "listen"] then "server" 343 else concatStringsSep "." path; 344 recurse = path: attrs: let 345 mapper = name: val: let 346 newPath = path ++ [ name ]; 347 scalar = if val == true then "true" 348 else if val == false then "false" 349 else toString val; 350 in if isAttrs val then recurse newPath val 351 else [ "${mkKey newPath}=${scalar}" ]; 352 in concatLists (mapAttrsToList mapper attrs); 353 in recurse []; 354 }; 355 }; 356 }; 357 358 imports = [ 359 (mkRemovedOptionModule ["services" "taskserver" "extraConfig"] '' 360 This option was removed in favor of `services.taskserver.config` with 361 different semantics (it's now a list of attributes instead of lines). 362 363 Please look up the documentation of `services.taskserver.config' to get 364 more information about the new way to pass additional configuration 365 options. 366 '') 367 ]; 368 369 config = mkMerge [ 370 (mkIf cfg.enable { 371 environment.systemPackages = [ nixos-taskserver ]; 372 373 users.users = optionalAttrs (cfg.user == "taskd") { 374 taskd = { 375 uid = config.ids.uids.taskd; 376 description = "Taskserver user"; 377 group = cfg.group; 378 }; 379 }; 380 381 users.groups = optionalAttrs (cfg.group == "taskd") { 382 taskd.gid = config.ids.gids.taskd; 383 }; 384 385 services.taskserver.config = { 386 # systemd related 387 daemon = false; 388 log = "-"; 389 390 # logging 391 debug = cfg.debug; 392 ip.log = cfg.ipLog; 393 394 # general 395 ciphers = cfg.ciphers; 396 confirmation = cfg.confirmation; 397 extensions = cfg.extensions; 398 queue.size = cfg.queueSize; 399 request.limit = cfg.requestLimit; 400 401 # client 402 client.allow = cfg.allowedClientIDs; 403 client.deny = cfg.disallowedClientIDs; 404 405 # server 406 trust = cfg.trust; 407 server = { 408 listen = "${cfg.listenHost}:${toString cfg.listenPort}"; 409 } // (if needToCreateCA then { 410 cert = "${cfg.dataDir}/keys/server.cert"; 411 key = "${cfg.dataDir}/keys/server.key"; 412 crl = "${cfg.dataDir}/keys/server.crl"; 413 } else { 414 cert = "${cfg.pki.manual.server.cert}"; 415 key = "${cfg.pki.manual.server.key}"; 416 ${mapNullable (_: "crl") cfg.pki.manual.server.crl} = "${cfg.pki.manual.server.crl}"; 417 }); 418 419 ca.cert = if needToCreateCA then "${cfg.dataDir}/keys/ca.cert" 420 else "${cfg.pki.manual.ca.cert}"; 421 }; 422 423 systemd.services.taskserver-init = { 424 wantedBy = [ "taskserver.service" ]; 425 before = [ "taskserver.service" ]; 426 description = "Initialize Taskserver Data Directory"; 427 428 preStart = '' 429 mkdir -m 0770 -p "${cfg.dataDir}" 430 chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}" 431 ''; 432 433 script = '' 434 ${taskd} init 435 touch "${cfg.dataDir}/.is_initialized" 436 ''; 437 438 environment.TASKDDATA = cfg.dataDir; 439 440 unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized"; 441 442 serviceConfig.Type = "oneshot"; 443 serviceConfig.User = cfg.user; 444 serviceConfig.Group = cfg.group; 445 serviceConfig.PermissionsStartOnly = true; 446 serviceConfig.PrivateNetwork = true; 447 serviceConfig.PrivateDevices = true; 448 serviceConfig.PrivateTmp = true; 449 }; 450 451 systemd.services.taskserver = { 452 description = "Taskwarrior Server"; 453 454 wantedBy = [ "multi-user.target" ]; 455 after = [ "network.target" ]; 456 457 environment.TASKDDATA = cfg.dataDir; 458 459 preStart = let 460 jsonOrgs = builtins.toJSON cfg.organisations; 461 jsonFile = pkgs.writeText "orgs.json" jsonOrgs; 462 helperTool = "${nixos-taskserver}/bin/nixos-taskserver"; 463 in "${helperTool} process-json '${jsonFile}'"; 464 465 serviceConfig = { 466 ExecStart = let 467 mkCfgFlag = flag: escapeShellArg "--${flag}"; 468 cfgFlags = concatMapStringsSep " " mkCfgFlag cfg.config; 469 in "@${taskd} taskd server ${cfgFlags}"; 470 ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID"; 471 Restart = "on-failure"; 472 PermissionsStartOnly = true; 473 PrivateTmp = true; 474 PrivateDevices = true; 475 User = cfg.user; 476 Group = cfg.group; 477 }; 478 }; 479 }) 480 (mkIf (cfg.enable && needToCreateCA) { 481 systemd.services.taskserver-ca = { 482 wantedBy = [ "taskserver.service" ]; 483 after = [ "taskserver-init.service" ]; 484 before = [ "taskserver.service" ]; 485 description = "Initialize CA for TaskServer"; 486 serviceConfig.Type = "oneshot"; 487 serviceConfig.UMask = "0077"; 488 serviceConfig.PrivateNetwork = true; 489 serviceConfig.PrivateTmp = true; 490 491 script = '' 492 silent_certtool() { 493 if ! output="$("${certtool}" "$@" 2>&1)"; then 494 echo "GNUTLS certtool invocation failed with output:" >&2 495 echo "$output" >&2 496 fi 497 } 498 499 mkdir -m 0700 -p "${cfg.dataDir}/keys" 500 chown root:root "${cfg.dataDir}/keys" 501 502 if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then 503 silent_certtool -p \ 504 --bits ${toString cfg.pki.auto.bits} \ 505 --outfile "${cfg.dataDir}/keys/ca.key" 506 silent_certtool -s \ 507 --template "${pkgs.writeText "taskserver-ca.template" '' 508 cn = ${cfg.fqdn} 509 expiration_days = ${toString cfg.pki.auto.expiration.ca} 510 cert_signing_key 511 ca 512 ''}" \ 513 --load-privkey "${cfg.dataDir}/keys/ca.key" \ 514 --outfile "${cfg.dataDir}/keys/ca.cert" 515 516 chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert" 517 chmod g+r "${cfg.dataDir}/keys/ca.cert" 518 fi 519 520 if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then 521 silent_certtool -p \ 522 --bits ${toString cfg.pki.auto.bits} \ 523 --outfile "${cfg.dataDir}/keys/server.key" 524 525 silent_certtool -c \ 526 --template "${pkgs.writeText "taskserver-cert.template" '' 527 cn = ${cfg.fqdn} 528 expiration_days = ${toString cfg.pki.auto.expiration.server} 529 tls_www_server 530 encryption_key 531 signing_key 532 ''}" \ 533 --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ 534 --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ 535 --load-privkey "${cfg.dataDir}/keys/server.key" \ 536 --outfile "${cfg.dataDir}/keys/server.cert" 537 538 chgrp "${cfg.group}" \ 539 "${cfg.dataDir}/keys/server.key" \ 540 "${cfg.dataDir}/keys/server.cert" 541 542 chmod g+r \ 543 "${cfg.dataDir}/keys/server.key" \ 544 "${cfg.dataDir}/keys/server.cert" 545 fi 546 547 if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then 548 silent_certtool --generate-crl \ 549 --template "${pkgs.writeText "taskserver-crl.template" '' 550 expiration_days = ${toString cfg.pki.auto.expiration.crl} 551 ''}" \ 552 --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ 553 --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ 554 --outfile "${cfg.dataDir}/keys/server.crl" 555 556 chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl" 557 chmod g+r "${cfg.dataDir}/keys/server.crl" 558 fi 559 560 chmod go+x "${cfg.dataDir}/keys" 561 ''; 562 }; 563 }) 564 (mkIf (cfg.enable && cfg.openFirewall) { 565 networking.firewall.allowedTCPPorts = [ cfg.listenPort ]; 566 }) 567 ]; 568 569 meta.doc = ./default.md; 570}