ca5ab3a501
GitOrigin-RevId: 4a01ca36d6bfc133bc617e661916a81327c9bbc8
339 lines
10 KiB
Nix
339 lines
10 KiB
Nix
{ 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} --force" ];
|
|
|
|
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;
|
|
|
|
};
|
|
}
|