aboutsummaryrefslogtreecommitdiff
path: root/modules/services/nginx
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2022-04-13 11:24:02 -0700
committerFranck Cuny <franck@fcuny.net>2022-04-13 11:24:02 -0700
commit1636706b98afceb1b073c25b52a904f267c61910 (patch)
tree30a8943fc2473c8e568c09bf3e098664023cb998 /modules/services/nginx
parentfish: only start sway when sway is installed (diff)
downloadinfra-1636706b98afceb1b073c25b52a904f267c61910.tar.gz
nginx: add nginx as a reverse proxy
This will ultimately replace traefik.
Diffstat (limited to 'modules/services/nginx')
-rw-r--r--modules/services/nginx/default.nix326
-rw-r--r--modules/services/nginx/sso/default.nix80
2 files changed, 406 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; };
+ }];
+ }];
+ };
+ };
+}
diff --git a/modules/services/nginx/sso/default.nix b/modules/services/nginx/sso/default.nix
new file mode 100644
index 0000000..27ed7d6
--- /dev/null
+++ b/modules/services/nginx/sso/default.nix
@@ -0,0 +1,80 @@
+# I must override the module to allow having runtime secrets
+{ config, lib, pkgs, utils, ... }:
+let
+ cfg = config.services.nginx.sso;
+ pkg = lib.getBin cfg.package;
+ confPath = "/var/lib/nginx-sso/config.json";
+in {
+ disabledModules = [ "services/security/nginx-sso.nix" ];
+ options.services.nginx.sso = with lib; {
+ enable = mkEnableOption "nginx-sso service";
+ package = mkOption {
+ type = types.package;
+ default = pkgs.nginx-sso;
+ defaultText = "pkgs.nginx-sso";
+ description = ''
+ The nginx-sso package that should be used.
+ '';
+ };
+ configuration = mkOption {
+ type = types.attrsOf types.unspecified;
+ default = { };
+ example = literalExample ''
+ {
+ listen = { addr = "127.0.0.1"; port = 8080; };
+ providers.token.tokens = {
+ myuser = "MyToken";
+ };
+ acl = {
+ rule_sets = [
+ {
+ rules = [ { field = "x-application"; equals = "MyApp"; } ];
+ allow = [ "myuser" ];
+ }
+ ];
+ };
+ }
+ '';
+ description = ''
+ nginx-sso configuration
+ (<link xlink:href="https://github.com/Luzifer/nginx-sso/wiki/Main-Configuration">documentation</link>)
+ as a Nix attribute set.
+ '';
+ };
+ };
+ config = lib.mkIf cfg.enable {
+ systemd.services.nginx-sso = {
+ description = "Nginx SSO Backend";
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ StateDirectory = "nginx-sso";
+ WorkingDirectory = "/var/lib/nginx-sso";
+ # The files to be merged might not have the correct permissions
+ ExecStartPre = "+${
+ pkgs.writeScript "merge-nginx-sso-config" ''
+ #!${pkgs.bash}/bin/bash
+ rm -f '${confPath}'
+ ${utils.genJqSecretsReplacementSnippet cfg.configuration confPath}
+ # Fix permissions
+ chown nginx-sso:nginx-sso ${confPath}
+ chmod 0600 ${confPath}
+ ''
+ }";
+ ExecStart = lib.mkForce ''
+ ${pkg}/bin/nginx-sso \
+ --config ${confPath} \
+ --frontend-dir ${pkg}/share/frontend
+ '';
+ Restart = "always";
+ User = "nginx-sso";
+ Group = "nginx-sso";
+ };
+ };
+ users.users.nginx-sso = {
+ isSystemUser = true;
+ group = "nginx-sso";
+ };
+ users.groups.nginx-sso = { };
+ };
+}