diff --git a/darwin/default.nix b/darwin/default.nix index 8000dabc..93c90fb5 100644 --- a/darwin/default.nix +++ b/darwin/default.nix @@ -9,9 +9,13 @@ name: device: nix-darwin.lib.darwinSystem { system = device.system; + specialArgs = { + inherit device; + }; modules = [ {nixpkgs.overlays = overlays;} ./${device.name}/configuration.nix + inputs.sops-nix.darwinModules.sops home-manager.darwinModules.home-manager { nixpkgs.config.allowUnfree = true; diff --git a/darwin/shiro/services/caddy.nix b/darwin/shiro/services/caddy.nix index 2138f50f..a909f7ba 100644 --- a/darwin/shiro/services/caddy.nix +++ b/darwin/shiro/services/caddy.nix @@ -14,6 +14,10 @@ services = { caddy = { enable = true; + environmentFile = config.sops.templates."HETZNER_API_KEY.env".path; + globalConfig = '' + debug + ''; extraConfig = '' (hetzner) { tls { @@ -26,17 +30,8 @@ ''; package = pkgs.caddy.withPlugins { plugins = ["github.com/caddy-dns/hetzner@v1.0.0"]; - # hash = "sha256-9ea0CfOHG7JhejB73HjfXQpnonn+ZRBqLNz1fFRkcDQ="; - # hash = "sha256-9ea0CfOHG7JhejB73HjfXQpnonn+ZRBqLNz1fFRkcDQ=" hash = "sha256-YUrprDZQL+cX3P8fVLKHouXTMG4rw3sCaQdGqiq37uA="; }; }; }; - systemd.services.caddy = { - serviceConfig = { - EnvironmentFile = config.sops.templates."HETZNER_API_KEY.env".path; - Requires = ["sops.service"]; - After = ["sops.service"]; - }; - }; } diff --git a/darwin/shiro/services/default.nix b/darwin/shiro/services/default.nix index 272f0482..c651d810 100644 --- a/darwin/shiro/services/default.nix +++ b/darwin/shiro/services/default.nix @@ -1,10 +1,12 @@ {...}: { imports = [ + ../../../modules/darwin/caddy ./yabai.nix ./skhd.nix ./tailscale.nix ./autossh.nix - # ./caddy.nix + ./caddy.nix + ./sops.nix # ./lmstudio.nix # ./colima.nix # ./zerotier.nix diff --git a/darwin/shiro/services/sops.nix b/darwin/shiro/services/sops.nix new file mode 100644 index 00000000..bdceda11 --- /dev/null +++ b/darwin/shiro/services/sops.nix @@ -0,0 +1,13 @@ +{ + # config, + # pkgs, + inputs, + device, + ... +}: { + sops = { + defaultSopsFile = ../../../secrets/secrets.yaml; + defaultSopsFormat = "yaml"; + age.keyFile = "/Users/${device.user}/.config/sops/age/keys.txt"; + }; +} diff --git a/flake.nix b/flake.nix index 2497a63f..7e00d459 100644 --- a/flake.nix +++ b/flake.nix @@ -330,6 +330,7 @@ in import ./darwin { inherit devices inputs nixpkgs home-manager overlays nur nix-darwin; + sops-nix = inputs.sops-nix; }; homeConfigurations = { diff --git a/modules/darwin/caddy/default.nix b/modules/darwin/caddy/default.nix new file mode 100644 index 00000000..43d035a6 --- /dev/null +++ b/modules/darwin/caddy/default.nix @@ -0,0 +1,416 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.caddy; + + virtualHosts = attrValues cfg.virtualHosts; + + mkVHostConf = hostOpts: '' + ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} { + ${optionalString ( + hostOpts.listenAddresses != [] + ) "bind ${concatStringsSep " " hostOpts.listenAddresses}"} + log { + ${hostOpts.logFormat} + } + + ${hostOpts.extraConfig} + } + ''; + + settingsFormat = pkgs.formats.json {}; + + configFile = + if cfg.settings != {} + then settingsFormat.generate "caddy.json" cfg.settings + else let + Caddyfile = pkgs.writeTextDir "Caddyfile" '' + { + ${cfg.globalConfig} + } + ${cfg.extraConfig} + ${concatMapStringsSep "\n" mkVHostConf virtualHosts} + ''; + + Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" {} '' + mkdir -p $out + cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile + ${lib.getExe cfg.package} fmt --overwrite $out/Caddyfile + ''; + in "${ + if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform + then Caddyfile-formatted + else Caddyfile + }/Caddyfile"; +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} + 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. + ::: + ''; + }; + + group = mkOption { + default = "caddy"; + type = types.str; + description = '' + Group under which caddy runs. + + ::: {.note} + If left as the default value this group will automatically be created + on system activation, otherwise you are responsible for + ensuring the group exists before the Caddy service starts. + ::: + ''; + }; + + package = mkPackageOption pkgs "caddy" {}; + + dataDir = mkOption { + type = types.path; + default = "/usr/local/var/lib/caddy"; + description = '' + The data directory for caddy. + + ::: {.note} + 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. + + Caddy v2 replaced `CADDYPATH` with XDG directories. + See . + ::: + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/usr/local/var/log/caddy"; + description = '' + Directory for storing Caddy access logs. + + ::: {.note} + 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. + ::: + ''; + }; + + logFormat = mkOption { + type = types.lines; + default = '' + level ERROR + ''; + example = literalExpression '' + mkForce "level INFO"; + ''; + description = '' + Configuration for the default logger. See + + 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 = + if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") + then "caddyfile" + else null; + defaultText = literalExpression '' + if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then "caddyfile" else null + ''; + example = literalExpression "nginx"; + type = with types; nullOr str; + description = '' + Name of the config adapter to use. + See + for the full list. + + If `null` is specified, the `--adapter` argument is omitted when + starting or restarting Caddy. Notably, this allows specification of a + configuration file in Caddy's native JSON format, as long as the + filename does not start with `Caddyfile` (in which case the `caddyfile` + adapter is implicitly enabled). See + for details. + + ::: {.note} + Any value other than `null` or `caddyfile` is only valid when providing + your own `configFile`. + ::: + ''; + }; + + resume = mkOption { + default = false; + type = types.bool; + description = '' + Use saved config, if any (and prefer over any specified configuration passed with `--config`). + ''; + }; + + 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 `Caddyfile`. + + Refer to + 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 `Caddyfile`. + ''; + }; + + 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 = null; + example = "https://acme-v02.api.letsencrypt.org/directory"; + type = with types; nullOr str; + description = '' + ::: {.note} + Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca) + in the global options block of the resulting Caddyfile. + ::: + + The URL to the ACME CA's directory. It is strongly recommended to set + this to `https://acme-staging-v02.api.letsencrypt.org/directory` for + Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/) + while testing or in development. + + Value `null` should be prefered for production setups, + as it omits the `acme_ca` option to enable + [automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback). + ''; + }; + + 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. + ''; + }; + + settings = mkOption { + type = settingsFormat.type; + default = {}; + description = '' + Structured configuration for Caddy to generate a Caddy JSON configuration file. + See for available options. + + ::: {.warning} + Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream. + There are only very few exception to this. + + Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or + {option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead. + ::: + + ::: {.note} + Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified. + ::: + ''; + }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/secrets/caddy.env"; + description = '' + Environment file as defined in {manpage}`launchd.plist(5)`. + + You can use environment variables to pass secrets to the service without adding + them to the world-redable nix store. + + ``` + # in configuration.nix + services.caddy.environmentFile = "/run/secrets/caddy.env"; + services.caddy.globalConfig = ''' + { + acme_ca https://acme.zerossl.com/v2/DV90 + acme_eab { + key_id {$EAB_KEY_ID} + mac_key {$EAB_MAC_KEY} + } + } + '''; + ``` + + ``` + # in /run/secrets/caddy.env + EAB_KEY_ID=secret + EAB_MAC_KEY=secret + ``` + + Find more examples + [here](https://caddyserver.com/docs/caddyfile/concepts#environment-variables) + ''; + }; + + serviceConfig = mkOption { + type = types.attrs; + default = {}; + description = '' + Additional launchd service configuration. + ''; + }; + }; + + # implementation + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null; + message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`"; + } + ]; + + services.caddy.globalConfig = '' + ${optionalString (cfg.email != null) "email ${cfg.email}"} + ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"} + log { + ${cfg.logFormat} + } + ''; + + launchd.user.agents.caddy = { + path = [cfg.package]; + + # Use script when environment file is provided, otherwise direct command + script = mkIf (cfg.environmentFile != null) '' + if [ -f "${cfg.environmentFile}" ]; then + set -a + source ${cfg.environmentFile} + set +a + fi + exec ${lib.getExe cfg.package} run --config ${cfg.configFile} ${ + optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}" + } ${optionalString cfg.resume "--resume"} + ''; + + command = mkIf (cfg.environmentFile == null) "${lib.getExe cfg.package} run --config ${cfg.configFile} ${ + optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}" + } ${optionalString cfg.resume "--resume"}"; + + serviceConfig = + { + KeepAlive = true; + RunAtLoad = true; + WorkingDirectory = cfg.dataDir; + StandardOutPath = "${cfg.logDir}/caddy.log"; + StandardErrorPath = "${cfg.logDir}/caddy.error.log"; + } + // cfg.serviceConfig + // (optionalAttrs (cfg.user != "caddy") { + UserName = cfg.user; + }); + }; + + # Note: User management is handled differently in Darwin + # Users should be created manually or through other means + + system.activationScripts.caddy = mkIf cfg.enable '' + # Create data directory + if [ ! -d "${cfg.dataDir}" ]; then + mkdir -p "${cfg.dataDir}" + chown ${cfg.user}:${cfg.group} "${cfg.dataDir}" + chmod 750 "${cfg.dataDir}" + fi + + # Create log directory + if [ ! -d "${cfg.logDir}" ]; then + mkdir -p "${cfg.logDir}" + chown ${cfg.user}:${cfg.group} "${cfg.logDir}" + chmod 750 "${cfg.logDir}" + fi + ''; + }; +} diff --git a/modules/darwin/caddy/vhost-options.nix b/modules/darwin/caddy/vhost-options.nix new file mode 100644 index 00000000..e9a4bf85 --- /dev/null +++ b/modules/darwin/caddy/vhost-options.nix @@ -0,0 +1,69 @@ +{cfg}: { + config, + lib, + name, + ... +}: let + inherit (lib) literalExpression mkOption types; +in { + options = { + hostName = mkOption { + type = types.str; + default = name; + description = "Canonical hostname for the server."; + }; + + serverAliases = mkOption { + type = with types; listOf str; + default = []; + example = [ + "www.example.org" + "example.org" + ]; + description = '' + Additional names of virtual hosts served by this virtual host configuration. + ''; + }; + + listenAddresses = mkOption { + type = with types; listOf str; + description = '' + A list of host interfaces to bind to for this virtual host. + ''; + default = []; + example = [ + "127.0.0.1" + "::1" + ]; + }; + + logFormat = mkOption { + type = types.lines; + default = '' + output file ${cfg.logDir}/access-${lib.replaceStrings ["/" " "] ["_" "_"] config.hostName}.log + ''; + defaultText = '' + output file ''${config.services.caddy.logDir}/access-''${hostName}.log + ''; + example = literalExpression '' + mkForce ''' + output discard + '''; + ''; + description = '' + Configuration for HTTP request logging (also known as access logs). See + + for details. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional lines of configuration appended to this virtual host in the + automatically generated `Caddyfile`. + ''; + }; + }; +} diff --git a/modules/nixos/caddy/default.nix b/modules/nixos/caddy/default.nix new file mode 100644 index 00000000..f9c50bca --- /dev/null +++ b/modules/nixos/caddy/default.nix @@ -0,0 +1,487 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; + +let + cfg = config.services.caddy; + + certs = config.security.acme.certs; + virtualHosts = attrValues cfg.virtualHosts; + acmeEnabledVhosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts; + vhostCertNames = unique (map (hostOpts: hostOpts.useACMEHost) acmeEnabledVhosts); + dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server + independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server + + mkVHostConf = + hostOpts: + let + sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory; + in + '' + ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} { + ${optionalString ( + hostOpts.listenAddresses != [ ] + ) "bind ${concatStringsSep " " hostOpts.listenAddresses}"} + ${optionalString ( + hostOpts.useACMEHost != null + ) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"} + log { + ${hostOpts.logFormat} + } + + ${hostOpts.extraConfig} + } + ''; + + settingsFormat = pkgs.formats.json { }; + + configFile = + if cfg.settings != { } then + settingsFormat.generate "caddy.json" cfg.settings + else + let + Caddyfile = pkgs.writeTextDir "Caddyfile" '' + { + ${cfg.globalConfig} + } + ${cfg.extraConfig} + ${concatMapStringsSep "\n" mkVHostConf virtualHosts} + ''; + + Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { } '' + mkdir -p $out + cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile + ${lib.getExe cfg.package} fmt --overwrite $out/Caddyfile + ''; + in + "${ + if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile + }/Caddyfile"; + + etcConfigFile = "caddy/caddy_config"; + + configPath = "/etc/${etcConfigFile}"; + + mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib; +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} + 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. + ::: + ''; + }; + + group = mkOption { + default = "caddy"; + type = types.str; + description = '' + Group under which caddy runs. + + ::: {.note} + If left as the default value this group will automatically be created + on system activation, otherwise you are responsible for + ensuring the group exists before the Caddy service starts. + ::: + ''; + }; + + package = mkPackageOption pkgs "caddy" { }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/caddy"; + description = '' + The data directory for caddy. + + ::: {.note} + 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. + + Caddy v2 replaced `CADDYPATH` with XDG directories. + See . + ::: + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/caddy"; + description = '' + Directory for storing Caddy access logs. + + ::: {.note} + 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. + ::: + ''; + }; + + logFormat = mkOption { + type = types.lines; + default = '' + level ERROR + ''; + example = literalExpression '' + mkForce "level INFO"; + ''; + description = '' + Configuration for the default logger. See + + 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. + + The configuration file is exposed at {file}`${configPath}`. + ''; + }; + + adapter = mkOption { + default = + if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then + "caddyfile" + else + null; + defaultText = literalExpression '' + if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then "caddyfile" else null + ''; + example = literalExpression "nginx"; + type = with types; nullOr str; + description = '' + Name of the config adapter to use. + See + for the full list. + + If `null` is specified, the `--adapter` argument is omitted when + starting or restarting Caddy. Notably, this allows specification of a + configuration file in Caddy's native JSON format, as long as the + filename does not start with `Caddyfile` (in which case the `caddyfile` + adapter is implicitly enabled). See + for details. + + ::: {.note} + Any value other than `null` or `caddyfile` is only valid when providing + your own `configFile`. + ::: + ''; + }; + + resume = mkOption { + default = false; + type = types.bool; + description = '' + Use saved config, if any (and prefer over any specified configuration passed with `--config`). + ''; + }; + + 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 `Caddyfile`. + + Refer to + 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 `Caddyfile`. + ''; + }; + + 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 = null; + example = "https://acme-v02.api.letsencrypt.org/directory"; + type = with types; nullOr str; + description = '' + ::: {.note} + Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca) + in the global options block of the resulting Caddyfile. + ::: + + The URL to the ACME CA's directory. It is strongly recommended to set + this to `https://acme-staging-v02.api.letsencrypt.org/directory` for + Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/) + while testing or in development. + + Value `null` should be prefered for production setups, + as it omits the `acme_ca` option to enable + [automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback). + ''; + }; + + 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. + ''; + }; + + enableReload = mkOption { + default = true; + type = types.bool; + description = '' + Reload Caddy instead of restarting it when configuration file changes. + + Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin) + to not be turned off. + + If you enable this option, consider setting [`grace_period`](https://caddyserver.com/docs/caddyfile/options#grace-period) + to a non-infinite value in {option}`services.caddy.globalConfig` + to prevent Caddy waiting for active connections to finish, + which could delay the reload essentially indefinitely. + ''; + }; + + settings = mkOption { + type = settingsFormat.type; + default = { }; + description = '' + Structured configuration for Caddy to generate a Caddy JSON configuration file. + See for available options. + + ::: {.warning} + Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream. + There are only very few exception to this. + + Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or + {option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead. + ::: + + ::: {.note} + Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified. + ::: + ''; + }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/secrets/caddy.env"; + description = '' + Environment file as defined in {manpage}`systemd.exec(5)`. + + You can use environment variables to pass secrets to the service without adding + them to the world-redable nix store. + + ``` + # in configuration.nix + services.caddy.environmentFile = "/run/secrets/caddy.env"; + services.caddy.globalConfig = ''' + { + acme_ca https://acme.zerossl.com/v2/DV90 + acme_eab { + key_id {$EAB_KEY_ID} + mac_key {$EAB_MAC_KEY} + } + } + '''; + ``` + + ``` + # in /run/secrets/caddy.env + EAB_KEY_ID=secret + EAB_MAC_KEY=secret + ``` + + Find more examples + [here](https://caddyserver.com/docs/caddyfile/concepts#environment-variables) + ''; + }; + }; + + # implementation + config = mkIf cfg.enable { + + assertions = [ + { + assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null; + message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`"; + } + ] + ++ map ( + name: + mkCertOwnershipAssertion { + cert = config.security.acme.certs.${name}; + groups = config.users.groups; + services = [ config.systemd.services.caddy ]; + } + ) vhostCertNames; + + services.caddy.globalConfig = '' + ${optionalString (cfg.email != null) "email ${cfg.email}"} + ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"} + log { + ${cfg.logFormat} + } + ''; + + # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes + boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000; + boot.kernel.sysctl."net.core.wmem_max" = mkDefault 2500000; + + systemd.packages = [ cfg.package ]; + systemd.services.caddy = { + wants = map (certName: "acme-finished-${certName}.target") vhostCertNames; + after = + map (certName: "acme-selfsigned-${certName}.service") vhostCertNames + ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa + before = map (certName: "acme-${certName}.service") dependentCertNames; + + wantedBy = [ "multi-user.target" ]; + startLimitIntervalSec = 14400; + startLimitBurst = 10; + reloadTriggers = optional cfg.enableReload cfg.configFile; + restartTriggers = optional (!cfg.enableReload) cfg.configFile; + + serviceConfig = + let + runOptions = ''--config ${configPath} ${ + optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}" + }''; + in + { + # Override the `ExecStart` line from upstream's systemd unit file by our own: + # 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 = [ + "" + ''${lib.getExe cfg.package} run ${runOptions} ${optionalString cfg.resume "--resume"}'' + ]; + # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration + ExecReload = [ + "" + ] + ++ lib.optional cfg.enableReload "${lib.getExe cfg.package} reload ${runOptions} --force"; + User = cfg.user; + Group = cfg.group; + ReadWritePaths = [ cfg.dataDir ]; + StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ]; + LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ]; + Restart = "on-failure"; + RestartPreventExitStatus = 1; + RestartSec = "5s"; + EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; + + # 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 + certCfg = map ( + certName: + nameValuePair certName { + group = mkDefault cfg.group; + reloadServices = [ "caddy.service" ]; + } + ) vhostCertNames; + in + listToAttrs certCfg; + + environment.etc.${etcConfigFile}.source = cfg.configFile; + }; +} diff --git a/modules/nixos/caddy/vhost-options.nix b/modules/nixos/caddy/vhost-options.nix new file mode 100644 index 00000000..73ef4b87 --- /dev/null +++ b/modules/nixos/caddy/vhost-options.nix @@ -0,0 +1,88 @@ +{ cfg }: +{ + config, + lib, + name, + ... +}: +let + inherit (lib) literalExpression mkOption types; +in +{ + options = { + + hostName = mkOption { + type = types.str; + default = name; + description = "Canonical hostname for the server."; + }; + + serverAliases = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ + "www.example.org" + "example.org" + ]; + description = '' + Additional names of virtual hosts served by this virtual host configuration. + ''; + }; + + listenAddresses = mkOption { + type = with types; listOf str; + description = '' + A list of host interfaces to bind to for this virtual host. + ''; + default = [ ]; + example = [ + "127.0.0.1" + "::1" + ]; + }; + + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A host of an existing Let's Encrypt certificate to use. + This is mostly useful if you use DNS challenges but Caddy does not + currently support your provider. + + *Note that this option does not create any certificates, nor + does it add subdomains to existing ones – you will need to create them + manually using [](#opt-security.acme.certs).* + ''; + }; + + logFormat = mkOption { + type = types.lines; + default = '' + output file ${cfg.logDir}/access-${lib.replaceStrings [ "/" " " ] [ "_" "_" ] config.hostName}.log + ''; + defaultText = '' + output file ''${config.services.caddy.logDir}/access-''${hostName}.log + ''; + example = literalExpression '' + mkForce ''' + output discard + '''; + ''; + description = '' + Configuration for HTTP request logging (also known as access logs). See + + for details. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional lines of configuration appended to this virtual host in the + automatically generated `Caddyfile`. + ''; + }; + + }; +}