Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructure acme module #91121

Merged
merged 6 commits into from
Sep 6, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
644 changes: 409 additions & 235 deletions nixos/modules/security/acme.nix

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions nixos/modules/security/acme.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ 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";
};
};

# 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";
Expand Down Expand Up @@ -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>

Expand Down
5 changes: 2 additions & 3 deletions nixos/modules/services/networking/prosody.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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> = "[email protected]";
<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>
Expand Down
77 changes: 55 additions & 22 deletions nixos/modules/services/web-servers/apache-httpd/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ let

cfg = config.services.httpd;

certs = config.security.acme.certs;

runtimeDir = "/run/httpd";

pkg = cfg.package.out;
Expand All @@ -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 (
Expand Down Expand Up @@ -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/"
Expand Down Expand Up @@ -347,7 +356,6 @@ let
cat ${php.phpIni} > $out
echo "$options" >> $out
'';

in


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ];

Expand Down Expand Up @@ -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";
};
};

};
}
88 changes: 46 additions & 42 deletions nixos/modules/services/web-servers/nginx/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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 = {
Expand Down
Loading