at master 15 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 18 mkVHostConf = 19 hostOpts: 20 let 21 sslCertDir = certs.${hostOpts.useACMEHost}.directory; 22 in 23 '' 24 ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} { 25 ${optionalString ( 26 hostOpts.listenAddresses != [ ] 27 ) "bind ${concatStringsSep " " hostOpts.listenAddresses}"} 28 ${optionalString ( 29 hostOpts.useACMEHost != null 30 ) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"} 31 ${optionalString (hostOpts.logFormat != null) '' 32 log { 33 ${hostOpts.logFormat} 34 } 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 assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null; 387 message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`"; 388 } 389 ] 390 ++ map ( 391 name: 392 mkCertOwnershipAssertion { 393 cert = certs.${name}; 394 groups = config.users.groups; 395 services = [ config.systemd.services.caddy ]; 396 } 397 ) vhostCertNames; 398 399 services.caddy.globalConfig = '' 400 ${optionalString (cfg.email != null) "email ${cfg.email}"} 401 ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"} 402 log { 403 ${cfg.logFormat} 404 } 405 ''; 406 407 # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes 408 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000; 409 boot.kernel.sysctl."net.core.wmem_max" = mkDefault 2500000; 410 411 systemd.packages = [ cfg.package ]; 412 systemd.services.caddy = { 413 wants = map (certName: "acme-${certName}.service") vhostCertNames; 414 after = map (certName: "acme-${certName}.service") vhostCertNames; 415 416 wantedBy = [ "multi-user.target" ]; 417 startLimitIntervalSec = 14400; 418 startLimitBurst = 10; 419 reloadTriggers = optional cfg.enableReload cfg.configFile; 420 restartTriggers = optional (!cfg.enableReload) cfg.configFile; 421 422 serviceConfig = 423 let 424 runOptions = ''--config ${configPath} ${ 425 optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}" 426 }''; 427 in 428 { 429 # Override the `ExecStart` line from upstream's systemd unit file by our own: 430 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= 431 # 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. 432 ExecStart = [ 433 "" 434 ''${lib.getExe cfg.package} run ${runOptions} ${optionalString cfg.resume "--resume"}'' 435 ]; 436 # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration 437 ExecReload = [ 438 "" 439 ] 440 ++ lib.optional cfg.enableReload "${lib.getExe cfg.package} reload ${runOptions} --force"; 441 User = cfg.user; 442 Group = cfg.group; 443 ReadWritePaths = [ cfg.dataDir ]; 444 StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ]; 445 LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ]; 446 Restart = "on-failure"; 447 RestartPreventExitStatus = 1; 448 RestartSec = "5s"; 449 EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; 450 451 # TODO: attempt to upstream these options 452 NoNewPrivileges = true; 453 PrivateDevices = true; 454 ProtectHome = true; 455 }; 456 }; 457 458 users.users = optionalAttrs (cfg.user == "caddy") { 459 caddy = { 460 group = cfg.group; 461 uid = config.ids.uids.caddy; 462 home = cfg.dataDir; 463 }; 464 }; 465 466 users.groups = optionalAttrs (cfg.group == "caddy") { 467 caddy.gid = config.ids.gids.caddy; 468 }; 469 470 security.acme.certs = 471 let 472 certCfg = map ( 473 certName: 474 nameValuePair certName { 475 group = mkDefault cfg.group; 476 reloadServices = [ "caddy.service" ]; 477 } 478 ) vhostCertNames; 479 in 480 listToAttrs certCfg; 481 482 environment.etc.${etcConfigFile}.source = cfg.configFile; 483 }; 484}