diff options
Diffstat (limited to 'modules/websites/default.nix')
-rw-r--r-- | modules/websites/default.nix | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/modules/websites/default.nix b/modules/websites/default.nix new file mode 100644 index 00000000..e57f505a --- /dev/null +++ b/modules/websites/default.nix | |||
@@ -0,0 +1,199 @@ | |||
1 | { lib, config, ... }: with lib; | ||
2 | let | ||
3 | cfg = config.services.websites; | ||
4 | in | ||
5 | { | ||
6 | options.services.websitesCerts = mkOption { | ||
7 | description = "Default websites configuration for certificates as accepted by acme"; | ||
8 | }; | ||
9 | options.services.websites = with types; mkOption { | ||
10 | default = {}; | ||
11 | description = "Each type of website to enable will target a distinct httpd server"; | ||
12 | type = attrsOf (submodule { | ||
13 | options = { | ||
14 | enable = mkEnableOption "Enable websites of this type"; | ||
15 | adminAddr = mkOption { | ||
16 | type = str; | ||
17 | description = "Admin e-mail address of the instance"; | ||
18 | }; | ||
19 | httpdName = mkOption { | ||
20 | type = str; | ||
21 | description = "Name of the httpd instance to assign this type to"; | ||
22 | }; | ||
23 | ips = mkOption { | ||
24 | type = listOf string; | ||
25 | default = []; | ||
26 | description = "ips to listen to"; | ||
27 | }; | ||
28 | modules = mkOption { | ||
29 | type = listOf str; | ||
30 | default = []; | ||
31 | description = "Additional modules to load in Apache"; | ||
32 | }; | ||
33 | extraConfig = mkOption { | ||
34 | type = listOf lines; | ||
35 | default = []; | ||
36 | description = "Additional configuration to append to Apache"; | ||
37 | }; | ||
38 | nosslVhost = mkOption { | ||
39 | description = "A default nossl vhost for captive portals"; | ||
40 | default = {}; | ||
41 | type = submodule { | ||
42 | options = { | ||
43 | enable = mkEnableOption "Add default no-ssl vhost for this instance"; | ||
44 | host = mkOption { | ||
45 | type = string; | ||
46 | description = "The hostname to use for this vhost"; | ||
47 | }; | ||
48 | root = mkOption { | ||
49 | type = path; | ||
50 | default = ./nosslVhost; | ||
51 | description = "The root folder to serve"; | ||
52 | }; | ||
53 | indexFile = mkOption { | ||
54 | type = string; | ||
55 | default = "index.html"; | ||
56 | description = "The index file to show."; | ||
57 | }; | ||
58 | }; | ||
59 | }; | ||
60 | }; | ||
61 | fallbackVhost = mkOption { | ||
62 | description = "The fallback vhost that will be defined as first vhost in Apache"; | ||
63 | type = submodule { | ||
64 | options = { | ||
65 | certName = mkOption { type = string; }; | ||
66 | hosts = mkOption { type = listOf string; }; | ||
67 | root = mkOption { type = nullOr path; }; | ||
68 | extraConfig = mkOption { type = listOf lines; default = []; }; | ||
69 | }; | ||
70 | }; | ||
71 | }; | ||
72 | vhostConfs = mkOption { | ||
73 | default = {}; | ||
74 | description = "List of vhosts to define for Apache"; | ||
75 | type = attrsOf (submodule { | ||
76 | options = { | ||
77 | certName = mkOption { type = string; }; | ||
78 | addToCerts = mkOption { | ||
79 | type = bool; | ||
80 | default = false; | ||
81 | description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null"; | ||
82 | }; | ||
83 | certMainHost = mkOption { | ||
84 | type = nullOr string; | ||
85 | description = "Use that host as 'main host' for acme certs"; | ||
86 | default = null; | ||
87 | }; | ||
88 | hosts = mkOption { type = listOf string; }; | ||
89 | root = mkOption { type = nullOr path; }; | ||
90 | extraConfig = mkOption { type = listOf lines; default = []; }; | ||
91 | }; | ||
92 | }); | ||
93 | }; | ||
94 | }; | ||
95 | }); | ||
96 | }; | ||
97 | |||
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 = [ "*" ]; | ||
103 | enableSSL = false; | ||
104 | logFormat = "combinedVhost"; | ||
105 | documentRoot = "${config.security.acme.directory}/acme-challenge"; | ||
106 | extraConfig = '' | ||
107 | RewriteEngine on | ||
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 | ||
113 | ''; | ||
114 | }; | ||
115 | nosslVhost = ips: cfg: { | ||
116 | listen = map (ip: { inherit ip; port = 80; }) ips; | ||
117 | hostName = cfg.host; | ||
118 | enableSSL = false; | ||
119 | logFormat = "combinedVhost"; | ||
120 | documentRoot = cfg.root; | ||
121 | extraConfig = '' | ||
122 | <Directory ${cfg.root}> | ||
123 | DirectoryIndex ${cfg.indexFile} | ||
124 | AllowOverride None | ||
125 | Require all granted | ||
126 | |||
127 | RewriteEngine on | ||
128 | RewriteRule ^/(.+) / [L] | ||
129 | </Directory> | ||
130 | ''; | ||
131 | }; | ||
132 | toVhost = ips: vhostConf: { | ||
133 | enableSSL = true; | ||
134 | sslServerCert = "${config.security.acme.directory}/${vhostConf.certName}/cert.pem"; | ||
135 | sslServerKey = "${config.security.acme.directory}/${vhostConf.certName}/key.pem"; | ||
136 | sslServerChain = "${config.security.acme.directory}/${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; | ||
143 | }; | ||
144 | in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | ||
145 | icfg.httpdName (mkIf icfg.enable { | ||
146 | enable = true; | ||
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) ]; | ||
159 | }) | ||
160 | ) cfg; | ||
161 | |||
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 | ||
166 | ) typesToManage); | ||
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: | ||
171 | let | ||
172 | nonNull = builtins.filter (v: !isNull v.certMainHost) group; | ||
173 | domains = lists.unique (map (v: v.certMainHost) nonNull); | ||
174 | in | ||
175 | if builtins.length domains == 0 | ||
176 | then null | ||
177 | else assert (builtins.length domains == 1); (elemAt domains 0); | ||
178 | extraDomains = group: | ||
179 | let | ||
180 | mainDomain = groupToDomain group; | ||
181 | in | ||
182 | lists.remove mainDomain ( | ||
183 | lists.unique ( | ||
184 | lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group) | ||
185 | ) | ||
186 | ); | ||
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)); | ||
193 | } | ||
194 | else { | ||
195 | extraDomains = builtins.listToAttrs ( | ||
196 | map (d: attrsets.nameValuePair d null) (extraDomains g)); | ||
197 | } | ||
198 | ) groupedCerts; | ||
199 | } | ||