--- /dev/null
+{
+ description = "Module to handle multiple separate apache instances (using containers)";
+ inputs.myuids = {
+ url = "path:../myuids";
+ };
+ inputs.files-watcher = {
+ url = "path:../files-watcher";
+ };
+
+ outputs = { self, myuids, files-watcher }: {
+ nixosModule = { lib, config, pkgs, options, ... }:
+ with lib;
+ let
+ cfg = config.services.websites;
+ hostConfig = config;
+ toHttpdConfig = icfg:
+ let
+ nosslVhost = ips: cfg: {
+ listen = map (ip: { inherit ip; port = 80; }) ips;
+ hostName = cfg.host;
+ logFormat = "combinedVhost";
+ documentRoot = cfg.root;
+ extraConfig = ''
+ <Directory ${cfg.root}>
+ DirectoryIndex ${cfg.indexFile}
+ AllowOverride None
+ Require all granted
+
+ RewriteEngine on
+ RewriteRule ^/(.+) / [L]
+ </Directory>
+ '';
+ };
+ toVhost = ips: vhostConf: {
+ acmeRoot = hostConfig.security.acme.certs.${vhostConf.certName}.webroot;
+ forceSSL = vhostConf.forceSSL or true;
+ useACMEHost = vhostConf.certName;
+ logFormat = "combinedVhost";
+ listen = if vhostConf.forceSSL
+ then lists.flatten (map (ip: [{ inherit ip; port = 443; ssl = true; } { inherit ip; port = 80; }]) ips)
+ else map (ip: { inherit ip; port = 443; ssl = true; }) ips;
+ hostName = builtins.head vhostConf.hosts;
+ serverAliases = builtins.tail vhostConf.hosts or [];
+ documentRoot = vhostConf.root;
+ extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
+ };
+ toVhostNoSSL = ips: vhostConf: {
+ logFormat = "combinedVhost";
+ listen = map (ip: { inherit ip; port = 80; }) ips;
+ hostName = builtins.head vhostConf.hosts;
+ serverAliases = builtins.tail vhostConf.hosts or [];
+ documentRoot = vhostConf.root;
+ extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
+ };
+ in {
+ enable = true;
+ logPerVirtualHost = true;
+ mpm = "event";
+ # https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.0.2t&guideline=5.4
+ # test with https://www.ssllabs.com/ssltest/analyze.html?d=www.immae.eu&s=176.9.151.154&latest
+ sslProtocols = "all -SSLv3 -TLSv1 -TLSv1.1";
+ sslCiphers = builtins.concatStringsSep ":" [
+ "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256"
+ "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384"
+ "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305"
+ "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384"
+ ];
+ inherit (icfg) adminAddr;
+ logFormat = "combinedVhost";
+ extraModules = lists.unique icfg.modules;
+ extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
+
+ virtualHosts = with attrsets; {
+ ___fallbackVhost = toVhost icfg.ips icfg.fallbackVhost;
+ } // (optionalAttrs icfg.nosslVhost.enable {
+ nosslVhost = nosslVhost icfg.ips icfg.nosslVhost;
+ }) // (mapAttrs' (n: v: nameValuePair ("nossl_" + n) (toVhostNoSSL icfg.ips v)) icfg.vhostNoSSLConfs)
+ // (mapAttrs' (n: v: nameValuePair ("ssl_" + n) (toVhost icfg.ips v)) icfg.vhostConfs);
+ };
+ in
+ {
+ options.services.websites = with types; {
+ env = mkOption {
+ default = {};
+ description = "Each type of website to enable will target a distinct httpd server";
+ type = attrsOf (submodule ({ name, config, ... }: {
+ options = {
+ enable = mkEnableOption "Enable websites of this type";
+ moduleType = mkOption {
+ type = enum [ "container" "main" ];
+ default = "container";
+ description = ''
+ How to deploy the web environment:
+ - container -> inside a dedicated container (running only httpd)
+ - main -> as main services.httpd module
+ '';
+ };
+ adminAddr = mkOption {
+ type = str;
+ description = "Admin e-mail address of the instance";
+ };
+ user = mkOption {
+ type = str;
+ description = "Username of httpd service";
+ readOnly = true;
+ default = if config.moduleType == "container"
+ then hostConfig.containers."httpd-${name}".config.services.httpd.user
+ else hostConfig.services.httpd.user;
+ };
+ group = mkOption {
+ type = str;
+ description = "Group of httpd service";
+ readOnly = true;
+ default = if config.moduleType == "container"
+ then hostConfig.containers."httpd-${name}".config.services.httpd.group
+ else hostConfig.services.httpd.group;
+ };
+ httpdName = mkOption {
+ type = str;
+ description = "Name of the httpd instance to assign this type to";
+ };
+ ips = mkOption {
+ type = listOf str;
+ default = [];
+ description = "ips to listen to";
+ };
+ bindMounts = mkOption {
+ type = attrsOf unspecified;
+ default = {};
+ description = "bind mounts to add to container";
+ };
+ modules = mkOption {
+ type = listOf str;
+ default = [];
+ description = "Additional modules to load in Apache";
+ };
+ extraConfig = mkOption {
+ type = listOf lines;
+ default = [];
+ description = "Additional configuration to append to Apache";
+ };
+ nosslVhost = mkOption {
+ description = "A default nossl vhost for captive portals";
+ default = {};
+ type = submodule {
+ options = {
+ enable = mkEnableOption "Add default no-ssl vhost for this instance";
+ host = mkOption {
+ type = str;
+ description = "The hostname to use for this vhost";
+ };
+ root = mkOption {
+ type = path;
+ description = "The root folder to serve";
+ };
+ indexFile = mkOption {
+ type = str;
+ default = "index.html";
+ description = "The index file to show.";
+ };
+ };
+ };
+ };
+ fallbackVhost = mkOption {
+ description = "The fallback vhost that will be defined as first vhost in Apache";
+ type = submodule {
+ options = {
+ certName = mkOption { type = str; };
+ hosts = mkOption { type = listOf str; };
+ root = mkOption { type = nullOr path; };
+ forceSSL = mkOption {
+ type = bool;
+ default = true;
+ description = ''
+ Automatically create a corresponding non-ssl vhost
+ that will only redirect to the ssl version
+ '';
+ };
+ extraConfig = mkOption { type = listOf lines; default = []; };
+ };
+ };
+ };
+ vhostNoSSLConfs = mkOption {
+ default = {};
+ description = "List of no ssl vhosts to define for Apache";
+ type = attrsOf (submodule {
+ options = {
+ hosts = mkOption { type = listOf str; };
+ root = mkOption { type = nullOr path; };
+ extraConfig = mkOption { type = listOf lines; default = []; };
+ };
+ });
+ };
+ vhostConfs = mkOption {
+ default = {};
+ description = "List of vhosts to define for Apache";
+ type = attrsOf (submodule {
+ options = {
+ certName = mkOption { type = str; };
+ hosts = mkOption { type = listOf str; };
+ root = mkOption { type = nullOr path; };
+ forceSSL = mkOption {
+ type = bool;
+ default = true;
+ description = ''
+ Automatically create a corresponding non-ssl vhost
+ that will only redirect to the ssl version
+ '';
+ };
+ extraConfig = mkOption { type = listOf lines; default = []; };
+ };
+ });
+ };
+ watchPaths = mkOption {
+ type = listOf str;
+ default = [];
+ description = ''
+ Paths to watch that should trigger a reload of httpd
+ '';
+ };
+ };
+ }));
+ };
+ };
+
+ config = lib.mkMerge [
+ {
+ assertions = [
+ {
+ assertion = builtins.length (builtins.attrNames (lib.filterAttrs (k: v: v.enable && v.moduleType == "main") cfg.env)) <= 1;
+ message = ''
+ Only one enabled environment can have moduleType = "main"
+ '';
+ }
+ ];
+ }
+
+ {
+ environment.etc = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+ "httpd/${name}/httpd.conf" { source = (pkgs.nixos {
+ imports = [
+ {
+ config.security.acme.acceptTerms = true;
+ config.security.acme.preliminarySelfsigned = false;
+ config.security.acme.certs =
+ lib.mapAttrs (n: lib.filterAttrs (n': v': n' != "directory")) config.security.acme.certs;
+ config.security.acme.defaults = config.security.acme.defaults;
+ config.networking.hostName = "${hostConfig.networking.hostName}-${name}";
+ config.services.httpd = toHttpdConfig icfg;
+ }
+ ];
+ }).config.services.httpd.configFile;
+ }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env);
+
+ system.activationScripts.httpd-containers = {
+ deps = [ "etc" ];
+ text = builtins.concatStringsSep "\n" (
+ lib.mapAttrsToList (n: v: ''
+ install -d -m 0750 -o ${v.user} -g ${v.group} /var/log/httpd/${n} /var/lib/nixos-containers/httpd-${n}-mounts/conf
+ install -Dm644 -o ${v.user} -g ${v.group} /etc/httpd/${n}/httpd.conf /var/lib/nixos-containers/httpd-${n}-mounts/conf/
+ '') (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env)
+ );
+ };
+
+ security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg:
+ let
+ containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs
+ ++ [ icfg.fallbackVhost.certName ]);
+ in
+ lib.genAttrs containerCertNames (n:
+ { postRun = "machinectl shell httpd-${name} /run/current-system/sw/bin/systemctl reload httpd.service"; }
+ )
+ ) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env)
+ );
+ containers = let hostConfig = config; in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+ "httpd-${name}" {
+ autoStart = true;
+ privateNetwork = false;
+ bindMounts = {
+ "/var/log/httpd" = {
+ hostPath = "/var/log/httpd/${name}";
+ isReadOnly = false;
+ };
+ "/etc/httpd" = {
+ hostPath = "/var/lib/nixos-containers/httpd-${name}-mounts/conf";
+ };
+ } // icfg.bindMounts;
+
+ config = { config, options, ... }: {
+ imports = [
+ myuids.nixosModule
+ files-watcher.nixosModule
+ ];
+ config = lib.mkMerge [
+ {
+ # This value determines the NixOS release with which your system is
+ # to be compatible, in order to avoid breaking some software such as
+ # database servers. You should change this only after NixOS release
+ # notes say you should.
+ # https://nixos.org/nixos/manual/release-notes.html
+ system.stateVersion = "23.05"; # Did you read the comment?
+ }
+ {
+ users.mutableUsers = false;
+ users.allowNoPasswordLogin = true;
+ users.users.acme.uid = config.ids.uids.acme;
+ users.users.acme.group = "acme";
+ users.groups.acme.gid = config.ids.gids.acme;
+ }
+ {
+ services.logrotate.settings.httpd.enable = false;
+ }
+ {
+ environment.etc."httpd/httpd.conf".enable = false;
+ services.httpd = {
+ enable = true;
+ configFile = "/etc/httpd/httpd.conf";
+ };
+
+ services.filesWatcher.http-config-reload = {
+ paths = [ "/etc/httpd/httpd.conf" ];
+ waitTime = 2;
+ restart = true;
+ };
+ services.filesWatcher.httpd = {
+ paths = icfg.watchPaths;
+ waitTime = 5;
+ };
+
+ users.users.${icfg.user}.extraGroups = [ "acme" "keys" ];
+ systemd.services.http-config-reload = {
+ wants = [ "httpd.service" ];
+ wantedBy = [ "multi-user.target" ];
+ restartTriggers = [ config.services.httpd.configFile ];
+ serviceConfig.Type = "oneshot";
+ serviceConfig.TimeoutSec = 60;
+ serviceConfig.RemainAfterExit = true;
+ script = ''
+ if ${pkgs.systemd}/bin/systemctl -q is-active httpd.service ; then
+ ${config.services.httpd.package.out}/bin/httpd -f ${config.services.httpd.configFile} -t && \
+ ${pkgs.systemd}/bin/systemctl reload httpd.service
+ fi
+ '';
+ };
+ }
+ ];
+ };
+ }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env);
+ }
+
+ {
+ services.httpd = lib.concatMapAttrs (name: toHttpdConfig)
+ (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+ users.users = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+ config.services.httpd.user { extraGroups = [ "acme" ]; }
+ ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+ services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+ "httpd" {
+ paths = icfg.watchPaths;
+ waitTime = 5;
+ }
+ ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+ services.logrotate.settings.httpd.enable = false;
+ systemd.services = lib.concatMapAttrs (name: v: {
+ httpd.restartTriggers = lib.mkForce [];
+ })
+ (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+ security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg:
+ let
+ containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs
+ ++ [ icfg.fallbackVhost.certName ]);
+ in
+ lib.genAttrs containerCertNames (n:
+ { postRun = "systemctl reload httpd.service"; }
+ )
+ ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env)
+ );
+
+ }
+ ];
+ };
+ };
+}
+
+