diff --git a/hosts/spore/secrets/jwt-secret.age b/hosts/spore/secrets/jwt-secret.age deleted file mode 100644 index 40234e89..00000000 Binary files a/hosts/spore/secrets/jwt-secret.age and /dev/null differ diff --git a/hosts/spore/secrets/oauth2-proxy-env.age b/hosts/spore/secrets/oauth2-proxy-env.age new file mode 100644 index 00000000..54e7dbbe Binary files /dev/null and b/hosts/spore/secrets/oauth2-proxy-env.age differ diff --git a/hosts/spore/secrets/pocket-id-encryption-key.age b/hosts/spore/secrets/pocket-id-encryption-key.age new file mode 100644 index 00000000..471b8cd7 --- /dev/null +++ b/hosts/spore/secrets/pocket-id-encryption-key.age @@ -0,0 +1,7 @@ +age-encryption.org/v1 +-> ssh-ed25519 stFZUQ aFqQX0Rpg18dsE3tTTcg3mxbyaskZwmeHmwRXTTAizc +/Gwz5TQ7ladAcB3ED82VVIWndbImy2g3tTjF6HeWPnU +-> ssh-ed25519 3EWhnQ vskXoOXSQeBFLf7AV7ojly2EVcElbuclX0fTvLk6Og8 +eyaqaKa7Bq5n/+0xVqsBwyx5Y5OeWOHDJEY7MIasHCU +--- EFeE0j6r+vRDo3hf7AcB7ZtmXmwRK7wigxu5ucLPyhA +L%_,RrK_#='fI ssh-ed25519 rSr+rA BWdzZruTjk0r53+3pf/1BYQOiyRl1iz6CvU4OYjS2j0 -2VbYsxaGN7Xq86CyOp/mWFyIJczq+iFvDZP4+vYBY/A --> ssh-ed25519 KYfd6A iUFqZ6D1bkZBwfxCnS1c0GF+Nb0vP9HPETog9FlsjWA -SQDCQih5zJjUFXciEYRd1k6G7Q/3zrMUoTcTJI7EJa0 --> ssh-ed25519 3EWhnQ xjTNgb7vaDgERaevBfBJx+SrDkb4YRibLl+Q718fE20 -rDKqVg5mNhIc3PoMDyaV9HqL6AHdcfEJb+68WIUSC/s --> ssh-ed25519 stFZUQ Q9YL41TlmX8EAMnGuRC2GlJtiE9E5VM3hJLu2tSiyiA -pd/5ooXcByM3M7KkooZCYwXV7YrAc8gBLC6XhgNcjtE --> ssh-ed25519 CiBwDg ukW5B8fW8my53/K2O+47vOJgL11yBqZkPcdf3qIjD1Q -lzKjiPrp0+pnmjklXez46ZU/1o4a8XWgmHTP/Y+tPgY ---- 6qsMeU9IG5whSVCeV36FoRUVsw0uzayARz6fd7hpS2Y -`O5G8vk֛qݱPFQkn ]\]|"(x++?N^z= gАT5 o4Ͷs%̡kv| #iDFGႫģqO?ʪh'@B3!u$d%[ ɘQ5Xu- \ No newline at end of file diff --git a/hosts/spore/secrets/storage-encryption-key.age b/hosts/spore/secrets/storage-encryption-key.age deleted file mode 100644 index d5307be3..00000000 Binary files a/hosts/spore/secrets/storage-encryption-key.age and /dev/null differ diff --git a/hosts/spore/services/authelia.nix b/hosts/spore/services/authelia.nix deleted file mode 100644 index 3c82c199..00000000 --- a/hosts/spore/services/authelia.nix +++ /dev/null @@ -1,145 +0,0 @@ -{ - config, - pkgs, - ... -}: { - age.secrets.jwt-secret = { - file = ./../secrets/jwt-secret.age; - mode = "440"; - owner = "authelia-main"; - group = "authelia-main"; - }; - age.secrets.session-secret = { - file = ./../secrets/session-secret.age; - mode = "440"; - owner = "authelia-main"; - group = "authelia-main"; - }; - age.secrets.storage-encryption-key = { - file = ./../secrets/storage-encryption-key.age; - mode = "440"; - owner = "authelia-main"; - group = "authelia-main"; - }; - age.secrets.notifier-smtp-password = { - file = ./../secrets/notifier-smtp-password.age; - mode = "440"; - owner = "authelia-main"; - group = "authelia-main"; - }; - services.authelia.instances.main = { - enable = true; - secrets.jwtSecretFile = config.age.secrets.jwt-secret.path; - secrets.sessionSecretFile = config.age.secrets.session-secret.path; - secrets.storageEncryptionKeyFile = config.age.secrets.storage-encryption-key.path; - environmentVariables = { - # N.B.: `secrets.notifierSmtpPasswordFile` is not yet defined - AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = config.age.secrets.notifier-smtp-password.path; - }; - settings = { - theme = "auto"; - default_redirection_url = "https://zx.dev"; - default_2fa_method = "webauthn"; - log.level = "debug"; - server.disable_healthcheck = true; - totp = { - disable = false; - issuer = "auth.zx.dev"; - algorithm = "sha1"; - digits = 6; - period = 30; - skew = 1; - secret_size = 32; - }; - webauthn = { - disable = false; - timeout = "60s"; - display_name = "Authelia"; - attestation_conveyance_preference = "indirect"; - user_verification = "preferred"; - }; - authentication_backend = { - password_reset = { - disable = false; - custom_url = ""; - }; - refresh_interval = "5m"; - file = { - path = pkgs.writeText "users.yaml" '' - users: - corey: - disabled: false - displayname: "Corey" - password: "$argon2id$v=19$m=65536,t=3,p=4$/jiiAPT7PHKhKxb3ncrLBQ$8cWd3PZKhBJvEz8KfGnC3FEYS1LTMSKRbzR1WsDbCIg" - email: corey@x64.co - groups: - - admins - ''; - watch = false; - password = { - algorithm = "argon2"; - argon2 = { - variant = "argon2id"; - iterations = 3; - memory = 65536; - parallelism = 4; - key_length = 32; - salt_length = 16; - }; - }; - }; - }; - password_policy = { - standard = { - enabled = false; - min_length = 8; - max_length = 0; - require_uppercase = true; - require_lowercase = true; - require_number = true; - require_special = true; - }; - zxcvbn = { - enabled = false; - min_score = 3; - }; - }; - access_control = { - default_policy = "deny"; - rules = [ - { - domain = "*.zx.dev"; - policy = "two_factor"; - } - ]; - }; - session = { - name = "authelia_session"; - domain = "zx.dev"; - same_site = "lax"; - expiration = "1h"; - inactivity = "5m"; - remember_me_duration = "1M"; - }; - regulation = { - max_retries = 3; - find_time = "2m"; - ban_time = "5m"; - }; - storage.local.path = "/var/lib/authelia-main/db.sqlite3"; - notifier = { - disable_startup_check = false; - smtp = { - host = "smtp.mailgun.org"; - port = 587; - timeout = "5s"; - username = "stackptr@sandbox8379461c3c124a3d997b7b39f997da8e.mailgun.org"; - sender = "Authelia "; - identifier = "auth.zx.dev"; - subject = "[Authelia] {title}"; - startup_check_address = "test@zx.dev"; - }; - }; - }; - }; -} diff --git a/hosts/spore/services/default.nix b/hosts/spore/services/default.nix index 27c46ef7..3ab0cb0b 100644 --- a/hosts/spore/services/default.nix +++ b/hosts/spore/services/default.nix @@ -4,7 +4,6 @@ ... }: { imports = [ - ./authelia.nix ./db.nix ./homepage-dashboard.nix ./mastodon.nix diff --git a/hosts/spore/services/web/auth.nix b/hosts/spore/services/web/auth.nix new file mode 100644 index 00000000..6860cd3b --- /dev/null +++ b/hosts/spore/services/web/auth.nix @@ -0,0 +1,34 @@ +{ + config, + pkgs, + ... +}: { + age.secrets.oauth2-proxy-env = { + file = ./../../secrets/oauth2-proxy-env.age; + mode = "440"; + owner = "oauth2-proxy"; + group = "oauth2-proxy"; + }; + age.secrets.pocket-id-encryption-key = { + file = ./../../secrets/pocket-id-encryption-key.age; + mode = "440"; + owner = config.services.pocket-id.user; + group = config.services.pocket-id.group; + }; + + rc.web.auth = { + enable = true; + issuer = { + host = "id.zx.dev"; + useACMEHost = "zx.dev"; + encryptionKeyFile = config.age.secrets.pocket-id-encryption-key.path; + }; + authProxy = { + host = "oauth.zx.dev"; + domain = ".zx.dev"; + clientID = "shared-sso"; + useACMEHost = "zx.dev"; + keyFile = config.age.secrets.oauth2-proxy-env.path; + }; + }; +} diff --git a/hosts/spore/services/web/default.nix b/hosts/spore/services/web/default.nix index 26523608..bfa77c80 100644 --- a/hosts/spore/services/web/default.nix +++ b/hosts/spore/services/web/default.nix @@ -1,7 +1,7 @@ # Web services and nginx configuration { imports = [ - ./nginx-options.nix + ./auth.nix ./ssl-acme.nix ./nginx-config.nix ./srv.nix diff --git a/hosts/spore/services/web/nginx-options.nix b/hosts/spore/services/web/nginx-options.nix deleted file mode 100644 index a098ed92..00000000 --- a/hosts/spore/services/web/nginx-options.nix +++ /dev/null @@ -1,124 +0,0 @@ -# Extended nginx virtual host options for Authelia integration -{ - config, - pkgs, - lib, - ... -}: let - vhostOptionsAuth = {config, ...}: { - options = { - enableAutheliaAuth = lib.mkEnableOption "Enable authelia auth"; - }; - config = lib.mkIf config.enableAutheliaAuth { - locations."/authelia".extraConfig = '' - set $upstream_authelia http://127.0.0.1:9091/api/verify; - - ## Essential Proxy Configuration - internal; - proxy_pass $upstream_authelia; - - ## Headers - ## The headers starting with X-* are required. - proxy_set_header X-Original-URL $scheme://$http_host$request_uri; - proxy_set_header X-Original-Method $request_method; - proxy_set_header X-Forwarded-Method $request_method; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header X-Forwarded-Uri $request_uri; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header Content-Length ""; - proxy_set_header Connection ""; - - ## Basic Proxy Configuration - proxy_pass_request_body off; - proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead - proxy_redirect http:// $scheme://; - proxy_http_version 1.1; - proxy_cache_bypass $cookie_session; - proxy_no_cache $cookie_session; - proxy_buffers 4 32k; - client_body_buffer_size 128k; - - ## Advanced Proxy Configuration - send_timeout 5m; - proxy_read_timeout 240; - proxy_send_timeout 240; - proxy_connect_timeout 240; - ''; - locations."/".extraConfig = '' - ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. - auth_request /authelia; - - ## Set the $target_url variable based on the original request. - - ## Requires nginx http_set_misc module. - set_escape_uri $target_url $scheme://$http_host$request_uri; - - ## Save the upstream response headers from Authelia to variables. - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - - ## Inject the response headers from the variables into the request made to the backend. - proxy_set_header Remote-User $user; - proxy_set_header Remote-Groups $groups; - proxy_set_header Remote-Name $name; - proxy_set_header Remote-Email $email; - - ## If the subreqest returns 200 pass to the backend, if the subrequest returns 401 redirect to the portal. - error_page 401 =302 https://auth.zx.dev/?rd=$target_url; - ''; - }; - }; - - vhostOptionsProxy = {config, ...}: { - options = { - useAutheliaProxyConf = lib.mkEnableOption "Use recommended authelia proxy configuration"; - }; - config = lib.mkIf config.useAutheliaProxyConf { - locations."/".extraConfig = '' - ## Headers - proxy_set_header Host $host; - proxy_set_header X-Original-URL $scheme://$http_host$request_uri; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header X-Forwarded-Uri $request_uri; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Connection ""; - - ## Basic Proxy Configuration - client_body_buffer_size 128k; - proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead. - proxy_redirect http:// $scheme://; - proxy_http_version 1.1; - proxy_cache_bypass $cookie_session; - proxy_no_cache $cookie_session; - proxy_buffers 64 256k; - - ## Trusted Proxies Configuration - ## Please read the following documentation before configuring this: - ## https://www.authelia.com/integration/proxies/nginx/#trusted-proxies - # set_real_ip_from 10.0.0.0/8; - # set_real_ip_from 172.16.0.0/12; - # set_real_ip_from 192.168.0.0/16; - # set_real_ip_from fc00::/7; - real_ip_header X-Forwarded-For; - real_ip_recursive on; - - ## Advanced Proxy Configuration - send_timeout 5m; - proxy_read_timeout 360; - proxy_send_timeout 360; - proxy_connect_timeout 360; - ''; - }; - }; -in { - # Extend nginx virtual host options for Authelia integration - options.services.nginx.virtualHosts = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule [vhostOptionsAuth vhostOptionsProxy]); - }; -} diff --git a/hosts/spore/services/web/virtual-hosts.nix b/hosts/spore/services/web/virtual-hosts.nix index 0ebcab1f..27b959c1 100644 --- a/hosts/spore/services/web/virtual-hosts.nix +++ b/hosts/spore/services/web/virtual-hosts.nix @@ -33,17 +33,10 @@ "/pgp".return = "302 https://keyoxide.org/hkp/413d1a0152bcb08d2e3ddacaf88c08579051ab48"; }; }; - "auth.zx.dev" = { - forceSSL = true; - useACMEHost = "zx.dev"; - useAutheliaProxyConf = true; - locations."/".proxyPass = "http://127.0.0.1:9091"; - locations."/api/verify".proxyPass = "http://127.0.0.1:9091"; - }; "torrents.zx.dev" = { forceSSL = true; useACMEHost = "zx.dev"; - enableAutheliaAuth = true; + requireAuth = true; locations."/".proxyPass = "http://glyph.rove-duck.ts.net:9091"; locations."~ (/transmission)?/rpc".proxyPass = "http://glyph.rove-duck.ts.net:9091"; }; @@ -60,13 +53,13 @@ "files.zx.dev" = { forceSSL = true; useACMEHost = "zx.dev"; - enableAutheliaAuth = true; + requireAuth = true; locations."/".proxyPass = "http://glyph.rove-duck.ts.net:8080"; }; "home.zx.dev" = { forceSSL = true; useACMEHost = "zx.dev"; - enableAutheliaAuth = true; + requireAuth = true; locations."/".proxyPass = "http://127.0.0.1:8082"; }; }; diff --git a/justfile b/justfile index e6b43c39..e211a17d 100644 --- a/justfile +++ b/justfile @@ -55,7 +55,7 @@ build-host host: # Check specific service configurations check-services: @echo "🔍 Checking individual service configurations..." - nix flake check --print-build-logs | grep -E "(spore|glyph)-(nginx|authelia|postgresql|samba|transmission)" || echo "Service checks completed" + nix flake check --print-build-logs | grep -E "(spore|glyph)-(nginx|postgresql|samba|transmission)" || echo "Service checks completed" # List all available hosts list-hosts: diff --git a/lib/secrets/spore.nix b/lib/secrets/spore.nix index 15083a31..024b5716 100644 --- a/lib/secrets/spore.nix +++ b/lib/secrets/spore.nix @@ -3,15 +3,14 @@ let in { "hosts/spore/secrets/cloudflare-dns.age".publicKeys = keys; "hosts/spore/secrets/homepage-env.age".publicKeys = keys; - "hosts/spore/secrets/jwt-secret.age".publicKeys = keys; "hosts/spore/secrets/mastodon-s3-env.age".publicKeys = keys; "hosts/spore/secrets/mastodon-secret-key-base.age".publicKeys = keys; "hosts/spore/secrets/mastodon-vapid-public-key.age".publicKeys = keys; "hosts/spore/secrets/mastodon-vapid-private-key.age".publicKeys = keys; "hosts/spore/secrets/notifier-smtp-password.age".publicKeys = keys; + "hosts/spore/secrets/oauth2-proxy-env.age".publicKeys = keys; + "hosts/spore/secrets/pocket-id-encryption-key.age".publicKeys = keys; "hosts/spore/secrets/restic-env.age".publicKeys = keys; "hosts/spore/secrets/restic-password.age".publicKeys = keys; - "hosts/spore/secrets/session-secret.age".publicKeys = keys; - "hosts/spore/secrets/storage-encryption-key.age".publicKeys = keys; "hosts/spore/secrets/tailscale-auth-key.age".publicKeys = keys; } diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index 2db159c7..79beb510 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -1,6 +1,7 @@ # NixOS-specific configuration modules { imports = [ + ./web ./users.nix ./ssh.nix ./sudo.nix diff --git a/modules/nixos/web/auth.nix b/modules/nixos/web/auth.nix new file mode 100644 index 00000000..aeafc043 --- /dev/null +++ b/modules/nixos/web/auth.nix @@ -0,0 +1,218 @@ +{ + config, + lib, + ... +}: let + inherit (lib) mkIf mkOption; + + cfg = config.rc.web.auth; + + # TODO: Source from web-servers/nginx/vhost-options.nix + useACMEHost = mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + A host of an existing Let's Encrypt certificate to use. + ''; + }; +in { + options = { + rc.web.auth = { + enable = lib.mkEnableOption "Web authentication stack"; + + issuer = { + inherit useACMEHost; + + host = lib.mkOption { + type = lib.types.str; + description = '' + The OAuth issuer host. + ''; + example = "id.example.org"; + }; + + encryptionKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Key used to encrypt OAuth issuer data, including the private keys. + ''; + example = "/run/keys/oauth-issuer-encryption-key"; + }; + }; + + authProxy = { + inherit useACMEHost; + + host = lib.mkOption { + type = lib.types.str; + description = '' + The host where OAuth proxy is accessed for shared SSO. + ''; + example = "auth.example.org"; + }; + + domain = lib.mkOption { + type = lib.types.str; + description = '' + The domain in which proxy redirection occurs and cookies are scoped. + ''; + example = ".example.org"; + }; + + clientID = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + The client ID for the OAuth proxy. + ''; + example = "123456.apps.googleusercontent.com"; + }; + + keyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + oauth2-proxy allows passing sensitive configuration via environment variables. + Make a file that contains lines like + OAUTH2_PROXY_CLIENT_SECRET=asdfasdfasdf.apps.googleuserscontent.com + and specify the path here. + ''; + example = "/run/keys/oauth2-proxy"; + }; + }; + }; + + services.nginx.virtualHosts = let + requireAuthOption = {config, ...}: { + options = { + requireAuth = lib.mkEnableOption "Require authentication to access host."; + }; + config = lib.mkIf config.requireAuth { + locations."= /oauth2/auth" = { + proxyPass = "http://127.0.0.1:4180"; + extraConfig = '' + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header Content-Length ""; + proxy_pass_request_body off; + ''; + }; + locations."@oauth2_redirect" = { + extraConfig = '' + return 302 https://${cfg.authProxy.host}/oauth2/start?rd=$scheme://$http_host$request_uri; + ''; + }; + locations."/".extraConfig = '' + auth_request /oauth2/auth; + error_page 401 = @oauth2_redirect; + ''; + }; + }; + in + lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule [requireAuthOption]); + }; + }; + + config = lib.mkMerge [ + (mkIf cfg.enable { + services.pocket-id = { + enable = true; + settings = { + APP_URL = "https://${cfg.issuer.host}"; + TRUST_PROXY = true; + DB_PROVIDER = "postgres"; + DB_CONNECTION_STRING = "host=/run/postgresql user=pocketid dbname=pocketid"; + KEYS_STORAGE = "database"; + ENCRYPTION_KEY_FILE = cfg.issuer.encryptionKeyFile; + }; + }; + + services.postgresql = { + ensureDatabases = ["pocketid"]; + ensureUsers = [ + { + name = "pocketid"; + ensureDBOwnership = true; + } + ]; + }; + + systemd.services.pocket-id = { + wants = ["network-online.target"]; + after = ["postgresql.service" "network-online.target"]; + requires = ["postgresql.service"]; + }; + + services.oauth2-proxy = { + enable = true; + provider = "oidc"; + oidcIssuerUrl = "https://${cfg.issuer.host}"; + keyFile = cfg.authProxy.keyFile; + reverseProxy = true; + setXauthrequest = true; + clientID = cfg.authProxy.clientID; + redirectURL = "https://${cfg.authProxy.host}/oauth2/callback"; + nginx.domain = cfg.authProxy.host; + cookie.domain = cfg.authProxy.domain; + email.domains = ["*"]; + extraConfig = { + whitelist-domain = cfg.authProxy.domain; + insecure-oidc-allow-unverified-email = true; + }; + }; + + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + virtualHosts.${cfg.issuer.host} = { + forceSSL = true; + useACMEHost = cfg.issuer.useACMEHost; + locations."/" = { + proxyPass = "http://127.0.0.1:1411"; + proxyWebsockets = true; + }; + }; + + virtualHosts.${cfg.authProxy.host} = { + forceSSL = true; + useACMEHost = cfg.authProxy.useACMEHost; + locations."/oauth2/" = { + proxyPass = "http://127.0.0.1:4180"; + extraConfig = '' + proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri; + ''; + }; + locations."/oauth2/auth" = { + proxyPass = "http://127.0.0.1:4180"; + extraConfig = '' + proxy_set_header Content-Length ""; + proxy_pass_request_body off; + ''; + }; + locations."/" = { + extraConfig = '' + default_type text/html; + add_header Content-Type "text/html; charset=utf-8"; + return 200 'Auth

oauth2-proxy at ${cfg.authProxy.host}

'; + ''; + }; + }; + }; + }) + (let + vhosts = config.services.nginx.virtualHosts; + vhostsRequiringAuth = mapNames (lib.filter (set: set.value.requireAuth == true) (lib.attrsToList vhosts)); + mapNames = e: toString (lib.map (set: set.name) e); + in + mkIf (!cfg.enable && vhostsRequiringAuth != "") { + warnings = [ + "The following nginx hosts have requireAuth, but config.rc.web.auth is not enabled: ${vhostsRequiringAuth}" + ]; + }) + ]; +} diff --git a/modules/nixos/web/default.nix b/modules/nixos/web/default.nix new file mode 100644 index 00000000..12f5c325 --- /dev/null +++ b/modules/nixos/web/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./auth.nix + ]; +}