]> git.immae.eu Git - perso/Immae/Config/Nix.git/blame - modules/websites/default.nix
Add a filesWatcher service to restart them when secrets change
[perso/Immae/Config/Nix.git] / modules / websites / default.nix
CommitLineData
daf64e3f
IB
1{ lib, config, ... }: with lib;
2let
3 cfg = config.services.websites;
4in
5{
7df420c2
IB
6 options.services.websitesCerts = mkOption {
7 description = "Default websites configuration for certificates as accepted by acme";
8 };
daf64e3f
IB
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; };
7df420c2
IB
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 };
daf64e3f
IB
88 hosts = mkOption { type = listOf string; };
89 root = mkOption { type = nullOr path; };
90 extraConfig = mkOption { type = listOf lines; default = []; };
91 };
92 });
93 };
17f6eae9
IB
94 watchPaths = mkOption {
95 type = listOf string;
96 default = [];
97 description = ''
98 Paths to watch that should trigger a reload of httpd
99 '';
100 };
daf64e3f
IB
101 };
102 });
103 };
104
105 config.services.httpd = let
106 redirectVhost = ips: { # Should go last, catchall http -> https redirect
107 listen = map (ip: { inherit ip; port = 80; }) ips;
108 hostName = "redirectSSL";
109 serverAliases = [ "*" ];
110 enableSSL = false;
111 logFormat = "combinedVhost";
9ade8f6e 112 documentRoot = "${config.security.acme.directory}/acme-challenge";
daf64e3f
IB
113 extraConfig = ''
114 RewriteEngine on
115 RewriteCond "%{REQUEST_URI}" "!^/\.well-known"
116 RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301]
117 # To redirect in specific "VirtualHost *:80", do
118 # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1
119 # rather than rewrite
120 '';
121 };
122 nosslVhost = ips: cfg: {
123 listen = map (ip: { inherit ip; port = 80; }) ips;
124 hostName = cfg.host;
125 enableSSL = false;
126 logFormat = "combinedVhost";
127 documentRoot = cfg.root;
128 extraConfig = ''
129 <Directory ${cfg.root}>
130 DirectoryIndex ${cfg.indexFile}
131 AllowOverride None
132 Require all granted
133
134 RewriteEngine on
135 RewriteRule ^/(.+) / [L]
136 </Directory>
137 '';
138 };
139 toVhost = ips: vhostConf: {
140 enableSSL = true;
9ade8f6e
IB
141 sslServerCert = "${config.security.acme.directory}/${vhostConf.certName}/cert.pem";
142 sslServerKey = "${config.security.acme.directory}/${vhostConf.certName}/key.pem";
143 sslServerChain = "${config.security.acme.directory}/${vhostConf.certName}/chain.pem";
daf64e3f
IB
144 logFormat = "combinedVhost";
145 listen = map (ip: { inherit ip; port = 443; }) ips;
146 hostName = builtins.head vhostConf.hosts;
147 serverAliases = builtins.tail vhostConf.hosts or [];
148 documentRoot = vhostConf.root;
149 extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
150 };
151 in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
152 icfg.httpdName (mkIf icfg.enable {
153 enable = true;
154 listen = map (ip: { inherit ip; port = 443; }) icfg.ips;
155 stateDir = "/run/httpd_${name}";
156 logPerVirtualHost = true;
157 multiProcessingModule = "worker";
158 inherit (icfg) adminAddr;
159 logFormat = "combinedVhost";
160 extraModules = lists.unique icfg.modules;
161 extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
162 virtualHosts = [ (toVhost icfg.ips icfg.fallbackVhost) ]
163 ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ]
164 ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs)
165 ++ [ (redirectVhost icfg.ips) ];
166 })
167 ) cfg;
7df420c2 168
17f6eae9
IB
169 config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
170 "httpd${icfg.httpdName}" {
171 paths = icfg.watchPaths;
172 waitTime = 5;
173 }
174 ) cfg;
175
7df420c2
IB
176 config.security.acme.certs = let
177 typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg;
178 flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v:
179 attrValues v.vhostConfs
180 ) typesToManage);
181 groupedCerts = attrsets.filterAttrs
182 (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group)
183 (lists.groupBy (v: v.certName) flatVhosts);
184 groupToDomain = group:
185 let
186 nonNull = builtins.filter (v: !isNull v.certMainHost) group;
187 domains = lists.unique (map (v: v.certMainHost) nonNull);
188 in
189 if builtins.length domains == 0
190 then null
191 else assert (builtins.length domains == 1); (elemAt domains 0);
192 extraDomains = group:
193 let
194 mainDomain = groupToDomain group;
195 in
196 lists.remove mainDomain (
197 lists.unique (
198 lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group)
199 )
200 );
201 in attrsets.mapAttrs (k: g:
202 if (!isNull (groupToDomain g))
203 then config.services.websitesCerts // {
204 domain = groupToDomain g;
205 extraDomains = builtins.listToAttrs (
206 map (d: attrsets.nameValuePair d null) (extraDomains g));
207 }
208 else {
209 extraDomains = builtins.listToAttrs (
210 map (d: attrsets.nameValuePair d null) (extraDomains g));
211 }
212 ) groupedCerts;
daf64e3f 213}