1 { lib, config, ... }: with lib;
3 cfg = config.services.websites;
6 options.services.websitesCerts = mkOption {
7 description = "Default websites configuration for certificates as accepted by acme";
9 options.services.websites = with types; mkOption {
11 description = "Each type of website to enable will target a distinct httpd server";
12 type = attrsOf (submodule {
14 enable = mkEnableOption "Enable websites of this type";
15 adminAddr = mkOption {
17 description = "Admin e-mail address of the instance";
19 httpdName = mkOption {
21 description = "Name of the httpd instance to assign this type to";
26 description = "ips to listen to";
31 description = "Additional modules to load in Apache";
33 extraConfig = mkOption {
36 description = "Additional configuration to append to Apache";
38 nosslVhost = mkOption {
39 description = "A default nossl vhost for captive portals";
43 enable = mkEnableOption "Add default no-ssl vhost for this instance";
46 description = "The hostname to use for this vhost";
50 default = ./nosslVhost;
51 description = "The root folder to serve";
53 indexFile = mkOption {
55 default = "index.html";
56 description = "The index file to show.";
61 fallbackVhost = mkOption {
62 description = "The fallback vhost that will be defined as first vhost in Apache";
65 certName = mkOption { type = string; };
66 hosts = mkOption { type = listOf string; };
67 root = mkOption { type = nullOr path; };
68 extraConfig = mkOption { type = listOf lines; default = []; };
72 vhostConfs = mkOption {
74 description = "List of vhosts to define for Apache";
75 type = attrsOf (submodule {
77 certName = mkOption { type = string; };
78 addToCerts = mkOption {
81 description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null";
83 certMainHost = mkOption {
85 description = "Use that host as 'main host' for acme certs";
88 hosts = mkOption { type = listOf string; };
89 root = mkOption { type = nullOr path; };
90 extraConfig = mkOption { type = listOf lines; default = []; };
98 config.services.httpd = let
99 redirectVhost = ips: { # Should go last, catchall http -> https redirect
100 listen = map (ip: { inherit ip; port = 80; }) ips;
101 hostName = "redirectSSL";
102 serverAliases = [ "*" ];
104 logFormat = "combinedVhost";
105 documentRoot = "/var/lib/acme/acme-challenge";
108 RewriteCond "%{REQUEST_URI}" "!^/\.well-known"
109 RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301]
110 # To redirect in specific "VirtualHost *:80", do
111 # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1
112 # rather than rewrite
115 nosslVhost = ips: cfg: {
116 listen = map (ip: { inherit ip; port = 80; }) ips;
119 logFormat = "combinedVhost";
120 documentRoot = cfg.root;
122 <Directory ${cfg.root}>
123 DirectoryIndex ${cfg.indexFile}
128 RewriteRule ^/(.+) / [L]
132 toVhost = ips: vhostConf: {
134 sslServerCert = "/var/lib/acme/${vhostConf.certName}/cert.pem";
135 sslServerKey = "/var/lib/acme/${vhostConf.certName}/key.pem";
136 sslServerChain = "/var/lib/acme/${vhostConf.certName}/chain.pem";
137 logFormat = "combinedVhost";
138 listen = map (ip: { inherit ip; port = 443; }) ips;
139 hostName = builtins.head vhostConf.hosts;
140 serverAliases = builtins.tail vhostConf.hosts or [];
141 documentRoot = vhostConf.root;
142 extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
144 in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
145 icfg.httpdName (mkIf icfg.enable {
147 listen = map (ip: { inherit ip; port = 443; }) icfg.ips;
148 stateDir = "/run/httpd_${name}";
149 logPerVirtualHost = true;
150 multiProcessingModule = "worker";
151 inherit (icfg) adminAddr;
152 logFormat = "combinedVhost";
153 extraModules = lists.unique icfg.modules;
154 extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
155 virtualHosts = [ (toVhost icfg.ips icfg.fallbackVhost) ]
156 ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ]
157 ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs)
158 ++ [ (redirectVhost icfg.ips) ];
162 config.security.acme.certs = let
163 typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg;
164 flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v:
165 attrValues v.vhostConfs
167 groupedCerts = attrsets.filterAttrs
168 (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group)
169 (lists.groupBy (v: v.certName) flatVhosts);
170 groupToDomain = group:
172 nonNull = builtins.filter (v: !isNull v.certMainHost) group;
173 domains = lists.unique (map (v: v.certMainHost) nonNull);
175 if builtins.length domains == 0
177 else assert (builtins.length domains == 1); (elemAt domains 0);
178 extraDomains = group:
180 mainDomain = groupToDomain group;
182 lists.remove mainDomain (
184 lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group)
187 in attrsets.mapAttrs (k: g:
188 if (!isNull (groupToDomain g))
189 then config.services.websitesCerts // {
190 domain = groupToDomain g;
191 extraDomains = builtins.listToAttrs (
192 map (d: attrsets.nameValuePair d null) (extraDomains g));
195 extraDomains = builtins.listToAttrs (
196 map (d: attrsets.nameValuePair d null) (extraDomains g));