]> git.immae.eu Git - perso/Immae/Config/Nix.git/blob - modules/websites/default.nix
767a7b2324a1bf45acec546b23c5544e974f76a5
[perso/Immae/Config/Nix.git] / modules / websites / default.nix
1 { lib, config, ... }: 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 string;
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 = string;
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 = string;
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 = string; };
83 hosts = mkOption { type = listOf string; };
84 root = mkOption { type = nullOr path; };
85 extraConfig = mkOption { type = listOf lines; default = []; };
86 };
87 };
88 };
89 vhostNoSSLConfs = mkOption {
90 default = {};
91 description = "List of no ssl vhosts to define for Apache";
92 type = attrsOf (submodule {
93 options = {
94 hosts = mkOption { type = listOf string; };
95 root = mkOption { type = nullOr path; };
96 extraConfig = mkOption { type = listOf lines; default = []; };
97 };
98 });
99 };
100 vhostConfs = mkOption {
101 default = {};
102 description = "List of vhosts to define for Apache";
103 type = attrsOf (submodule {
104 options = {
105 certName = mkOption { type = string; };
106 addToCerts = mkOption {
107 type = bool;
108 default = false;
109 description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null";
110 };
111 certMainHost = mkOption {
112 type = nullOr string;
113 description = "Use that host as 'main host' for acme certs";
114 default = null;
115 };
116 hosts = mkOption { type = listOf string; };
117 root = mkOption { type = nullOr path; };
118 extraConfig = mkOption { type = listOf lines; default = []; };
119 };
120 });
121 };
122 watchPaths = mkOption {
123 type = listOf string;
124 default = [];
125 description = ''
126 Paths to watch that should trigger a reload of httpd
127 '';
128 };
129 };
130 });
131 };
132 # Readonly variables
133 webappDirsPaths = mkOption {
134 type = attrsOf path;
135 readOnly = true;
136 description = ''
137 Full paths of the webapp dir
138 '';
139 default = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
140 name "/run/current-system/${cfg.webappDirsName}/${name}"
141 ) cfg.webappDirs;
142 };
143 };
144
145 config.services.httpd = let
146 redirectVhost = ips: { # Should go last, catchall http -> https redirect
147 listen = map (ip: { inherit ip; port = 80; }) ips;
148 hostName = "redirectSSL";
149 serverAliases = [ "*" ];
150 enableSSL = false;
151 logFormat = "combinedVhost";
152 documentRoot = "/var/lib/acme/acme-challenge";
153 extraConfig = ''
154 RewriteEngine on
155 RewriteCond "%{REQUEST_URI}" "!^/\.well-known"
156 RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301]
157 # To redirect in specific "VirtualHost *:80", do
158 # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1
159 # rather than rewrite
160 '';
161 };
162 nosslVhost = ips: cfg: {
163 listen = map (ip: { inherit ip; port = 80; }) ips;
164 hostName = cfg.host;
165 enableSSL = false;
166 logFormat = "combinedVhost";
167 documentRoot = cfg.root;
168 extraConfig = ''
169 <Directory ${cfg.root}>
170 DirectoryIndex ${cfg.indexFile}
171 AllowOverride None
172 Require all granted
173
174 RewriteEngine on
175 RewriteRule ^/(.+) / [L]
176 </Directory>
177 '';
178 };
179 toVhost = ips: vhostConf: {
180 enableSSL = true;
181 sslServerCert = "${config.security.acme2.certs."${vhostConf.certName}".directory}/cert.pem";
182 sslServerKey = "${config.security.acme2.certs."${vhostConf.certName}".directory}/key.pem";
183 sslServerChain = "${config.security.acme2.certs."${vhostConf.certName}".directory}/chain.pem";
184 logFormat = "combinedVhost";
185 listen = map (ip: { inherit ip; port = 443; }) ips;
186 hostName = builtins.head vhostConf.hosts;
187 serverAliases = builtins.tail vhostConf.hosts or [];
188 documentRoot = vhostConf.root;
189 extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
190 };
191 toVhostNoSSL = ips: vhostConf: {
192 enableSSL = false;
193 logFormat = "combinedVhost";
194 listen = map (ip: { inherit ip; port = 80; }) ips;
195 hostName = builtins.head vhostConf.hosts;
196 serverAliases = builtins.tail vhostConf.hosts or [];
197 documentRoot = vhostConf.root;
198 extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
199 };
200 in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
201 icfg.httpdName (mkIf icfg.enable {
202 enable = true;
203 listen = map (ip: { inherit ip; port = 443; }) icfg.ips;
204 stateDir = "/run/httpd_${name}";
205 logPerVirtualHost = true;
206 multiProcessingModule = "worker";
207 # https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.0.2t&guideline=5.4
208 sslProtocols = "all -SSLv3 -TLSv1 -TLSv1.1";
209 sslCiphers = builtins.concatStringsSep ":" [
210 "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256"
211 "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384"
212 "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305"
213 "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384"
214 ];
215 inherit (icfg) adminAddr;
216 logFormat = "combinedVhost";
217 extraModules = lists.unique icfg.modules;
218 extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
219 virtualHosts = [ (toVhost icfg.ips icfg.fallbackVhost) ]
220 ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ]
221 ++ (attrsets.mapAttrsToList (n: v: toVhostNoSSL icfg.ips v) icfg.vhostNoSSLConfs)
222 ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs)
223 ++ [ (redirectVhost icfg.ips) ];
224 })
225 ) cfg.env;
226
227 config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
228 "httpd${icfg.httpdName}" {
229 paths = icfg.watchPaths;
230 waitTime = 5;
231 }
232 ) cfg.env;
233
234 config.security.acme2.certs = let
235 typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg.env;
236 flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v:
237 attrValues v.vhostConfs
238 ) typesToManage);
239 groupedCerts = attrsets.filterAttrs
240 (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group)
241 (lists.groupBy (v: v.certName) flatVhosts);
242 groupToDomain = group:
243 let
244 nonNull = builtins.filter (v: !isNull v.certMainHost) group;
245 domains = lists.unique (map (v: v.certMainHost) nonNull);
246 in
247 if builtins.length domains == 0
248 then null
249 else assert (builtins.length domains == 1); (elemAt domains 0);
250 extraDomains = group:
251 let
252 mainDomain = groupToDomain group;
253 in
254 lists.remove mainDomain (
255 lists.unique (
256 lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group)
257 )
258 );
259 in attrsets.mapAttrs (k: g:
260 if (!isNull (groupToDomain g))
261 then cfg.certs // {
262 domain = groupToDomain g;
263 extraDomains = builtins.listToAttrs (
264 map (d: attrsets.nameValuePair d null) (extraDomains g));
265 }
266 else {
267 extraDomains = builtins.listToAttrs (
268 map (d: attrsets.nameValuePair d null) (extraDomains g));
269 }
270 ) groupedCerts;
271
272 config.system.extraSystemBuilderCmds = lib.mkIf (builtins.length (builtins.attrValues cfg.webappDirs) > 0) ''
273 mkdir -p $out/${cfg.webappDirsName}
274 ${builtins.concatStringsSep "\n"
275 (attrsets.mapAttrsToList
276 (name: path: "ln -s ${path} $out/${cfg.webappDirsName}/${name}") cfg.webappDirs)
277 }
278 '';
279 }