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