-
-
Save andrew-d/582a80006d3c1334382dc5a16affb1d4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ config, lib, pkgs, ... }: | |
let | |
inherit (lib) concatMap escapeShellArg escapeShellArgs mkIf mkOption optionals types; | |
cfg = config.roles.lego-letsencrypt; | |
in | |
{ | |
options = { | |
roles.lego-letsencrypt = { | |
enable = mkOption { | |
default = false; | |
type = types.bool; | |
description = '' | |
Obtain Let's Encrypt certificates by using lego. | |
''; | |
}; | |
staging = mkOption { | |
default = false; | |
type = types.bool; | |
description = '' | |
Use the staging Let's Encrypt server (useful for testing). | |
''; | |
}; | |
directory = mkOption { | |
default = "/var/lib/lego"; | |
type = types.str; | |
description = '' | |
Directory where certs and other state will be stored. | |
''; | |
}; | |
group = mkOption { | |
default = "certs"; | |
type = types.str; | |
description = '' | |
Group to create with access to certificates. | |
''; | |
}; | |
domain = mkOption { | |
type = types.str; | |
description = '' | |
Domain name to obtain a certificate for. | |
''; | |
}; | |
alternateNames = mkOption { | |
type = types.listOf types.str; | |
default = []; | |
description = '' | |
Alternate names to add to the certificate. | |
''; | |
}; | |
email = mkOption { | |
type = types.nullOr types.str; | |
default = null; | |
description = "Contact email address for the CA to be able to reach you."; | |
}; | |
renewDays = mkOption { | |
type = types.int; | |
default = 30; | |
description = '' | |
The number of days left on a certificate to renew it. | |
''; | |
}; | |
zoneID = mkOption { | |
type = types.str; | |
description = '' | |
Route53 zone ID to update. | |
''; | |
}; | |
credentialsFile = mkOption { | |
type = types.nullOr (types.uniq types.string); | |
default = null; | |
description = '' | |
Shell script that will be evaluated to obtain AWS credentials. | |
''; | |
}; | |
}; | |
}; | |
config = (mkIf cfg.enable { | |
# Create group for certificates so we can allow other things to read them. | |
users.groups."${cfg.group}".gid = 401; | |
# Main systemd services | |
systemd.services = | |
let | |
globalOptions = | |
[ "--accept-tos" | |
"--dns" "route53" | |
"--domains" cfg.domain | |
"--path" cfg.directory | |
"--pem" ] | |
++ optionals (cfg.email != null) [ "--email" cfg.email ] | |
++ optionals cfg.staging [ "--server" "https://acme-staging.api.letsencrypt.org/directory" ] | |
++ concatMap (d: ["--domains" d]) cfg.alternateNames; | |
certFile = "${cfg.directory}/certificates/${cfg.domain}.crt"; | |
keyFile = "${cfg.directory}/certificates/${cfg.domain}.key"; | |
hashFile = "${cfg.directory}/certificates/.${cfg.domain}.sha256"; | |
prelude = '' | |
${if cfg.credentialsFile != null then '' | |
# Load secrets | |
source ${cfg.credentialsFile} | |
'' else ""} | |
# Export configuration | |
export AWS_HOSTED_ZONE_ID="${cfg.zoneID}" | |
export AWS_REGION=us-west-2 | |
''; | |
postlude = '' | |
# We only need to do things if we changed the key/cert | |
if ! sha256sum --quiet -c ${escapeShellArg hashFile} >/dev/null 2>&1 ; then | |
# Create PKCS#12 file with no password (we mostly care about file permissions) | |
( | |
cd '${cfg.directory}/certificates' | |
umask 027 | |
${pkgs.openssl}/bin/openssl pkcs12 \ | |
-export \ | |
-in ${cfg.domain}.crt \ | |
-inkey ${cfg.domain}.key \ | |
-out ${cfg.domain}.p12 \ | |
-passout pass:x | |
) | |
# Ensure it has the right group | |
${pkgs.coreutils}/bin/chgrp '${cfg.group}' '${cfg.directory}/certificates/${cfg.domain}.p12' | |
# Hash the input files so we know when things have changed. | |
${pkgs.coreutils}/bin/sha256sum ${escapeShellArg certFile} ${escapeShellArg keyFile} > ${escapeShellArg hashFile} | |
fi | |
''; | |
in { | |
# Obtain the certificate if it doesn't exist. | |
"lego-letsencrypt-initial" = { | |
description = "Obtain Let's Encrypt certificate"; | |
serviceConfig = { | |
Type = "oneshot"; | |
User = "root"; | |
PrivateTmp = true; | |
}; | |
preStart = '' | |
mkdir -p '${cfg.directory}' | |
# Certificates directory should be group-readable | |
mkdir -p '${cfg.directory}/certificates' | |
chgrp '${cfg.group}' '${cfg.directory}/certificates' | |
chmod 0775 '${cfg.directory}/certificates' | |
''; | |
script = '' | |
${prelude} | |
# Obtain the actual certificate | |
${pkgs.lego}/bin/lego \ | |
${escapeShellArgs globalOptions} \ | |
run | |
''; | |
postStop = postlude; | |
unitConfig = { | |
# Only obtain the cert if it doesn't exist. | |
ConditionPathExists = "!${certFile}"; | |
}; | |
before = [ "lego-letsencrypt.target" ]; | |
wantedBy = [ "lego-letsencrypt.target" ]; | |
}; | |
# Renew the certificate if it exists. | |
"lego-letsencrypt-renew" = { | |
description = "Renew Let's Encrypt certificates"; | |
serviceConfig = { | |
Type = "oneshot"; | |
User = "root"; | |
PrivateTmp = true; | |
}; | |
script = '' | |
${prelude} | |
# Renew the certificate | |
${pkgs.lego}/bin/lego \ | |
${escapeShellArgs globalOptions} \ | |
renew \ | |
--days ${toString cfg.renewDays} | |
''; | |
postStop = postlude; | |
unitConfig = { | |
# Only renew the cert if it exists. | |
ConditionPathExists = "${certFile}"; | |
}; | |
}; | |
}; | |
systemd.timers."lego-letsencrypt-renew" = { | |
description = "Periodically renew certificates"; | |
wantedBy = [ "timers.target" ]; | |
timerConfig = { | |
OnCalendar = "weekly"; | |
Persistent = true; # Start immediately after computer is started | |
AccuracySec = "10m"; # Timer accuracy; better for power if not too granular | |
RandomizedDelaySec = "1h"; # Delay during the time period so we don't hammer Let's Encrypt | |
}; | |
}; | |
# Create a target for other things to depend upon. | |
systemd.targets."lego-letsencrypt" = {}; | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment