at 25.11-pre 16 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9 10let 11 cfg = config.services.caddy; 12 13 certs = config.security.acme.certs; 14 virtualHosts = attrValues cfg.virtualHosts; 15 acmeEnabledVhosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts; 16 vhostCertNames = unique (map (hostOpts: hostOpts.useACMEHost) acmeEnabledVhosts); 17 dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server 18 independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server 19 20 mkVHostConf = 21 hostOpts: 22 let 23 sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory; 24 in 25 '' 26 ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} { 27 ${optionalString ( 28 hostOpts.listenAddresses != [ ] 29 ) "bind ${concatStringsSep " " hostOpts.listenAddresses}"} 30 ${optionalString ( 31 hostOpts.useACMEHost != null 32 ) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"} 33 log { 34 ${hostOpts.logFormat} 35 } 36 37 ${hostOpts.extraConfig} 38 } 39 ''; 40 41 settingsFormat = pkgs.formats.json { }; 42 43 configFile = 44 if cfg.settings != { } then 45 settingsFormat.generate "caddy.json" cfg.settings 46 else 47 let 48 Caddyfile = pkgs.writeTextDir "Caddyfile" '' 49 { 50 ${cfg.globalConfig} 51 } 52 ${cfg.extraConfig} 53 ${concatMapStringsSep "\n" mkVHostConf virtualHosts} 54 ''; 55 56 Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { } '' 57 mkdir -p $out 58 cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile 59 ${lib.getExe cfg.package} fmt --overwrite $out/Caddyfile 60 ''; 61 in 62 "${ 63 if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile 64 }/Caddyfile"; 65 66 etcConfigFile = "caddy/caddy_config"; 67 68 configPath = "/etc/${etcConfigFile}"; 69 70 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib; 71in 72{ 73 imports = [ 74 (mkRemovedOptionModule [ 75 "services" 76 "caddy" 77 "agree" 78 ] "this option is no longer necessary for Caddy 2") 79 (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ]) 80 (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ]) 81 ]; 82 83 # interface 84 options.services.caddy = { 85 enable = mkEnableOption "Caddy web server"; 86 87 user = mkOption { 88 default = "caddy"; 89 type = types.str; 90 description = '' 91 User account under which caddy runs. 92 93 ::: {.note} 94 If left as the default value this user will automatically be created 95 on system activation, otherwise you are responsible for 96 ensuring the user exists before the Caddy service starts. 97 ::: 98 ''; 99 }; 100 101 group = mkOption { 102 default = "caddy"; 103 type = types.str; 104 description = '' 105 Group under which caddy runs. 106 107 ::: {.note} 108 If left as the default value this group will automatically be created 109 on system activation, otherwise you are responsible for 110 ensuring the group exists before the Caddy service starts. 111 ::: 112 ''; 113 }; 114 115 package = mkPackageOption pkgs "caddy" { }; 116 117 dataDir = mkOption { 118 type = types.path; 119 default = "/var/lib/caddy"; 120 description = '' 121 The data directory for caddy. 122 123 ::: {.note} 124 If left as the default value this directory will automatically be created 125 before the Caddy server starts, otherwise you are responsible for ensuring 126 the directory exists with appropriate ownership and permissions. 127 128 Caddy v2 replaced `CADDYPATH` with XDG directories. 129 See <https://caddyserver.com/docs/conventions#file-locations>. 130 ::: 131 ''; 132 }; 133 134 logDir = mkOption { 135 type = types.path; 136 default = "/var/log/caddy"; 137 description = '' 138 Directory for storing Caddy access logs. 139 140 ::: {.note} 141 If left as the default value this directory will automatically be created 142 before the Caddy server starts, otherwise the sysadmin is responsible for 143 ensuring the directory exists with appropriate ownership and permissions. 144 ::: 145 ''; 146 }; 147 148 logFormat = mkOption { 149 type = types.lines; 150 default = '' 151 level ERROR 152 ''; 153 example = literalExpression '' 154 mkForce "level INFO"; 155 ''; 156 description = '' 157 Configuration for the default logger. See 158 <https://caddyserver.com/docs/caddyfile/options#log> 159 for details. 160 ''; 161 }; 162 163 configFile = mkOption { 164 type = types.path; 165 default = configFile; 166 defaultText = "A Caddyfile automatically generated by values from services.caddy.*"; 167 example = literalExpression '' 168 pkgs.writeText "Caddyfile" ''' 169 example.com 170 171 root * /var/www/wordpress 172 php_fastcgi unix//run/php/php-version-fpm.sock 173 file_server 174 '''; 175 ''; 176 description = '' 177 Override the configuration file used by Caddy. By default, 178 NixOS generates one automatically. 179 180 The configuration file is exposed at {file}`${configPath}`. 181 ''; 182 }; 183 184 adapter = mkOption { 185 default = 186 if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then 187 "caddyfile" 188 else 189 null; 190 defaultText = literalExpression '' 191 if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then "caddyfile" else null 192 ''; 193 example = literalExpression "nginx"; 194 type = with types; nullOr str; 195 description = '' 196 Name of the config adapter to use. 197 See <https://caddyserver.com/docs/config-adapters> 198 for the full list. 199 200 If `null` is specified, the `--adapter` argument is omitted when 201 starting or restarting Caddy. Notably, this allows specification of a 202 configuration file in Caddy's native JSON format, as long as the 203 filename does not start with `Caddyfile` (in which case the `caddyfile` 204 adapter is implicitly enabled). See 205 <https://caddyserver.com/docs/command-line#caddy-run> for details. 206 207 ::: {.note} 208 Any value other than `null` or `caddyfile` is only valid when providing 209 your own `configFile`. 210 ::: 211 ''; 212 }; 213 214 resume = mkOption { 215 default = false; 216 type = types.bool; 217 description = '' 218 Use saved config, if any (and prefer over any specified configuration passed with `--config`). 219 ''; 220 }; 221 222 globalConfig = mkOption { 223 type = types.lines; 224 default = ""; 225 example = '' 226 debug 227 servers { 228 protocol { 229 experimental_http3 230 } 231 } 232 ''; 233 description = '' 234 Additional lines of configuration appended to the global config section 235 of the `Caddyfile`. 236 237 Refer to <https://caddyserver.com/docs/caddyfile/options#global-options> 238 for details on supported values. 239 ''; 240 }; 241 242 extraConfig = mkOption { 243 type = types.lines; 244 default = ""; 245 example = '' 246 example.com { 247 encode gzip 248 log 249 root /srv/http 250 } 251 ''; 252 description = '' 253 Additional lines of configuration appended to the automatically 254 generated `Caddyfile`. 255 ''; 256 }; 257 258 virtualHosts = mkOption { 259 type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; })); 260 default = { }; 261 example = literalExpression '' 262 { 263 "hydra.example.com" = { 264 serverAliases = [ "www.hydra.example.com" ]; 265 extraConfig = ''' 266 encode gzip 267 root * /srv/http 268 '''; 269 }; 270 }; 271 ''; 272 description = '' 273 Declarative specification of virtual hosts served by Caddy. 274 ''; 275 }; 276 277 acmeCA = mkOption { 278 default = null; 279 example = "https://acme-v02.api.letsencrypt.org/directory"; 280 type = with types; nullOr str; 281 description = '' 282 ::: {.note} 283 Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca) 284 in the global options block of the resulting Caddyfile. 285 ::: 286 287 The URL to the ACME CA's directory. It is strongly recommended to set 288 this to `https://acme-staging-v02.api.letsencrypt.org/directory` for 289 Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/) 290 while testing or in development. 291 292 Value `null` should be prefered for production setups, 293 as it omits the `acme_ca` option to enable 294 [automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback). 295 ''; 296 }; 297 298 email = mkOption { 299 default = null; 300 type = with types; nullOr str; 301 description = '' 302 Your email address. Mainly used when creating an ACME account with your 303 CA, and is highly recommended in case there are problems with your 304 certificates. 305 ''; 306 }; 307 308 enableReload = mkOption { 309 default = true; 310 type = types.bool; 311 description = '' 312 Reload Caddy instead of restarting it when configuration file changes. 313 314 Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin) 315 to not be turned off. 316 317 If you enable this option, consider setting [`grace_period`](https://caddyserver.com/docs/caddyfile/options#grace-period) 318 to a non-infinite value in {option}`services.caddy.globalConfig` 319 to prevent Caddy waiting for active connections to finish, 320 which could delay the reload essentially indefinitely. 321 ''; 322 }; 323 324 settings = mkOption { 325 type = settingsFormat.type; 326 default = { }; 327 description = '' 328 Structured configuration for Caddy to generate a Caddy JSON configuration file. 329 See <https://caddyserver.com/docs/json/> for available options. 330 331 ::: {.warning} 332 Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream. 333 There are only very few exception to this. 334 335 Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or 336 {option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead. 337 ::: 338 339 ::: {.note} 340 Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified. 341 ::: 342 ''; 343 }; 344 345 environmentFile = mkOption { 346 type = with types; nullOr path; 347 default = null; 348 example = "/run/secrets/caddy.env"; 349 description = '' 350 Environment file as defined in {manpage}`systemd.exec(5)`. 351 352 You can use environment variables to pass secrets to the service without adding 353 them to the world-redable nix store. 354 355 ``` 356 # in configuration.nix 357 services.caddy.environmentFile = "/run/secrets/caddy.env"; 358 services.caddy.globalConfig = ''' 359 { 360 acme_ca https://acme.zerossl.com/v2/DV90 361 acme_eab { 362 key_id {$EAB_KEY_ID} 363 mac_key {$EAB_MAC_KEY} 364 } 365 } 366 '''; 367 ``` 368 369 ``` 370 # in /run/secrets/caddy.env 371 EAB_KEY_ID=secret 372 EAB_MAC_KEY=secret 373 ``` 374 375 Find more examples 376 [here](https://caddyserver.com/docs/caddyfile/concepts#environment-variables) 377 ''; 378 }; 379 }; 380 381 # implementation 382 config = mkIf cfg.enable { 383 384 assertions = 385 [ 386 { 387 assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null; 388 message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`"; 389 } 390 ] 391 ++ map ( 392 name: 393 mkCertOwnershipAssertion { 394 cert = config.security.acme.certs.${name}; 395 groups = config.users.groups; 396 services = [ config.systemd.services.caddy ]; 397 } 398 ) vhostCertNames; 399 400 services.caddy.globalConfig = '' 401 ${optionalString (cfg.email != null) "email ${cfg.email}"} 402 ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"} 403 log { 404 ${cfg.logFormat} 405 } 406 ''; 407 408 # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes 409 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000; 410 boot.kernel.sysctl."net.core.wmem_max" = mkDefault 2500000; 411 412 systemd.packages = [ cfg.package ]; 413 systemd.services.caddy = { 414 wants = map (certName: "acme-finished-${certName}.target") vhostCertNames; 415 after = 416 map (certName: "acme-selfsigned-${certName}.service") vhostCertNames 417 ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa 418 before = map (certName: "acme-${certName}.service") dependentCertNames; 419 420 wantedBy = [ "multi-user.target" ]; 421 startLimitIntervalSec = 14400; 422 startLimitBurst = 10; 423 reloadTriggers = optional cfg.enableReload cfg.configFile; 424 restartTriggers = optional (!cfg.enableReload) cfg.configFile; 425 426 serviceConfig = 427 let 428 runOptions = ''--config ${configPath} ${ 429 optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}" 430 }''; 431 in 432 { 433 # Override the `ExecStart` line from upstream's systemd unit file by our own: 434 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= 435 # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect. 436 ExecStart = [ 437 "" 438 ''${lib.getExe cfg.package} run ${runOptions} ${optionalString cfg.resume "--resume"}'' 439 ]; 440 # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration 441 ExecReload = [ 442 "" 443 ] ++ lib.optional cfg.enableReload "${lib.getExe cfg.package} reload ${runOptions} --force"; 444 User = cfg.user; 445 Group = cfg.group; 446 ReadWritePaths = [ cfg.dataDir ]; 447 StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ]; 448 LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ]; 449 Restart = "on-failure"; 450 RestartPreventExitStatus = 1; 451 RestartSec = "5s"; 452 EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; 453 454 # TODO: attempt to upstream these options 455 NoNewPrivileges = true; 456 PrivateDevices = true; 457 ProtectHome = true; 458 }; 459 }; 460 461 users.users = optionalAttrs (cfg.user == "caddy") { 462 caddy = { 463 group = cfg.group; 464 uid = config.ids.uids.caddy; 465 home = cfg.dataDir; 466 }; 467 }; 468 469 users.groups = optionalAttrs (cfg.group == "caddy") { 470 caddy.gid = config.ids.gids.caddy; 471 }; 472 473 security.acme.certs = 474 let 475 certCfg = map ( 476 certName: 477 nameValuePair certName { 478 group = mkDefault cfg.group; 479 reloadServices = [ "caddy.service" ]; 480 } 481 ) vhostCertNames; 482 in 483 listToAttrs certCfg; 484 485 environment.etc.${etcConfigFile}.source = cfg.configFile; 486 }; 487}