summary refs log tree commit diff
path: root/nixos/modules/services/web-servers
diff options
context:
space:
mode:
authorAaron Andersen <aaron@fosslib.net>2021-12-25 17:01:54 -0500
committerGitHub <noreply@github.com>2021-12-25 17:01:54 -0500
commitbaa0e61569468570b07025c06443f7dc19dee105 (patch)
treed1c92cf40385252848d8d849edcbc9f4571af18a /nixos/modules/services/web-servers
parenta83390d767a52b1478eee7b0da2a028b81fc41f9 (diff)
parent81a67a3353b09c0abade5f2d17e91d23873fc7fb (diff)
downloadnixpkgs-baa0e61569468570b07025c06443f7dc19dee105.tar
nixpkgs-baa0e61569468570b07025c06443f7dc19dee105.tar.gz
nixpkgs-baa0e61569468570b07025c06443f7dc19dee105.tar.bz2
nixpkgs-baa0e61569468570b07025c06443f7dc19dee105.tar.lz
nixpkgs-baa0e61569468570b07025c06443f7dc19dee105.tar.xz
nixpkgs-baa0e61569468570b07025c06443f7dc19dee105.tar.zst
nixpkgs-baa0e61569468570b07025c06443f7dc19dee105.zip
Merge pull request #147973 from aanderse/nixos/caddy
nixos/caddy: introduce several new options
Diffstat (limited to 'nixos/modules/services/web-servers')
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix323
-rw-r--r--nixos/modules/services/web-servers/caddy/vhost-options.nix71
2 files changed, 271 insertions, 123 deletions
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index ed27dd375c8..d51effa31c9 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -4,111 +4,159 @@ 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
-  '';
-
-  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
+  virtualHosts = attrValues cfg.virtualHosts;
+  acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
+
+  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
-        }
+    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>
       '';
-      type = types.lines;
+    };
+
+    group = mkOption {
+      default = "caddy";
+      type = types.str;
       description = ''
-        Verbatim Caddyfile to use.
-        Caddy v2 supports multiple config formats via adapters (see <option>services.caddy.adapter</option>).
+        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>
       '';
     };
 
-    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
-            '''''';
-          };
-        };
+    package = mkOption {
+      default = pkgs.caddy;
+      defaultText = literalExpression "pkgs.caddy";
+      type = types.package;
+      description = ''
+        Caddy package to use.
       '';
-      description = "Declarative vhost config";
     };
 
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/caddy";
+      description = ''
+        The data directory for caddy.
 
-    user = mkOption {
-      default = "caddy";
-      type = types.str;
-      description = "User account under which caddy runs.";
+        <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>
+      '';
     };
 
-    group = mkOption {
-      default = "caddy";
-      type = types.str;
-      description = "Group account under which caddy runs.";
+    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 {
@@ -117,7 +165,13 @@ in
       type = types.str;
       description = ''
         Name of the config adapter to use.
-        See https://caddyserver.com/docs/config-adapters for the full list.
+        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>
       '';
     };
 
@@ -125,54 +179,87 @@ in
       default = false;
       type = types.bool;
       description = ''
-        Use saved config, if any (and prefer over configuration passed with <option>services.caddy.config</option>).
+        Use saved config, if any (and prefer over any specified configuration passed with <literal>--config</literal>).
       '';
     };
 
-    ca = mkOption {
-      default = "https://acme-v02.api.letsencrypt.org/directory";
-      example = "https://acme-staging-v02.api.letsencrypt.org/directory";
-      type = types.nullOr types.str;
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        example.com {
+          encode gzip
+          log
+          root /srv/http
+        }
+      '';
       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)
+        Additional lines of configuration appended to the automatically
+        generated <literal>Caddyfile</literal>.
       '';
     };
 
-    email = mkOption {
-      default = "";
-      type = types.str;
-      description = "Email address (for Let's Encrypt certificate)";
+    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.
+      '';
     };
 
-    dataDir = mkOption {
-      default = "/var/lib/caddy";
-      type = types.path;
+    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 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.
+        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.
 
-        Caddy v2 replaced CADDYPATH with XDG directories.
-        See https://caddyserver.com/docs/conventions#file-locations.
+        Set it to <literal>null</literal> if you want to write a more
+        fine-grained configuration manually.
       '';
     };
 
-    package = mkOption {
-      default = pkgs.caddy;
-      defaultText = literalExpression "pkgs.caddy";
-      type = types.package;
+    email = mkOption {
+      default = null;
+      type = with types; nullOr str;
       description = ''
-        Caddy package to use.
+        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 1f74295fc9a..f240ec605c2 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.
+
+        <emphasis>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 <xref linkend="opt-security.acme.certs"/>. Additionally,
+        you should probably add the <literal>caddy</literal> user to the
+        <literal>acme</literal> group to grant access to the certificates.</emphasis>
+      '';
+    };
+
+    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
+        <link xlink:href="https://caddyserver.com/docs/caddyfile/directives/log#log"/>
+        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 <literal>Caddyfile</literal>.
       '';
     };
+
   };
 }