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