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