blob: fd788f7d26013ed3b7715b48d354e654e5b8050b (
plain) (
tree)
|
|
{
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)
);
}
];
};
};
}
|