diff options
Diffstat (limited to 'nixos/modules/services/web-servers/caddy/default.nix')
-rw-r--r-- | nixos/modules/services/web-servers/caddy/default.nix | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix new file mode 100644 index 00000000000..2b8c6f2e308 --- /dev/null +++ b/nixos/modules/services/web-servers/caddy/default.nix @@ -0,0 +1,339 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.caddy; + + virtualHosts = attrValues cfg.virtualHosts; + acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts; + + mkVHostConf = hostOpts: + let + sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory; + in + '' + ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} { + bind ${concatStringsSep " " hostOpts.listenAddresses} + ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"} + log { + ${hostOpts.logFormat} + } + + ${hostOpts.extraConfig} + } + ''; + + configFile = + let + Caddyfile = pkgs.writeText "Caddyfile" '' + { + ${cfg.globalConfig} + } + ${cfg.extraConfig} + ''; + + Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } '' + ${cfg.package}/bin/caddy fmt ${Caddyfile} > $out + ''; + in + if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile; + + acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts); + + mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; +in +{ + imports = [ + (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2") + (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ]) + (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ]) + ]; + + # interface + options.services.caddy = { + enable = mkEnableOption "Caddy web server"; + + user = mkOption { + default = "caddy"; + type = types.str; + description = '' + User account under which caddy runs. + + <note><para> + If left as the default value this user will automatically be created + on system activation, otherwise you are responsible for + ensuring the user exists before the Caddy service starts. + </para></note> + ''; + }; + + group = mkOption { + default = "caddy"; + type = types.str; + description = '' + Group account under which caddy runs. + + <note><para> + If left as the default value this user will automatically be created + on system activation, otherwise you are responsible for + ensuring the user exists before the Caddy service starts. + </para></note> + ''; + }; + + package = mkOption { + default = pkgs.caddy; + defaultText = literalExpression "pkgs.caddy"; + type = types.package; + description = '' + Caddy package to use. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/caddy"; + description = '' + The data directory for caddy. + + <note> + <para> + If left as the default value this directory will automatically be created + before the Caddy server starts, otherwise you are responsible for ensuring + the directory exists with appropriate ownership and permissions. + </para> + <para> + Caddy v2 replaced <literal>CADDYPATH</literal> with XDG directories. + See <link xlink:href="https://caddyserver.com/docs/conventions#file-locations"/>. + </para> + </note> + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/caddy"; + description = '' + Directory for storing Caddy access logs. + + <note><para> + If left as the default value this directory will automatically be created + before the Caddy server starts, otherwise the sysadmin is responsible for + ensuring the directory exists with appropriate ownership and permissions. + </para></note> + ''; + }; + + logFormat = mkOption { + type = types.lines; + default = '' + level ERROR + ''; + example = literalExpression '' + mkForce "level INFO"; + ''; + description = '' + Configuration for the default logger. See + <link xlink:href="https://caddyserver.com/docs/caddyfile/options#log"/> + for details. + ''; + }; + + configFile = mkOption { + type = types.path; + default = configFile; + defaultText = "A Caddyfile automatically generated by values from services.caddy.*"; + example = literalExpression '' + pkgs.writeText "Caddyfile" ''' + example.com + + root * /var/www/wordpress + php_fastcgi unix//run/php/php-version-fpm.sock + file_server + '''; + ''; + description = '' + Override the configuration file used by Caddy. By default, + NixOS generates one automatically. + ''; + }; + + adapter = mkOption { + default = "caddyfile"; + example = "nginx"; + type = types.str; + description = '' + Name of the config adapter to use. + See <link xlink:href="https://caddyserver.com/docs/config-adapters"/> + for the full list. + + <note><para> + Any value other than <literal>caddyfile</literal> is only valid when + providing your own <option>configFile</option>. + </para></note> + ''; + }; + + resume = mkOption { + default = false; + type = types.bool; + description = '' + Use saved config, if any (and prefer over any specified configuration passed with <literal>--config</literal>). + ''; + }; + + globalConfig = mkOption { + type = types.lines; + default = ""; + example = '' + debug + servers { + protocol { + experimental_http3 + } + } + ''; + description = '' + Additional lines of configuration appended to the global config section + of the <literal>Caddyfile</literal>. + + Refer to <link xlink:href="https://caddyserver.com/docs/caddyfile/options#global-options"/> + for details on supported values. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + example.com { + encode gzip + log + root /srv/http + } + ''; + description = '' + Additional lines of configuration appended to the automatically + generated <literal>Caddyfile</literal>. + ''; + }; + + virtualHosts = mkOption { + type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; })); + default = {}; + example = literalExpression '' + { + "hydra.example.com" = { + serverAliases = [ "www.hydra.example.com" ]; + extraConfig = ''' + encode gzip + root /srv/http + '''; + }; + }; + ''; + description = '' + Declarative specification of virtual hosts served by Caddy. + ''; + }; + + acmeCA = mkOption { + default = "https://acme-v02.api.letsencrypt.org/directory"; + example = "https://acme-staging-v02.api.letsencrypt.org/directory"; + type = with types; nullOr str; + description = '' + The URL to the ACME CA's directory. It is strongly recommended to set + this to Let's Encrypt's staging endpoint for testing or development. + + Set it to <literal>null</literal> if you want to write a more + fine-grained configuration manually. + ''; + }; + + email = mkOption { + default = null; + type = with types; nullOr str; + description = '' + Your email address. Mainly used when creating an ACME account with your + CA, and is highly recommended in case there are problems with your + certificates. + ''; + }; + + }; + + # implementation + config = mkIf cfg.enable { + + assertions = [ + { assertion = cfg.adapter != "caddyfile" -> cfg.configFile != configFile; + message = "Any value other than 'caddyfile' is only valid when providing your own `services.caddy.configFile`"; + } + ] ++ map (name: mkCertOwnershipAssertion { + inherit (cfg) group user; + cert = config.security.acme.certs.${name}; + groups = config.users.groups; + }) acmeHosts; + + services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts; + services.caddy.globalConfig = '' + ${optionalString (cfg.email != null) "email ${cfg.email}"} + ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"} + log { + ${cfg.logFormat} + } + ''; + + systemd.packages = [ cfg.package ]; + systemd.services.caddy = { + wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts; + after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts; + before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts; + + wantedBy = [ "multi-user.target" ]; + startLimitIntervalSec = 14400; + startLimitBurst = 10; + + serviceConfig = { + # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= + # 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. + ExecStart = [ "" "${cfg.package}/bin/caddy run --config ${cfg.configFile} --adapter ${cfg.adapter} ${optionalString cfg.resume "--resume"}" ]; + ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${cfg.configFile} --adapter ${cfg.adapter}" ]; + + ExecStartPre = "${cfg.package}/bin/caddy validate --config ${cfg.configFile} --adapter ${cfg.adapter}"; + User = cfg.user; + Group = cfg.group; + ReadWriteDirectories = cfg.dataDir; + StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ]; + LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ]; + Restart = "on-abnormal"; + SupplementaryGroups = mkIf (length acmeVHosts != 0) [ "acme" ]; + + # TODO: attempt to upstream these options + NoNewPrivileges = true; + PrivateDevices = true; + ProtectHome = true; + }; + }; + + users.users = optionalAttrs (cfg.user == "caddy") { + caddy = { + group = cfg.group; + uid = config.ids.uids.caddy; + home = cfg.dataDir; + }; + }; + + users.groups = optionalAttrs (cfg.group == "caddy") { + caddy.gid = config.ids.gids.caddy; + }; + + security.acme.certs = + let + reloads = map (useACMEHost: nameValuePair useACMEHost { reloadServices = [ "caddy.service" ]; }) acmeHosts; + in + listToAttrs reloads; + + }; +} |