mirror of
https://git.sr.ht/~magic_rb/dotfiles
synced 2024-12-13 02:12:00 +01:00
190 lines
5.7 KiB
Nix
190 lines
5.7 KiB
Nix
|
# SPDX-FileCopyrightText: 2020 Serokell <https://serokell.io/>
|
||
|
#
|
||
|
# SPDX-License-Identifier: MPL-2.0
|
||
|
{
|
||
|
config,
|
||
|
lib,
|
||
|
pkgs,
|
||
|
...
|
||
|
}: let
|
||
|
cfg = config.services.acme-sh;
|
||
|
dnstype = lib.types.enum ["dns_aws" "dns_dnsimple" "dns_hetzner"];
|
||
|
submod = with lib; {
|
||
|
domains = mkOption {
|
||
|
type =
|
||
|
types.coercedTo
|
||
|
(types.listOf types.str)
|
||
|
(f: lib.genAttrs f (x: cfg.dns)) (types.attrsOf dnstype);
|
||
|
default = {"${cfg.mainDomain}" = cfg.dns;};
|
||
|
};
|
||
|
mainDomain = mkOption {
|
||
|
type = types.str;
|
||
|
description = "domain to use as primary domain for the cert";
|
||
|
};
|
||
|
postRun = mkOption {
|
||
|
type = types.str;
|
||
|
default = "true";
|
||
|
};
|
||
|
keyFile = mkOption {
|
||
|
type = types.str;
|
||
|
default = "/dev/null";
|
||
|
};
|
||
|
user = mkOption {
|
||
|
type = types.str;
|
||
|
default = "root";
|
||
|
description = "User running the ACME client.";
|
||
|
};
|
||
|
|
||
|
group = mkOption {
|
||
|
type = types.str;
|
||
|
default = "root";
|
||
|
description = "Group running the ACME client.";
|
||
|
};
|
||
|
|
||
|
server = mkOption {
|
||
|
type = types.str;
|
||
|
default = "letsencrypt";
|
||
|
description = "Certificate Authority to use";
|
||
|
};
|
||
|
dns = mkOption {
|
||
|
type = dnstype;
|
||
|
};
|
||
|
renewInterval = mkOption {
|
||
|
type = types.str;
|
||
|
default = "weekly";
|
||
|
description = ''
|
||
|
Systemd calendar expression when to check for renewal. See
|
||
|
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
||
|
<manvolnum>7</manvolnum></citerefentry>.
|
||
|
'';
|
||
|
};
|
||
|
production = mkOption {
|
||
|
type = types.bool;
|
||
|
default = true;
|
||
|
};
|
||
|
statePath = mkOption {
|
||
|
readOnly = true;
|
||
|
type = types.str;
|
||
|
};
|
||
|
certDirectory = mkOption {
|
||
|
readOnly = true;
|
||
|
type = types.str;
|
||
|
};
|
||
|
keyPath = mkOption {
|
||
|
readOnly = true;
|
||
|
type = types.str;
|
||
|
};
|
||
|
certPath = mkOption {
|
||
|
readOnly = true;
|
||
|
type = types.str;
|
||
|
};
|
||
|
consulLock = mkOption {
|
||
|
type = types.nullOr types.str;
|
||
|
default = null;
|
||
|
example = "vault";
|
||
|
};
|
||
|
};
|
||
|
in {
|
||
|
options.services.acme-sh = with lib; {
|
||
|
stateDir = lib.mkOption {
|
||
|
type = types.str;
|
||
|
default = "/var/lib/acme.sh";
|
||
|
};
|
||
|
certs = lib.mkOption {
|
||
|
type = types.attrsOf (types.submodule ({
|
||
|
config,
|
||
|
name,
|
||
|
...
|
||
|
}: (with config; {
|
||
|
options = submod;
|
||
|
config.certDirectory = "${statePath}/cert";
|
||
|
config.statePath = "${cfg.stateDir}/${name}";
|
||
|
config.keyPath = "${certDirectory}/${mainDomain}.key";
|
||
|
config.certPath = "${certDirectory}/fullchain.cer";
|
||
|
})));
|
||
|
default = {};
|
||
|
};
|
||
|
};
|
||
|
config = {
|
||
|
systemd.services = lib.mapAttrs' (name: value:
|
||
|
lib.nameValuePair "acme-sh-${name}" (with value; {
|
||
|
description = "Renew ACME Certificate for ${name}";
|
||
|
after =
|
||
|
["network.target" "network-online.target"]
|
||
|
# wait for consul if we use it for locking
|
||
|
++ lib.optionals (consulLock != null) ["consul.service"];
|
||
|
wants = ["network-online.target"];
|
||
|
serviceConfig = {
|
||
|
Type = "oneshot";
|
||
|
PermissionsStartOnly = true;
|
||
|
User = user;
|
||
|
Group = group;
|
||
|
PrivateTmp = true;
|
||
|
EnvironmentFile = keyFile;
|
||
|
SuccessExitStatus = "0 2";
|
||
|
};
|
||
|
path = with pkgs; [acme-sh systemd util-linuxMinimal procps];
|
||
|
preStart = ''
|
||
|
mkdir -p ${cfg.stateDir}
|
||
|
chown 'root:root' ${cfg.stateDir}
|
||
|
chmod 755 ${cfg.stateDir}
|
||
|
mkdir -p "${statePath}" "${certDirectory}"
|
||
|
chown -R '${user}:${group}' "${statePath}"
|
||
|
chmod 750 "${statePath}"
|
||
|
rm -f "${statePath}/renewed"
|
||
|
'';
|
||
|
environment.LE_WORKING_DIR = statePath;
|
||
|
environment.SHELL = "${pkgs.bash}/bin/bash";
|
||
|
script = let
|
||
|
mapDomain = name: dns: ''-d "${name}" --dns ${dns}'';
|
||
|
primary = mapDomain mainDomain domains."${mainDomain}";
|
||
|
domainsStr = lib.concatStringsSep " " ([primary] ++ (lib.remove primary (lib.mapAttrsToList mapDomain domains)));
|
||
|
cmd = pkgs.writeShellScript "renew" ''
|
||
|
set -euo pipefail
|
||
|
acme.sh --server ${server} --issue ${lib.optionalString (!production) "--test"} ${domainsStr} --syslog 6 > /dev/null
|
||
|
acme.sh --install-cert ${domainsStr} \
|
||
|
--key-file ${keyPath} \
|
||
|
--fullchain-file ${certPath} \
|
||
|
--reloadcmd "touch ${statePath}/renewed" \
|
||
|
--syslog 6 > /dev/null
|
||
|
'';
|
||
|
in
|
||
|
if consulLock == null
|
||
|
then ''
|
||
|
${cmd}
|
||
|
rm -f "$LE_WORKING_DIR/account.conf"
|
||
|
''
|
||
|
else ''
|
||
|
# consul lock does not expose the exit code, because of platform compatiblity or something
|
||
|
# write it to the 'ecode' file, or exit 1 if it fails altogether
|
||
|
if ${config.services.consul.package}/bin/consul lock -verbose "${consulLock}" '${cmd}; echo $? > ${statePath}/ecode'; then
|
||
|
rm -f "$LE_WORKING_DIR/account.conf"
|
||
|
exit $(cat ${statePath}/ecode)
|
||
|
else
|
||
|
rm -f "$LE_WORKING_DIR/account.conf"
|
||
|
exit 1
|
||
|
fi
|
||
|
'';
|
||
|
postStart = ''
|
||
|
if [ -e "${statePath}/renewed" ]; then
|
||
|
${postRun}
|
||
|
rm -f "${statePath}/renewed"
|
||
|
fi
|
||
|
'';
|
||
|
}))
|
||
|
cfg.certs;
|
||
|
systemd.timers = lib.mapAttrs' (name: value:
|
||
|
lib.nameValuePair "acme-sh-${name}" (with value; {
|
||
|
wantedBy = ["timers.target"];
|
||
|
timerConfig = {
|
||
|
OnCalendar = renewInterval;
|
||
|
Unit = "acme-sh-${name}.service";
|
||
|
Persistent = "yes";
|
||
|
AccuracySec = "5m";
|
||
|
RandomizedDelaySec = "1h";
|
||
|
};
|
||
|
}))
|
||
|
cfg.certs;
|
||
|
};
|
||
|
}
|