diff options
Diffstat (limited to 'sys/web')
| -rw-r--r-- | sys/web/default.nix | 7 | ||||
| -rw-r--r-- | sys/web/nginx.nix | 94 | ||||
| -rw-r--r-- | sys/web/php-fpm.nix | 154 | ||||
| -rw-r--r-- | sys/web/sites/default.nix | 8 | ||||
| -rw-r--r-- | sys/web/sites/home.nix | 38 | ||||
| -rw-r--r-- | sys/web/sites/host.nix | 106 | ||||
| -rw-r--r-- | sys/web/sites/portal.nix | 42 | ||||
| -rw-r--r-- | sys/web/sites/pxe.nix | 180 |
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}"; + }; + }; + }; + }; + }; +} |
