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