diff --git a/hosts/deertopia/configuration.nix b/hosts/deertopia/configuration.nix index 33f02ad..75bf261 100755 --- a/hosts/deertopia/configuration.nix +++ b/hosts/deertopia/configuration.nix @@ -92,12 +92,13 @@ }; deertopia = { - slskd.enable = true; - jellyfin.enable = true; - nginx.enable = true; - webdav.enable = true; + authelia.enable = true; bepasty.enable = true; + jellyfin.enable = true; lldap.enable = true; + nginx.enable = true; + slskd.enable = true; + webdav.enable = true; # A simple default webpage. This should probably live somewhere else. nginx.vhosts."www" = { diff --git a/modules/nixos/deertopia/authelia.nix b/modules/nixos/deertopia/authelia.nix new file mode 100644 index 0000000..1d83e3d --- /dev/null +++ b/modules/nixos/deertopia/authelia.nix @@ -0,0 +1,183 @@ +{ config, lib, pkgs, ... }: + +let cfg = config.sydnix.deertopia.authelia; +in { + options.sydnix.deertopia.authelia = { + enable = lib.mkEnableOption ''Deertopia's Authelia''; + httpPort = lib.mkOption { + description = '' + The port on which Authelia's web UI will be served. + ''; + type = lib.types.port; + default = 9091; + }; + stateDirectory = lib.mkOption { + description = '' + The directory under which Authelia's general state will be stored. + ''; + type = lib.types.path; + default = "/var/lib/authelia-deertopia"; + }; + bindUserName = lib.mkOption { + description = '' + The name of the LDAP user Authelia will bind as. + ''; + type = lib.types.str; + default = "authelia"; + }; + }; + + config = lib.mkIf cfg.enable { + sydnix.sops.secrets = + let e = { + mode = "0600"; + owner = config.services.authelia.instances."deertopia".user; + group = config.services.authelia.instances."deertopia".group; + }; + in { + authelia-jwt-secret = e; + authelia-session-secret = e; + authelia-storage-encryption-key = e; + authelia-authentication-backend-ldap-password = e; + }; + + # I don't think the Authelia NixOS module exposes or even creates any paths + # for the service's state. No big deal, we'll do it ourselves… + # + # It is obligatory that I mention tmpfiles.d(5) every time this setting is used. + systemd.tmpfiles.settings."10-authelia".${cfg.stateDirectory} = { + v.user = config.services.authelia.instances."deertopia".user; + v.group = config.services.authelia.instances."deertopia".group; + }; + + # See: + # - https://github.com/authelia/authelia/blob/v4.38.19/config.template.yml + # - https://matwick.ca/authelia-nginx-sso/ + # - https://www.gandalfk7.it/posts/20220713_01_sso-with-lldap-authelia-and-nginx/ + services.authelia.instances."deertopia" = { + enable = true; + # "Automatic" secrets are seemingly broken and offer little more than + # some assertions from the Nix module. + secrets.manual = true; + environmentVariables = { + AUTHELIA_JWT_SECRET_FILE = + "/run/secrets/authelia-jwt-secret"; + AUTHELIA_SESSION_SECRET_FILE = + "/run/secrets/authelia-session-secret"; + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = + "/run/secrets/authelia-storage-encryption-key"; + AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = + "/run/secrets/authelia-authentication-backend-ldap-password"; + }; + settings = { + default_2fa_method = "totp"; + theme = "auto"; + server = { + address = "tcp://:${builtins.toString cfg.httpPort}"; + asset_path = "${cfg.stateDirectory}/assets"; + # Necessary for Nginx integration. No, I do not understand what it + # does. + endpoints.authz.auth-request.implementation = "AuthRequest"; + }; + authentication_backend = + let base-dn = config.services.lldap.settings.ldap_base_dn; + ldap-port = + builtins.toString config.services.lldap.settings.ldap_port; + in { + password_reset.disable = false; + refresh_interval = "1 minutes"; + ldap = { + implementation = "custom"; + address = "ldap://127.0.0.1:${ldap-port}"; + timeout = "5s"; + start_tls = "false"; + base_dn = base-dn; + additional_users_dn = "ou=people"; + additional_groups_dn = "ou=groups"; + groups_filter = "(member={dn})"; + users_filter = "(&({username_attribute}={input})(objectClass=person))"; + attributes = { + username = "uid"; + group_name = "cn"; + mail = "mail"; + display_name = "displayName"; + }; + user = "uid=${cfg.bindUserName},ou=people,${base-dn}"; + }; + }; + access_control = { + default_policy = "deny"; + rules = [ + { + # TODO: Remove this. It's only used for a quick demo for myself. + # The domain choice is arbitrary. It's just one I happen to have + # set up. + domain = "ldap.deertopia.net"; + policy = "one_factor"; + } + ]; + }; + session = { + name = "authelia_session"; + same_site = "lax"; + inactivity = "5 minutes"; + expiration = "1 hour"; + remember_me = "1 month"; + cookies = [ + { + domain = "deertopia.net"; + authelia_url = "https://auth.deertopia.net"; + # TODO: Remove this. It's only used for a quick demo for myself. + # The domain choice is arbitrary. It's just one I happen to have + # set up. + default_redirection_url = "https://ldap.deertopia.net"; + } + ]; + }; + storage.local.path = "${cfg.stateDirectory}/db.sqlite"; + notifier = { + disable_startup_check = false; + filesystem.filename = "${cfg.stateDirectory}/notifications"; + }; + # Default is false, which prevents anything from showing up when you run + # `systemctl status authelia-deertopia`, which is really, really confusing. + log = { + keep_stdout = true; + file_path = "${cfg.stateDirectory}/authelia.log"; + }; + }; + }; + + sydnix.deertopia.nginx.vhosts."auth".vhost = { + forceSSL = true; + enableACME = true; + extraConfig = '' + set $upstream http://127.0.0.1:${builtins.toString cfg.httpPort}; + ''; + locations."/".extraConfig = '' + include ${./authelia/proxy.conf}; + proxy_pass $upstream; + ''; + locations."/api/verify".proxyPass = "$upstream"; + locations."/api/authz".proxyPass = "$upstream"; + }; + + # TODO: Remove this. It's only used for a quick demo for myself. The + # domain choice is arbitrary. It's just one I happen to have set up. + sydnix.deertopia.nginx.vhosts."ldap" = { + directory = null; + vhost = { + forceSSL = true; + enableACME = true; + extraConfig = '' + include ${./authelia/authelia-location.conf}; + ''; + locations."/".extraConfig = '' + include ${./authelia/authelia-authrequest.conf}; + include ${./authelia/proxy.conf}; + root /persist/deertopia.net/ldap; + ''; + }; + }; + }; +} diff --git a/modules/nixos/deertopia/authelia/authelia-authrequest.conf b/modules/nixos/deertopia/authelia/authelia-authrequest.conf new file mode 100644 index 0000000..864802a --- /dev/null +++ b/modules/nixos/deertopia/authelia/authelia-authrequest.conf @@ -0,0 +1,34 @@ +# Adapted from https://www.authelia.com/integration/proxies/nginx/#authelia-authrequest + +## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. +auth_request /internal/authelia/authz; + +## Save the upstream metadata 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 metadata 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-Email $email; +proxy_set_header Remote-Name $name; + +## Configure the redirection when the authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method' +## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url +## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily. + +## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint. +auth_request_set $redirection_url $upstream_http_location; + +## Modern Method: When there is a 401 response code from the authz endpoint redirect to the $redirection_url. +error_page 401 =302 $redirection_url; + +## Legacy Method: Set $target_url to the original requested URL. +## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module. +# set_escape_uri $target_url $scheme://$http_host$request_uri; + +## Legacy Method: When there is a 401 response code from the authz endpoint redirect to the portal with the 'rd' +## URL parameter set to $target_url. This requires users update 'auth.deertopia.net/' with their external authelia URL. +# error_page 401 =302 https://auth.deertopia.net/?rd=$target_url; \ No newline at end of file diff --git a/modules/nixos/deertopia/authelia/authelia-location.conf b/modules/nixos/deertopia/authelia/authelia-location.conf new file mode 100644 index 0000000..3d6871d --- /dev/null +++ b/modules/nixos/deertopia/authelia/authelia-location.conf @@ -0,0 +1,34 @@ +# Adapted from https://www.authelia.com/integration/proxies/nginx/#authelia-locationconf + +set $upstream_authelia http://127.0.0.1:9091/api/authz/auth-request; + +## Virtual endpoint created by nginx to forward auth requests. +location /internal/authelia/authz { + ## Essential Proxy Configuration + internal; + proxy_pass $upstream_authelia; + + ## Headers + ## The headers starting with X-* are required. + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-URL $scheme://$http_host$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; +} \ No newline at end of file diff --git a/modules/nixos/deertopia/authelia/proxy.conf b/modules/nixos/deertopia/authelia/proxy.conf new file mode 100644 index 0000000..fb34e38 --- /dev/null +++ b/modules/nixos/deertopia/authelia/proxy.conf @@ -0,0 +1,36 @@ +# Adapted from https://www.authelia.com/integration/proxies/nginx/#proxyconf + +## 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; + +## 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; \ No newline at end of file diff --git a/modules/nixos/deertopia/lldap.nix b/modules/nixos/deertopia/lldap.nix index 86746ce..de2b4d0 100644 --- a/modules/nixos/deertopia/lldap.nix +++ b/modules/nixos/deertopia/lldap.nix @@ -20,7 +20,7 @@ in { sydnix.sops.secrets = let e = { - mode = "0600"; + mode = "0440"; owner = "lldap"; group = "lldap"; }; @@ -63,39 +63,5 @@ in { enable = true; keysZoneName = "auth_cache"; }; - sydnix.deertopia.nginx.vhosts."ldap".vhost = - let consultant = "http://localhost:9090"; - port = builtins.toString config.services.lldap.settings.http_port; - base-dn = config.services.lldap.settings.ldap_base_dn; - nginx-bind-user = "nginx-bind-user"; - in { - forceSSL = true; - enableACME = true; - locations."/".extraConfig = '' - auth_request /auth-proxy; - error_page 401 =200 /login; - proxy_pass ${consultant}; - ''; - locations."/login".extraConfig = '' - proxy_pass ${consultant}/login; - proxy_set_header X-Target $request_uri; - ''; - locations."= /auth-proxy".extraConfig = '' - internal; - proxy_pass ${consultant}; - proxy_pass_request_body off; - proxy_pass_request_headers off; - proxy_set_header Content-Length ""; - proxy_cache auth_cache; - proxy_cache_valid 200 10m; - proxy_cache_key "$http_authorization$cookie_nginxauth"; - # proxy_set_header X-Ldap-URL "ldap://localhost:${port}"; - # proxy_set_header X-Ldap-BaseDN "cn=people,${base-dn}"; - # proxy_set_header X-Ldap-BindDN "cn=${nginx-bind-user},${base-dn}"; - # proxy_set_header X-Ldap-BindPass "secret123"; - # proxy_set_header X-CookieName "nginxauth"; - proxy_set_header Cookie nginxauth=$cookie_nginxauth; - ''; - }; }; } diff --git a/modules/nixos/deertopia/nginx.nix b/modules/nixos/deertopia/nginx.nix index 95042fc..5de5e6e 100644 --- a/modules/nixos/deertopia/nginx.nix +++ b/modules/nixos/deertopia/nginx.nix @@ -38,7 +38,7 @@ in }; directory = lib.mkOption { description = "Host's root directory."; - type = lib.types.str; + type = lib.types.nullOr lib.types.path; default = "${cfg.root}/${name}"; }; user = lib.mkOption { diff --git a/secrets.yaml b/secrets.yaml index a74ee3c..05ad0dd 100755 --- a/secrets.yaml +++ b/secrets.yaml @@ -1,3 +1,9 @@ +#ENC[AES256_GCM,data:Z29FZBHP9C1KIPMES4Cjn6TxU4RaTHwZA1ZfQtmRpXF3SveUbfTptxHQaTDMAEqsWWK9c/urwp9CTxdNs0Qf5tXE,iv:7bBMRZeAuQoLAskGvKjH0LbWOE6EyvRcI9P6fV2K4uA=,tag:JghSgIo0vUx9E+7Cmbp6LQ==,type:comment] +#ENC[AES256_GCM,data:dghwASV7JnuiR48VQoIvWFJNzb5TrqhC,iv:TZULQpLvzrccByWL/M49jrSr8p8Maez8hV5zdXx0SAY=,tag:IlCO9Ynh/8qnGP5CLGLK8w==,type:comment] +#ENC[AES256_GCM,data:HtLXsR082arYWbJ+SVPNyK30/Zejxi/AvwzDHQAGH0VaEGO8cjy7Fhix13A=,iv:ZFO+7ct9sW8HlBZZcVPeeDXuB5dUzkmdy4ae9y87wW0=,tag:lUI+v48LqdYmLQU6SSnRcw==,type:comment] +#ENC[AES256_GCM,data:wccTbdNyIjQq5Aw80VB+V8VZTaGTeU6/twK8d6XIAhYz+i7o1uy9xOCgH9H8XPh775acT4Q=,iv:5WJwt8dM28qNQ0fqMawVd6vFHGZGHjpBKul2xlh4lEY=,tag:+61HFoSfbwASUf6CK89s6g==,type:comment] +#ENC[AES256_GCM,data:slsUWPXnYmCSvoL07viGGxBP4CoWEAeN8VeH7MLzZ5XvutlDUlcusjy4ImemK6V6Teu8P1/AMTImVGwVePFZ68RfFQ==,iv:QVVbRw42MKLpin66hhmDI1GHq3q6QipeUAX2cErwhkc=,tag:WwnPfbuJdsNQ3ghC1oWW0A==,type:comment] +#ENC[AES256_GCM,data:9qeeSaKMamcYGb4VVWDlBr5JuBLsAWS/u4VGaQ2b6ZWmNYpizjnb9E8YG3qn8FmMKz0=,iv:of9Zk/pXCoQDvy4GfbGysUSALKHwdULAZxjH7yaJfgM=,tag:P3pyMxgE7GTShgy34XEROQ==,type:comment] nextcloud-admin: ENC[AES256_GCM,data:K1KogDUmyAEm9JMlZ2SmYgekgA==,iv:GOlNIrxDCTSA1z763a5S0t3GDJZwMayGT1qzhPfzOus=,tag:zeuY48G9BED3o8O2mYU4QQ==,type:str] bepasty-secret-key: ENC[AES256_GCM,data:N+5keSslQDj1v0t4yOZw+fcxoIPslsb2J1jI8jYX0+nR9exoUdjJJJPsVVjm83+ej6wguzMJgy1/eIufFmau8Q==,iv:Ye3HB2WsF+zrqK8aYDn3BsVKdhl9hNbL1HSZBCEYlSY=,tag:u4jD83/ExzS867QR9k7ObA==,type:str] slskd-credentials: ENC[AES256_GCM,data:n3KmG6igp7+BmnHafW0dXp7uKEV/JY54VR/IQf7pV1k/60zDuFIrR3Bp0YL9sBTfT10qJNCd9GJXvdg46bLBoHc6Rr/Y+vHjLIIQZEX3wDfa1C9JCXmAh2Igyx3GCCOvntjYhVy2A/2JxIcvpSPC1Hua74oCbYXZ0hI3aeyu00LTVlpNZUz1JrVj2xgMFMltRL7Xt8hfSBFmG3W8j3kXBJIDxSIOxeYt8K/itBnzBtBOuWw9DqXVHy9F8Pmq2muUhiXGkEr6GWUJhg==,iv:w8p4yygdJv31VcICWFzyJoN47j5ax6N6PdPNM2JVcr8=,tag:lcWtdoiGPBw1uMkyzA3RMw==,type:str] @@ -5,6 +11,10 @@ bepasty-secret-config: ENC[AES256_GCM,data:QPJBVW272ixtybIk3xmEa1R3qZ09WGpx4kyf2 lldap-ldap-user-pass: ENC[AES256_GCM,data:aBpn+kOiMUT9pQ==,iv:VPNwzXjVc3pdhQprtkzbZWsXhG5zbMPA/NGnaQF+iCo=,tag:exV9IE+tFxcLoR4NEPsOzQ==,type:str] lldap-jwt-secret: ENC[AES256_GCM,data:VgI57HMh3iyapgVrdOgZM5HUMUdi+MHpl1yWA/FAPnk=,iv:6Q5Ta+pUs8km64Kpsqt+gJXkqmo+Juplid9CPyzGXDA=,tag:+ZvQGhM5feRIn//g/kkcwg==,type:str] lldap-secret-env: ENC[AES256_GCM,data:0d2GaIULE7tQLtpz6hHkopl1eUC3zLzjX+XgOK6tPuHXf4kPookdalPOJuO0CXFF,iv:7yx7hD+dTJSlVKgoT7Zhz0syBrgxFrcSGKdMWqO0ScY=,tag:+0W7NZXTYJFxKh/Ej7STLw==,type:str] +authelia-jwt-secret: ENC[AES256_GCM,data:uKWCq7x0mSZJKXDDhMNNPFCglLchlbzCDd68GaohEVX5z3h3P28UmWXzg1iVRb//iDgt8yCpsz7jRUjssPS7zQ==,iv:2DuCz3FlHB33jCTscZWi5xVmzwhu62WNo+RLSvXA2Y4=,tag:GULqtCBZTjXSEOC3m3kZiQ==,type:str] +authelia-session-secret: ENC[AES256_GCM,data:4RXVjaR4O3Zy0MbS/yHV/YKTlJyrL0PmBhYQxYiadI3R/aoZaT7VwPyMVRgia031au6UojZFooETdWdzEVKRwA==,iv:rdUk5UsWI56myFu3necp+iIzMNMkzRZQcOGmjG3UD4I=,tag:pqFFuLb5TdPic/n+Ccf/cQ==,type:str] +authelia-storage-encryption-key: ENC[AES256_GCM,data:z/k/wXyLp53lZ50oaca/QIs55kF9iKT5ck/s6clFnhyLPkjFeTnVz9Met6klCrs/IkfPHOu50bS2o894D0Xa+A==,iv:Kd8xv6Rk1tTKYmp5/wFlj4HRqjVJQT5QzlpUQO9AF8o=,tag:nNzUumbV9Fgt+DveAmXY2w==,type:str] +authelia-authentication-backend-ldap-password: ENC[AES256_GCM,data:VWHW3rjjYCiEw2TuDCAXBhkTMVFsjjQmHByB6H8SwNuF5rAxsZTN99jF9+BE66S3GBtgMJ7loJ/RHkZ4ukC1lQ==,iv:8Iz/ydhN6cnVqlUt0zsp0N6OGuiDwgu858MsJsp7SNM=,tag:8O9lbI//3CR0D7ATGmfLsw==,type:str] sops: kms: [] gcp_kms: [] @@ -29,8 +39,8 @@ sops: TXFLY2l0UHJ3Z0NGZjVpbTQ2UC8yaTQKA7wTmW9Ha6T2KmCr/nkXdizgv8+V6SAp ZhDO+uDQ1evIh2wLWMOXNJ3d/zplLCOTzR2xkqBIUp5V7MXj45RUIA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-02-19T05:23:29Z" - mac: ENC[AES256_GCM,data:JQ3iHWwSYjxZMN/Ug/Wl7KQz95q3fIa8dyzhDHJHpmpGhcH8d/xW4Iee19oxGwamuDTxMzRC5om0Hoaz/Pibpj0zjHxkGIltr5z6O83zsvXjx7FndoLXzU2ZiQXMfutYiC6tvQFTmK7NZusLjBkDZ4Zg8xQIx/ReSU7HAQgnMYM=,iv:+Z7qqKjI4nq5SzuQQ06BMaFWWCgMP0lSzzd11Mvj90k=,tag:h8HmZI2ofRYdrxd+RQIkcQ==,type:str] + lastmodified: "2025-02-21T22:42:25Z" + mac: ENC[AES256_GCM,data:a/xPkNMYY6rhiy3aPqQIVneSLvDkLVeZ0ugtiGKUrOn540CnSn2tCNACoqTfGRuOExpWqTjs6ihoE8R9eN8hIY3VKCRhXBFkO+sEZKwsF/YsXQcRprDKSQdRTjYBDa8OURlJlevLGLy1N+UY7l3IPW9cD5WhBW/nwqP++WnvQbc=,iv:PxsAguORboTxe+bL5OlVEQwTg+o+WBm7dY1IC08OcQY=,tag:JV9FwvwHFK7kRQHREnz5Vw==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.9.4