1 { lib, config, ... }: with lib;
3 cfg = config.services.websites;
6 options.services.websites = with types; {
8 description = "Default websites configuration for certificates as accepted by acme";
10 webappDirs = mkOption {
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.
16 type = types.attrsOf types.path;
19 webappDirsName = mkOption {
23 Name of the webapp dir to create in /run/current-system
28 description = "Each type of website to enable will target a distinct httpd server";
29 type = attrsOf (submodule {
31 enable = mkEnableOption "Enable websites of this type";
32 adminAddr = mkOption {
34 description = "Admin e-mail address of the instance";
36 httpdName = mkOption {
38 description = "Name of the httpd instance to assign this type to";
43 description = "ips to listen to";
48 description = "Additional modules to load in Apache";
50 extraConfig = mkOption {
53 description = "Additional configuration to append to Apache";
55 nosslVhost = mkOption {
56 description = "A default nossl vhost for captive portals";
60 enable = mkEnableOption "Add default no-ssl vhost for this instance";
63 description = "The hostname to use for this vhost";
67 default = ./nosslVhost;
68 description = "The root folder to serve";
70 indexFile = mkOption {
72 default = "index.html";
73 description = "The index file to show.";
78 fallbackVhost = mkOption {
79 description = "The fallback vhost that will be defined as first vhost in Apache";
82 certName = mkOption { type = string; };
83 hosts = mkOption { type = listOf string; };
84 root = mkOption { type = nullOr path; };
85 extraConfig = mkOption { type = listOf lines; default = []; };
89 vhostConfs = mkOption {
91 description = "List of vhosts to define for Apache";
92 type = attrsOf (submodule {
94 certName = mkOption { type = string; };
95 addToCerts = mkOption {
98 description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null";
100 certMainHost = mkOption {
101 type = nullOr string;
102 description = "Use that host as 'main host' for acme certs";
105 hosts = mkOption { type = listOf string; };
106 root = mkOption { type = nullOr path; };
107 extraConfig = mkOption { type = listOf lines; default = []; };
111 watchPaths = mkOption {
112 type = listOf string;
115 Paths to watch that should trigger a reload of httpd
122 webappDirsPaths = mkOption {
126 Full paths of the webapp dir
128 default = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
129 name "/run/current-system/${cfg.webappDirsName}/${name}"
134 config.services.httpd = let
135 redirectVhost = ips: { # Should go last, catchall http -> https redirect
136 listen = map (ip: { inherit ip; port = 80; }) ips;
137 hostName = "redirectSSL";
138 serverAliases = [ "*" ];
140 logFormat = "combinedVhost";
141 documentRoot = "${config.security.acme.directory}/acme-challenge";
144 RewriteCond "%{REQUEST_URI}" "!^/\.well-known"
145 RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301]
146 # To redirect in specific "VirtualHost *:80", do
147 # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1
148 # rather than rewrite
151 nosslVhost = ips: cfg: {
152 listen = map (ip: { inherit ip; port = 80; }) ips;
155 logFormat = "combinedVhost";
156 documentRoot = cfg.root;
158 <Directory ${cfg.root}>
159 DirectoryIndex ${cfg.indexFile}
164 RewriteRule ^/(.+) / [L]
168 toVhost = ips: vhostConf: {
170 sslServerCert = "${config.security.acme.directory}/${vhostConf.certName}/cert.pem";
171 sslServerKey = "${config.security.acme.directory}/${vhostConf.certName}/key.pem";
172 sslServerChain = "${config.security.acme.directory}/${vhostConf.certName}/chain.pem";
173 logFormat = "combinedVhost";
174 listen = map (ip: { inherit ip; port = 443; }) ips;
175 hostName = builtins.head vhostConf.hosts;
176 serverAliases = builtins.tail vhostConf.hosts or [];
177 documentRoot = vhostConf.root;
178 extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
180 in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
181 icfg.httpdName (mkIf icfg.enable {
183 listen = map (ip: { inherit ip; port = 443; }) icfg.ips;
184 stateDir = "/run/httpd_${name}";
185 logPerVirtualHost = true;
186 multiProcessingModule = "worker";
187 inherit (icfg) adminAddr;
188 logFormat = "combinedVhost";
189 extraModules = lists.unique icfg.modules;
190 extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
191 virtualHosts = [ (toVhost icfg.ips icfg.fallbackVhost) ]
192 ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ]
193 ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs)
194 ++ [ (redirectVhost icfg.ips) ];
198 config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
199 "httpd${icfg.httpdName}" {
200 paths = icfg.watchPaths;
205 config.security.acme.certs = let
206 typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg.env;
207 flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v:
208 attrValues v.vhostConfs
210 groupedCerts = attrsets.filterAttrs
211 (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group)
212 (lists.groupBy (v: v.certName) flatVhosts);
213 groupToDomain = group:
215 nonNull = builtins.filter (v: !isNull v.certMainHost) group;
216 domains = lists.unique (map (v: v.certMainHost) nonNull);
218 if builtins.length domains == 0
220 else assert (builtins.length domains == 1); (elemAt domains 0);
221 extraDomains = group:
223 mainDomain = groupToDomain group;
225 lists.remove mainDomain (
227 lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group)
230 in attrsets.mapAttrs (k: g:
231 if (!isNull (groupToDomain g))
233 domain = groupToDomain g;
234 extraDomains = builtins.listToAttrs (
235 map (d: attrsets.nameValuePair d null) (extraDomains g));
238 extraDomains = builtins.listToAttrs (
239 map (d: attrsets.nameValuePair d null) (extraDomains g));
243 config.system.extraSystemBuilderCmds = lib.mkIf (builtins.length (builtins.attrValues cfg.webappDirs) > 0) ''
244 mkdir -p $out/${cfg.webappDirsName}
245 ${builtins.concatStringsSep "\n"
246 (attrsets.mapAttrsToList
247 (name: path: "ln -s ${path} $out/${cfg.webappDirsName}/${name}") cfg.webappDirs)