diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml
index 0b8651e8f4261..a19d9bb005092 100644
--- a/nixos/doc/manual/release-notes/rl-2009.xml
+++ b/nixos/doc/manual/release-notes/rl-2009.xml
@@ -394,6 +394,20 @@ php.override {
+
+
+ The ACME module has been overhauled for simplicity and maintainability.
+ Cert generation now implicitly uses the acme
+ user, and the security.acme.certs._name_.user option
+ has been removed. Instead, certificate access from other services is now
+ managed through group permissions. The module no longer runs lego
+ twice under certain conditions, and will correctly renew certificates if
+ their configuration is changed. Services which reload nginx and httpd after
+ certificate renewal are now properly configured too so you no longer have
+ to do this manually if you are using HTTPS enabled virtual hosts. A mechanism
+ for regenerating certs on demand has also been added and documented.
+
+
Gollum received a major update to version 5.x and you may have to change
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index 29635dbe86430..8e67d4ff87166 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -1,11 +1,314 @@
-{ 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..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..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}";
+ selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
+
+ # 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
+ # Flags are separated to avoid collisions
+ hashData = with builtins; ''
+ ${concatStringsSep " " data.extraLegoFlags} -
+ ${concatStringsSep " " data.extraLegoRunFlags} -
+ ${concatStringsSep " " data.extraLegoRenewFlags} -
+ ${toString acmeServer} ${toString data.dnsProvider}
+ ${toString data.ocspMustStaple} ${data.keyType}
+ '';
+ mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
+ certDir = mkHash hashData;
+ domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
+ othersHash = mkHash "${toString acmeServer} ${data.keyType}";
+ 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" ]
+ ++ data.extraLegoRenewFlags
+ );
+
+ in {
+ inherit accountDir selfsignedDeps;
+
+ 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" ];
+ requires = [ "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-fixperms.service" ] ++ selfsignedDeps;
+ wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps;
+
+ # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
+ wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ];
+
+ path = with pkgs; [ lego coreutils diffutils ];
+
+ 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}";
+
+ # 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 ";
+
+ # Only try loading the credentialsFile if the dns challenge is enabled
+ EnvironmentFile = mkIf useDns data.credentialsFile;
+
+ # Run as root (Prefixed with +)
+ ExecStartPost = "+" + (pkgs.writeShellScript "acme-postrun" ''
+ cd /var/lib/acme/${escapeShellArg cert}
+ if [ -e renewed ]; then
+ rm renewed
+ ${data.postRun}
+ fi
+ '');
+ };
+
+ # Working directory will be /tmp
+ script = ''
+ set -euo pipefail
+
+ echo '${domainHash}' > domainhash.txt
+
+ # Check if we can renew
+ if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' ]; then
+
+ # When domains are updated, there's no need to do a full
+ # Lego run, but it's likely renew won't work if days is too low.
+ if [ -e certificates/domainhash.txt ] && cmp -s domainhash.txt certificates/domainhash.txt; then
+ lego ${renewOpts} --days ${toString cfg.validMinDays}
+ else
+ # Any number > 90 works, but this one is over 9000 ;-)
+ lego ${renewOpts} --days 9001
+ fi
+
+ # Otherwise do a full run
+ else
+ lego ${runOpts}
+ fi
+
+ mv domainhash.txt certificates/
+ chmod 640 certificates/*
+ chmod -R 700 accounts/*
+
+ # Group might change between runs, re-apply it
+ chown 'acme:${data.group}' certificates/*
+
+ # Copy all certs to the "real" certs directory
+ CERT='certificates/${keyName}.crt'
+ if [ -e "$CERT" ] && ! cmp -s "$CERT" out/fullchain.pem; then
+ touch out/renewed
+ echo Installing new certificate
+ cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
+ cp -vp 'certificates/${keyName}.key' out/key.pem
+ cp -vp '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
+ '';
+ };
+ };
+
+ 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 +344,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
- () 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 root user.
Executed in the same directory with the new certificate.
'';
@@ -82,18 +369,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 +462,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 +536,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 +548,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..dnsProvider` and
- `security.acme.certs..webroot` are mutually exclusive.
- '';
- }
{
assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
message = ''
You must define `security.acme.certs..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 +592,78 @@ in
to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
'';
}
- ];
-
- 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}
-
- # 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
-
- chmod ${fileMode} *.pem
- chown '${data.user}:${data.group}' *.pem
-
- if [ "$KEY_CHANGED" = "yes" ]; then
- : # noop in case postRun is empty
- ${data.postRun}
- fi
- '';
- in
- "+${script}";
- };
-
- };
- selfsignedService = {
- description = "Create preliminary self-signed certificate for ${cert}";
- path = [ pkgs.openssl ];
- script =
- ''
- workdir="$(mktemp -d)"
-
- # 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 = {};
- })
+ ] ++ (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));
+ users.users.acme = {
+ home = "/var/lib/acme";
+ group = "acme";
+ isSystemUser = true;
+ };
+
+ users.groups.acme = {};
+
+ 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)));
+
+ systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
+
+ # .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 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" ];
+ requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
+ after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
+ }) certConfigs;
+ })
];
meta = {
diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml
index f802faee97490..17e94bc12fb21 100644
--- a/nixos/modules/security/acme.xml
+++ b/nixos/modules/security/acme.xml
@@ -72,7 +72,7 @@ services.nginx = {
"foo.example.com" = {
forceSSL = true;
enableACME = true;
- # All serverAliases will be added as extra domains on the certificate.
+ # All serverAliases will be added as extra domain names on the certificate.
serverAliases = [ "bar.example.com" ];
locations."/" = {
root = "/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.
- security.acme.certs."foo.example.com".extraDomains."baz.example.com" = null;
+ # but we have to append extraDomainNames manually.
+ security.acme.certs."foo.example.com".extraDomainNames = [ "baz.example.com" ];
"baz.example.com" = {
forceSSL = true;
useACMEHost = "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.
- extraDomains = [ "mail.example.com" ];
+ extraDomainNames = [ "mail.example.com" ];
};
@@ -251,4 +251,16 @@ chmod 400 /var/lib/secrets/certs.secret
journalctl -fu acme-example.com.service and watching its log output.
+
+ Regenerating certificates
+
+
+ Should you need to regenerate a particular certificate in a hurry, such
+ as when a vulnerability is found in Let's Encrypt, there is now a convenient
+ mechanism for doing so. Running systemctl clean acme-example.com.service
+ will remove all certificate files for the given domain, allowing you to then
+ systemctl start acme-example.com.service to generate fresh
+ ones.
+
+
diff --git a/nixos/modules/services/networking/prosody.xml b/nixos/modules/services/networking/prosody.xml
index 7859cb1578b7e..14b7c60f1a05d 100644
--- a/nixos/modules/services/networking/prosody.xml
+++ b/nixos/modules/services/networking/prosody.xml
@@ -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
- extraDomains module option.
+ extraDomainNames module option.
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" = {
webroot = "/var/www/example.org";
email = "root@example.org";
- extraDomains."conference.example.org" = null;
- extraDomains."upload.example.org" = null;
+ extraDomainNames = [ "conference.example.org" "upload.example.org" ];
};
};
};
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index fc4c2945394c3..6dd1c85132c91 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -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..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/httpd -f ${httpdConf} -t";
+ ExecStart = "/run/current-system/systemd/bin/systemctl reload httpd.service";
+ };
+ };
+
};
}
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 461888c4cc4f0..975b56d478229 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -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 = {
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index a81884737213e..64193ed8498cb 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -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,97 @@ 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 ];
+
+ # Set log level to info so that we can see when the service is reloaded
services.nginx.enable = true;
+ services.nginx.logError = "stderr info";
- services.nginx.virtualHosts."a.example.test" = {
+ # First tests configure a basic cert and run a bunch of openssl checks
+ 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.
+ # It also tests that postRun is exec'd as root
+ specialisation.cert-change.configuration = { pkgs, ... }: {
+ security.acme.certs."a.example.test".keyType = "ec384";
+ security.acme.certs."a.example.test".postRun = ''
+ set -euo pipefail
+ touch test
+ chown root:root test
+ echo testing > test
+ '';
+ };
+
+ # 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 +151,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 +168,168 @@ 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.
+
''
+ import time
+
+
+ has_switched = False
+
+
+ def switch_to(node, name):
+ global has_switched
+ if has_switched:
+ node.succeed(
+ "${switchToNewServer}"
+ )
+ has_switched = True
+ node.succeed(
+ f"/run/current-system/specialisation/{name}/bin/switch-to-configuration test"
+ )
+
+
+ # 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"):
+ actual_issuer = node.succeed(
+ f"openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname}"
+ ).partition("=")[2]
+ print(f"{fname} issuer: {actual_issuer}")
+ assert issuer.lower() in actual_issuer.lower()
+
+
+ # Ensure cert comes before chain in fullchain.pem
+ def check_fullchain(node, cert_name):
+ subject_data = node.succeed(
+ f"openssl crl2pkcs7 -nocrl -certfile /var/lib/acme/{cert_name}/fullchain.pem"
+ " | openssl pkcs7 -print_certs -noout"
+ )
+ for line in subject_data.lower().split("\n"):
+ if "subject" in line:
+ print(f"First subject in fullchain.pem: ", line)
+ assert cert_name.lower() in line
+ return
+
+ assert False
+
+
+ def check_connection(node, domain, retries=3):
+ assert retries >= 0
+
+ result = node.succeed(
+ "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt"
+ f" -servername {domain} -connect {domain}:443 < /dev/null 2>&1"
+ )
+
+ for line in result.lower().split("\n"):
+ if "verification" in line and "error" in line:
+ time.sleep(1)
+ return check_connection(node, domain, retries - 1)
+
+
+ def check_connection_key_bits(node, domain, bits, retries=3):
+ assert retries >= 0
+
+ result = node.succeed(
+ "openssl s_client -CAfile /tmp/ca.crt"
+ f" -servername {domain} -connect {domain}:443 < /dev/null"
+ " | openssl x509 -noout -text | grep -i Public-Key"
+ )
+ print("Key type:", result)
+
+ if bits not in result:
+ time.sleep(1)
+ return check_connection_key_bits(node, domain, bits, retries - 1)
+
+
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")
- client.succeed(
- "curl --cacert /tmp/ca.crt https://a.example.test/ | grep -qF 'hello world'"
- )
+ check_fullchain(webserver, "a.example.test")
+ check_issuer(webserver, "a.example.test", "pebble")
+ check_connection(client, "a.example.test")
- 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 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"):
+ webserver.succeed("systemctl start test-renew-nginx.target")
+ 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")
+ check_connection_key_bits(client, "a.example.test", "384")
+ webserver.succeed("grep testing /var/lib/acme/a.example.test/test")
+
+ 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)"):
+ # Check the key hash before and after adding an alias. It should not change.
+ # The previous test reverts the ed384 change
+ webserver.wait_for_unit("acme-finished-a.example.test.target")
+ keyhash_old = webserver.succeed("md5sum /var/lib/acme/a.example.test/key.pem")
+ 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")
+ keyhash_new = webserver.succeed("md5sum /var/lib/acme/a.example.test/key.pem")
+ assert keyhash_old == keyhash_new
+
+ 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")
+ check_issuer(webserver, "c.example.test", "minica")
+ webserver.succeed("systemctl start httpd-config-reload.service")
+ webserver.succeed("systemctl start test-renew-httpd.target")
+ 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")
'';
})
diff --git a/nixos/tests/common/acme/client/default.nix b/nixos/tests/common/acme/client/default.nix
index 80893da025244..1e9885e375c7f 100644
--- a/nixos/tests/common/acme/client/default.nix
+++ b/nixos/tests/common/acme/client/default.nix
@@ -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 ];
}
diff --git a/nixos/tests/common/acme/server/default.nix b/nixos/tests/common/acme/server/default.nix
index 1a0ee882572ce..4d8e664c4e17f 100644
--- a/nixos/tests/common/acme/server/default.nix
+++ b/nixos/tests/common/acme/server/default.nix
@@ -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;
-
- wfeDomain = "acme.test";
- wfeCertFile = snakeOilCerts.${wfeDomain}.cert;
- wfeKeyFile = snakeOilCerts.${wfeDomain}.key;
+ testCerts = import ./snakeoil-certs.nix {
+ minica = pkgs.minica;
+ mkDerivation = pkgs.stdenv.mkDerivation;
+ };
+ domain = testCerts.domain;
- 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 nodes attribute to
- inject the snakeoil CA certificate used in the ACME server into
- .
- '';
+ options.test-support.acme = with lib; {
+ caDomain = mkOption {
+ type = types.str;
+ readOnly = true;
+ default = domain;
+ description = ''
+ A domain name to use with the nodes 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 nodes attribute to
+ inject the test CA certificate used in the ACME server into
+ .
+ '';
+ };
};
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}";
};
};
};
diff --git a/nixos/tests/common/acme/server/mkcerts.nix b/nixos/tests/common/acme/server/mkcerts.nix
deleted file mode 100644
index c9616bf9672cc..0000000000000
--- a/nixos/tests/common/acme/server/mkcerts.nix
+++ /dev/null
@@ -1,69 +0,0 @@
-{ pkgs ? import {}
-, 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"
-''
diff --git a/nixos/tests/common/acme/server/mkcerts.sh b/nixos/tests/common/acme/server/mkcerts.sh
deleted file mode 100755
index cc7f8ca650dd4..0000000000000
--- a/nixos/tests/common/acme/server/mkcerts.sh
+++ /dev/null
@@ -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
diff --git a/nixos/tests/common/acme/server/snakeoil-certs.nix b/nixos/tests/common/acme/server/snakeoil-certs.nix
index 7325b027c7ef0..4b6a38b8fa307 100644
--- a/nixos/tests/common/acme/server/snakeoil-certs.nix
+++ b/nixos/tests/common/acme/server/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";
+ };
}
diff --git a/nixos/tests/postfix-raise-smtpd-tls-security-level.nix b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
index b3c2156122d20..5fad1fed75b20 100644
--- a/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
+++ b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
@@ -1,6 +1,3 @@
-let
- certs = import ./common/acme/server/snakeoil-certs.nix;
-in
import ./make-test-python.nix {
name = "postfix";
diff --git a/nixos/tests/postfix.nix b/nixos/tests/postfix.nix
index b0674ca3a0d29..37ae76afec107 100644
--- a/nixos/tests/postfix.nix
+++ b/nixos/tests/postfix.nix
@@ -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()
'';
diff --git a/pkgs/tools/security/minica/default.nix b/pkgs/tools/security/minica/default.nix
new file mode 100644
index 0000000000000..20ae3878a71fb
--- /dev/null
+++ b/pkgs/tools/security/minica/default.nix
@@ -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;
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 36268b0ee88c0..cab987cfd0772 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -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 { };