summaryrefslogtreecommitdiff
path: root/sys/web
diff options
context:
space:
mode:
Diffstat (limited to 'sys/web')
-rw-r--r--sys/web/default.nix7
-rw-r--r--sys/web/nginx.nix94
-rw-r--r--sys/web/php-fpm.nix154
-rw-r--r--sys/web/sites/default.nix8
-rw-r--r--sys/web/sites/home.nix38
-rw-r--r--sys/web/sites/host.nix106
-rw-r--r--sys/web/sites/portal.nix42
-rw-r--r--sys/web/sites/pxe.nix180
8 files changed, 629 insertions, 0 deletions
diff --git a/sys/web/default.nix b/sys/web/default.nix
new file mode 100644
index 0000000..2fc769f
--- /dev/null
+++ b/sys/web/default.nix
@@ -0,0 +1,7 @@
+{
+ imports = [
+ ./nginx.nix
+ ./php-fpm.nix
+ ./sites
+ ];
+}
diff --git a/sys/web/nginx.nix b/sys/web/nginx.nix
new file mode 100644
index 0000000..a054289
--- /dev/null
+++ b/sys/web/nginx.nix
@@ -0,0 +1,94 @@
+{
+ config,
+ lib,
+ ...
+}:
+with lib; let
+ cfg = config.local.web;
+ inherit (config.local) domains;
+in {
+ options.local.web = {
+ enable = mkEnableOption "web server";
+
+ defaultACMEHost = mkOption {
+ type = types.str;
+ };
+
+ ownedCerts = mkOption {
+ type = with lib.types; listOf str;
+ default = [];
+ };
+ };
+
+ config = mkIf cfg.enable {
+ services = {
+ fail2ban.jails = {
+ # https://discourse.nixos.org/t/fail2ban-with-nginx-and-authelia/31419
+ nginx-botsearch.settings = {
+ # Usar log en vez de journalctl
+ # TODO: Pasar todo a systemd?
+ backend = "pyinotify";
+ logpath = "/var/log/nginx/*.log";
+ journalmatch = "";
+ };
+
+ nginx-bad-request.settings = {
+ backend = "pyinotify";
+ logpath = "/var/log/nginx/*.log";
+ journalmatch = "";
+
+ maxretry = 10;
+ };
+ };
+
+ nginx = {
+ enable = true;
+
+ recommendedGzipSettings = true;
+ recommendedOptimisation = true;
+ recommendedProxySettings = true;
+ recommendedTlsSettings = true;
+
+ logError = "/var/log/nginx/error.log";
+ sslDhparam = config.security.dhparams.params.nginx.path;
+ clientMaxBodySize = "42M";
+
+ mapHashBucketSize = 128;
+
+ virtualHosts.default = {
+ default = true;
+
+ addSSL = true;
+ useACMEHost = cfg.defaultACMEHost;
+
+ locations."/".extraConfig = ''
+ return 403;
+ '';
+ };
+ };
+ };
+
+ local.certs = listToAttrs (map
+ (name: {
+ inherit name;
+ value.enable = true;
+ })
+ cfg.ownedCerts);
+
+ networking.firewall.allowedTCPPorts = [80 443];
+
+ security = {
+ acme.certs = listToAttrs (map
+ (name: {
+ name = domains.${name}.main;
+ value = {
+ group = mkDefault config.services.nginx.group;
+ reloadServices = ["nginx.service"];
+ };
+ })
+ cfg.ownedCerts);
+
+ dhparams.params.nginx = {};
+ };
+ };
+}
diff --git a/sys/web/php-fpm.nix b/sys/web/php-fpm.nix
new file mode 100644
index 0000000..33efe1a
--- /dev/null
+++ b/sys/web/php-fpm.nix
@@ -0,0 +1,154 @@
+# Based on <https://gist.github.com/aanderse/3344baef2c3b86c8a1e98e63bd9256ea>
+# See also:
+# - <https://albert.cx/20181125-use-separate-systemd-units-for-php-fpm-pools>
+# - <https://freedesktop.org/wiki/Software/systemd/DaemonSocketActivation/>
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+with lib; let
+ cfg = config.services.php-fpm-isolated;
+
+ configFile = {
+ pool,
+ poolOpts,
+ runtimeDir,
+ sockFile,
+ pidFile,
+ }: let
+ config = {
+ global = {
+ daemonize = false;
+ error_log = "syslog";
+ pid = pidFile;
+ };
+
+ "${pool}" = let
+ enforced = {
+ inherit (poolOpts) user group;
+ listen = sockFile;
+ };
+
+ defaults = {
+ "pm" = "dynamic";
+ "pm.max_children" = 16;
+ "pm.min_spare_servers" = 1;
+ "pm.max_spare_servers" = 4;
+ "pm.start_servers" = 1;
+ "catch_workers_output" = true;
+ "php_admin_flag[log_errors]" = true;
+ "env[PATH]" = makeBinPath [pkgs.php];
+ };
+
+ env =
+ mapAttrs'
+ (name: value: {
+ name = "env[${name}]";
+ value = "\"${escape ["\""] value}\"";
+ })
+ poolOpts.env;
+ in
+ defaults // poolOpts.config // env // enforced;
+ };
+ in
+ (pkgs.formats.ini {}).generate "php-fpm-pool-${pool}.conf" config;
+in {
+ options.services.php-fpm-isolated.pools = mkOption {
+ default = {};
+
+ type = with types;
+ attrsOf (submodule {
+ options = {
+ enable = mkEnableOption "PHP-FPM pool";
+
+ user = mkOption {
+ type = str;
+ };
+
+ group = mkOption {
+ type = str;
+ };
+
+ unveil = mkOption {
+ type = listOf (either package str);
+ };
+
+ env = mkOption {
+ type = attrsOf str;
+ default = {};
+ };
+
+ config = mkOption {
+ type = attrsOf (oneOf [int str bool]);
+ default = {};
+ };
+ };
+ });
+ };
+
+ config.systemd = let
+ php-fpm = "${pkgs.php}/bin/php-fpm";
+
+ unitsFor = pool: poolOpts: let
+ runtimeBase = "php-fpm-isolated/${pool}";
+ runtimeDir = "/run/${runtimeBase}";
+ pidFile = "${runtimeDir}/${pool}.pid";
+ sockFile = "${runtimeDir}/${pool}.sock";
+ in {
+ name = "php-fpm-pool-${pool}";
+
+ value.service = {
+ description = "PHP-FPM process manager for pool '${pool}'";
+ after = ["network.target"];
+
+ confinement.enable = true;
+
+ serviceConfig = {
+ Type = "notify";
+ ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
+ PIDFile = pidFile;
+
+ Environment = "FPM_SOCKETS=${sockFile}=3";
+
+ ExecStart = let
+ fpmConfig = configFile {
+ inherit pool poolOpts runtimeDir sockFile pidFile;
+ };
+ in "${php-fpm} --nodaemonize --fpm-config ${fpmConfig} --pid ${pidFile}";
+
+ PrivateTmp = true;
+ PrivateNetwork = true;
+ PrivateDevices = true;
+ # XXX: We need AF_NETLINK to make the sendmail SUID binary from postfix work
+ RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+
+ User = poolOpts.user;
+ Group = poolOpts.group;
+ RuntimeDirectory = runtimeBase;
+
+ BindReadOnlyPaths = let
+ unveiled = map builtins.toString poolOpts.unveil;
+ in
+ ["/run/systemd/journal/socket"] ++ unveiled;
+ };
+ };
+
+ value.socket = {
+ description = "PHP-FPM socket for pool '${pool}'";
+ listenStreams = [sockFile];
+
+ socketConfig = {
+ User = poolOpts.user;
+ Group = poolOpts.group;
+ };
+ };
+ };
+
+ units = mapAttrs' unitsFor (filterAttrs (_: pool: pool.enable) cfg.pools);
+ in {
+ sockets = mapAttrs (_: unit: unit.socket) units;
+ services = mapAttrs (_: unit: unit.service) units;
+ };
+}
diff --git a/sys/web/sites/default.nix b/sys/web/sites/default.nix
new file mode 100644
index 0000000..85b6020
--- /dev/null
+++ b/sys/web/sites/default.nix
@@ -0,0 +1,8 @@
+{
+ imports = [
+ ./home.nix
+ ./host.nix
+ ./portal.nix
+ ./pxe.nix
+ ];
+}
diff --git a/sys/web/sites/home.nix b/sys/web/sites/home.nix
new file mode 100644
index 0000000..fed9b84
--- /dev/null
+++ b/sys/web/sites/home.nix
@@ -0,0 +1,38 @@
+{
+ config,
+ lib,
+ ...
+}:
+with lib; let
+ cfg = config.local.web.sites.home;
+ inherit (config.local) domains;
+in {
+ options.local.web.sites.home = {
+ enable = mkEnableOption "home site";
+
+ proxyUrl = mkOption {
+ type = types.str;
+ };
+ };
+
+ config = mkIf cfg.enable {
+ local.web = {
+ enable = mkDefault true;
+ ownedCerts = ["home"];
+ };
+
+ services.nginx.virtualHosts.${domains.home.main} = {
+ forceSSL = true;
+ useACMEHost = domains.home.main;
+
+ locations."/".extraConfig = ''
+ proxy_pass ${cfg.proxyUrl};
+ proxy_redirect http:// https://;
+
+ # Necesario debido a websockets
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ '';
+ };
+ };
+}
diff --git a/sys/web/sites/host.nix b/sys/web/sites/host.nix
new file mode 100644
index 0000000..ea6cc23
--- /dev/null
+++ b/sys/web/sites/host.nix
@@ -0,0 +1,106 @@
+{
+ config,
+ lib,
+ ...
+}:
+with lib; let
+ cfg = config.local.web.sites.host;
+
+ inherit (config.local) domains;
+ inherit (config.local.net) hostname;
+
+ users = filterAttrs (_: user: user.install) config.local.users;
+ hostDomain = domains.${hostDomainName};
+ hostDomainName = "host-${hostname}";
+
+ userCerts = flatten (flatten (mapAttrsToList
+ (name: user:
+ map
+ (cert: {
+ fprint = config.local.pki.byPath.${cert}.fingerprint.sha1-lower;
+ inherit name;
+ })
+ user.mail.certs)
+ users));
+in {
+ options.local.web.sites.host = {
+ enable = mkEnableOption "host site, restricted to per-user client certs";
+ };
+
+ config = mkIf cfg.enable {
+ local.web = {
+ enable = mkDefault true;
+ ownedCerts = [hostDomainName];
+ };
+
+ services = {
+ nginx = {
+ appendHttpConfig = ''
+ map $ssl_client_fingerprint $host_user_from_fprint {
+ default "";
+ ${concatMapStringsSep "\n " (pair: "\"${escapeRegex pair.fprint}\" \"${pair.name}\";") userCerts}
+ }
+ '';
+
+ virtualHosts = {
+ ${hostDomain.main} = {
+ forceSSL = true;
+ useACMEHost = hostDomain.main;
+
+ extraConfig = ''
+ ssl_verify_depth 2;
+ ssl_verify_client optional;
+ ssl_client_certificate ${config.local.pki.ca.mail.fullchain};
+
+ #if ($ssl_client_verify != "SUCCESS") {
+ #return 403;
+ #}
+ '';
+
+ locations =
+ {
+ "/".return = 403;
+ }
+ // concatMapAttrs
+ (name: user: let
+ userLocation = config: {
+ extraConfig =
+ ''
+ if ($host_user_from_fprint != "${name}") {
+ return 403;
+ }
+ ''
+ + config;
+ };
+
+ userLocations =
+ {
+ "/${name}" = ''
+ return 404;
+ '';
+ }
+ // optionalAttrs user.mail.dav {
+ "/${name}/dav" = ''
+ proxy_pass http://unix:/run/host-www/${name}/dav.sock;
+ '';
+ };
+ in
+ mapAttrs (_: userLocation) userLocations)
+ (filterAttrs (_: user: user.mail.certs != []) users);
+ };
+ };
+ };
+ };
+
+ systemd.tmpfiles.settings."10-run-host-www" =
+ concatMapAttrs
+ (name: _: {
+ "/run/host-www/${name}".d = {
+ mode = "0750";
+ user = name;
+ group = "nginx";
+ };
+ })
+ users;
+ };
+}
diff --git a/sys/web/sites/portal.nix b/sys/web/sites/portal.nix
new file mode 100644
index 0000000..c4d948e
--- /dev/null
+++ b/sys/web/sites/portal.nix
@@ -0,0 +1,42 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+with lib; let
+ cfg = config.local.web.sites.portal;
+ inherit (config.local) domains;
+in {
+ options.local.web.sites.portal = {
+ enable = mkEnableOption "public non-fqdn portal";
+ };
+
+ config = mkIf cfg.enable {
+ local.web = {
+ enable = mkDefault true;
+ ownedCerts = ["host" "sysret"];
+ defaultACMEHost = domains.host.main;
+ };
+
+ services.nginx.virtualHosts = {
+ ${domains.host.www} = {
+ forceSSL = true;
+ useACMEHost = domains.host.main;
+ serverAliases = [domains.host.main];
+ };
+
+ ${domains.sysret.main} = {
+ forceSSL = true;
+ useACMEHost = domains.sysret.main;
+ serverAliases = [domains.sysret.www];
+
+ locations = {
+ "/".alias = "${pkgs.local.sysret-static}/";
+ "/fsociety".return = "301 https://meet.posixlycorrect.com/%C6%92%C6%A8%C5%8F%C4%8B%D3%80%C9%99%CF%AE%D0%A3";
+ "/.well-known/openpgpkey/hu/".alias = "/var/lib/pgp-wkd/";
+ };
+ };
+ };
+ };
+}
diff --git a/sys/web/sites/pxe.nix b/sys/web/sites/pxe.nix
new file mode 100644
index 0000000..54f3e56
--- /dev/null
+++ b/sys/web/sites/pxe.nix
@@ -0,0 +1,180 @@
+{
+ config,
+ flakes,
+ lib,
+ pkgs,
+ ...
+}:
+with lib; let
+ cfg = config.local.web.sites.pxe;
+
+ pxeEnabled = machine: machine.config.local.boot.pxe.enable;
+ machines = listToAttrs (map (name: {
+ inherit name;
+ value = flakes.self.nixosConfigurations.${name};
+ })
+ cfg.machines);
+
+ stage2For = name: machine:
+ # https://purpleidea.com/blog/2025/01/23/working-around-an-ipxe-issue/
+ # "The answer:
+ # They told me that one of their engineers had struggled with the same issue for a while. The solution:
+ # Add the imgfree iPXE command before you boot!
+ # I wish the docs would hint that this is often required, but now I know, and now you know too!
+ # You can see that line here."
+ pkgs.writeText "stage2-${name}.ipxe" ''
+ #!ipxe
+ echo stage2: booting ${name}...
+ imgfree
+ kernel kernel init=${machine.config.system.build.toplevel}/init ${concatStringsSep " " machine.config.boot.kernelParams} || goto fail
+ initrd initrd || goto fail
+ boot ||
+ :fail
+ echo stage2: failed, rebooting in 5 seconds...
+ sleep 5
+ reboot --warm
+ '';
+
+ machineLocations = name: machine: let
+ mac = machine.config.local.boot.pxe.mac;
+ build = machine.config.system.build;
+ in {
+ "= /${mac}/initrd".alias = "${build.initialRamdisk}/initrd";
+ "= /${mac}/kernel".alias = "${build.kernel}/${pkgs.stdenv.hostPlatform.linux-kernel.target}";
+ "= /${mac}/stage2.ipxe".alias = "${stage2For name machine}";
+ };
+in {
+ options.local.web.sites.pxe = {
+ enable = mkEnableOption "PXE netboot image server";
+
+ linkLocal6 = mkOption {
+ type = types.str;
+ };
+
+ network = mkOption {
+ type = types.str;
+ };
+
+ machines = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions =
+ mapAttrsToList (name: machine: {
+ message = "PXE boot is not enabled in config '${name}'";
+ assertion = pxeEnabled machine;
+ })
+ machines;
+
+ local.web.enable = mkDefault true;
+
+ networking = {
+ firewall.extraCommands = ''
+ ip6tables -t filter -A local-input -p udp -i ${cfg.network} --dport 1792 -d ${cfg.linkLocal6} -j ACCEPT
+ '';
+ };
+
+ services = {
+ nginx = {
+ virtualHosts = {
+ "[${cfg.linkLocal6}]" = {
+ listenAddresses = ["[${cfg.linkLocal6}]"];
+
+ addSSL = false;
+ forceSSL = false;
+
+ locations = mergeAttrsList (mapAttrsToList machineLocations (filterAttrs (_: pxeEnabled) machines));
+ };
+ };
+ };
+ };
+
+ systemd = {
+ network = {
+ networks.${cfg.network}.addresses = [
+ {
+ Address = "${cfg.linkLocal6}/128";
+ AddPrefixRoute = "no";
+ PreferredLifetime = "0";
+ }
+ ];
+ };
+
+ services = {
+ pxe-store-upload = {
+ path = [config.nix.package pkgs.sshfs pkgs.squashfsTools];
+
+ script = let
+ host = lib.throw "TODO: pxe host";
+ machine = machines.${host};
+ in ''
+ set -e
+
+ pxe_ip='${machine.config.local.boot.pxe.linkLocal6}'
+ pxe_host='${host}'
+ pxe_system='${machine.config.system.build.toplevel}'
+
+ mountpoint="./pxe-initrd.$pxe_host"
+ store_img="$mountpoint/nix-store.squashfs"
+
+ mkdir -p "$mountpoint"
+
+ export PATH="/run/wrappers/bin:$PATH"
+ sshfs \
+ "root@[$pxe_ip%${cfg.network}]:/nix/.ro-store-img/" \
+ "$mountpoint" \
+ -o idmap=user
+
+ nix-store --query --requisites "$pxe_system" | \
+ xargs -I{} mksquashfs {} - \
+ -stream -comp zstd -one-file-system -no-xattrs \
+ >"$store_img.tmp"
+
+ mksquashfs -fix "$store_img.tmp"
+
+ sync "$store_img.tmp"
+ mv -- "$store_img.tmp" "$store_img"
+ fusermount3 -u "$mountpoint"
+ '';
+
+ serviceConfig = {
+ #AmbientCapabilities = ["CAP_SYS_ADMIN"];
+ #CapabilityBoundingSet = ["CAP_SYS_ADMIN"];
+ #BindPaths = ["/dev/fuse"];
+ #BindReadOnlyPaths = ["/nix/store"];
+ #DeviceAllow = ["/dev/fuse rwm"];
+ #DevicePolicy = "closed";
+ #MountAPIVFS = true;
+ ProtectSystem = "strict";
+ ProtectControlGroups = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ #PrivateDevices = true;
+ #PrivateTmp = true;
+ #PrivateUsers = true;
+ #TemporaryFileSystem = ["/"];
+ #DynamicUser = true;
+ RuntimeDirectory = "pxe-store-upload";
+ WorkingDirectory = "/run/pxe-store-upload";
+ };
+ };
+ };
+
+ sockets = {
+ pxe-store-upload = {
+ after = ["network-online.target"];
+ wants = ["network-online.target"];
+ wantedBy = ["sockets.target"];
+
+ socketConfig = {
+ ListenDatagram = "[${cfg.linkLocal6}]:1792%%${cfg.network}";
+ };
+ };
+ };
+ };
+ };
+}