]>
Commit | Line | Data |
---|---|---|
1 | { lib, config, pkgs, ... }: with lib; | |
2 | let | |
3 | cfg = config.services.websites; | |
4 | in | |
5 | { | |
6 | options.services.websites = with types; { | |
7 | certs = mkOption { | |
8 | description = "Default websites configuration for certificates as accepted by acme"; | |
9 | }; | |
10 | env = mkOption { | |
11 | default = {}; | |
12 | description = "Each type of website to enable will target a distinct httpd server"; | |
13 | type = attrsOf (submodule { | |
14 | options = { | |
15 | enable = mkEnableOption "Enable websites of this type"; | |
16 | adminAddr = mkOption { | |
17 | type = str; | |
18 | description = "Admin e-mail address of the instance"; | |
19 | }; | |
20 | httpdName = mkOption { | |
21 | type = str; | |
22 | description = "Name of the httpd instance to assign this type to"; | |
23 | }; | |
24 | ips = mkOption { | |
25 | type = listOf str; | |
26 | default = []; | |
27 | description = "ips to listen to"; | |
28 | }; | |
29 | modules = mkOption { | |
30 | type = listOf str; | |
31 | default = []; | |
32 | description = "Additional modules to load in Apache"; | |
33 | }; | |
34 | extraConfig = mkOption { | |
35 | type = listOf lines; | |
36 | default = []; | |
37 | description = "Additional configuration to append to Apache"; | |
38 | }; | |
39 | nosslVhost = mkOption { | |
40 | description = "A default nossl vhost for captive portals"; | |
41 | default = {}; | |
42 | type = submodule { | |
43 | options = { | |
44 | enable = mkEnableOption "Add default no-ssl vhost for this instance"; | |
45 | host = mkOption { | |
46 | type = str; | |
47 | description = "The hostname to use for this vhost"; | |
48 | }; | |
49 | root = mkOption { | |
50 | type = path; | |
51 | default = ./nosslVhost; | |
52 | description = "The root folder to serve"; | |
53 | }; | |
54 | indexFile = mkOption { | |
55 | type = str; | |
56 | default = "index.html"; | |
57 | description = "The index file to show."; | |
58 | }; | |
59 | }; | |
60 | }; | |
61 | }; | |
62 | fallbackVhost = mkOption { | |
63 | description = "The fallback vhost that will be defined as first vhost in Apache"; | |
64 | type = submodule { | |
65 | options = { | |
66 | certName = mkOption { type = str; }; | |
67 | hosts = mkOption { type = listOf str; }; | |
68 | root = mkOption { type = nullOr path; }; | |
69 | forceSSL = mkOption { | |
70 | type = bool; | |
71 | default = true; | |
72 | description = '' | |
73 | Automatically create a corresponding non-ssl vhost | |
74 | that will only redirect to the ssl version | |
75 | ''; | |
76 | }; | |
77 | extraConfig = mkOption { type = listOf lines; default = []; }; | |
78 | }; | |
79 | }; | |
80 | }; | |
81 | vhostNoSSLConfs = mkOption { | |
82 | default = {}; | |
83 | description = "List of no ssl vhosts to define for Apache"; | |
84 | type = attrsOf (submodule { | |
85 | options = { | |
86 | hosts = mkOption { type = listOf str; }; | |
87 | root = mkOption { type = nullOr path; }; | |
88 | extraConfig = mkOption { type = listOf lines; default = []; }; | |
89 | }; | |
90 | }); | |
91 | }; | |
92 | vhostConfs = mkOption { | |
93 | default = {}; | |
94 | description = "List of vhosts to define for Apache"; | |
95 | type = attrsOf (submodule { | |
96 | options = { | |
97 | certName = mkOption { type = str; }; | |
98 | addToCerts = mkOption { | |
99 | type = bool; | |
100 | default = false; | |
101 | description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null"; | |
102 | }; | |
103 | certMainHost = mkOption { | |
104 | type = nullOr str; | |
105 | description = "Use that host as 'main host' for acme certs"; | |
106 | default = null; | |
107 | }; | |
108 | hosts = mkOption { type = listOf str; }; | |
109 | root = mkOption { type = nullOr path; }; | |
110 | forceSSL = mkOption { | |
111 | type = bool; | |
112 | default = true; | |
113 | description = '' | |
114 | Automatically create a corresponding non-ssl vhost | |
115 | that will only redirect to the ssl version | |
116 | ''; | |
117 | }; | |
118 | extraConfig = mkOption { type = listOf lines; default = []; }; | |
119 | }; | |
120 | }); | |
121 | }; | |
122 | watchPaths = mkOption { | |
123 | type = listOf str; | |
124 | default = []; | |
125 | description = '' | |
126 | Paths to watch that should trigger a reload of httpd | |
127 | ''; | |
128 | }; | |
129 | }; | |
130 | }); | |
131 | }; | |
132 | }; | |
133 | ||
134 | config.services.httpd = let | |
135 | nosslVhost = ips: cfg: { | |
136 | listen = map (ip: { inherit ip; port = 80; }) ips; | |
137 | hostName = cfg.host; | |
138 | logFormat = "combinedVhost"; | |
139 | documentRoot = cfg.root; | |
140 | extraConfig = '' | |
141 | <Directory ${cfg.root}> | |
142 | DirectoryIndex ${cfg.indexFile} | |
143 | AllowOverride None | |
144 | Require all granted | |
145 | ||
146 | RewriteEngine on | |
147 | RewriteRule ^/(.+) / [L] | |
148 | </Directory> | |
149 | ''; | |
150 | }; | |
151 | toVhost = ips: vhostConf: { | |
152 | forceSSL = vhostConf.forceSSL or true; | |
153 | useACMEHost = vhostConf.certName; | |
154 | logFormat = "combinedVhost"; | |
155 | listen = if vhostConf.forceSSL | |
156 | then lists.flatten (map (ip: [{ inherit ip; port = 443; ssl = true; } { inherit ip; port = 80; }]) ips) | |
157 | else map (ip: { inherit ip; port = 443; ssl = true; }) ips; | |
158 | hostName = builtins.head vhostConf.hosts; | |
159 | serverAliases = builtins.tail vhostConf.hosts or []; | |
160 | documentRoot = vhostConf.root; | |
161 | extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig; | |
162 | }; | |
163 | toVhostNoSSL = ips: vhostConf: { | |
164 | logFormat = "combinedVhost"; | |
165 | listen = map (ip: { inherit ip; port = 80; }) ips; | |
166 | hostName = builtins.head vhostConf.hosts; | |
167 | serverAliases = builtins.tail vhostConf.hosts or []; | |
168 | documentRoot = vhostConf.root; | |
169 | extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig; | |
170 | }; | |
171 | in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | |
172 | icfg.httpdName (mkIf icfg.enable { | |
173 | enable = true; | |
174 | logPerVirtualHost = true; | |
175 | multiProcessingModule = "worker"; | |
176 | # https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.0.2t&guideline=5.4 | |
177 | # test with https://www.ssllabs.com/ssltest/analyze.html?d=www.immae.eu&s=176.9.151.154&latest | |
178 | sslProtocols = "all -SSLv3 -TLSv1 -TLSv1.1"; | |
179 | sslCiphers = builtins.concatStringsSep ":" [ | |
180 | "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256" | |
181 | "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384" | |
182 | "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305" | |
183 | "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384" | |
184 | ]; | |
185 | inherit (icfg) adminAddr; | |
186 | logFormat = "combinedVhost"; | |
187 | extraModules = lists.unique icfg.modules; | |
188 | extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig; | |
189 | ||
190 | virtualHosts = with attrsets; { | |
191 | ___fallbackVhost = toVhost icfg.ips icfg.fallbackVhost; | |
192 | } // (optionalAttrs icfg.nosslVhost.enable { | |
193 | nosslVhost = nosslVhost icfg.ips icfg.nosslVhost; | |
194 | }) // (mapAttrs' (n: v: nameValuePair ("nossl_" + n) (toVhostNoSSL icfg.ips v)) icfg.vhostNoSSLConfs) | |
195 | // (mapAttrs' (n: v: nameValuePair ("ssl_" + n) (toVhost icfg.ips v)) icfg.vhostConfs); | |
196 | }) | |
197 | ) cfg.env; | |
198 | ||
199 | config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | |
200 | "httpd${icfg.httpdName}" { | |
201 | paths = icfg.watchPaths; | |
202 | waitTime = 5; | |
203 | } | |
204 | ) cfg.env; | |
205 | ||
206 | config.security.acme.certs = let | |
207 | typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg.env; | |
208 | flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v: | |
209 | attrValues v.vhostConfs | |
210 | ) typesToManage); | |
211 | groupedCerts = attrsets.filterAttrs | |
212 | (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group) | |
213 | (lists.groupBy (v: v.certName) flatVhosts); | |
214 | groupToDomain = group: | |
215 | let | |
216 | nonNull = builtins.filter (v: !isNull v.certMainHost) group; | |
217 | domains = lists.unique (map (v: v.certMainHost) nonNull); | |
218 | in | |
219 | if builtins.length domains == 0 | |
220 | then null | |
221 | else assert (builtins.length domains == 1); (elemAt domains 0); | |
222 | extraDomains = group: | |
223 | let | |
224 | mainDomain = groupToDomain group; | |
225 | in | |
226 | lists.remove mainDomain ( | |
227 | lists.unique ( | |
228 | lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group) | |
229 | ) | |
230 | ); | |
231 | in attrsets.mapAttrs (k: g: | |
232 | if (!isNull (groupToDomain g)) | |
233 | then cfg.certs // { | |
234 | domain = groupToDomain g; | |
235 | extraDomains = builtins.listToAttrs ( | |
236 | map (d: attrsets.nameValuePair d null) (extraDomains g)); | |
237 | } | |
238 | else { | |
239 | extraDomains = builtins.listToAttrs ( | |
240 | map (d: attrsets.nameValuePair d null) (extraDomains g)); | |
241 | } | |
242 | ) groupedCerts; | |
243 | ||
244 | config.systemd.services = let | |
245 | package = httpdName: config.services.httpd.${httpdName}.package.out; | |
246 | cfgFile = httpdName: config.services.httpd.${httpdName}.configFile; | |
247 | serviceChange = attrsets.mapAttrs' (name: icfg: | |
248 | attrsets.nameValuePair | |
249 | "httpd${icfg.httpdName}" { | |
250 | stopIfChanged = false; | |
251 | serviceConfig.ExecStart = | |
252 | lib.mkForce "@${package icfg.httpdName}/bin/httpd httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf"; | |
253 | serviceConfig.ExecStop = | |
254 | lib.mkForce "${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -k graceful-stop"; | |
255 | serviceConfig.ExecReload = | |
256 | lib.mkForce "${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -k graceful"; | |
257 | } | |
258 | ) cfg.env; | |
259 | serviceReload = attrsets.mapAttrs' (name: icfg: | |
260 | attrsets.nameValuePair | |
261 | "httpd${icfg.httpdName}-config-reload" { | |
262 | wants = [ "httpd${icfg.httpdName}.service" ]; | |
263 | wantedBy = [ "multi-user.target" ]; | |
264 | restartTriggers = [ (cfgFile icfg.httpdName) ]; | |
265 | # commented, because can cause extra delays during activate for this config: | |
266 | # services.nginx.virtualHosts."_".locations."/".proxyPass = "http://blabla:3000"; | |
267 | # stopIfChanged = false; | |
268 | serviceConfig.Type = "oneshot"; | |
269 | serviceConfig.TimeoutSec = 60; | |
270 | script = '' | |
271 | if ${pkgs.systemd}/bin/systemctl -q is-active httpd${icfg.httpdName}.service ; then | |
272 | ${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -t && \ | |
273 | ${pkgs.systemd}/bin/systemctl reload httpd${icfg.httpdName}.service | |
274 | fi | |
275 | ''; | |
276 | serviceConfig.RemainAfterExit = true; | |
277 | } | |
278 | ) cfg.env; | |
279 | in | |
280 | serviceChange // serviceReload; | |
281 | } |