about summary refs log tree commit diff
path: root/modules/services/nginx/default.nix
diff options
context:
space:
mode:
Diffstat (limited to 'modules/services/nginx/default.nix')
-rw-r--r--modules/services/nginx/default.nix326
1 files changed, 326 insertions, 0 deletions
diff --git a/modules/services/nginx/default.nix b/modules/services/nginx/default.nix
new file mode 100644
index 0000000..0020111
--- /dev/null
+++ b/modules/services/nginx/default.nix
@@ -0,0 +1,326 @@
+# A simple abstraction layer for almost all of my services' needs
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.my.services.nginx;
+  virtualHostOption = with lib;
+    types.submodule {
+      options = {
+        subdomain = mkOption {
+          type = types.str;
+          example = "dev";
+          description = ''
+            Which subdomain, under config.networking.domain, to use
+            for this virtual host.
+          '';
+        };
+        port = mkOption {
+          type = with types; nullOr port;
+          default = null;
+          example = 8080;
+          description = ''
+            Which port to proxy to, through 127.0.0.1, for this virtual host.
+            This option is incompatible with `root`.
+          '';
+        };
+        root = mkOption {
+          type = with types; nullOr path;
+          default = null;
+          example = "/var/www/blog";
+          description = ''
+            The root folder for this virtual host.  This option is incompatible
+            with `port`.
+          '';
+        };
+        sso = { enable = mkEnableOption "SSO authentication"; };
+        extraConfig = mkOption {
+          type = types.attrs; # FIXME: forward type of virtualHosts
+          example = litteralExample ''
+            {
+              locations."/socket" = {
+                proxyPass = "http://127.0.0.1:8096/";
+                proxyWebsockets = true;
+              };
+            }
+          '';
+          default = { };
+          description = ''
+            Any extra configuration that should be applied to this virtual host.
+          '';
+        };
+      };
+    };
+in {
+  imports = [ ./sso ];
+  options.my.services.nginx = with lib; {
+    enable = mkEnableOption "Nginx";
+    monitoring = {
+      enable = my.mkDisableOption "monitoring through grafana and prometheus";
+    };
+    virtualHosts = mkOption {
+      type = types.listOf virtualHostOption;
+      default = [ ];
+      example = litteralExample ''
+        [
+          {
+            subdomain = "gitea";
+            port = 8080;
+          }
+          {
+            subdomain = "dev";
+            root = "/var/www/dev";
+          }
+          {
+            subdomain = "jellyfin";
+            port = 8096;
+            extraConfig = {
+              locations."/socket" = {
+                proxyPass = "http://127.0.0.1:8096/";
+                proxyWebsockets = true;
+              };
+            };
+          }
+        ]
+      '';
+      description = ''
+        List of virtual hosts to set-up using default settings.
+      '';
+    };
+    sso = {
+      authKeyFile = mkOption {
+        type = types.str;
+        example = "/var/lib/nginx-sso/auth-key.txt";
+        description = ''
+          Path to the auth key.
+        '';
+      };
+      subdomain = mkOption {
+        type = types.str;
+        default = "login";
+        example = "auth";
+        description = "Which subdomain, to use for SSO.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 8082;
+        example = 8080;
+        description = "Port to use for internal webui.";
+      };
+      users = mkOption {
+        type = types.attrsOf (types.submodule {
+          options = {
+            passwordHashFile = mkOption {
+              type = types.str;
+              example = "/var/lib/nginx-sso/alice/password-hash.txt";
+              description = "Path to file containing the user's password hash.";
+            };
+          };
+        });
+        example = litteralExample ''
+          {
+            alice = {
+              passwordHashFile = "/var/lib/nginx-sso/alice/password-hash.txt";
+            };
+          }
+        '';
+        description = "Definition of users";
+      };
+      groups = mkOption {
+        type = with types; attrsOf (listOf str);
+        example = litteralExample ''
+          {
+            root = [ "alice" ];
+            users = [ "alice" "bob" ];
+          }
+        '';
+        description = "Groups of users";
+      };
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    assertions = [ ] ++ (lib.flip builtins.map cfg.virtualHosts
+      ({ subdomain, ... }@args:
+        let
+          conflicts = [ "port" "root" ];
+          optionsNotNull = builtins.map (v: args.${v} != null) conflicts;
+          optionsSet = lib.filter lib.id optionsNotNull;
+        in {
+          assertion = builtins.length optionsSet == 1;
+          message = ''
+            Subdomain '${subdomain}' must have exactly one of ${
+              lib.concatStringsSep ", " (builtins.map (v: "'${v}'") conflicts)
+            } configured.
+          '';
+        })) ++ (let
+          ports = lib.my.mapFilter (v: v != null) ({ port, ... }: port)
+            cfg.virtualHosts;
+          portCounts = lib.my.countValues ports;
+          nonUniquesCounts = lib.filterAttrs (_: v: v != 1) portCounts;
+          nonUniques = builtins.attrNames nonUniquesCounts;
+          mkAssertion = port: {
+            assertion = false;
+            message = "Port ${port} cannot appear in multiple virtual hosts.";
+          };
+        in map mkAssertion nonUniques) ++ (let
+          subs = map ({ subdomain, ... }: subdomain) cfg.virtualHosts;
+          subsCounts = lib.my.countValues subs;
+          nonUniquesCounts = lib.filterAttrs (_: v: v != 1) subsCounts;
+          nonUniques = builtins.attrNames nonUniquesCounts;
+          mkAssertion = v: {
+            assertion = false;
+            message = ''
+              Subdomain '${v}' cannot appear in multiple virtual hosts.
+            '';
+          };
+        in map mkAssertion nonUniques);
+    services.nginx = {
+      enable = true;
+      statusPage = true; # For monitoring scraping.
+      recommendedGzipSettings = true;
+      recommendedOptimisation = true;
+      recommendedTlsSettings = true;
+      recommendedProxySettings = true;
+      virtualHosts = let
+        domain = "fcuny.net";
+        mkVHost = ({ subdomain, ... }@args:
+          lib.nameValuePair "${subdomain}.${domain}" (lib.my.recursiveMerge [
+            # Base configuration
+            {
+              forceSSL = true;
+              useACMEHost = domain;
+            }
+            # Proxy to port
+            (lib.optionalAttrs (args.port != null) {
+              locations."/".proxyPass =
+                "http://127.0.0.1:${toString args.port}";
+            })
+            # Serve filesystem content
+            (lib.optionalAttrs (args.root != null) { inherit (args) root; })
+            # VHost specific configuration
+            args.extraConfig
+            # SSO configuration
+            (lib.optionalAttrs args.sso.enable {
+              extraConfig = (args.extraConfig.extraConfig or "") + ''
+                error_page 401 = @error401;
+              '';
+              locations."@error401".return = ''
+                302 https://${cfg.sso.subdomain}.fcuny.net/login?go=$scheme://$http_host$request_uri
+              '';
+              locations."/" = {
+                extraConfig = (args.extraConfig.locations."/".extraConfig or "")
+                  + ''
+                    # Use SSO
+                    auth_request /sso-auth;
+                    # Set username through header
+                    auth_request_set $username $upstream_http_x_username;
+                    proxy_set_header X-User $username;
+                    # Renew SSO cookie on request
+                    auth_request_set $cookie $upstream_http_set_cookie;
+                    add_header Set-Cookie $cookie;
+                  '';
+              };
+              locations."/sso-auth" = {
+                proxyPass = "http://localhost:${toString cfg.sso.port}/auth";
+                extraConfig = ''
+                  # Do not allow requests from outside
+                  internal;
+                  # Do not forward the request body
+                  proxy_pass_request_body off;
+                  proxy_set_header Content-Length "";
+                  # Set X-Application according to subdomain for matching
+                  proxy_set_header X-Application "${subdomain}";
+                  # Set origin URI for matching
+                  proxy_set_header X-Origin-URI $request_uri;
+                '';
+              };
+            })
+          ]));
+      in lib.my.genAttrs' cfg.virtualHosts mkVHost;
+      sso = {
+        enable = true;
+        configuration = {
+          listen = {
+            addr = "127.0.0.1";
+            inherit (cfg.sso) port;
+          };
+          audit_log = {
+            target = [ "fd://stdout" ];
+            events = [
+              "access_denied"
+              "login_success"
+              "login_failure"
+              "logout"
+              "validate"
+            ];
+            headers = [ "x-origin-uri" "x-application" ];
+          };
+          cookie = {
+            domain = ".fcuny.net";
+            secure = true;
+            authentication_key = { _secret = cfg.sso.authKeyFile; };
+          };
+          login = {
+            title = "fcuny.net's SSO";
+            default_method = "simple";
+            hide_mfa_field = false;
+            names = { simple = "Username / Password"; };
+          };
+          providers = {
+            simple = let applyUsers = lib.flip lib.mapAttrs cfg.sso.users;
+            in {
+              users = applyUsers (_: v: { _secret = v.passwordHashFile; });
+              inherit (cfg.sso) groups;
+            };
+          };
+          acl = {
+            rule_sets = [{
+              rules = [{
+                field = "x-application";
+                present = true;
+              }];
+              allow = [ "@root" ];
+            }];
+          };
+        };
+      };
+    };
+
+    my.services.nginx.virtualHosts = [{
+      subdomain = "login";
+      inherit (cfg.sso) port;
+    }];
+
+    networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+    # Nginx needs to be able to read the certificates
+    users.users.nginx.extraGroups = [ "acme" ];
+
+    security.acme = {
+      defaults.email = "franck@fcuny.net";
+      acceptTerms = true;
+    };
+
+    services.grafana.provision.dashboards = lib.mkIf cfg.monitoring.enable [{
+      name = "NGINX";
+      options.path = pkgs.nur.repos.alarsyo.grafanaDashboards.nginx;
+      disableDeletion = true;
+    }];
+
+    services.prometheus = lib.mkIf cfg.monitoring.enable {
+      exporters.nginx = {
+        enable = true;
+        listenAddress = "127.0.0.1";
+      };
+      scrapeConfigs = [{
+        job_name = "nginx";
+        static_configs = [{
+          targets = [
+            "127.0.0.1:${
+              toString config.services.prometheus.exporters.nginx.port
+            }"
+          ];
+          labels = { instance = config.networking.hostName; };
+        }];
+      }];
+    };
+  };
+}