at 23.11-pre 11 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 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 configFile = 28 let 29 Caddyfile = pkgs.writeTextDir "Caddyfile" '' 30 { 31 ${cfg.globalConfig} 32 } 33 ${cfg.extraConfig} 34 ''; 35 36 Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } '' 37 mkdir -p $out 38 cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile 39 caddy fmt --overwrite $out/Caddyfile 40 ''; 41 in 42 "${if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile}/Caddyfile"; 43 44 acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts); 45 46 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; 47in 48{ 49 imports = [ 50 (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2") 51 (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ]) 52 (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ]) 53 ]; 54 55 # interface 56 options.services.caddy = { 57 enable = mkEnableOption (lib.mdDoc "Caddy web server"); 58 59 user = mkOption { 60 default = "caddy"; 61 type = types.str; 62 description = lib.mdDoc '' 63 User account under which caddy runs. 64 65 ::: {.note} 66 If left as the default value this user will automatically be created 67 on system activation, otherwise you are responsible for 68 ensuring the user exists before the Caddy service starts. 69 ::: 70 ''; 71 }; 72 73 group = mkOption { 74 default = "caddy"; 75 type = types.str; 76 description = lib.mdDoc '' 77 Group account under which caddy runs. 78 79 ::: {.note} 80 If left as the default value this user will automatically be created 81 on system activation, otherwise you are responsible for 82 ensuring the user exists before the Caddy service starts. 83 ::: 84 ''; 85 }; 86 87 package = mkOption { 88 default = pkgs.caddy; 89 defaultText = literalExpression "pkgs.caddy"; 90 type = types.package; 91 description = lib.mdDoc '' 92 Caddy package to use. 93 ''; 94 }; 95 96 dataDir = mkOption { 97 type = types.path; 98 default = "/var/lib/caddy"; 99 description = lib.mdDoc '' 100 The data directory for caddy. 101 102 ::: {.note} 103 If left as the default value this directory will automatically be created 104 before the Caddy server starts, otherwise you are responsible for ensuring 105 the directory exists with appropriate ownership and permissions. 106 107 Caddy v2 replaced `CADDYPATH` with XDG directories. 108 See <https://caddyserver.com/docs/conventions#file-locations>. 109 ::: 110 ''; 111 }; 112 113 logDir = mkOption { 114 type = types.path; 115 default = "/var/log/caddy"; 116 description = lib.mdDoc '' 117 Directory for storing Caddy access logs. 118 119 ::: {.note} 120 If left as the default value this directory will automatically be created 121 before the Caddy server starts, otherwise the sysadmin is responsible for 122 ensuring the directory exists with appropriate ownership and permissions. 123 ::: 124 ''; 125 }; 126 127 logFormat = mkOption { 128 type = types.lines; 129 default = '' 130 level ERROR 131 ''; 132 example = literalExpression '' 133 mkForce "level INFO"; 134 ''; 135 description = lib.mdDoc '' 136 Configuration for the default logger. See 137 <https://caddyserver.com/docs/caddyfile/options#log> 138 for details. 139 ''; 140 }; 141 142 configFile = mkOption { 143 type = types.path; 144 default = configFile; 145 defaultText = "A Caddyfile automatically generated by values from services.caddy.*"; 146 example = literalExpression '' 147 pkgs.writeTextDir "Caddyfile" ''' 148 example.com 149 150 root * /var/www/wordpress 151 php_fastcgi unix//run/php/php-version-fpm.sock 152 file_server 153 '''; 154 ''; 155 description = lib.mdDoc '' 156 Override the configuration file used by Caddy. By default, 157 NixOS generates one automatically. 158 ''; 159 }; 160 161 adapter = mkOption { 162 default = null; 163 example = literalExpression "nginx"; 164 type = with types; nullOr str; 165 description = lib.mdDoc '' 166 Name of the config adapter to use. 167 See <https://caddyserver.com/docs/config-adapters> 168 for the full list. 169 170 If `null` is specified, the `--adapter` argument is omitted when 171 starting or restarting Caddy. Notably, this allows specification of a 172 configuration file in Caddy's native JSON format, as long as the 173 filename does not start with `Caddyfile` (in which case the `caddyfile` 174 adapter is implicitly enabled). See 175 <https://caddyserver.com/docs/command-line#caddy-run> for details. 176 177 ::: {.note} 178 Any value other than `null` or `caddyfile` is only valid when providing 179 your own `configFile`. 180 ::: 181 ''; 182 }; 183 184 resume = mkOption { 185 default = false; 186 type = types.bool; 187 description = lib.mdDoc '' 188 Use saved config, if any (and prefer over any specified configuration passed with `--config`). 189 ''; 190 }; 191 192 globalConfig = mkOption { 193 type = types.lines; 194 default = ""; 195 example = '' 196 debug 197 servers { 198 protocol { 199 experimental_http3 200 } 201 } 202 ''; 203 description = lib.mdDoc '' 204 Additional lines of configuration appended to the global config section 205 of the `Caddyfile`. 206 207 Refer to <https://caddyserver.com/docs/caddyfile/options#global-options> 208 for details on supported values. 209 ''; 210 }; 211 212 extraConfig = mkOption { 213 type = types.lines; 214 default = ""; 215 example = '' 216 example.com { 217 encode gzip 218 log 219 root /srv/http 220 } 221 ''; 222 description = lib.mdDoc '' 223 Additional lines of configuration appended to the automatically 224 generated `Caddyfile`. 225 ''; 226 }; 227 228 virtualHosts = mkOption { 229 type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; })); 230 default = {}; 231 example = literalExpression '' 232 { 233 "hydra.example.com" = { 234 serverAliases = [ "www.hydra.example.com" ]; 235 extraConfig = ''' 236 encode gzip 237 root /srv/http 238 '''; 239 }; 240 }; 241 ''; 242 description = lib.mdDoc '' 243 Declarative specification of virtual hosts served by Caddy. 244 ''; 245 }; 246 247 acmeCA = mkOption { 248 default = "https://acme-v02.api.letsencrypt.org/directory"; 249 example = "https://acme-staging-v02.api.letsencrypt.org/directory"; 250 type = with types; nullOr str; 251 description = lib.mdDoc '' 252 The URL to the ACME CA's directory. It is strongly recommended to set 253 this to Let's Encrypt's staging endpoint for testing or development. 254 255 Set it to `null` if you want to write a more 256 fine-grained configuration manually. 257 ''; 258 }; 259 260 email = mkOption { 261 default = null; 262 type = with types; nullOr str; 263 description = lib.mdDoc '' 264 Your email address. Mainly used when creating an ACME account with your 265 CA, and is highly recommended in case there are problems with your 266 certificates. 267 ''; 268 }; 269 270 }; 271 272 # implementation 273 config = mkIf cfg.enable { 274 275 assertions = [ 276 { assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null; 277 message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`"; 278 } 279 ] ++ map (name: mkCertOwnershipAssertion { 280 inherit (cfg) group user; 281 cert = config.security.acme.certs.${name}; 282 groups = config.users.groups; 283 }) acmeHosts; 284 285 services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts; 286 services.caddy.globalConfig = '' 287 ${optionalString (cfg.email != null) "email ${cfg.email}"} 288 ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"} 289 log { 290 ${cfg.logFormat} 291 } 292 ''; 293 294 # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size 295 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000; 296 297 systemd.packages = [ cfg.package ]; 298 systemd.services.caddy = { 299 wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts; 300 after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts; 301 before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts; 302 303 wantedBy = [ "multi-user.target" ]; 304 startLimitIntervalSec = 14400; 305 startLimitBurst = 10; 306 307 serviceConfig = { 308 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= 309 # 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. 310 ExecStart = [ "" ''${cfg.package}/bin/caddy run --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"} ${optionalString cfg.resume "--resume"}'' ]; 311 ExecReload = [ "" ''${cfg.package}/bin/caddy reload --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"} --force'' ]; 312 ExecStartPre = ''${cfg.package}/bin/caddy validate --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"}''; 313 User = cfg.user; 314 Group = cfg.group; 315 ReadWriteDirectories = cfg.dataDir; 316 StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ]; 317 LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ]; 318 Restart = "on-abnormal"; 319 320 # TODO: attempt to upstream these options 321 NoNewPrivileges = true; 322 PrivateDevices = true; 323 ProtectHome = true; 324 }; 325 }; 326 327 users.users = optionalAttrs (cfg.user == "caddy") { 328 caddy = { 329 group = cfg.group; 330 uid = config.ids.uids.caddy; 331 home = cfg.dataDir; 332 }; 333 }; 334 335 users.groups = optionalAttrs (cfg.group == "caddy") { 336 caddy.gid = config.ids.gids.caddy; 337 }; 338 339 security.acme.certs = 340 let 341 certCfg = map (useACMEHost: nameValuePair useACMEHost { 342 group = mkDefault cfg.group; 343 reloadServices = [ "caddy.service" ]; 344 }) acmeHosts; 345 in 346 listToAttrs certCfg; 347 348 }; 349}