feat(darwin): integrate Caddy server with SOPS for secret management
All checks were successful
Flake checker / Build Nix targets (push) Successful in 9m29s
All checks were successful
Flake checker / Build Nix targets (push) Successful in 9m29s
This commit is contained in:
416
modules/darwin/caddy/default.nix
Normal file
416
modules/darwin/caddy/default.nix
Normal file
@@ -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 <https://caddyserver.com/docs/conventions#file-locations>.
|
||||
:::
|
||||
'';
|
||||
};
|
||||
|
||||
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
|
||||
<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 =
|
||||
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 <https://caddyserver.com/docs/config-adapters>
|
||||
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
|
||||
<https://caddyserver.com/docs/command-line#caddy-run> 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 <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 `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 <https://caddyserver.com/docs/json/> 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
|
||||
'';
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user