# All our domains fall in one or more of three categories # proxyPass, basically handle the tls and pass the http traffick on # redirect, redirect to another uri # webHost, host the content of some directory under this domain # We will use this by defining helper functions that generates config # for those categories and then apply them to the required parameters. { lib, config, ... }: let cfg = config.services.reverseProxy; # the equivalent of library imports in other languages inherit (lib) isString mkEnableOption mkOption; inherit (lib.attrsets) concatMapAttrs; inherit (lib.lists) foldr; withGeneralHostConfig = virtualHostConfig: { forceSSL = true; useACMEHost = "mathebau.de"; locations = { # notice that nix will also parse this string and we need to escape \ # so after nix processing "~ /\.git/" will end up in the nginx config, # with the proper regex escape. # I find this behaiviour unexpected and a bit weird, but it catches some footguns # Note that this also applies to redirects. "~ /\\.git/" = { extraConfig = "deny all;"; }; "~ /\\.ht" = { extraConfig = "deny all;"; }; "/.well-known/security.txt" = { return = '' 200 "Contact: mailto:root@mathebau.de Expires: 2029-12-31T23:00:00.000Z Preferred-Languages: de,en" ''; }; } // virtualHostConfig.locations; } // virtualHostConfig; # The function to generate proxyPass directives # We always proxy pass "/", but in theory this can also work for other paths proxyWithCache = let proxyHelper = { targetMachine, targetPort, websockets, }: { proxyPass = "http://${targetMachine}:${toString targetPort}"; proxyWebsockets = websockets; recommendedProxySettings = true; }; cacheHelper = targetData: proxyHelper targetData // { extraConfig = '' gzip on; gzip_proxied any; gzip_types *; expires max; ''; }; in concatMapAttrs (domain: domainData: { ${domain}.locations = concatMapAttrs (path: targetData: { ${path} = proxyHelper targetData; # this instruncts clients to cache files with this list of extension for 24h. # This might break things. For example if a machine tries to generate these # things dynamically, but might be load reducing, i don't get why atom, rss, xml and txt are in this list "~* ^${path}.+\\.(atom|bmp|bz2|doc|docx|eot|exe|gif|gz|ico|jpeg|jpg|mid|midi|mp4|ogg|ogv|otf|pdf|png|ppt|pptx|rar|rss|rtf|svg|svgz|swf|tar|tgz|ttf|txt|wav|webp|woff|woff2|xls|zip|css|js|xml)$" = cacheHelper targetData; }) domainData; }); proxyPasses = proxyWithCache cfg.proxies; redirect = let redirectHelper = { target, code ? "303", }: {return = code + " " + target;}; in concatMapAttrs ( domain: redirectData: { ${domain}.locations = concatMapAttrs ( path: targetData: if isString targetData then { ${path} = redirectHelper { target = targetData; }; } else {${path} = redirectHelper targetData;} ) redirectData; } ); # TODO import these from a file redirects = redirect cfg.redirects; # Helper function to merge all the different configurations together. # Later configurations take precedence. in { imports = []; options.services.reverseProxy = { enable = mkEnableOption "mathebau reverse Proxy"; redirects = mkOption { type = lib.types.attrsOf (lib.types.attrsOf (lib.types.either lib.types.str (lib.types.submodule { options = { code = mkOption { type = lib.types.str; # todo make matching regex description = "the http status code of the redirect"; default = "303"; }; target = mkOption { type = lib.types.str; description = "the target url of the redirect, for more details on format see the nginx docu"; }; }; }))); description = "the redirects that this reverse proxy should serve"; default = {}; }; proxies = mkOption { type = lib.types.attrsOf (lib.types.attrsOf (lib.types.submodule { options = { targetMachine = mkOption { type = lib.types.str; description = "the machine to proxy to"; }; targetPort = mkOption { type = lib.types.port; description = "the port the target machine listens on"; default = 80; }; websockets = mkOption { type = lib.types.bool; description = "wether or not websocket support should be enabled"; default = false; }; }; })); description = "the proxy pass directive this reverse proxy should serve"; default = {}; }; }; config = lib.mkIf cfg.enable { services.nginx = { enable = true; virtualHosts = concatMapAttrs (name: config: {${name} = withGeneralHostConfig config;}) (foldr lib.attrsets.recursiveUpdate {} [redirects proxyPasses]); # We should already set this by default in roles/vm.nix for all vm that run nginx, but to be sure... recommendedTlsSettings = lib.mkForce true; # We set form-action to self so in general webforms are allowed. On a finer bases # we could set this to none for some hosts (or even locations) but this would clutter # the config. Same is true for base-uri appendHttpConfig = '' add_header Content-Security-Policy "base-uri 'self'; default-src 'self'; frame-ancestor 'none'; form-action 'self';" always; add_header X-Content-Type-Options nosniff; ''; }; # TODO: we need to rebuild this for dns challenges, # this does not work with our proxy pass challenge hand through things. security.acme = { acceptTerms = true; certs."mathebau.de" = { webroot = "/var/lib/acme/acme-challenge"; email = "root@mathebau.de"; extraDomainNames = [ "cloud.mathechor.de" "download.mathebau.de" "events.mathebau.de" "fb04184.mathematik.tu-darmstadt.de" "fswiki.mathebau.de" "gitea.mathebau.de" "imap.mathebau.de" "intern.mathebau.de" "lists.mathebau.de" "matheball.de" "matheball.mathebau.de" "mathechor.de" "meet.mathebau.de" "owowiki.mathebau.de" "smtp.mathebau.de" "sprechstunden.mathebau.de" "surveys.mathebau.de" "theateraufnahmen.mathebau.de" "theaterskript.mathebau.de" "www.koma89.tu-darmstadt.de" "www.matheball.de" "www.mathebau.de" "www.mathechor.de" ]; }; }; users.users.nginx.extraGroups = ["acme"]; }; }