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