nixos/acme: Restructure module
- Use an acme user and group, allow group override only - Use hashes to determine when certs actually need to regenerate - Avoid running lego more than necessary - Harden permissions - Support "systemctl clean" for cert regeneration - Support reuse of keys between some configuration changes - Permissions fix services solves for previously root owned certs - Add a note about multiple account creation and emails - Migrate extraDomains to a list - Deprecate user option - Use minica for self-signed certs - Rewrite all tests I thought of a few more cases where things may go wrong, and added tests to cover them. In particular, the web server reload services were depending on the target - which stays alive, meaning that the renewal timer wouldn't be triggering a reload and old certs would stay on the web servers. I encountered some problems ensuring that the reload took place without accidently triggering it as part of the test. The sync commands I added ended up being essential and I'm not sure why, it seems like either node.succeed ends too early or there's an oddity of the vm's filesystem I'm not aware of. - Fix duplicate systemd rules on reload services Since useACMEHost is not unique to every vhost, if one cert was reused many times it would create duplicate entries in ${server}-config-reload.service for wants, before and ConditionPathExists
This commit is contained in:
parent
6ab387699a
commit
982c5a1f0e
15 changed files with 839 additions and 729 deletions
|
@ -1,11 +1,309 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
{ config, lib, pkgs, options, ... }:
|
||||
with lib;
|
||||
let
|
||||
|
||||
cfg = config.security.acme;
|
||||
|
||||
# Used to calculate timer accuracy for coalescing
|
||||
numCerts = length (builtins.attrNames cfg.certs);
|
||||
_24hSecs = 60 * 60 * 24;
|
||||
|
||||
# There are many services required to make cert renewals work.
|
||||
# They all follow a common structure:
|
||||
# - They inherit this commonServiceConfig
|
||||
# - They all run as the acme user
|
||||
# - They all use BindPath and StateDirectory where possible
|
||||
# to set up a sort of build environment in /tmp
|
||||
# The Group can vary depending on what the user has specified in
|
||||
# security.acme.certs.<cert>.group on some of the services.
|
||||
commonServiceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "acme";
|
||||
Group = mkDefault "acme";
|
||||
UMask = 0027;
|
||||
StateDirectoryMode = 750;
|
||||
ProtectSystem = "full";
|
||||
PrivateTmp = true;
|
||||
|
||||
WorkingDirectory = "/tmp";
|
||||
};
|
||||
|
||||
# In order to avoid race conditions creating the CA for selfsigned certs,
|
||||
# we have a separate service which will create the necessary files.
|
||||
selfsignCAService = {
|
||||
description = "Generate self-signed certificate authority";
|
||||
|
||||
path = with pkgs; [ minica ];
|
||||
|
||||
unitConfig = {
|
||||
ConditionPathExists = "!/var/lib/acme/.minica/key.pem";
|
||||
};
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
StateDirectory = "acme/.minica";
|
||||
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
|
||||
};
|
||||
|
||||
# Working directory will be /tmp
|
||||
script = ''
|
||||
minica \
|
||||
--ca-key ca/key.pem \
|
||||
--ca-cert ca/cert.pem \
|
||||
--domains selfsigned.local
|
||||
|
||||
chmod 600 ca/*
|
||||
'';
|
||||
};
|
||||
|
||||
# Previously, all certs were owned by whatever user was configured in
|
||||
# config.security.acme.certs.<cert>.user. Now everything is owned by and
|
||||
# run by the acme user.
|
||||
userMigrationService = {
|
||||
description = "Fix owner and group of all ACME certificates";
|
||||
|
||||
script = with builtins; concatStringsSep "\n" (mapAttrsToList (cert: data: ''
|
||||
for fixpath in /var/lib/acme/${escapeShellArg cert} /var/lib/acme/.lego/${escapeShellArg cert}; do
|
||||
if [ -d "$fixpath" ]; then
|
||||
chmod -R 750 "$fixpath"
|
||||
chown -R acme:${data.group} "$fixpath"
|
||||
fi
|
||||
done
|
||||
'') certConfigs);
|
||||
|
||||
# We don't want this to run every time a renewal happens
|
||||
serviceConfig.RemainAfterExit = true;
|
||||
};
|
||||
|
||||
certToConfig = cert: data: let
|
||||
acmeServer = if data.server != null then data.server else cfg.server;
|
||||
useDns = data.dnsProvider != null;
|
||||
destPath = "/var/lib/acme/${cert}";
|
||||
|
||||
# Minica and lego have a "feature" which replaces * with _. We need
|
||||
# to make this substitution to reference the output files from both programs.
|
||||
# End users never see this since we rename the certs.
|
||||
keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
|
||||
|
||||
# FIXME when mkChangedOptionModule supports submodules, change to that.
|
||||
# This is a workaround
|
||||
extraDomains = data.extraDomainNames ++ (
|
||||
optionals
|
||||
(data.extraDomains != "_mkMergedOptionModule")
|
||||
(builtins.attrNames data.extraDomains)
|
||||
);
|
||||
|
||||
# Create hashes for cert data directories based on configuration
|
||||
hashData = with builtins; ''
|
||||
${data.domain} ${data.keyType}
|
||||
${concatStringsSep " " (
|
||||
extraDomains
|
||||
++ data.extraLegoFlags
|
||||
++ data.extraLegoRunFlags
|
||||
++ data.extraLegoRenewFlags
|
||||
)}
|
||||
${toString acmeServer} ${toString data.dnsProvider}
|
||||
${toString data.ocspMustStaple}
|
||||
'';
|
||||
mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
|
||||
certDir = mkHash hashData;
|
||||
othersHash = mkHash "${toString acmeServer} ${data.keyType}";
|
||||
keyDir = "key-" + othersHash;
|
||||
accountDir = "/var/lib/acme/.lego/accounts/" + othersHash;
|
||||
|
||||
protocolOpts = if useDns then (
|
||||
[ "--dns" data.dnsProvider ]
|
||||
++ optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ]
|
||||
) else (
|
||||
[ "--http" "--http.webroot" data.webroot ]
|
||||
);
|
||||
|
||||
commonOpts = [
|
||||
"--accept-tos" # Checking the option is covered by the assertions
|
||||
"--path" "."
|
||||
"-d" data.domain
|
||||
"--email" data.email
|
||||
"--key-type" data.keyType
|
||||
] ++ protocolOpts
|
||||
++ optionals data.ocspMustStaple [ "--must-staple" ]
|
||||
++ optionals (acmeServer != null) [ "--server" acmeServer ]
|
||||
++ concatMap (name: [ "-d" name ]) extraDomains
|
||||
++ data.extraLegoFlags;
|
||||
|
||||
runOpts = escapeShellArgs (
|
||||
commonOpts
|
||||
++ [ "run" ]
|
||||
++ data.extraLegoRunFlags
|
||||
);
|
||||
renewOpts = escapeShellArgs (
|
||||
commonOpts
|
||||
++ [ "renew" "--reuse-key" "--days" (toString cfg.validMinDays) ]
|
||||
++ data.extraLegoRenewFlags
|
||||
);
|
||||
|
||||
in {
|
||||
inherit accountDir;
|
||||
|
||||
webroot = data.webroot;
|
||||
group = data.group;
|
||||
|
||||
renewTimer = {
|
||||
description = "Renew ACME Certificate for ${cert}";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.renewInterval;
|
||||
Unit = "acme-${cert}.service";
|
||||
Persistent = "yes";
|
||||
|
||||
# Allow systemd to pick a convenient time within the day
|
||||
# to run the check.
|
||||
# This allows the coalescing of multiple timer jobs.
|
||||
# We divide by the number of certificates so that if you
|
||||
# have many certificates, the renewals are distributed over
|
||||
# the course of the day to avoid rate limits.
|
||||
AccuracySec = "${toString (_24hSecs / numCerts)}s";
|
||||
|
||||
# Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
|
||||
RandomizedDelaySec = "24h";
|
||||
};
|
||||
};
|
||||
|
||||
selfsignService = {
|
||||
description = "Generate self-signed certificate for ${cert}";
|
||||
after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
|
||||
wants = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
|
||||
|
||||
path = with pkgs; [ minica ];
|
||||
|
||||
unitConfig = {
|
||||
ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
|
||||
};
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
Group = data.group;
|
||||
|
||||
StateDirectory = "acme/${cert}";
|
||||
|
||||
BindPaths = "/var/lib/acme/.minica:/tmp/ca /var/lib/acme/${cert}:/tmp/${keyName}";
|
||||
};
|
||||
|
||||
# Working directory will be /tmp
|
||||
# minica will output to a folder sharing the name of the first domain
|
||||
# in the list, which will be ${data.domain}
|
||||
script = ''
|
||||
minica \
|
||||
--ca-key ca/key.pem \
|
||||
--ca-cert ca/cert.pem \
|
||||
--domains ${escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
|
||||
|
||||
# Create files to match directory layout for real certificates
|
||||
cd '${keyName}'
|
||||
cp ../ca/cert.pem chain.pem
|
||||
cat cert.pem chain.pem > fullchain.pem
|
||||
cat key.pem fullchain.pem > full.pem
|
||||
|
||||
chmod 640 *
|
||||
|
||||
# Group might change between runs, re-apply it
|
||||
chown 'acme:${data.group}' *
|
||||
'';
|
||||
};
|
||||
|
||||
renewService = {
|
||||
description = "Renew ACME certificate for ${cert}";
|
||||
after = [ "network.target" "network-online.target" "acme-selfsigned-${cert}.service" "acme-fixperms.service" ];
|
||||
wants = [ "network-online.target" "acme-selfsigned-${cert}.service" "acme-fixperms.service" ];
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
|
||||
wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ];
|
||||
|
||||
path = with pkgs; [ lego coreutils ];
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
Group = data.group;
|
||||
|
||||
# AccountDir dir will be created by tmpfiles to ensure correct permissions
|
||||
# And to avoid deletion during systemctl clean
|
||||
# acme/.lego/${cert} is listed so that it is deleted during systemctl clean
|
||||
StateDirectory = "acme/${cert} acme/.lego/${cert} acme/.lego/${cert}/${certDir} acme/.lego/${cert}/${keyDir}";
|
||||
|
||||
# Needs to be space separated, but can't use a multiline string because that'll include newlines
|
||||
BindPaths =
|
||||
"${accountDir}:/tmp/accounts " +
|
||||
"/var/lib/acme/${cert}:/tmp/out " +
|
||||
"/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates " +
|
||||
"/var/lib/acme/.lego/${cert}/${keyDir}:/tmp/keys";
|
||||
|
||||
# Only try loading the credentialsFile if the dns challenge is enabled
|
||||
EnvironmentFile = mkIf useDns data.credentialsFile;
|
||||
};
|
||||
|
||||
# Working directory will be /tmp
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
# Safely copy keyDir contents into certificates (it might be empty).
|
||||
cp -af keys/. certificates/
|
||||
|
||||
# Check if we can renew
|
||||
if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' ]; then
|
||||
lego ${renewOpts}
|
||||
|
||||
# Otherwise do a full run
|
||||
else
|
||||
lego ${runOpts}
|
||||
fi
|
||||
|
||||
chmod 640 certificates/*
|
||||
chmod -R 700 accounts/*
|
||||
|
||||
# Group might change between runs, re-apply it
|
||||
chown 'acme:${data.group}' certificates/*
|
||||
|
||||
# Copy the key to keyDir
|
||||
cp -pf 'certificates/${keyName}.key' 'keys/'
|
||||
|
||||
# Copy all certs to the "real" certs directory
|
||||
CERT='certificates/${keyName}.crt'
|
||||
CERT_CHANGED=no
|
||||
if [ -e "$CERT" -a "$CERT" -nt out/fullchain.pem ]; then
|
||||
CERT_CHANGED=yes
|
||||
cp -p 'certificates/${keyName}.crt' out/fullchain.pem
|
||||
cp -p 'certificates/${keyName}.key' out/key.pem
|
||||
cp -p 'certificates/${keyName}.issuer.crt' out/chain.pem
|
||||
ln -sf fullchain.pem out/cert.pem
|
||||
cat out/key.pem out/fullchain.pem > out/full.pem
|
||||
fi
|
||||
|
||||
if [ "$CERT_CHANGED" = "yes" ]; then
|
||||
cd out
|
||||
set +euo pipefail
|
||||
${data.postRun}
|
||||
fi
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
certConfigs = mapAttrs certToConfig cfg.certs;
|
||||
|
||||
certOpts = { name, ... }: {
|
||||
options = {
|
||||
# user option has been removed
|
||||
user = mkOption {
|
||||
visible = false;
|
||||
default = "_mkRemovedOptionModule";
|
||||
};
|
||||
|
||||
# allowKeysForGroup option has been removed
|
||||
allowKeysForGroup = mkOption {
|
||||
visible = false;
|
||||
default = "_mkRemovedOptionModule";
|
||||
};
|
||||
|
||||
# extraDomains was replaced with extraDomainNames
|
||||
extraDomains = mkOption {
|
||||
visible = false;
|
||||
default = "_mkMergedOptionModule";
|
||||
};
|
||||
|
||||
webroot = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
|
@ -41,35 +339,19 @@ let
|
|||
description = "Contact email address for the CA to be able to reach you.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "root";
|
||||
description = "User running the ACME client.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "root";
|
||||
default = "acme";
|
||||
description = "Group running the ACME client.";
|
||||
};
|
||||
|
||||
allowKeysForGroup = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Give read permissions to the specified group
|
||||
(<option>security.acme.cert.<name>.group</option>) to read SSL private certificates.
|
||||
'';
|
||||
};
|
||||
|
||||
postRun = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
example = "systemctl reload nginx.service";
|
||||
example = "cp full.pem backup.pem";
|
||||
description = ''
|
||||
Commands to run after new certificates go live. Typically
|
||||
the web server and other servers using certificates need to
|
||||
be reloaded.
|
||||
Commands to run after new certificates go live. Note that
|
||||
these commands run as the acme user and configured group.
|
||||
|
||||
Executed in the same directory with the new certificate.
|
||||
'';
|
||||
|
@ -82,18 +364,17 @@ let
|
|||
description = "Directory where certificate and other state is stored.";
|
||||
};
|
||||
|
||||
extraDomains = mkOption {
|
||||
type = types.attrsOf (types.nullOr types.str);
|
||||
default = {};
|
||||
extraDomainNames = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = literalExample ''
|
||||
{
|
||||
"example.org" = null;
|
||||
"mydomain.org" = null;
|
||||
}
|
||||
[
|
||||
"example.org"
|
||||
"mydomain.org"
|
||||
]
|
||||
'';
|
||||
description = ''
|
||||
A list of extra domain names, which are included in the one certificate to be issued.
|
||||
Setting a distinct server root is deprecated and not functional in 20.03+
|
||||
'';
|
||||
};
|
||||
|
||||
|
@ -176,24 +457,8 @@ let
|
|||
};
|
||||
};
|
||||
|
||||
in
|
||||
in {
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
imports = [
|
||||
(mkRemovedOptionModule [ "security" "acme" "production" ] ''
|
||||
Use security.acme.server to define your staging ACME server URL instead.
|
||||
|
||||
To use Let's Encrypt's staging server, use security.acme.server =
|
||||
"https://acme-staging-v02.api.letsencrypt.org/directory".
|
||||
''
|
||||
)
|
||||
(mkRemovedOptionModule [ "security" "acme" "directory"] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
|
||||
(mkRemovedOptionModule [ "security" "acme" "preDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
||||
(mkRemovedOptionModule [ "security" "acme" "activationDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
||||
(mkChangedOptionModule [ "security" "acme" "validMin"] [ "security" "acme" "validMinDays"] (config: config.security.acme.validMin / (24 * 3600)))
|
||||
];
|
||||
options = {
|
||||
security.acme = {
|
||||
|
||||
|
@ -266,7 +531,7 @@ in
|
|||
"example.com" = {
|
||||
webroot = "/var/www/challenges/";
|
||||
email = "foo@example.com";
|
||||
extraDomains = { "www.example.com" = null; "foo.example.com" = null; };
|
||||
extraDomainNames = [ "www.example.com" "foo.example.com" ];
|
||||
};
|
||||
"bar.example.com" = {
|
||||
webroot = "/var/www/challenges/";
|
||||
|
@ -278,25 +543,40 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
###### implementation
|
||||
imports = [
|
||||
(mkRemovedOptionModule [ "security" "acme" "production" ] ''
|
||||
Use security.acme.server to define your staging ACME server URL instead.
|
||||
|
||||
To use the let's encrypt staging server, use security.acme.server =
|
||||
"https://acme-staging-v02.api.letsencrypt.org/directory".
|
||||
''
|
||||
)
|
||||
(mkRemovedOptionModule [ "security" "acme" "directory" ] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
|
||||
(mkRemovedOptionModule [ "security" "acme" "preDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
||||
(mkRemovedOptionModule [ "security" "acme" "activationDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
||||
(mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
|
||||
];
|
||||
|
||||
config = mkMerge [
|
||||
(mkIf (cfg.certs != { }) {
|
||||
|
||||
# FIXME Most of these custom warnings and filters for security.acme.certs.* are required
|
||||
# because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
|
||||
warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then ''
|
||||
The option definition `security.acme.certs.${cert}.extraDomains` has changed
|
||||
to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
|
||||
Setting a custom webroot for extra domains is not possible, instead use separate certs.
|
||||
'' else "") cfg.certs);
|
||||
|
||||
assertions = let
|
||||
certs = (mapAttrsToList (k: v: v) cfg.certs);
|
||||
certs = attrValues cfg.certs;
|
||||
in [
|
||||
{
|
||||
assertion = all (certOpts: certOpts.dnsProvider == null || certOpts.webroot == null) certs;
|
||||
message = ''
|
||||
Options `security.acme.certs.<name>.dnsProvider` and
|
||||
`security.acme.certs.<name>.webroot` are mutually exclusive.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
|
||||
message = ''
|
||||
You must define `security.acme.certs.<name>.email` or
|
||||
`security.acme.email` to register with the CA.
|
||||
`security.acme.email` to register with the CA. Note that using
|
||||
many different addresses for certs may trigger account rate limits.
|
||||
'';
|
||||
}
|
||||
{
|
||||
|
@ -307,184 +587,78 @@ in
|
|||
to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
|
||||
'';
|
||||
}
|
||||
];
|
||||
] ++ (builtins.concatLists (mapAttrsToList (cert: data: [
|
||||
{
|
||||
assertion = data.user == "_mkRemovedOptionModule";
|
||||
message = ''
|
||||
The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it.
|
||||
Certificate user is now hard coded to the "acme" user. If you would
|
||||
like another user to have access, consider adding them to the
|
||||
"acme" group or changing security.acme.certs.${cert}.group.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
|
||||
message = ''
|
||||
The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it.
|
||||
All certs are readable by the configured group. If this is undesired,
|
||||
consider changing security.acme.certs.${cert}.group to an unused group.
|
||||
'';
|
||||
}
|
||||
# * in the cert value breaks building of systemd services, and makes
|
||||
# referencing them as a user quite weird too. Best practice is to use
|
||||
# the domain option.
|
||||
{
|
||||
assertion = ! hasInfix "*" cert;
|
||||
message = ''
|
||||
The cert option path `security.acme.certs.${cert}.dnsProvider`
|
||||
cannot contain a * character.
|
||||
Instead, set `security.acme.certs.${cert}.domain = "${cert}";`
|
||||
and remove the wildcard from the path.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = data.dnsProvider == null || data.webroot == null;
|
||||
message = ''
|
||||
Options `security.acme.certs.${cert}.dnsProvider` and
|
||||
`security.acme.certs.${cert}.webroot` are mutually exclusive.
|
||||
'';
|
||||
}
|
||||
]) cfg.certs));
|
||||
|
||||
systemd.services = let
|
||||
services = concatLists servicesLists;
|
||||
servicesLists = mapAttrsToList certToServices cfg.certs;
|
||||
certToServices = cert: data:
|
||||
let
|
||||
# StateDirectory must be relative, and will be created under /var/lib by systemd
|
||||
lpath = "acme/${cert}";
|
||||
apath = "/var/lib/${lpath}";
|
||||
spath = "/var/lib/acme/.lego/${cert}";
|
||||
keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
|
||||
requestedDomains = pipe ([ data.domain ] ++ (attrNames data.extraDomains)) [
|
||||
(domains: sort builtins.lessThan domains)
|
||||
(domains: concatStringsSep "," domains)
|
||||
];
|
||||
fileMode = if data.allowKeysForGroup then "640" else "600";
|
||||
globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ]
|
||||
++ optionals (cfg.acceptTerms) [ "--accept-tos" ]
|
||||
++ optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ]
|
||||
++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains)
|
||||
++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" data.webroot ])
|
||||
++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)]
|
||||
++ data.extraLegoFlags;
|
||||
certOpts = optionals data.ocspMustStaple [ "--must-staple" ];
|
||||
runOpts = escapeShellArgs (globalOpts ++ [ "run" ] ++ certOpts ++ data.extraLegoRunFlags);
|
||||
renewOpts = escapeShellArgs (globalOpts ++
|
||||
[ "renew" "--days" (toString cfg.validMinDays) ] ++
|
||||
certOpts ++ data.extraLegoRenewFlags);
|
||||
acmeService = {
|
||||
description = "Renew ACME Certificate for ${cert}";
|
||||
path = with pkgs; [ openssl ];
|
||||
after = [ "network.target" "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
wantedBy = mkIf (!config.boot.isContainer) [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = data.user;
|
||||
Group = data.group;
|
||||
PrivateTmp = true;
|
||||
StateDirectory = "acme/.lego/${cert} acme/.lego/accounts ${lpath}";
|
||||
StateDirectoryMode = if data.allowKeysForGroup then "750" else "700";
|
||||
WorkingDirectory = spath;
|
||||
# Only try loading the credentialsFile if the dns challenge is enabled
|
||||
EnvironmentFile = if data.dnsProvider != null then data.credentialsFile else null;
|
||||
ExecStart = pkgs.writeScript "acme-start" ''
|
||||
#!${pkgs.runtimeShell} -e
|
||||
test -L ${spath}/accounts -o -d ${spath}/accounts || ln -s ../accounts ${spath}/accounts
|
||||
LEGO_ARGS=(${runOpts})
|
||||
if [ -e ${spath}/certificates/${keyName}.crt ]; then
|
||||
REQUESTED_DOMAINS="${requestedDomains}"
|
||||
EXISTING_DOMAINS="$(openssl x509 -in ${spath}/certificates/${keyName}.crt -noout -ext subjectAltName | tail -n1 | sed -e 's/ *DNS://g')"
|
||||
if [ "''${REQUESTED_DOMAINS}" == "''${EXISTING_DOMAINS}" ]; then
|
||||
LEGO_ARGS=(${renewOpts})
|
||||
fi
|
||||
fi
|
||||
${pkgs.lego}/bin/lego ''${LEGO_ARGS[@]}
|
||||
'';
|
||||
ExecStartPost =
|
||||
let
|
||||
script = pkgs.writeScript "acme-post-start" ''
|
||||
#!${pkgs.runtimeShell} -e
|
||||
cd ${apath}
|
||||
users.users.acme = {
|
||||
home = "/var/lib/acme";
|
||||
group = "acme";
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
# Test that existing cert is older than new cert
|
||||
KEY=${spath}/certificates/${keyName}.key
|
||||
KEY_CHANGED=no
|
||||
if [ -e $KEY -a $KEY -nt key.pem ]; then
|
||||
KEY_CHANGED=yes
|
||||
cp -p ${spath}/certificates/${keyName}.key key.pem
|
||||
cp -p ${spath}/certificates/${keyName}.crt fullchain.pem
|
||||
cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem
|
||||
ln -sf fullchain.pem cert.pem
|
||||
cat key.pem fullchain.pem > full.pem
|
||||
fi
|
||||
users.groups.acme = {};
|
||||
|
||||
chmod ${fileMode} *.pem
|
||||
chown '${data.user}:${data.group}' *.pem
|
||||
systemd.services = {
|
||||
"acme-fixperms" = userMigrationService;
|
||||
} // (mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs)
|
||||
// (optionalAttrs (cfg.preliminarySelfsigned) ({
|
||||
"acme-selfsigned-ca" = selfsignCAService;
|
||||
} // (mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs)));
|
||||
|
||||
if [ "$KEY_CHANGED" = "yes" ]; then
|
||||
: # noop in case postRun is empty
|
||||
${data.postRun}
|
||||
fi
|
||||
'';
|
||||
in
|
||||
"+${script}";
|
||||
};
|
||||
systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
|
||||
|
||||
};
|
||||
selfsignedService = {
|
||||
description = "Create preliminary self-signed certificate for ${cert}";
|
||||
path = [ pkgs.openssl ];
|
||||
script =
|
||||
''
|
||||
workdir="$(mktemp -d)"
|
||||
# .lego and .lego/accounts specified to fix any incorrect permissions
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/acme/.lego - acme acme"
|
||||
"d /var/lib/acme/.lego/accounts - acme acme"
|
||||
] ++ (unique (concatMap (conf: [
|
||||
"d ${conf.accountDir} - acme acme"
|
||||
] ++ (optional (conf.webroot != null) "d ${conf.webroot}/.well-known/acme-challenge - acme ${conf.group}")
|
||||
) (attrValues certConfigs)));
|
||||
|
||||
# Create CA
|
||||
openssl genrsa -des3 -passout pass:xxxx -out $workdir/ca.pass.key 2048
|
||||
openssl rsa -passin pass:xxxx -in $workdir/ca.pass.key -out $workdir/ca.key
|
||||
openssl req -new -key $workdir/ca.key -out $workdir/ca.csr \
|
||||
-subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=Security Department/CN=example.com"
|
||||
openssl x509 -req -days 1 -in $workdir/ca.csr -signkey $workdir/ca.key -out $workdir/ca.crt
|
||||
|
||||
# Create key
|
||||
openssl genrsa -des3 -passout pass:xxxx -out $workdir/server.pass.key 2048
|
||||
openssl rsa -passin pass:xxxx -in $workdir/server.pass.key -out $workdir/server.key
|
||||
openssl req -new -key $workdir/server.key -out $workdir/server.csr \
|
||||
-subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
|
||||
openssl x509 -req -days 1 -in $workdir/server.csr -CA $workdir/ca.crt \
|
||||
-CAkey $workdir/ca.key -CAserial $workdir/ca.srl -CAcreateserial \
|
||||
-out $workdir/server.crt
|
||||
|
||||
# Copy key to destination
|
||||
cp $workdir/server.key ${apath}/key.pem
|
||||
|
||||
# Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
|
||||
cat $workdir/{server.crt,ca.crt} > "${apath}/fullchain.pem"
|
||||
|
||||
# Create full.pem for e.g. lighttpd
|
||||
cat $workdir/{server.key,server.crt,ca.crt} > "${apath}/full.pem"
|
||||
|
||||
# Give key acme permissions
|
||||
chown '${data.user}:${data.group}' "${apath}/"{key,fullchain,full}.pem
|
||||
chmod ${fileMode} "${apath}/"{key,fullchain,full}.pem
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
PrivateTmp = true;
|
||||
StateDirectory = lpath;
|
||||
User = data.user;
|
||||
Group = data.group;
|
||||
};
|
||||
unitConfig = {
|
||||
# Do not create self-signed key when key already exists
|
||||
ConditionPathExists = "!${apath}/key.pem";
|
||||
};
|
||||
};
|
||||
in (
|
||||
[ { name = "acme-${cert}"; value = acmeService; } ]
|
||||
++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; }
|
||||
);
|
||||
servicesAttr = listToAttrs services;
|
||||
in
|
||||
servicesAttr;
|
||||
|
||||
systemd.tmpfiles.rules =
|
||||
map (data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}") (filter (data: data.webroot != null) (attrValues cfg.certs));
|
||||
|
||||
systemd.timers = let
|
||||
# Allow systemd to pick a convenient time within the day
|
||||
# to run the check.
|
||||
# This allows the coalescing of multiple timer jobs.
|
||||
# We divide by the number of certificates so that if you
|
||||
# have many certificates, the renewals are distributed over
|
||||
# the course of the day to avoid rate limits.
|
||||
numCerts = length (attrNames cfg.certs);
|
||||
_24hSecs = 60 * 60 * 24;
|
||||
AccuracySec = "${toString (_24hSecs / numCerts)}s";
|
||||
in flip mapAttrs' cfg.certs (cert: data: nameValuePair
|
||||
("acme-${cert}")
|
||||
({
|
||||
description = "Renew ACME Certificate for ${cert}";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.renewInterval;
|
||||
Unit = "acme-${cert}.service";
|
||||
Persistent = "yes";
|
||||
inherit AccuracySec;
|
||||
# Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
|
||||
RandomizedDelaySec = "24h";
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
systemd.targets.acme-selfsigned-certificates = mkIf cfg.preliminarySelfsigned {};
|
||||
systemd.targets.acme-certificates = {};
|
||||
# Create some targets which can be depended on to be "active" after cert renewals
|
||||
systemd.targets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
|
||||
wantedBy = [ "default.target" ];
|
||||
wants = [ "acme-${cert}.service" "acme-selfsigned-${cert}.service" ];
|
||||
after = [ "acme-${cert}.service" "acme-selfsigned-${cert}.service" ];
|
||||
}) certConfigs;
|
||||
})
|
||||
|
||||
];
|
||||
|
||||
meta = {
|
||||
|
|
|
@ -72,7 +72,7 @@ services.nginx = {
|
|||
"foo.example.com" = {
|
||||
<link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
|
||||
<link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
|
||||
# All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomains">extra domains</link> on the certificate.
|
||||
# All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomainNames">extra domain names</link> on the certificate.
|
||||
<link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "bar.example.com" ];
|
||||
locations."/" = {
|
||||
<link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
|
||||
|
@ -80,8 +80,8 @@ services.nginx = {
|
|||
};
|
||||
|
||||
# We can also add a different vhost and reuse the same certificate
|
||||
# but we have to append extraDomains manually.
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomains">security.acme.certs."foo.example.com".extraDomains."baz.example.com"</link> = null;
|
||||
# but we have to append extraDomainNames manually.
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."foo.example.com".extraDomainNames</link> = [ "baz.example.com" ];
|
||||
"baz.example.com" = {
|
||||
<link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
|
||||
<link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">useACMEHost</link> = "foo.example.com";
|
||||
|
@ -165,7 +165,7 @@ services.httpd = {
|
|||
# Since we have a wildcard vhost to handle port 80,
|
||||
# we can generate certs for anything!
|
||||
# Just make sure your DNS resolves them.
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomains">extraDomains</link> = [ "mail.example.com" ];
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "mail.example.com" ];
|
||||
};
|
||||
</programlisting>
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ services.prosody = {
|
|||
you'll need a single TLS certificate covering your main endpoint,
|
||||
the MUC one as well as the HTTP Upload one. We can generate such a
|
||||
certificate by leveraging the ACME
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomains">extraDomains</link> module option.
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> module option.
|
||||
</para>
|
||||
<para>
|
||||
Provided the setup detailed in the previous section, you'll need the following acme configuration to generate
|
||||
|
@ -78,8 +78,7 @@ security.acme = {
|
|||
"example.org" = {
|
||||
<link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/www/example.org";
|
||||
<link linkend="opt-security.acme.certs._name_.email">email</link> = "root@example.org";
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomains">extraDomains."conference.example.org"</link> = null;
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomains">extraDomains."upload.example.org"</link> = null;
|
||||
<link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "conference.example.org" "upload.example.org" ];
|
||||
};
|
||||
};
|
||||
};</programlisting>
|
||||
|
|
|
@ -6,6 +6,8 @@ let
|
|||
|
||||
cfg = config.services.httpd;
|
||||
|
||||
certs = config.security.acme.certs;
|
||||
|
||||
runtimeDir = "/run/httpd";
|
||||
|
||||
pkg = cfg.package.out;
|
||||
|
@ -26,6 +28,13 @@ let
|
|||
|
||||
vhosts = attrValues cfg.virtualHosts;
|
||||
|
||||
# certName is used later on to determine systemd service names.
|
||||
acmeEnabledVhosts = map (hostOpts: hostOpts // {
|
||||
certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName;
|
||||
}) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts);
|
||||
|
||||
dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
|
||||
|
||||
mkListenInfo = hostOpts:
|
||||
if hostOpts.listen != [] then hostOpts.listen
|
||||
else (
|
||||
|
@ -125,13 +134,13 @@ let
|
|||
|
||||
useACME = hostOpts.enableACME || hostOpts.useACMEHost != null;
|
||||
sslCertDir =
|
||||
if hostOpts.enableACME then config.security.acme.certs.${hostOpts.hostName}.directory
|
||||
else if hostOpts.useACMEHost != null then config.security.acme.certs.${hostOpts.useACMEHost}.directory
|
||||
if hostOpts.enableACME then certs.${hostOpts.hostName}.directory
|
||||
else if hostOpts.useACMEHost != null then certs.${hostOpts.useACMEHost}.directory
|
||||
else abort "This case should never happen.";
|
||||
|
||||
sslServerCert = if useACME then "${sslCertDir}/full.pem" else hostOpts.sslServerCert;
|
||||
sslServerCert = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerCert;
|
||||
sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
|
||||
sslServerChain = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerChain;
|
||||
sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain;
|
||||
|
||||
acmeChallenge = optionalString useACME ''
|
||||
Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
|
||||
|
@ -347,7 +356,6 @@ let
|
|||
cat ${php.phpIni} > $out
|
||||
echo "$options" >> $out
|
||||
'';
|
||||
|
||||
in
|
||||
|
||||
|
||||
|
@ -647,14 +655,17 @@ in
|
|||
wwwrun.gid = config.ids.gids.wwwrun;
|
||||
};
|
||||
|
||||
security.acme.certs = mapAttrs (name: hostOpts: {
|
||||
user = cfg.user;
|
||||
group = mkDefault cfg.group;
|
||||
email = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
|
||||
webroot = hostOpts.acmeRoot;
|
||||
extraDomains = genAttrs hostOpts.serverAliases (alias: null);
|
||||
postRun = "systemctl reload httpd.service";
|
||||
}) (filterAttrs (name: hostOpts: hostOpts.enableACME) cfg.virtualHosts);
|
||||
security.acme.certs = let
|
||||
acmePairs = map (hostOpts: nameValuePair hostOpts.hostName {
|
||||
group = mkDefault cfg.group;
|
||||
webroot = hostOpts.acmeRoot;
|
||||
extraDomainNames = hostOpts.serverAliases;
|
||||
# Use the vhost-specific email address if provided, otherwise let
|
||||
# security.acme.email or security.acme.certs.<cert>.email be used.
|
||||
email = mkOverride 2000 (if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr);
|
||||
# Filter for enableACME-only vhosts. Don't want to create dud certs
|
||||
}) (filter (hostOpts: hostOpts.useACMEHost == null) acmeEnabledVhosts);
|
||||
in listToAttrs acmePairs;
|
||||
|
||||
environment.systemPackages = [
|
||||
apachectl
|
||||
|
@ -724,16 +735,12 @@ in
|
|||
"Z '${cfg.logDir}' - ${svc.User} ${svc.Group}"
|
||||
];
|
||||
|
||||
systemd.services.httpd =
|
||||
let
|
||||
vhostsACME = filter (hostOpts: hostOpts.enableACME) vhosts;
|
||||
in
|
||||
{ description = "Apache HTTPD";
|
||||
|
||||
systemd.services.httpd = {
|
||||
description = "Apache HTTPD";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = concatLists (map (hostOpts: [ "acme-${hostOpts.hostName}.service" "acme-selfsigned-${hostOpts.hostName}.service" ]) vhostsACME);
|
||||
after = [ "network.target" "fs.target" ] ++ map (hostOpts: "acme-selfsigned-${hostOpts.hostName}.service") vhostsACME;
|
||||
before = map (hostOpts: "acme-${hostOpts.hostName}.service") vhostsACME;
|
||||
wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
|
||||
after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
|
||||
before = map (certName: "acme-${certName}.service") dependentCertNames;
|
||||
|
||||
path = [ pkg pkgs.coreutils pkgs.gnugrep ];
|
||||
|
||||
|
@ -767,5 +774,31 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
# postRun hooks on cert renew can't be used to restart Apache since renewal
|
||||
# runs as the unprivileged acme user. sslTargets are added to wantedBy + before
|
||||
# which allows the acme-finished-$cert.target to signify the successful updating
|
||||
# of certs end-to-end.
|
||||
systemd.services.httpd-config-reload = let
|
||||
sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
|
||||
sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
|
||||
in mkIf (sslServices != []) {
|
||||
wantedBy = sslServices ++ [ "multi-user.target" ];
|
||||
# Before the finished targets, after the renew services.
|
||||
# This service might be needed for HTTP-01 challenges, but we only want to confirm
|
||||
# certs are updated _after_ config has been reloaded.
|
||||
before = sslTargets;
|
||||
after = sslServices;
|
||||
# Block reloading if not all certs exist yet.
|
||||
# Happens when config changes add new vhosts/certs.
|
||||
unitConfig.ConditionPathExists = map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
TimeoutSec = 60;
|
||||
ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active httpd.service";
|
||||
ExecStartPre = "${pkg}/bin/apachectl configtest";
|
||||
ExecStart = "/run/current-system/systemd/bin/systemctl reload httpd.service";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,23 +6,23 @@ let
|
|||
cfg = config.services.nginx;
|
||||
certs = config.security.acme.certs;
|
||||
vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
|
||||
acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME && vhostConfig.useACMEHost == null) vhostsConfigs;
|
||||
acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null) vhostsConfigs;
|
||||
dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
|
||||
virtualHosts = mapAttrs (vhostName: vhostConfig:
|
||||
let
|
||||
serverName = if vhostConfig.serverName != null
|
||||
then vhostConfig.serverName
|
||||
else vhostName;
|
||||
certName = if vhostConfig.useACMEHost != null
|
||||
then vhostConfig.useACMEHost
|
||||
else serverName;
|
||||
in
|
||||
vhostConfig // {
|
||||
inherit serverName;
|
||||
} // (optionalAttrs vhostConfig.enableACME {
|
||||
sslCertificate = "${certs.${serverName}.directory}/fullchain.pem";
|
||||
sslCertificateKey = "${certs.${serverName}.directory}/key.pem";
|
||||
sslTrustedCertificate = "${certs.${serverName}.directory}/full.pem";
|
||||
}) // (optionalAttrs (vhostConfig.useACMEHost != null) {
|
||||
sslCertificate = "${certs.${vhostConfig.useACMEHost}.directory}/fullchain.pem";
|
||||
sslCertificateKey = "${certs.${vhostConfig.useACMEHost}.directory}/key.pem";
|
||||
sslTrustedCertificate = "${certs.${vhostConfig.useACMEHost}.directory}/fullchain.pem";
|
||||
inherit serverName certName;
|
||||
} // (optionalAttrs (vhostConfig.enableACME || vhostConfig.useACMEHost != null) {
|
||||
sslCertificate = "${certs.${certName}.directory}/fullchain.pem";
|
||||
sslCertificateKey = "${certs.${certName}.directory}/key.pem";
|
||||
sslTrustedCertificate = "${certs.${certName}.directory}/chain.pem";
|
||||
})
|
||||
) cfg.virtualHosts;
|
||||
enableIPv6 = config.networking.enableIPv6;
|
||||
|
@ -691,12 +691,12 @@ in
|
|||
systemd.services.nginx = {
|
||||
description = "Nginx Web Server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = concatLists (map (vhostConfig: ["acme-${vhostConfig.serverName}.service" "acme-selfsigned-${vhostConfig.serverName}.service"]) acmeEnabledVhosts);
|
||||
after = [ "network.target" ] ++ map (vhostConfig: "acme-selfsigned-${vhostConfig.serverName}.service") acmeEnabledVhosts;
|
||||
wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
|
||||
after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
|
||||
# Nginx needs to be started in order to be able to request certificates
|
||||
# (it's hosting the acme challenge after all)
|
||||
# This fixes https://github.com/NixOS/nixpkgs/issues/81842
|
||||
before = map (vhostConfig: "acme-${vhostConfig.serverName}.service") acmeEnabledVhosts;
|
||||
before = map (certName: "acme-${certName}.service") dependentCertNames;
|
||||
stopIfChanged = false;
|
||||
preStart = ''
|
||||
${cfg.preStart}
|
||||
|
@ -753,37 +753,41 @@ in
|
|||
source = configFile;
|
||||
};
|
||||
|
||||
systemd.services.nginx-config-reload = mkIf cfg.enableReload {
|
||||
wants = [ "nginx.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
restartTriggers = [ configFile ];
|
||||
# commented, because can cause extra delays during activate for this config:
|
||||
# services.nginx.virtualHosts."_".locations."/".proxyPass = "http://blabla:3000";
|
||||
# stopIfChanged = false;
|
||||
serviceConfig.Type = "oneshot";
|
||||
serviceConfig.TimeoutSec = 60;
|
||||
script = ''
|
||||
if /run/current-system/systemd/bin/systemctl -q is-active nginx.service ; then
|
||||
/run/current-system/systemd/bin/systemctl reload nginx.service
|
||||
fi
|
||||
'';
|
||||
serviceConfig.RemainAfterExit = true;
|
||||
# postRun hooks on cert renew can't be used to restart Nginx since renewal
|
||||
# runs as the unprivileged acme user. sslTargets are added to wantedBy + before
|
||||
# which allows the acme-finished-$cert.target to signify the successful updating
|
||||
# of certs end-to-end.
|
||||
systemd.services.nginx-config-reload = let
|
||||
sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
|
||||
sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
|
||||
in mkIf (cfg.enableReload || sslServices != []) {
|
||||
wants = optionals (cfg.enableReload) [ "nginx.service" ];
|
||||
wantedBy = sslServices ++ [ "multi-user.target" ];
|
||||
# Before the finished targets, after the renew services.
|
||||
# This service might be needed for HTTP-01 challenges, but we only want to confirm
|
||||
# certs are updated _after_ config has been reloaded.
|
||||
before = sslTargets;
|
||||
after = sslServices;
|
||||
restartTriggers = optionals (cfg.enableReload) [ configFile ];
|
||||
# Block reloading if not all certs exist yet.
|
||||
# Happens when config changes add new vhosts/certs.
|
||||
unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames);
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
TimeoutSec = 60;
|
||||
ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service";
|
||||
ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service";
|
||||
};
|
||||
};
|
||||
|
||||
security.acme.certs = filterAttrs (n: v: v != {}) (
|
||||
let
|
||||
acmePairs = map (vhostConfig: { name = vhostConfig.serverName; value = {
|
||||
user = cfg.user;
|
||||
group = lib.mkDefault cfg.group;
|
||||
webroot = vhostConfig.acmeRoot;
|
||||
extraDomains = genAttrs vhostConfig.serverAliases (alias: null);
|
||||
postRun = ''
|
||||
/run/current-system/systemd/bin/systemctl reload nginx
|
||||
'';
|
||||
}; }) acmeEnabledVhosts;
|
||||
in
|
||||
listToAttrs acmePairs
|
||||
);
|
||||
security.acme.certs = let
|
||||
acmePairs = map (vhostConfig: nameValuePair vhostConfig.serverName {
|
||||
group = mkDefault cfg.group;
|
||||
webroot = vhostConfig.acmeRoot;
|
||||
extraDomainNames = vhostConfig.serverAliases;
|
||||
# Filter for enableACME-only vhosts. Don't want to create dud certs
|
||||
}) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);
|
||||
in listToAttrs acmePairs;
|
||||
|
||||
users.users = optionalAttrs (cfg.user == "nginx") {
|
||||
nginx = {
|
||||
|
|
|
@ -1,29 +1,43 @@
|
|||
let
|
||||
commonConfig = ./common/acme/client;
|
||||
|
||||
dnsScript = {writeScript, dnsAddress, bash, curl}: writeScript "dns-hook.sh" ''
|
||||
#!${bash}/bin/bash
|
||||
dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress;
|
||||
|
||||
dnsScript = {pkgs, nodes}: let
|
||||
dnsAddress = dnsServerIP nodes;
|
||||
in pkgs.writeShellScript "dns-hook.sh" ''
|
||||
set -euo pipefail
|
||||
echo '[INFO]' "[$2]" 'dns-hook.sh' $*
|
||||
if [ "$1" = "present" ]; then
|
||||
${curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt
|
||||
${pkgs.curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt
|
||||
else
|
||||
${curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt
|
||||
${pkgs.curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt
|
||||
fi
|
||||
'';
|
||||
|
||||
documentRoot = pkgs: pkgs.runCommand "docroot" {} ''
|
||||
mkdir -p "$out"
|
||||
echo hello world > "$out/index.html"
|
||||
'';
|
||||
|
||||
vhostBase = pkgs: {
|
||||
forceSSL = true;
|
||||
locations."/".root = documentRoot pkgs;
|
||||
};
|
||||
|
||||
in import ./make-test-python.nix ({ lib, ... }: {
|
||||
name = "acme";
|
||||
meta.maintainers = lib.teams.acme.members;
|
||||
|
||||
nodes = rec {
|
||||
nodes = {
|
||||
# The fake ACME server which will respond to client requests
|
||||
acme = { nodes, lib, ... }: {
|
||||
imports = [ ./common/acme/server ];
|
||||
networking.nameservers = lib.mkForce [
|
||||
nodes.dnsserver.config.networking.primaryIPAddress
|
||||
];
|
||||
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
|
||||
};
|
||||
|
||||
# A fake DNS server which can be configured with records as desired
|
||||
# Used to test DNS-01 challenge
|
||||
dnsserver = { nodes, pkgs, ... }: {
|
||||
networking.firewall.allowedTCPPorts = [ 8055 53 ];
|
||||
networking.firewall.allowedUDPPorts = [ 53 ];
|
||||
|
@ -39,112 +53,87 @@ in import ./make-test-python.nix ({ lib, ... }: {
|
|||
};
|
||||
};
|
||||
|
||||
acmeStandalone = { nodes, lib, config, pkgs, ... }: {
|
||||
imports = [ commonConfig ];
|
||||
networking.nameservers = lib.mkForce [
|
||||
nodes.dnsserver.config.networking.primaryIPAddress
|
||||
];
|
||||
networking.firewall.allowedTCPPorts = [ 80 ];
|
||||
security.acme.certs."standalone.test" = {
|
||||
webroot = "/var/lib/acme/acme-challenges";
|
||||
};
|
||||
systemd.targets."acme-finished-standalone.test" = {
|
||||
after = [ "acme-standalone.test.service" ];
|
||||
wantedBy = [ "acme-standalone.test.service" ];
|
||||
};
|
||||
services.nginx.enable = true;
|
||||
services.nginx.virtualHosts."standalone.test" = {
|
||||
locations."/.well-known/acme-challenge".root = "/var/lib/acme/acme-challenges";
|
||||
};
|
||||
};
|
||||
|
||||
webserver = { nodes, config, pkgs, lib, ... }: {
|
||||
# A web server which will be the node requesting certs
|
||||
webserver = { pkgs, nodes, lib, config, ... }: {
|
||||
imports = [ commonConfig ];
|
||||
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||
networking.nameservers = lib.mkForce [
|
||||
nodes.dnsserver.config.networking.primaryIPAddress
|
||||
];
|
||||
|
||||
# A target remains active. Use this to probe the fact that
|
||||
# a service fired eventhough it is not RemainAfterExit
|
||||
systemd.targets."acme-finished-a.example.test" = {
|
||||
after = [ "acme-a.example.test.service" ];
|
||||
wantedBy = [ "acme-a.example.test.service" ];
|
||||
};
|
||||
# OpenSSL will be used for more thorough certificate validation
|
||||
environment.systemPackages = [ pkgs.openssl ];
|
||||
|
||||
# First tests configure a basic cert and run a bunch of openssl checks
|
||||
services.nginx.enable = true;
|
||||
|
||||
services.nginx.virtualHosts."a.example.test" = {
|
||||
services.nginx.virtualHosts."a.example.test" = (vhostBase pkgs) // {
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
locations."/".root = pkgs.runCommand "docroot" {} ''
|
||||
mkdir -p "$out"
|
||||
echo hello world > "$out/index.html"
|
||||
'';
|
||||
};
|
||||
|
||||
security.acme.server = "https://acme.test/dir";
|
||||
# Used to determine if service reload was triggered
|
||||
systemd.targets.test-renew-nginx = {
|
||||
wants = [ "acme-a.example.test.service" ];
|
||||
after = [ "acme-a.example.test.service" "nginx-config-reload.service" ];
|
||||
};
|
||||
|
||||
specialisation.second-cert.configuration = {pkgs, ...}: {
|
||||
systemd.targets."acme-finished-b.example.test" = {
|
||||
after = [ "acme-b.example.test.service" ];
|
||||
wantedBy = [ "acme-b.example.test.service" ];
|
||||
# Cert config changes will not cause the nginx configuration to change.
|
||||
# This tests that the reload service is correctly triggered.
|
||||
specialisation.cert-change.configuration = { pkgs, ... }: {
|
||||
security.acme.certs."a.example.test".keyType = "ec384";
|
||||
};
|
||||
|
||||
# Now adding an alias to ensure that the certs are updated
|
||||
specialisation.nginx-aliases.configuration = { pkgs, ... }: {
|
||||
services.nginx.virtualHosts."a.example.test" = {
|
||||
serverAliases = [ "b.example.test" ];
|
||||
};
|
||||
services.nginx.virtualHosts."b.example.test" = {
|
||||
enableACME = true;
|
||||
};
|
||||
|
||||
# Test using Apache HTTPD
|
||||
specialisation.httpd-aliases.configuration = { pkgs, config, lib, ... }: {
|
||||
services.nginx.enable = lib.mkForce false;
|
||||
services.httpd.enable = true;
|
||||
services.httpd.adminAddr = config.security.acme.email;
|
||||
services.httpd.virtualHosts."c.example.test" = {
|
||||
serverAliases = [ "d.example.test" ];
|
||||
forceSSL = true;
|
||||
locations."/".root = pkgs.runCommand "docroot" {} ''
|
||||
mkdir -p "$out"
|
||||
echo hello world > "$out/index.html"
|
||||
'';
|
||||
enableACME = true;
|
||||
documentRoot = documentRoot pkgs;
|
||||
};
|
||||
|
||||
# Used to determine if service reload was triggered
|
||||
systemd.targets.test-renew-httpd = {
|
||||
wants = [ "acme-c.example.test.service" ];
|
||||
after = [ "acme-c.example.test.service" "httpd-config-reload.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.dns-01.configuration = {pkgs, config, nodes, lib, ...}: {
|
||||
# Validation via DNS-01 challenge
|
||||
specialisation.dns-01.configuration = { pkgs, config, nodes, ... }: {
|
||||
security.acme.certs."example.test" = {
|
||||
domain = "*.example.test";
|
||||
group = config.services.nginx.group;
|
||||
dnsProvider = "exec";
|
||||
dnsPropagationCheck = false;
|
||||
credentialsFile = with pkgs; writeText "wildcard.env" ''
|
||||
EXEC_PATH=${dnsScript { inherit writeScript bash curl; dnsAddress = nodes.dnsserver.config.networking.primaryIPAddress; }}
|
||||
credentialsFile = pkgs.writeText "wildcard.env" ''
|
||||
EXEC_PATH=${dnsScript { inherit pkgs nodes; }}
|
||||
'';
|
||||
user = config.services.nginx.user;
|
||||
group = config.services.nginx.group;
|
||||
};
|
||||
systemd.targets."acme-finished-example.test" = {
|
||||
after = [ "acme-example.test.service" ];
|
||||
wantedBy = [ "acme-example.test.service" ];
|
||||
};
|
||||
systemd.services."acme-example.test" = {
|
||||
before = [ "nginx.service" ];
|
||||
wantedBy = [ "nginx.service" ];
|
||||
};
|
||||
services.nginx.virtualHosts."c.example.test" = {
|
||||
forceSSL = true;
|
||||
sslCertificate = config.security.acme.certs."example.test".directory + "/cert.pem";
|
||||
sslTrustedCertificate = config.security.acme.certs."example.test".directory + "/full.pem";
|
||||
sslCertificateKey = config.security.acme.certs."example.test".directory + "/key.pem";
|
||||
locations."/".root = pkgs.runCommand "docroot" {} ''
|
||||
mkdir -p "$out"
|
||||
echo hello world > "$out/index.html"
|
||||
'';
|
||||
|
||||
services.nginx.virtualHosts."dns.example.test" = (vhostBase pkgs) // {
|
||||
useACMEHost = "example.test";
|
||||
};
|
||||
};
|
||||
|
||||
# When nginx depends on a service that is slow to start up, requesting used to fail
|
||||
# certificates fail. Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
|
||||
specialisation.slow-startup.configuration = { pkgs, config, nodes, lib, ...}: {
|
||||
# Validate service relationships by adding a slow start service to nginx' wants.
|
||||
# Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
|
||||
specialisation.slow-startup.configuration = { pkgs, config, nodes, lib, ... }: {
|
||||
systemd.services.my-slow-service = {
|
||||
wantedBy = [ "multi-user.target" "nginx.service" ];
|
||||
before = [ "nginx.service" ];
|
||||
preStart = "sleep 5";
|
||||
script = "${pkgs.python3}/bin/python -m http.server";
|
||||
};
|
||||
systemd.targets."acme-finished-d.example.com" = {
|
||||
after = [ "acme-d.example.com.service" ];
|
||||
wantedBy = [ "acme-d.example.com.service" ];
|
||||
};
|
||||
services.nginx.virtualHosts."d.example.com" = {
|
||||
|
||||
services.nginx.virtualHosts."slow.example.com" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://localhost:8000";
|
||||
|
@ -152,11 +141,13 @@ in import ./make-test-python.nix ({ lib, ... }: {
|
|||
};
|
||||
};
|
||||
|
||||
client = {nodes, lib, ...}: {
|
||||
# The client will be used to curl the webserver to validate configuration
|
||||
client = {nodes, lib, pkgs, ...}: {
|
||||
imports = [ commonConfig ];
|
||||
networking.nameservers = lib.mkForce [
|
||||
nodes.dnsserver.config.networking.primaryIPAddress
|
||||
];
|
||||
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
|
||||
|
||||
# OpenSSL will be used for more thorough certificate validation
|
||||
environment.systemPackages = [ pkgs.openssl ];
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -167,73 +158,155 @@ in import ./make-test-python.nix ({ lib, ... }: {
|
|||
in
|
||||
# Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true,
|
||||
# this is because a oneshot goes from inactive => activating => inactive, and never
|
||||
# reaches the active state. To work around this, we create some mock target units which
|
||||
# get pulled in by the oneshot units. The target units linger after activation, and hence we
|
||||
# can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do
|
||||
# reaches the active state. Targets do not have this issue.
|
||||
|
||||
''
|
||||
has_switched = False
|
||||
|
||||
|
||||
def switch_to(node, name):
|
||||
global has_switched
|
||||
if has_switched:
|
||||
node.succeed(
|
||||
"${switchToNewServer}"
|
||||
)
|
||||
has_switched = True
|
||||
node.succeed(
|
||||
"/run/current-system/specialisation/{}/bin/switch-to-configuration test".format(
|
||||
name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Ensures the issuer of our cert matches the chain
|
||||
# and matches the issuer we expect it to be.
|
||||
# It's a good validation to ensure the cert.pem and fullchain.pem
|
||||
# are not still selfsigned afer verification
|
||||
def check_issuer(node, cert_name, issuer):
|
||||
for fname in ("cert.pem", "fullchain.pem"):
|
||||
node.succeed(
|
||||
(
|
||||
"""openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname} \
|
||||
| tee /proc/self/fd/2 \
|
||||
| cut -d'=' -f2- \
|
||||
| grep "$(openssl x509 -noout -subject -in /var/lib/acme/{cert_name}/chain.pem \
|
||||
| cut -d'=' -f2-)\" \
|
||||
| grep -i '{issuer}'
|
||||
"""
|
||||
).format(cert_name=cert_name, issuer=issuer, fname=fname)
|
||||
)
|
||||
|
||||
|
||||
# Ensure cert comes before chain in fullchain.pem
|
||||
def check_fullchain(node, cert_name):
|
||||
node.succeed(
|
||||
(
|
||||
"""openssl crl2pkcs7 -nocrl -certfile /var/lib/acme/{cert_name}/fullchain.pem \
|
||||
| tee /proc/self/fd/2 \
|
||||
| openssl pkcs7 -print_certs -noout | head -1 | grep {cert_name}
|
||||
"""
|
||||
).format(cert_name=cert_name)
|
||||
)
|
||||
|
||||
|
||||
def check_connection(node, domain):
|
||||
node.succeed(
|
||||
(
|
||||
"""openssl s_client -brief -verify 2 -verify_return_error -CAfile /tmp/ca.crt \
|
||||
-servername {domain} -connect {domain}:443 < /dev/null 2>&1 \
|
||||
| tee /proc/self/fd/2
|
||||
"""
|
||||
).format(domain=domain)
|
||||
)
|
||||
|
||||
|
||||
client.start()
|
||||
dnsserver.start()
|
||||
|
||||
acme.wait_for_unit("default.target")
|
||||
dnsserver.wait_for_unit("pebble-challtestsrv.service")
|
||||
client.wait_for_unit("default.target")
|
||||
|
||||
client.succeed(
|
||||
'curl --data \'{"host": "acme.test", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a'
|
||||
)
|
||||
client.succeed(
|
||||
'curl --data \'{"host": "standalone.test", "addresses": ["${nodes.acmeStandalone.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a'
|
||||
'curl --data \'{"host": "acme.test", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
|
||||
)
|
||||
|
||||
acme.start()
|
||||
acmeStandalone.start()
|
||||
webserver.start()
|
||||
|
||||
acme.wait_for_unit("default.target")
|
||||
acme.wait_for_unit("pebble.service")
|
||||
|
||||
with subtest("can request certificate with HTTPS-01 challenge"):
|
||||
acmeStandalone.wait_for_unit("default.target")
|
||||
acmeStandalone.succeed("systemctl start acme-standalone.test.service")
|
||||
acmeStandalone.wait_for_unit("acme-finished-standalone.test.target")
|
||||
|
||||
client.wait_for_unit("default.target")
|
||||
|
||||
client.succeed("curl https://acme.test:15000/roots/0 > /tmp/ca.crt")
|
||||
client.succeed("curl https://acme.test:15000/intermediate-keys/0 >> /tmp/ca.crt")
|
||||
|
||||
with subtest("Can request certificate for nginx service"):
|
||||
with subtest("Can request certificate with HTTPS-01 challenge"):
|
||||
webserver.wait_for_unit("acme-finished-a.example.test.target")
|
||||
check_fullchain(webserver, "a.example.test")
|
||||
check_issuer(webserver, "a.example.test", "pebble")
|
||||
check_connection(client, "a.example.test")
|
||||
|
||||
with subtest("Can generate valid selfsigned certs"):
|
||||
webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
|
||||
webserver.succeed("systemctl start acme-selfsigned-a.example.test.service")
|
||||
check_fullchain(webserver, "a.example.test")
|
||||
check_issuer(webserver, "a.example.test", "minica")
|
||||
# Will succeed if nginx can load the certs
|
||||
webserver.succeed("systemctl start nginx-config-reload.service")
|
||||
|
||||
with subtest("Can reload nginx when timer triggers renewal"):
|
||||
# These syncs are required because of weird scenarios where the cert files
|
||||
# were not actually changed when the checks run.
|
||||
webserver.succeed("sync")
|
||||
webserver.succeed("systemctl start test-renew-nginx.target")
|
||||
webserver.succeed("sync")
|
||||
check_issuer(webserver, "a.example.test", "pebble")
|
||||
check_connection(client, "a.example.test")
|
||||
|
||||
with subtest("Can reload web server when cert configuration changes"):
|
||||
switch_to(webserver, "cert-change")
|
||||
webserver.wait_for_unit("acme-finished-a.example.test.target")
|
||||
client.succeed(
|
||||
"curl --cacert /tmp/ca.crt https://a.example.test/ | grep -qF 'hello world'"
|
||||
"""openssl s_client -CAfile /tmp/ca.crt -connect a.example.test:443 < /dev/null \
|
||||
| openssl x509 -noout -text | grep -i Public-Key | grep 384
|
||||
"""
|
||||
)
|
||||
|
||||
with subtest("Can add another certificate for nginx service"):
|
||||
webserver.succeed(
|
||||
"/run/current-system/specialisation/second-cert/bin/switch-to-configuration test"
|
||||
)
|
||||
webserver.wait_for_unit("acme-finished-b.example.test.target")
|
||||
client.succeed(
|
||||
"curl --cacert /tmp/ca.crt https://b.example.test/ | grep -qF 'hello world'"
|
||||
)
|
||||
with subtest("Can request certificate with HTTPS-01 when nginx startup is delayed"):
|
||||
switch_to(webserver, "slow-startup")
|
||||
webserver.wait_for_unit("acme-finished-slow.example.com.target")
|
||||
check_issuer(webserver, "slow.example.com", "pebble")
|
||||
check_connection(client, "slow.example.com")
|
||||
|
||||
with subtest("Can request certificate for vhost + aliases (nginx)"):
|
||||
switch_to(webserver, "nginx-aliases")
|
||||
webserver.wait_for_unit("acme-finished-a.example.test.target")
|
||||
check_issuer(webserver, "a.example.test", "pebble")
|
||||
check_connection(client, "a.example.test")
|
||||
check_connection(client, "b.example.test")
|
||||
|
||||
with subtest("Can request certificates for vhost + aliases (apache-httpd)"):
|
||||
switch_to(webserver, "httpd-aliases")
|
||||
webserver.wait_for_unit("acme-finished-c.example.test.target")
|
||||
check_issuer(webserver, "c.example.test", "pebble")
|
||||
check_connection(client, "c.example.test")
|
||||
check_connection(client, "d.example.test")
|
||||
|
||||
with subtest("Can reload httpd when timer triggers renewal"):
|
||||
# Switch to selfsigned first
|
||||
webserver.succeed("systemctl clean acme-c.example.test.service --what=state")
|
||||
webserver.succeed("systemctl start acme-selfsigned-c.example.test.service")
|
||||
webserver.succeed("sync")
|
||||
check_issuer(webserver, "c.example.test", "minica")
|
||||
webserver.succeed("systemctl start httpd-config-reload.service")
|
||||
webserver.succeed("systemctl start test-renew-httpd.target")
|
||||
webserver.succeed("sync")
|
||||
check_issuer(webserver, "c.example.test", "pebble")
|
||||
check_connection(client, "c.example.test")
|
||||
|
||||
with subtest("Can request wildcard certificates using DNS-01 challenge"):
|
||||
webserver.succeed(
|
||||
"${switchToNewServer}"
|
||||
)
|
||||
webserver.succeed(
|
||||
"/run/current-system/specialisation/dns-01/bin/switch-to-configuration test"
|
||||
)
|
||||
switch_to(webserver, "dns-01")
|
||||
webserver.wait_for_unit("acme-finished-example.test.target")
|
||||
client.succeed(
|
||||
"curl --cacert /tmp/ca.crt https://c.example.test/ | grep -qF 'hello world'"
|
||||
)
|
||||
|
||||
with subtest("Can request certificate of nginx when startup is delayed"):
|
||||
webserver.succeed(
|
||||
"${switchToNewServer}"
|
||||
)
|
||||
webserver.succeed(
|
||||
"/run/current-system/specialisation/slow-startup/bin/switch-to-configuration test"
|
||||
)
|
||||
webserver.wait_for_unit("acme-finished-d.example.com.target")
|
||||
client.succeed("curl --cacert /tmp/ca.crt https://d.example.com/")
|
||||
check_issuer(webserver, "example.test", "pebble")
|
||||
check_connection(client, "dns.example.test")
|
||||
'';
|
||||
})
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
{ lib, nodes, pkgs, ... }:
|
||||
|
||||
let
|
||||
acme-ca = nodes.acme.config.test-support.acme.caCert;
|
||||
in
|
||||
caCert = nodes.acme.config.test-support.acme.caCert;
|
||||
caDomain = nodes.acme.config.test-support.acme.caDomain;
|
||||
|
||||
{
|
||||
in {
|
||||
security.acme = {
|
||||
server = "https://acme.test/dir";
|
||||
server = "https://${caDomain}/dir";
|
||||
email = "hostmaster@example.test";
|
||||
acceptTerms = true;
|
||||
};
|
||||
|
||||
security.pki.certificateFiles = [ acme-ca ];
|
||||
security.pki.certificateFiles = [ caCert ];
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# config.test-support.acme.caCert
|
||||
#
|
||||
# This value can be used inside the configuration of other test nodes to inject
|
||||
# the snakeoil certificate into security.pki.certificateFiles or into package
|
||||
# the test certificate into security.pki.certificateFiles or into package
|
||||
# overlays.
|
||||
#
|
||||
# Another value that's needed if you don't use a custom resolver (see below for
|
||||
|
@ -50,19 +50,13 @@
|
|||
# Also make sure that whenever you use a resolver from a different test node
|
||||
# that it has to be started _before_ the ACME service.
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
|
||||
let
|
||||
snakeOilCerts = import ./snakeoil-certs.nix;
|
||||
testCerts = import ./snakeoil-certs.nix {
|
||||
minica = pkgs.minica;
|
||||
mkDerivation = pkgs.stdenv.mkDerivation;
|
||||
};
|
||||
domain = testCerts.domain;
|
||||
|
||||
wfeDomain = "acme.test";
|
||||
wfeCertFile = snakeOilCerts.${wfeDomain}.cert;
|
||||
wfeKeyFile = snakeOilCerts.${wfeDomain}.key;
|
||||
|
||||
siteDomain = "acme.test";
|
||||
siteCertFile = snakeOilCerts.${siteDomain}.cert;
|
||||
siteKeyFile = snakeOilCerts.${siteDomain}.key;
|
||||
pebble = pkgs.pebble;
|
||||
resolver = let
|
||||
message = "You need to define a resolver for the acme test module.";
|
||||
firstNS = lib.head config.networking.nameservers;
|
||||
|
@ -71,8 +65,9 @@ let
|
|||
pebbleConf.pebble = {
|
||||
listenAddress = "0.0.0.0:443";
|
||||
managementListenAddress = "0.0.0.0:15000";
|
||||
certificate = snakeOilCerts.${wfeDomain}.cert;
|
||||
privateKey = snakeOilCerts.${wfeDomain}.key;
|
||||
# These certs and keys are used for the Web Front End (WFE)
|
||||
certificate = testCerts.${domain}.cert;
|
||||
privateKey = testCerts.${domain}.key;
|
||||
httpPort = 80;
|
||||
tlsPort = 443;
|
||||
ocspResponderURL = "http://0.0.0.0:4002";
|
||||
|
@ -80,18 +75,30 @@ let
|
|||
};
|
||||
|
||||
pebbleConfFile = pkgs.writeText "pebble.conf" (builtins.toJSON pebbleConf);
|
||||
pebbleDataDir = "/root/pebble";
|
||||
|
||||
in {
|
||||
imports = [ ../../resolver.nix ];
|
||||
|
||||
options.test-support.acme.caCert = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = ''
|
||||
A certificate file to use with the <literal>nodes</literal> attribute to
|
||||
inject the snakeoil CA certificate used in the ACME server into
|
||||
<option>security.pki.certificateFiles</option>.
|
||||
'';
|
||||
options.test-support.acme = with lib; {
|
||||
caDomain = mkOption {
|
||||
type = types.str;
|
||||
readOnly = true;
|
||||
default = domain;
|
||||
description = ''
|
||||
A domain name to use with the <literal>nodes</literal> attribute to
|
||||
identify the CA server.
|
||||
'';
|
||||
};
|
||||
caCert = mkOption {
|
||||
type = types.path;
|
||||
readOnly = true;
|
||||
default = testCerts.ca.cert;
|
||||
description = ''
|
||||
A certificate file to use with the <literal>nodes</literal> attribute to
|
||||
inject the test CA certificate used in the ACME server into
|
||||
<option>security.pki.certificateFiles</option>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
|
@ -99,35 +106,32 @@ in {
|
|||
resolver.enable = let
|
||||
isLocalResolver = config.networking.nameservers == [ "127.0.0.1" ];
|
||||
in lib.mkOverride 900 isLocalResolver;
|
||||
acme.caCert = snakeOilCerts.ca.cert;
|
||||
};
|
||||
|
||||
# This has priority 140, because modules/testing/test-instrumentation.nix
|
||||
# already overrides this with priority 150.
|
||||
networking.nameservers = lib.mkOverride 140 [ "127.0.0.1" ];
|
||||
networking.firewall.enable = false;
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 15000 4002 ];
|
||||
|
||||
networking.extraHosts = ''
|
||||
127.0.0.1 ${wfeDomain}
|
||||
${config.networking.primaryIPAddress} ${wfeDomain} ${siteDomain}
|
||||
127.0.0.1 ${domain}
|
||||
${config.networking.primaryIPAddress} ${domain}
|
||||
'';
|
||||
|
||||
systemd.services = {
|
||||
pebble = {
|
||||
enable = true;
|
||||
description = "Pebble ACME server";
|
||||
requires = [ ];
|
||||
wantedBy = [ "network.target" ];
|
||||
preStart = ''
|
||||
mkdir ${pebbleDataDir}
|
||||
'';
|
||||
script = ''
|
||||
cd ${pebbleDataDir}
|
||||
${pebble}/bin/pebble -config ${pebbleConfFile}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
RuntimeDirectory = "pebble";
|
||||
WorkingDirectory = "/run/pebble";
|
||||
|
||||
# Required to bind on privileged ports.
|
||||
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
||||
|
||||
ExecStart = "${pkgs.pebble}/bin/pebble -config ${pebbleConfFile}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
{ pkgs ? import <nixpkgs> {}
|
||||
, lib ? pkgs.lib
|
||||
, domains ? [ "acme.test" ]
|
||||
}:
|
||||
|
||||
pkgs.runCommand "acme-snakeoil-ca" {
|
||||
nativeBuildInputs = [ pkgs.openssl ];
|
||||
} ''
|
||||
addpem() {
|
||||
local file="$1"; shift
|
||||
local storeFileName="$(IFS=.; echo "$*")"
|
||||
|
||||
echo -n " " >> "$out"
|
||||
|
||||
# Every following argument is an attribute, so let's recurse and check
|
||||
# every attribute whether it must be quoted and write it into $out.
|
||||
while [ -n "$1" ]; do
|
||||
if expr match "$1" '^[a-zA-Z][a-zA-Z0-9]*$' > /dev/null; then
|
||||
echo -n "$1" >> "$out"
|
||||
else
|
||||
echo -n '"' >> "$out"
|
||||
echo -n "$1" | sed -e 's/["$]/\\&/g' >> "$out"
|
||||
echo -n '"' >> "$out"
|
||||
fi
|
||||
shift
|
||||
[ -z "$1" ] || echo -n . >> "$out"
|
||||
done
|
||||
|
||||
echo " = builtins.toFile \"$storeFileName\" '''" >> "$out"
|
||||
sed -e 's/^/ /' "$file" >> "$out"
|
||||
|
||||
echo " ''';" >> "$out"
|
||||
}
|
||||
|
||||
echo '# Generated via mkcert.sh in the same directory.' > "$out"
|
||||
echo '{' >> "$out"
|
||||
|
||||
openssl req -newkey rsa:4096 -x509 -sha256 -days 36500 \
|
||||
-subj '/CN=Snakeoil CA' -nodes -out ca.pem -keyout ca.key
|
||||
|
||||
addpem ca.key ca key
|
||||
addpem ca.pem ca cert
|
||||
|
||||
${lib.concatMapStrings (fqdn: let
|
||||
opensslConfig = pkgs.writeText "snakeoil.cnf" ''
|
||||
[req]
|
||||
default_bits = 4096
|
||||
prompt = no
|
||||
default_md = sha256
|
||||
req_extensions = req_ext
|
||||
distinguished_name = dn
|
||||
[dn]
|
||||
CN = ${fqdn}
|
||||
[req_ext]
|
||||
subjectAltName = DNS:${fqdn}
|
||||
'';
|
||||
in ''
|
||||
export OPENSSL_CONF=${lib.escapeShellArg opensslConfig}
|
||||
openssl genrsa -out snakeoil.key 4096
|
||||
openssl req -new -key snakeoil.key -out snakeoil.csr
|
||||
openssl x509 -req -in snakeoil.csr -sha256 -set_serial 666 \
|
||||
-CA ca.pem -CAkey ca.key -out snakeoil.pem -days 36500 \
|
||||
-extfile "$OPENSSL_CONF" -extensions req_ext
|
||||
addpem snakeoil.key ${lib.escapeShellArg fqdn} key
|
||||
addpem snakeoil.pem ${lib.escapeShellArg fqdn} cert
|
||||
'') domains}
|
||||
|
||||
echo '}' >> "$out"
|
||||
''
|
|
@ -1,6 +0,0 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -p nix bash -i bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
storepath="$(nix-build --no-out-link mkcerts.nix)"
|
||||
cat "$storepath" > snakeoil-certs.nix
|
|
@ -1,172 +1,37 @@
|
|||
# Generated via mkcert.sh in the same directory.
|
||||
{
|
||||
ca.key = builtins.toFile "ca.key" ''
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
s9o9Sr49cibZ3CbMjCSNE3AOeVE1/Q==
|
||||
-----END PRIVATE KEY-----
|
||||
'';
|
||||
ca.cert = builtins.toFile "ca.cert" ''
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFDzCCAvegAwIBAgIUX0P6NfX4gRUpFz+TNV/f26GHokgwDQYJKoZIhvcNAQEL
|
||||
BQAwFjEUMBIGA1UEAwwLU25ha2VvaWwgQ0EwIBcNMjAwODI0MDc0MjEyWhgPMjEy
|
||||
MDA3MzEwNzQyMTJaMBYxFDASBgNVBAMMC1NuYWtlb2lsIENBMIICIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAg8AMIICCgKCAgEA3dRuzhaVyKB5668XcPoYiVQ2Op0AwyhbfRQS
|
||||
sJb+8doQr1+258rx2i+x+tXPvNDHBjyEq0J9/jI7kWbqsGsePQ3XTV2xreFqhpce
|
||||
CwHiN+zoDVFM/X6Da9qww1e+OL8KdePzw9X956qVnzoM87IfK8WnNJmeUnvR8TiK
|
||||
SOzUTbhyY4+x1meGMXjr4tvPa5qbtCT3KduZ48O4ehTXx7vBhqd+FI+sAMbm3Ucn
|
||||
1YlfM/RTH24JS7gPYtTokstzUKavFcARvrv9Iguz0LEI8Uq2oELtubHwE2CKxSUO
|
||||
GQr4dJ8aijHsjzwYunQHDKWtgGQKrar/gsFnaN+gbUuV4gu6wKYGJ/srXikB0m5w
|
||||
ohtlv26Em34PZUZN8ONXREhDQgA1Woy29xKL76dLQTvwQC7nb1o8wD26V5XlTa+p
|
||||
4FrRpbGUwuy1w4AanVtipc6WRnM/j1twE5cIqf2RUbewNebuXVZFj1aD7kNMNU8P
|
||||
oPXcVhUzZN4ftneyvOyMUG3L6sXiA0hFRTK8aNHuAhtrnTHHpWcAEZeojUwBE2ts
|
||||
yVtF7QHRlTPh2NUv5FOlPSd57mw7bQJADAAYz8Moyxu4X8lqlB3/cj7OPRrH7QFb
|
||||
yMvFyHrtLIQ9QRtdf1c78KVAokXPm7blotnq2eGXz6EOWKEvYLeEviKc+0zxgZfg
|
||||
FJPRztkCAwEAAaNTMFEwHQYDVR0OBBYEFNhBZxryvykCjfPO85xB3wof2enAMB8G
|
||||
A1UdIwQYMBaAFNhBZxryvykCjfPO85xB3wof2enAMA8GA1UdEwEB/wQFMAMBAf8w
|
||||
DQYJKoZIhvcNAQELBQADggIBAEZwlsQ+3yd1MVxLRy9RjoA8hI7iWBNmvPUyNjlb
|
||||
l/L9N+dZgdx9G5h/KPRUyzvUc/uk/ZxTWVPIOp13WI65qwsBKrwvYKiXiwzjt+9V
|
||||
CKDRc1sOghTSXk4FD3L5UcKvTQ2lRcFsqxbkopEwQWhoCuhe4vFyt3Nx8ZGLCBUD
|
||||
3I5zMHtO8FtpZWKJPw46Yc1kasv0nlfly/vUbnErYfgjWX1hgWUcRgYdKwO4sOZ7
|
||||
KbNma0WUsX5mWhXo4Kk7D15wATHO+j9s+j8m86duBL3A4HzpTo1DhHvBi0dkg0CO
|
||||
XuSdByIzVLIPh3yhCHN1loRCP2rbzKM8IQeU/X5Q4UJeC/x9ew8Kk+RKXoHc8Y2C
|
||||
JQO1DxuidyDJRhbb98wZo2YfIsdWQGjYZBe1XQRwBD28JnB+Rb9shml6lORWQn9y
|
||||
P/STo9uWm5zvOCfqwbnCoetljDweItINx622G9SafBwPZc3o79oL7QSl8DgCtN6g
|
||||
p0wGIlIBx+25w/96PqZcrYb8B7/uBHJviiKjBXDoIJWNiNRhW5HaFjeJdSKq7KIL
|
||||
I/PO9KuHafif36ksG69X02Rio2/cTjgjEW1hGHcDRyyJWWaj7bd2eWuouh6FF22b
|
||||
PA6FGY4vewDPnbLKLaix2ZIKxtedUDOH/qru3Mv58IFXmQ4iyM8oC8aOxYSQLZDn
|
||||
1yJD
|
||||
-----END CERTIFICATE-----
|
||||
'';
|
||||
"acme.test".key = builtins.toFile "acme.test.key" ''
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
'';
|
||||
"acme.test".cert = builtins.toFile "acme.test.cert" ''
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEwDCCAqigAwIBAgICApowDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLU25h
|
||||
a2VvaWwgQ0EwIBcNMjAwODI0MDc0MjEzWhgPMjEyMDA3MzEwNzQyMTNaMBQxEjAQ
|
||||
BgNVBAMMCWFjbWUudGVzdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
||||
AN3SZeAchx0XKm2c25bMxyEuXhFt03F075ZagtZ3nRixlPXZwGG3aQ1GIb1s/8vQ
|
||||
H0WdXfI2M9NY2bbecVmBtEKEFHZAnPUDz4dAMa5d/HBpYgeiGSGNKn7Vvc6y0oAj
|
||||
lJ1y/+OSakqQ+ewxzEkoiZd4aDqOJHgWjETf1Cuhdw8Q/52RLIfsgQYdo7N6sZXh
|
||||
MapF/CJ5oyvaRvI+6muAXuS4eIiCwR2xgDGsTO1YjQIOCoFsoHefuKgfOiRbRP3C
|
||||
fq9w+tgEY1WsVOnjzi9AMEv78eZbOgxUAu/tQqRgjXpoCz83HO9LNFdEgJxe98R2
|
||||
KkVoyRbbpsr8k5IkFi4NMw0DroFndwfQA7miBMdp74j8qYQLYzN2Sqy9szdWiH6s
|
||||
28Sfy/E0BWtlj7D1Nvos2v5vZDHy12eFzxf0icUPuclR/q8ceTlq12dd5wjMnlaG
|
||||
l0KH5hu6GbzkG7bk/a+Q0GXfVjdD9xt7MtP3TZ5TVZ/Lggb1yU0IUSrMtRREmXYH
|
||||
BTbaSRBuwURvRnOotFd/Wkkc0oWciiUgUIZXNBTE5V+OORGLITyoxAv/krqvcMvy
|
||||
7i4yJJKgW1YOOteyJYmy89Hw2P8q2apH0qoQlbFp62ttU8tzKbw/sJ7JE87hq7wl
|
||||
U3tynFqfsJHh9Mb4a/BV1+p5+PdmAn72UiEztxRfqlrfAgMBAAGjGDAWMBQGA1Ud
|
||||
EQQNMAuCCWFjbWUudGVzdDANBgkqhkiG9w0BAQsFAAOCAgEAM5WrCpBOmLrZ1QX8
|
||||
l6vxVXwoI8pnqyy3cbAm3aLRPbw4gb0Ot90Pv/LoMhP0fkrNOKwH/FGRjSXyti0X
|
||||
TheKrP7aEf6XL2/Xnb8rK2jYMQo6YJU9T+wBJA6Q+GBrc8SE75KfOi5NWJr8T4Ju
|
||||
Etb+G05hXClrN19VFzIoz3L4kRV+xNMialcOT3xQfHtXCQUgwAWpPlwcJA/Jf60m
|
||||
XsfwQwk2Ir16wq+Lc3y+mQ7d/dbG+FVrngFk4qN2B9M/Zyv4N9ZBbqeDUn3mYtJE
|
||||
FeJrwHgmwH6slf1gBN3gxUKRW7Bvzxk548NdmLOyN+Y4StsqbOaYGtShUJA7f1Ng
|
||||
qQqdgvxZ9MNwwMv9QVDZEnaaew3/oWOSmQGAai4hrc7gLMLJmIxzgfd5P6Dr06e4
|
||||
2zwsMuI8Qh/IDqu/CfmFYvaua0FEeyAtpoID9Y/KPM7fu9bJuxjZ6kqLVFkEi9nF
|
||||
/rCMchcSA8N2z/vLPabpNotO7OYH3VD7aQGTfCL82dMlp1vwZ39S3Z1TFLLh3MZ+
|
||||
BYcAv8kUvCV6kIdPAXvJRSQOJUlJRV7XiI2mwugdDzMx69wQ0Zc1e4WyGfiSiVYm
|
||||
ckSJ/EkxuwT/ZYLqCAKSFGMlFhad9g1Zyvd67XgfZq5p0pJTtGxtn5j8QHy6PM6m
|
||||
NbjvWnP8lDU8j2l3eSG58S14iGs=
|
||||
-----END CERTIFICATE-----
|
||||
'';
|
||||
# Minica can provide a CA key and cert, plus a key
|
||||
# and cert for our fake CA server's Web Front End (WFE).
|
||||
{ minica, mkDerivation }:
|
||||
let
|
||||
domain = "acme.test";
|
||||
|
||||
selfSignedCertData = mkDerivation {
|
||||
name = "test-certs";
|
||||
buildInputs = [ minica ];
|
||||
phases = [ "buildPhase" "installPhase" ];
|
||||
|
||||
buildPhase = ''
|
||||
mkdir ca
|
||||
minica \
|
||||
--ca-key ca/key.pem \
|
||||
--ca-cert ca/cert.pem \
|
||||
--domains ${domain}
|
||||
chmod 600 ca/*
|
||||
chmod 640 ${domain}/*.pem
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
mv ${domain} ca $out/
|
||||
'';
|
||||
};
|
||||
in {
|
||||
inherit domain;
|
||||
ca = {
|
||||
cert = "${selfSignedCertData}/ca/cert.pem";
|
||||
key = "${selfSignedCertData}/ca/key.pem";
|
||||
};
|
||||
"${domain}" = {
|
||||
cert = "${selfSignedCertData}/${domain}/cert.pem";
|
||||
key = "${selfSignedCertData}/${domain}/key.pem";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
let
|
||||
certs = import ./common/acme/server/snakeoil-certs.nix;
|
||||
in
|
||||
import ./make-test-python.nix {
|
||||
name = "postfix";
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
let
|
||||
certs = import ./common/acme/server/snakeoil-certs.nix;
|
||||
domain = certs.domain;
|
||||
in
|
||||
import ./make-test-python.nix {
|
||||
name = "postfix";
|
||||
|
@ -11,8 +12,8 @@ import ./make-test-python.nix {
|
|||
enableSubmission = true;
|
||||
enableSubmissions = true;
|
||||
sslCACert = certs.ca.cert;
|
||||
sslCert = certs."acme.test".cert;
|
||||
sslKey = certs."acme.test".key;
|
||||
sslCert = certs.${domain}.cert;
|
||||
sslKey = certs.${domain}.key;
|
||||
submissionsOptions = {
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_client_restrictions = "permit";
|
||||
|
@ -25,7 +26,7 @@ import ./make-test-python.nix {
|
|||
];
|
||||
|
||||
networking.extraHosts = ''
|
||||
127.0.0.1 acme.test
|
||||
127.0.0.1 ${domain}
|
||||
'';
|
||||
|
||||
environment.systemPackages = let
|
||||
|
@ -33,7 +34,7 @@ import ./make-test-python.nix {
|
|||
#!${pkgs.python3.interpreter}
|
||||
import smtplib
|
||||
|
||||
with smtplib.SMTP('acme.test') as smtp:
|
||||
with smtplib.SMTP('${domain}') as smtp:
|
||||
smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test\n\nTest data.')
|
||||
smtp.quit()
|
||||
'';
|
||||
|
@ -45,7 +46,7 @@ import ./make-test-python.nix {
|
|||
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
with smtplib.SMTP('acme.test') as smtp:
|
||||
with smtplib.SMTP('${domain}') as smtp:
|
||||
smtp.ehlo()
|
||||
smtp.starttls(context=ctx)
|
||||
smtp.ehlo()
|
||||
|
@ -60,7 +61,7 @@ import ./make-test-python.nix {
|
|||
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
with smtplib.SMTP_SSL(host='acme.test', context=ctx) as smtp:
|
||||
with smtplib.SMTP_SSL(host='${domain}', context=ctx) as smtp:
|
||||
smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test SMTPS\n\nTest data.')
|
||||
smtp.quit()
|
||||
'';
|
||||
|
|
34
pkgs/tools/security/minica/default.nix
Normal file
34
pkgs/tools/security/minica/default.nix
Normal file
|
@ -0,0 +1,34 @@
|
|||
{ lib, buildGoPackage, fetchFromGitHub }:
|
||||
|
||||
buildGoPackage rec {
|
||||
pname = "minica";
|
||||
version = "1.0.2";
|
||||
|
||||
goPackagePath = "github.com/jsha/minica";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "jsha";
|
||||
repo = "minica";
|
||||
rev = "v${version}";
|
||||
sha256 = "18518wp3dcjhf3mdkg5iwxqr3326n6jwcnqhyibphnb2a58ap7ny";
|
||||
};
|
||||
|
||||
buildFlagsArray = ''
|
||||
-ldflags=
|
||||
-X main.BuildVersion=${version}
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "A simple tool for generating self signed certificates.";
|
||||
longDescription = ''
|
||||
Minica is a simple CA intended for use in situations where the CA
|
||||
operator also operates each host where a certificate will be used. It
|
||||
automatically generates both a key and a certificate when asked to
|
||||
produce a certificate.
|
||||
'';
|
||||
homepage = "https://github.com/jsha/minica/";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ m1cr0man ];
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
};
|
||||
}
|
|
@ -4949,6 +4949,8 @@ in
|
|||
|
||||
minergate-cli = callPackage ../applications/misc/minergate-cli { };
|
||||
|
||||
minica = callPackage ../tools/security/minica { };
|
||||
|
||||
minidlna = callPackage ../tools/networking/minidlna { };
|
||||
|
||||
minisign = callPackage ../tools/security/minisign { };
|
||||
|
|
Loading…
Reference in a new issue