diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index ed27dd375c86..d51effa31c97 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -4,159 +4,81 @@ with lib;
let
cfg = config.services.caddy;
- vhostToConfig = vhostName: vhostAttrs: ''
- ${vhostName} ${builtins.concatStringsSep " " vhostAttrs.serverAliases} {
- ${vhostAttrs.extraConfig}
- }
- '';
- configFile = pkgs.writeText "Caddyfile" (builtins.concatStringsSep "\n"
- ([ cfg.config ] ++ (mapAttrsToList vhostToConfig cfg.virtualHosts)));
- formattedConfig = pkgs.runCommand "formattedCaddyFile" { } ''
- ${cfg.package}/bin/caddy fmt ${configFile} > $out
- '';
+ virtualHosts = attrValues cfg.virtualHosts;
+ acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
- tlsConfig = {
- apps.tls.automation.policies = [{
- issuers = [{
- inherit (cfg) ca email;
- module = "acme";
- }];
- }];
- };
-
- adaptedConfig = pkgs.runCommand "caddy-config-adapted.json" { } ''
- ${cfg.package}/bin/caddy adapt \
- --config ${formattedConfig} --adapter ${cfg.adapter} > $out
- '';
- tlsJSON = pkgs.writeText "tls.json" (builtins.toJSON tlsConfig);
-
- # merge the TLS config options we expose with the ones originating in the Caddyfile
- configJSON =
- if cfg.ca != null then
- let tlsConfigMerge = ''
- {"apps":
- {"tls":
- {"automation":
- {"policies":
- (if .[0].apps.tls.automation.policies == .[1]?.apps.tls.automation.policies
- then .[0].apps.tls.automation.policies
- else (.[0].apps.tls.automation.policies + .[1]?.apps.tls.automation.policies)
- end)
- }
- }
- }
- }'';
- in
- pkgs.runCommand "caddy-config.json" { } ''
- ${pkgs.jq}/bin/jq -s '.[0] * ${tlsConfigMerge}' ${adaptedConfig} ${tlsJSON} > $out
+ mkVHostConf = hostOpts:
+ let
+ sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory;
+ in
''
- else
- adaptedConfig;
+ ${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" ''
+ {
+ ${optionalString (cfg.email != null) "email ${cfg.email}"}
+ ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
+ log {
+ ${cfg.logFormat}
+ }
+ }
+ ${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;
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";
- config = mkOption {
- default = "";
- example = ''
- example.com {
- encode gzip
- log
- root /srv/http
- }
- '';
- type = types.lines;
- description = ''
- Verbatim Caddyfile to use.
- Caddy v2 supports multiple config formats via adapters (see ).
- '';
- };
-
- virtualHosts = mkOption {
- type = types.attrsOf (types.submodule (import ./vhost-options.nix {
- inherit config lib;
- }));
- default = { };
- example = literalExpression ''
- {
- "hydra.example.com" = {
- serverAliases = [ "www.hydra.example.com" ];
- extraConfig = ''''''
- encode gzip
- log
- root /srv/http
- '''''';
- };
- };
- '';
- description = "Declarative vhost config";
- };
-
-
user = mkOption {
default = "caddy";
type = types.str;
- description = "User account under which caddy runs.";
+ description = ''
+ User account under which caddy runs.
+
+
+ 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 account under which caddy runs.";
- };
-
- adapter = mkOption {
- default = "caddyfile";
- example = "nginx";
- type = types.str;
description = ''
- Name of the config adapter to use.
- See https://caddyserver.com/docs/config-adapters for the full list.
- '';
- };
+ Group account under which caddy runs.
- resume = mkOption {
- default = false;
- type = types.bool;
- description = ''
- Use saved config, if any (and prefer over configuration passed with ).
- '';
- };
-
- ca = mkOption {
- default = "https://acme-v02.api.letsencrypt.org/directory";
- example = "https://acme-staging-v02.api.letsencrypt.org/directory";
- type = types.nullOr types.str;
- description = ''
- Certificate authority ACME server. The default (Let's Encrypt
- production server) should be fine for most people. Set it to null if
- you don't want to include any authority (or if you want to write a more
- fine-graned configuration manually)
- '';
- };
-
- email = mkOption {
- default = "";
- type = types.str;
- description = "Email address (for Let's Encrypt certificate)";
- };
-
- dataDir = mkOption {
- default = "/var/lib/caddy";
- type = types.path;
- description = ''
- The data directory, for storing certificates. Before 17.09, this
- would create a .caddy directory. With 17.09 the contents of the
- .caddy directory are in the specified data directory instead.
-
- Caddy v2 replaced CADDYPATH with XDG directories.
- See https://caddyserver.com/docs/conventions#file-locations.
+
+ 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.
+
'';
};
@@ -168,11 +90,176 @@ in
Caddy package to use.
'';
};
+
+ dataDir = mkOption {
+ type = types.path;
+ default = "/var/lib/caddy";
+ description = ''
+ The data directory for caddy.
+
+
+
+ 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.
+
+
+ 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 = "caddyfile";
+ example = "nginx";
+ type = types.str;
+ description = ''
+ Name of the config adapter to use.
+ See
+ for the full list.
+
+
+ Any value other than caddyfile is only valid when
+ providing your own .
+
+ '';
+ };
+
+ resume = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Use saved config, if any (and prefer over any specified configuration passed with --config).
+ '';
+ };
+
+ 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 = "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 null 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`";
+ }
+ ];
+
+ services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts;
+
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;
@@ -180,13 +267,17 @@ in
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 ${optionalString cfg.resume "--resume"} --config ${configJSON}" ];
- ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${configJSON}" ];
+ 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;
@@ -200,7 +291,6 @@ in
group = cfg.group;
uid = config.ids.uids.caddy;
home = cfg.dataDir;
- createHome = true;
};
};
@@ -208,5 +298,12 @@ in
caddy.gid = config.ids.gids.caddy;
};
+ security.acme.certs =
+ let
+ eachACMEHost = unique (catAttrs "useACMEHost" acmeVHosts);
+ reloads = map (useACMEHost: nameValuePair useACMEHost { reloadServices = [ "caddy.service" ]; }) eachACMEHost;
+ in
+ listToAttrs reloads;
+
};
}
diff --git a/nixos/modules/services/web-servers/caddy/vhost-options.nix b/nixos/modules/services/web-servers/caddy/vhost-options.nix
index 1f74295fc9a2..f240ec605c29 100644
--- a/nixos/modules/services/web-servers/caddy/vhost-options.nix
+++ b/nixos/modules/services/web-servers/caddy/vhost-options.nix
@@ -1,15 +1,19 @@
-# This file defines the options that can be used both for the Nginx
-# main server configuration, and for the virtual hosts. (The latter
-# has additional options that affect the web server as a whole, like
-# the user/group to run under.)
-
-{ lib, ... }:
-
-with lib;
+{ 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 = types.listOf types.str;
+ type = with types; listOf str;
default = [ ];
example = [ "www.example.org" "example.org" ];
description = ''
@@ -17,12 +21,59 @@ with lib;
'';
};
+ 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 . Additionally,
+ you should probably add the caddy user to the
+ acme group to grant access to the certificates.
+ '';
+ };
+
+ logFormat = mkOption {
+ type = types.lines;
+ default = ''
+ output file ${cfg.logDir}/access-${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 = ''
- These lines go into the vhost verbatim
+ Additional lines of configuration appended to this virtual host in the
+ automatically generated Caddyfile.
'';
};
+
};
}