diff --git a/nixos/boxes/colony/vms/shill/containers/middleman/default.nix b/nixos/boxes/colony/vms/shill/containers/middleman/default.nix index aa27779..f837b55 100644 --- a/nixos/boxes/colony/vms/shill/containers/middleman/default.nix +++ b/nixos/boxes/colony/vms/shill/containers/middleman/default.nix @@ -45,12 +45,60 @@ owner = "acme"; group = "acme"; }; + "nginx-sso.yaml" = { + owner = "nginx-sso"; + group = "nginx-sso"; + }; }; }; firewall = { tcp.allowed = [ "http" "https" 8448 ]; }; + + nginx-sso = { + enable = true; + extraConfigFile = config.age.secrets."nginx-sso.yaml".path; + configuration = { + listen = { + addr = "[::]"; + port = 8082; + }; + login = { + title = "${lib.my.pubDomain} login"; + default_redirect = "https://${lib.my.pubDomain}"; + default_method = "google_oauth"; + names = { + google_oauth = "Google account"; + }; + }; + cookie = { + domain = ".${lib.my.pubDomain}"; + secure = true; + }; + audit_log = { + targets = [ "fd://stdout" ]; + events = [ + "access_denied" + "login_success" + "login_failure" + "logout" + #"validate" + ]; + }; + providers = { + google_oauth = { + client_id = "545475967061-cag4g1qf0pk33g3pdbom4v69562vboc8.apps.googleusercontent.com"; + redirect_url = "https://sso.${lib.my.pubDomain}/login"; + user_id_method = "user-id"; + }; + }; + }; + includes = { + endpoint = "http://localhost:8082"; + baseURL = "https://sso.${lib.my.pubDomain}"; + }; + }; }; users = { @@ -167,7 +215,9 @@ proxy_http_version 1.1; # proxy headers + proxy_set_header X-Origin-URI $request_uri; proxy_set_header Host $host; + proxy_set_header X-Host $http_host; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/nixos/boxes/colony/vms/shill/containers/middleman/vhosts.nix b/nixos/boxes/colony/vms/shill/containers/middleman/vhosts.nix index b136644..f3cf3d2 100644 --- a/nixos/boxes/colony/vms/shill/containers/middleman/vhosts.nix +++ b/nixos/boxes/colony/vms/shill/containers/middleman/vhosts.nix @@ -6,6 +6,17 @@ let dualStackListen' = l: map (addr: l // { inherit addr; }) [ "0.0.0.0" "[::]" ]; dualStackListen = ll: flatten (map dualStackListen' ll); + ssoServer = i: { + extraConfig = '' + include /etc/nginx/includes/sso/server-${i}.conf; + ''; + }; + ssoLoc = i: { + extraConfig = '' + include /etc/nginx/includes/sso/location-${i}.conf; + ''; + }; + mkWellKnown = type: content: pkgs.writeTextFile { name = "well-known-${type}"; destination = "/${type}"; @@ -34,6 +45,12 @@ let }; in { + my = { + nginx-sso.includes.instances = { + generic = {}; + }; + }; + services.nginx.virtualHosts = let hosts = { @@ -47,7 +64,12 @@ in ]; }; - "pass.nul.ie" = + "sso.${lib.my.pubDomain}" = { + locations."/".proxyPass = config.my.nginx-sso.includes.endpoint; + useACMEHost = lib.my.pubDomain; + }; + + "pass.${lib.my.pubDomain}" = let upstream = "http://vaultwarden-ctr.${config.networking.domain}"; in @@ -79,14 +101,14 @@ in locations = mkMerge [ { "/".proxyPass = "http://chatterbox-ctr.${config.networking.domain}:8008"; - "= /".return = "301 https://element.nul.ie"; + "= /".return = "301 https://element.${lib.my.pubDomain}"; } wellKnown ]; useACMEHost = lib.my.pubDomain; }; - "element.nul.ie" = + "element.${lib.my.pubDomain}" = let headers = '' add_header X-Frame-Options SAMEORIGIN; diff --git a/nixos/modules/_list.nix b/nixos/modules/_list.nix index e65797a..083b836 100644 --- a/nixos/modules/_list.nix +++ b/nixos/modules/_list.nix @@ -13,5 +13,6 @@ vms = ./vms.nix; network = ./network.nix; pdns = ./pdns.nix; + nginx-sso = ./nginx-sso.nix; }; } diff --git a/nixos/modules/network.nix b/nixos/modules/network.nix index c744417..caf969f 100644 --- a/nixos/modules/network.nix +++ b/nixos/modules/network.nix @@ -35,7 +35,7 @@ in config = mkMerge [ { networking = { - domain = mkDefault "int.nul.ie"; + domain = mkDefault "int.${lib.my.pubDomain}"; useDHCP = false; enableIPv6 = mkDefault true; useNetworkd = mkDefault true; diff --git a/nixos/modules/nginx-sso.nix b/nixos/modules/nginx-sso.nix new file mode 100644 index 0000000..4b4e18b --- /dev/null +++ b/nixos/modules/nginx-sso.nix @@ -0,0 +1,127 @@ +{ lib, pkgs, config, ... }: +let + inherit (lib) mkIf mkMerge getBin mapAttrsToList; + inherit (lib.my) mkOpt' mkBoolOpt'; + + includeOpts = { ... }: { + options = with lib.types; { + auth = { + path = mkOpt' str "/sso-auth" "HTTP path for SSO auth."; + redirect = mkOpt' str "$scheme://$http_host$request_uri" "URL to redirect to upon successful login."; + }; + logout = { + path = mkOpt' str "/sso-logout" "HTTP path for SSO logout."; + redirect = mkOpt' str "$scheme://$http_host/" "URL to redirect to upon successful logout."; + }; + }; + }; + + cfg = config.my.nginx-sso; + + pkg = getBin cfg.package; + baseConfig = pkgs.writeText "nginx-sso.yaml" (builtins.toJSON cfg.configuration); + runCfg = "/run/nginx-sso/config.yaml"; +in +{ + options.my.nginx-sso = with lib.types; { + enable = mkBoolOpt' true "Whether to enable custom nginx-sso."; + package = mkOpt' package pkgs.nginx-sso "nginx-sso package to use."; + configuration = mkOpt' (attrsOf unspecified) { } "nginx-sso configuration."; + extraConfigFile = mkOpt' (nullOr str) null "Path to configuration (e.g. for secrets)."; + + includes = { + endpoint = mkOpt' str "http://localhost:8082" "Upstream for proxied auth requests."; + baseURL = mkOpt' str null "Base URL for redirects."; + + instances = mkOpt' (attrsOf (submodule includeOpts)) { } "nginx includes instances."; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !config.services.nginx.sso.enable; + message = "Stock nginx-sso cannot be used with this module."; + } + ]; + + environment.etc = with cfg.includes; mkMerge (mapAttrsToList (n: i: { + "nginx/includes/sso/server-${n}.conf".text = '' + location ${i.auth.path} { + # Do not allow requests from outside + internal; + + # Access /auth endpoint to query login state + proxy_pass ${endpoint}/auth; + + # Do not forward the request body (nginx-sso does not care about it) + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + + # Set custom information for ACL matching: Each one is available as + # a field for matching: X-Host = x-host, ... + proxy_set_header X-Origin-URI $request_uri; + proxy_set_header X-Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Define where to send the user to login and specify how to get back + location @error401 { + return 302 ${baseURL}/login?go=${i.auth.redirect}; + } + + # If the user is lead to /logout redirect them to the logout endpoint + # of ngninx-sso which then will redirect the user to / on the current host + location ${i.logout.path} { + return 302 ${baseURL}/logout?go=${i.logout.redirect}; + } + ''; + "nginx/includes/sso/location-${n}.conf".text = '' + # Protect this location using the auth_request + auth_request ${i.auth.path}; + + # Redirect the user to the login page when they are not logged in + error_page 401 = @error401; + + # Automatically renew SSO cookie on request + auth_request_set $cookie $upstream_http_set_cookie; + add_header Set-Cookie $cookie; + ''; + }) instances); + + users = { + groups.nginx-sso = {}; + users.nginx-sso = { + group = "nginx-sso"; + isSystemUser = true; + }; + }; + + systemd.services.nginx-sso = { + description = "Nginx SSO Backend"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + preStart = '' + umask 066 + ${if (cfg.extraConfigFile != null) then '' + ${pkgs.yq-go}/bin/yq -P '. *= load("${cfg.extraConfigFile}")' "${baseConfig}" > ${runCfg} + '' else '' + ${pkgs.yq-go}/bin/yq -P "${baseConfig}" > ${runCfg} + ''} + ''; + serviceConfig = { + RuntimeDirectory = "nginx-sso"; + User = "nginx-sso"; + Group = "nginx-sso"; + ExecStart = [ + # Specify twice to clear original value + "" + ''${pkg}/bin/nginx-sso --frontend-dir ${pkg}/share/frontend --config ${runCfg}'' + ]; + }; + }; + }; +} diff --git a/secrets/nginx-sso.yaml.age b/secrets/nginx-sso.yaml.age new file mode 100644 index 0000000..1af6255 Binary files /dev/null and b/secrets/nginx-sso.yaml.age differ