at master 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 format = "setuptools"; 141 name = "nixos-taskserver"; 142 143 src = pkgs.runCommand "nixos-taskserver-src" { preferLocalBuild = true; } '' 144 mkdir -p "$out" 145 cat "${ 146 pkgs.replaceVars ./helper-tool.py { 147 inherit taskd certtool; 148 inherit (cfg) 149 dataDir 150 user 151 group 152 fqdn 153 ; 154 certBits = cfg.pki.auto.bits; 155 clientExpiration = cfg.pki.auto.expiration.client; 156 crlExpiration = cfg.pki.auto.expiration.crl; 157 isAutoConfig = if needToCreateCA then "True" else "False"; 158 } 159 }" > "$out/main.py" 160 cat > "$out/setup.py" <<EOF 161 from setuptools import setup 162 setup(name="nixos-taskserver", 163 py_modules=["main"], 164 install_requires=["Click"], 165 entry_points="[console_scripts]\\nnixos-taskserver=main:cli") 166 EOF 167 ''; 168 169 propagatedBuildInputs = [ click ]; 170 }; 171 172in 173{ 174 options = { 175 services.taskserver = { 176 enable = lib.mkOption { 177 type = lib.types.bool; 178 default = false; 179 description = 180 let 181 url = "https://nixos.org/manual/nixos/stable/index.html#module-services-taskserver"; 182 in 183 '' 184 Whether to enable the Taskwarrior 2 server. 185 186 More instructions about NixOS in conjunction with Taskserver can be 187 found [in the NixOS manual](${url}). 188 ''; 189 }; 190 191 user = lib.mkOption { 192 type = lib.types.str; 193 default = "taskd"; 194 description = "User for Taskserver."; 195 }; 196 197 group = lib.mkOption { 198 type = lib.types.str; 199 default = "taskd"; 200 description = "Group for Taskserver."; 201 }; 202 203 dataDir = lib.mkOption { 204 type = lib.types.path; 205 default = "/var/lib/taskserver"; 206 description = "Data directory for Taskserver."; 207 }; 208 209 ciphers = lib.mkOption { 210 type = lib.types.nullOr (lib.types.separatedString ":"); 211 default = null; 212 example = "NORMAL:-VERS-SSL3.0"; 213 description = 214 let 215 url = "https://gnutls.org/manual/html_node/Priority-Strings.html"; 216 in 217 '' 218 List of GnuTLS ciphers to use. See the GnuTLS documentation about 219 priority strings at <${url}> for full details. 220 ''; 221 }; 222 223 organisations = lib.mkOption { 224 type = lib.types.attrsOf (lib.types.submodule orgOptions); 225 default = { }; 226 example.myShinyOrganisation.users = [ 227 "alice" 228 "bob" 229 ]; 230 example.myShinyOrganisation.groups = [ 231 "staff" 232 "outsiders" 233 ]; 234 example.yetAnotherOrganisation.users = [ 235 "foo" 236 "bar" 237 ]; 238 description = '' 239 An attribute set where the keys name the organisation and the values 240 are a set of lists of {option}`users` and 241 {option}`groups`. 242 ''; 243 }; 244 245 confirmation = lib.mkOption { 246 type = lib.types.bool; 247 default = true; 248 description = '' 249 Determines whether certain commands are confirmed. 250 ''; 251 }; 252 253 debug = lib.mkOption { 254 type = lib.types.bool; 255 default = false; 256 description = '' 257 Logs debugging information. 258 ''; 259 }; 260 261 extensions = lib.mkOption { 262 type = lib.types.nullOr lib.types.path; 263 default = null; 264 description = '' 265 Fully qualified path of the Taskserver extension scripts. 266 Currently there are none. 267 ''; 268 }; 269 270 ipLog = lib.mkOption { 271 type = lib.types.bool; 272 default = false; 273 description = '' 274 Logs the IP addresses of incoming requests. 275 ''; 276 }; 277 278 queueSize = lib.mkOption { 279 type = lib.types.int; 280 default = 10; 281 description = '' 282 Size of the connection backlog, see {manpage}`listen(2)`. 283 ''; 284 }; 285 286 requestLimit = lib.mkOption { 287 type = lib.types.int; 288 default = 1048576; 289 description = '' 290 Size limit of incoming requests, in bytes. 291 ''; 292 }; 293 294 allowedClientIDs = lib.mkOption { 295 type = with lib.types; either str (listOf str); 296 default = [ ]; 297 example = [ "[Tt]ask [2-9]+" ]; 298 description = '' 299 A list of regular expressions that are matched against the reported 300 client id (such as `task 2.3.0`). 301 302 The values `all` or `none` have 303 special meaning. Overridden by any entry in the option 304 {option}`services.taskserver.disallowedClientIDs`. 305 ''; 306 }; 307 308 disallowedClientIDs = lib.mkOption { 309 type = with lib.types; either str (listOf str); 310 default = [ ]; 311 example = [ "[Tt]ask [2-9]+" ]; 312 description = '' 313 A list of regular expressions that are matched against the reported 314 client id (such as `task 2.3.0`). 315 316 The values `all` or `none` have 317 special meaning. Any entry here overrides those in 318 {option}`services.taskserver.allowedClientIDs`. 319 ''; 320 }; 321 322 listenHost = lib.mkOption { 323 type = lib.types.str; 324 default = "localhost"; 325 example = "::"; 326 description = '' 327 The address (IPv4, IPv6 or DNS) to listen on. 328 ''; 329 }; 330 331 listenPort = lib.mkOption { 332 type = lib.types.port; 333 default = 53589; 334 description = '' 335 Port number of the Taskserver. 336 ''; 337 }; 338 339 openFirewall = lib.mkOption { 340 type = lib.types.bool; 341 default = false; 342 description = '' 343 Whether to open the firewall for the specified Taskserver port. 344 ''; 345 }; 346 347 fqdn = lib.mkOption { 348 type = lib.types.str; 349 default = "localhost"; 350 description = '' 351 The fully qualified domain name of this server, which is also used 352 as the common name in the certificates. 353 ''; 354 }; 355 356 trust = lib.mkOption { 357 type = lib.types.enum [ 358 "allow all" 359 "strict" 360 ]; 361 default = "strict"; 362 description = '' 363 Determines how client certificates are validated. 364 365 The value `allow all` performs no client 366 certificate validation. This is not recommended. The value 367 `strict` causes the client certificate to be 368 validated against a CA. 369 ''; 370 }; 371 372 pki.manual = manualPkiOptions; 373 pki.auto = autoPkiOptions; 374 375 config = lib.mkOption { 376 type = lib.types.attrs; 377 example.client.cert = "/tmp/debugging.cert"; 378 description = '' 379 Configuration options to pass to Taskserver. 380 381 The options here are the same as described in 382 {manpage}`taskdrc(5)` from the `taskwarrior2` package, but with one difference: 383 384 The `server` option is 385 `server.listen` here, because the 386 `server` option would collide with other options 387 like `server.cert` and we would run in a type error 388 (attribute set versus string). 389 390 Nix types like integers or booleans are automatically converted to 391 the right values Taskserver would expect. 392 ''; 393 apply = 394 let 395 mkKey = 396 path: 397 if 398 path == [ 399 "server" 400 "listen" 401 ] 402 then 403 "server" 404 else 405 lib.concatStringsSep "." path; 406 recurse = 407 path: attrs: 408 let 409 mapper = 410 name: val: 411 let 412 newPath = path ++ [ name ]; 413 scalar = 414 if val == true then 415 "true" 416 else if val == false then 417 "false" 418 else 419 toString val; 420 in 421 if lib.isAttrs val then recurse newPath val else [ "${mkKey newPath}=${scalar}" ]; 422 in 423 lib.concatLists (lib.mapAttrsToList mapper attrs); 424 in 425 recurse [ ]; 426 }; 427 }; 428 }; 429 430 imports = [ 431 (lib.mkRemovedOptionModule [ "services" "taskserver" "extraConfig" ] '' 432 This option was removed in favor of `services.taskserver.config` with 433 different semantics (it's now a list of attributes instead of lines). 434 435 Please look up the documentation of `services.taskserver.config' to get 436 more information about the new way to pass additional configuration 437 options. 438 '') 439 ]; 440 441 config = lib.mkMerge [ 442 (lib.mkIf cfg.enable { 443 environment.systemPackages = [ nixos-taskserver ]; 444 445 users.users = lib.optionalAttrs (cfg.user == "taskd") { 446 taskd = { 447 uid = config.ids.uids.taskd; 448 description = "Taskserver user"; 449 group = cfg.group; 450 }; 451 }; 452 453 users.groups = lib.optionalAttrs (cfg.group == "taskd") { 454 taskd.gid = config.ids.gids.taskd; 455 }; 456 457 services.taskserver.config = { 458 # systemd related 459 daemon = false; 460 log = "-"; 461 462 # logging 463 debug = cfg.debug; 464 ip.log = cfg.ipLog; 465 466 # general 467 ciphers = cfg.ciphers; 468 confirmation = cfg.confirmation; 469 extensions = cfg.extensions; 470 queue.size = cfg.queueSize; 471 request.limit = cfg.requestLimit; 472 473 # client 474 client.allow = cfg.allowedClientIDs; 475 client.deny = cfg.disallowedClientIDs; 476 477 # server 478 trust = cfg.trust; 479 server = { 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}