From f388e18ef0975232dc6e9462fd0d899a763acb61 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 1 Nov 2024 21:26:34 +0400 Subject: [PATCH 001/115] minimal kanidm setup Only Roundcube and Dovecot communicate with Kanidm. --- sp-modules/auth/config-paths-needed.json | 7 + sp-modules/auth/flake.lock | 27 +++ sp-modules/auth/flake.nix | 34 ++++ sp-modules/auth/module.nix | 172 ++++++++++++++++++ sp-modules/roundcube/config-paths-needed.json | 1 + sp-modules/roundcube/module.nix | 57 +++++- 6 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 sp-modules/auth/config-paths-needed.json create mode 100644 sp-modules/auth/flake.lock create mode 100644 sp-modules/auth/flake.nix create mode 100644 sp-modules/auth/module.nix diff --git a/sp-modules/auth/config-paths-needed.json b/sp-modules/auth/config-paths-needed.json new file mode 100644 index 0000000..f75f298 --- /dev/null +++ b/sp-modules/auth/config-paths-needed.json @@ -0,0 +1,7 @@ +[ + ["mailserver", "fqdn"], + ["security", "acme", "certs"], + ["selfprivacy", "domain"], + ["selfprivacy", "modules"], + ["services"] +] diff --git a/sp-modules/auth/flake.lock b/sp-modules/auth/flake.lock new file mode 100644 index 0000000..d9ce328 --- /dev/null +++ b/sp-modules/auth/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs-unstable": { + "locked": { + "lastModified": 1725194671, + "narHash": "sha256-tLGCFEFTB5TaOKkpfw3iYT9dnk4awTP/q4w+ROpMfuw=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b833ff01a0d694b910daca6e2ff4a3f26dee478c", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b833ff01a0d694b910daca6e2ff4a3f26dee478c", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs-unstable": "nixpkgs-unstable" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix new file mode 100644 index 0000000..a86ec75 --- /dev/null +++ b/sp-modules/auth/flake.nix @@ -0,0 +1,34 @@ +{ + description = "User authentication and authorization module"; + + # TODO remove when working Kanidm lands in nixpkgs and Hydra + inputs.nixpkgs-unstable.url = github:alexoundos/nixpkgs/b84444cbd57e934312f6a03d2d783ed0b7f94957; + + outputs = { self, nixpkgs-unstable }: { + overlays.default = _final: prev: { + inherit (nixpkgs-unstable.legacyPackages.${prev.system}) + kanidm kanidm-provision oauth2-proxy; + }; + + nixosModules.default = { ... }: { + disabledModules = [ + "services/security/kanidm.nix" + "services/security/oauth2-proxy.nix" + "services/security/oauth2-proxy-nginx.nix" + ]; + imports = [ + (nixpkgs-unstable.legacyPackages.x86_64-linux.path + + /nixos/modules/services/security/kanidm.nix) + (nixpkgs-unstable.legacyPackages.x86_64-linux.path + + /nixos/modules/services/security/oauth2-proxy.nix) + (nixpkgs-unstable.legacyPackages.x86_64-linux.path + + /nixos/modules/services/security/oauth2-proxy-nginx.nix) + ./module.nix + ]; + nixpkgs.overlays = [ self.overlays.default ]; + }; + + configPathsNeeded = + builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + }; +} diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix new file mode 100644 index 0000000..684915e --- /dev/null +++ b/sp-modules/auth/module.nix @@ -0,0 +1,172 @@ +{ config, lib, pkgs, ... }: +let + domain = config.selfprivacy.domain; + cfg = config.selfprivacy.modules.auth; + auth-fqdn = cfg.subdomain + "." + domain; + oauth2-introspection-url = client_id: client_secret: + "https://${client_id}:${client_secret}@${auth-fqdn}/oauth2/token/introspect"; + oauth2-discovery-url = client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; + + kanidm-bind-address = "127.0.0.1:3013"; + ldap_host = "127.0.0.1"; + ldap_port = 3636; + + dovecot-oauth2-conf-file = pkgs.writeTextFile { + name = "dovecot-oauth2.conf.ext"; + text = '' + introspection_mode = post + introspection_url = ${oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"} + client_id = roundcube + client_secret = VERYSTRONGSECRETFORROUNDCUBE # FIXME + username_attribute = username + # scope = email groups profile openid dovecotprofile + scope = email profile openid + tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt + active_attribute = active + active_value = true + openid_configuration_url = ${oauth2-discovery-url "roundcube"} + debug = yes # FIXME + ''; + }; + + provisionAdminPassword = "abcd1234"; + provisionIdmAdminPassword = "abcd1234"; # FIXME +in +{ + options.selfprivacy.modules.auth = { + enable = lib.mkOption { + default = true; + type = lib.types.bool; + }; + subdomain = lib.mkOption { + default = "auth"; + type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + }; + }; + + config = lib.mkIf cfg.enable { + # kanidm uses TLS in internal connection with nginx too + # FIXME revise this: maybe kanidm must not have access to a public TLS + users.groups."acmereceivers".members = [ "kanidm" ]; + + services.kanidm = { + enableServer = true; + + # kanidm with Rust code patches for OAuth and admin passwords provisioning + # package = pkgs.kanidm.withSecretProvisioning; + # FIXME + package = pkgs.kanidm.withSecretProvisioning.overrideAttrs (_: { + version = "git"; + src = pkgs.fetchFromGitHub { + owner = "AleXoundOS"; + repo = "kanidm"; + rev = "a1a55f2e53facbfa504c7d64c44c3b5d0eb796c2"; + hash = "sha256-ADh4Zwn6EMt4CiOrvgG0RbmNMeR5i0ilVTxF46t/wm8="; + }; + doCheck = false; + }); + + serverSettings = { + inherit domain; + # The origin for webauthn. This is the url to the server, with the port + # included if it is non-standard (any port except 443). This must match or + # be a descendent of the domain name you configure above. If these two + # items are not consistent, the server WILL refuse to start! + origin = "https://" + auth-fqdn; + + # TODO revise this: maybe kanidm must not have access to a public TLS + tls_chain = + "${config.security.acme.certs.${domain}.directory}/fullchain.pem"; + tls_key = + "${config.security.acme.certs.${domain}.directory}/key.pem"; + + bindaddress = kanidm-bind-address; # nginx should connect to it + ldapbindaddress = "${ldap_host}:${toString ldap_port}"; + + # kanidm is behind a proxy + trust_x_forward_for = true; + + log_level = "trace"; # FIXME + }; + provision = { + enable = true; + autoRemove = false; + + # FIXME read randomly generated password from ? + adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword; + idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword; + }; + enableClient = true; + clientSettings = { + uri = "https://" + auth-fqdn; + verify_ca = false; # FIXME + verify_hostnames = false; # FIXME + }; + }; + + services.nginx = { + enable = true; + virtualHosts.${auth-fqdn} = { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = + "https://${kanidm-bind-address}"; + }; + }; + }; + + # TODO move to mailserver module everything below + mailserver.debug = true; # FIXME + mailserver.mailDirectory = "/var/vmail"; + services.dovecot2.extraConfig = '' + auth_mechanisms = xoauth2 oauthbearer + + passdb { + driver = oauth2 + mechanisms = xoauth2 oauthbearer + args = ${dovecot-oauth2-conf-file} + } + + userdb { + driver = static + args = uid=virtualMail gid=virtualMail home=/var/vmail/%u + } + + # provide SASL via unix socket to postfix + service auth { + unix_listener /var/lib/postfix/private/auth { + mode = 0660 + user = postfix + group = postfix + } + } + service auth { + unix_listener auth-userdb { + mode = 0660 + user = dovecot2 + } + unix_listener dovecot-auth { + mode = 0660 + # Assuming the default Postfix user and group + user = postfix + group = postfix + } + } + + #auth_username_format = %Ln + auth_debug = yes + auth_debug_passwords = yes # Be cautious with this in production as it logs passwords + auth_verbose = yes + mail_debug = yes + ''; + services.dovecot2.enablePAM = false; + services.postfix.extraConfig = '' + smtpd_sasl_local_domain = ${domain} + smtpd_relay_restrictions = permit_sasl_authenticated, reject + smtpd_sasl_type = dovecot + smtpd_sasl_path = private/auth + smtpd_sasl_auth_enable = yes + ''; + }; +} diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index a650a1e..4ee28e6 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -1,5 +1,6 @@ [ ["selfprivacy", "domain"], ["selfprivacy", "modules", "roundcube"], + ["selfprivacy", "modules", "auth"], ["mailserver", "fqdn"] ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 8067289..0c37558 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -1,7 +1,10 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; + auth-module = config.selfprivacy.modules.auth; + auth-fqdn = auth-module.subdomain + "." + domain; + oauth-client-id = "roundcube"; in { options.selfprivacy.modules.roundcube = { @@ -29,7 +32,6 @@ in }; config = lib.mkIf cfg.enable { - services.roundcube = { enable = true; # this is the url of the vhost, not necessarily the same as the fqdn of @@ -39,8 +41,22 @@ in # starttls needed for authentication, so the fqdn required to match # the certificate $config['smtp_server'] = "tls://${config.mailserver.fqdn}"; - $config['smtp_user'] = "%u"; - $config['smtp_pass'] = "%p"; + # $config['smtp_user'] = "%u"; + # $config['smtp_pass'] = "%p"; + '' + lib.strings.optionalString auth-module.enable '' + $config['oauth_provider'] = 'generic'; + $config['oauth_provider_name'] = 'kanidm'; # FIXME + $config['oauth_client_id'] = '${oauth-client-id}'; + $config['oauth_client_secret'] = 'VERYSTRONGSECRETFORROUNDCUBE'; # FIXME + + $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; + $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; + $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; + $config['oauth_scope'] = 'email profile openid'; + $config['oauth_auth_parameters'] = []; + $config['oauth_identity_fields'] = ['email']; + $config['oauth_login_redirect'] = true; + $config['auto_create_user'] = true; ''; }; services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { @@ -56,5 +72,38 @@ in description = "Roundcube service slice"; }; }; + services.kanidm.serverSettings.provision.systems.oauth2.roundcube = + lib.mkIf auth-module.enable { + displayName = "Roundcube"; + originUrl = "https://${cfg.subdomain}.${domain}/"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; # FIXME + preferShortUsername = false; + allowInsecureClientDisablePkce = true; # FIXME is it required? + scopeMaps.roundcube_users = [ + "email" + "openid" + "profile" + # "dovecotprofile" + # "groups" + ]; + }; + services.kanidm.provision.systems.oauth2.roundcube = + lib.mkIf auth-module.enable { + displayName = "Roundcube"; + originUrl = "https://${cfg.subdomain}.${domain}/"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; + # when true, name is passed to a service instead of name@domain + preferShortUsername = false; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps.roundcube_users = [ + "email" + # "groups" + "profile" + "openid" + # "dovecotprofile" + ]; + }; }; } From b5de64105c504cff38a69e3f947dc80752def323 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Tue, 5 Nov 2024 23:02:01 +0400 Subject: [PATCH 002/115] kanidm 1.4.0 --- sp-modules/auth/config-paths-needed.json | 3 +- sp-modules/auth/flake.nix | 27 +++++++--- sp-modules/auth/module.nix | 26 ++++----- sp-modules/roundcube/config-paths-needed.json | 1 + sp-modules/roundcube/module.nix | 54 ++++++++----------- 5 files changed, 56 insertions(+), 55 deletions(-) diff --git a/sp-modules/auth/config-paths-needed.json b/sp-modules/auth/config-paths-needed.json index f75f298..47db4d4 100644 --- a/sp-modules/auth/config-paths-needed.json +++ b/sp-modules/auth/config-paths-needed.json @@ -3,5 +3,6 @@ ["security", "acme", "certs"], ["selfprivacy", "domain"], ["selfprivacy", "modules"], - ["services"] + ["services"], + ["systemd", "services", "kanidm"] ] diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix index a86ec75..46b2d32 100644 --- a/sp-modules/auth/flake.nix +++ b/sp-modules/auth/flake.nix @@ -1,13 +1,24 @@ { description = "User authentication and authorization module"; - # TODO remove when working Kanidm lands in nixpkgs and Hydra - inputs.nixpkgs-unstable.url = github:alexoundos/nixpkgs/b84444cbd57e934312f6a03d2d783ed0b7f94957; + # TODO remove when Kanidm provisioning without groups assertion lands in NixOS + inputs.nixos-unstable.url = github:alexoundos/nixpkgs/679fd3fd318ce2d57d0cabfbd7f4b8857d78ae95; + # inputs.nixos-unstable.url = git+file:/data/nixpkgs?ref=kanidm-1.4.0&rev=3feae1d8a2681b57c07d3a212a083988da6b96d2; - outputs = { self, nixpkgs-unstable }: { + outputs = { self, nixos-unstable }: { overlays.default = _final: prev: { - inherit (nixpkgs-unstable.legacyPackages.${prev.system}) - kanidm kanidm-provision oauth2-proxy; + inherit (nixos-unstable.legacyPackages.${prev.system}) + kanidm oauth2-proxy; + kanidm-provision = + nixos-unstable.legacyPackages.${prev.system}.kanidm-provision.overrideAttrs (_: { + # version = "git"; + # src = prev.fetchFromGitHub { + # owner = "oddlama"; + # repo = "kanidm-provision"; + # rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; + # hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; + # }; + }); }; nixosModules.default = { ... }: { @@ -17,11 +28,11 @@ "services/security/oauth2-proxy-nginx.nix" ]; imports = [ - (nixpkgs-unstable.legacyPackages.x86_64-linux.path + (nixos-unstable.legacyPackages.x86_64-linux.path + /nixos/modules/services/security/kanidm.nix) - (nixpkgs-unstable.legacyPackages.x86_64-linux.path + (nixos-unstable.legacyPackages.x86_64-linux.path + /nixos/modules/services/security/oauth2-proxy.nix) - (nixpkgs-unstable.legacyPackages.x86_64-linux.path + (nixos-unstable.legacyPackages.x86_64-linux.path + /nixos/modules/services/security/oauth2-proxy-nginx.nix) ./module.nix ]; diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index 684915e..d49fcce 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -53,18 +53,18 @@ in enableServer = true; # kanidm with Rust code patches for OAuth and admin passwords provisioning - # package = pkgs.kanidm.withSecretProvisioning; + package = pkgs.kanidm.withSecretProvisioning; # FIXME - package = pkgs.kanidm.withSecretProvisioning.overrideAttrs (_: { - version = "git"; - src = pkgs.fetchFromGitHub { - owner = "AleXoundOS"; - repo = "kanidm"; - rev = "a1a55f2e53facbfa504c7d64c44c3b5d0eb796c2"; - hash = "sha256-ADh4Zwn6EMt4CiOrvgG0RbmNMeR5i0ilVTxF46t/wm8="; - }; - doCheck = false; - }); + # package = pkgs.kanidm.withSecretProvisioning.overrideAttrs (_: { + # version = "git"; + # src = pkgs.fetchFromGitHub { + # owner = "AleXoundOS"; + # repo = "kanidm"; + # rev = "a1a55f2e53facbfa504c7d64c44c3b5d0eb796c2"; + # hash = "sha256-ADh4Zwn6EMt4CiOrvgG0RbmNMeR5i0ilVTxF46t/wm8="; + # }; + # doCheck = false; + # }); serverSettings = { inherit domain; @@ -93,8 +93,8 @@ in autoRemove = false; # FIXME read randomly generated password from ? - adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword; - idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword; + # adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword; + # idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword; }; enableClient = true; clientSettings = { diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index 4ee28e6..f017fdd 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -2,5 +2,6 @@ ["selfprivacy", "domain"], ["selfprivacy", "modules", "roundcube"], ["selfprivacy", "modules", "auth"], + ["service", "kanidm"], ["mailserver", "fqdn"] ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 0c37558..5d5ecc0 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -72,38 +72,26 @@ in description = "Roundcube service slice"; }; }; - services.kanidm.serverSettings.provision.systems.oauth2.roundcube = - lib.mkIf auth-module.enable { - displayName = "Roundcube"; - originUrl = "https://${cfg.subdomain}.${domain}/"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; # FIXME - preferShortUsername = false; - allowInsecureClientDisablePkce = true; # FIXME is it required? - scopeMaps.roundcube_users = [ - "email" - "openid" - "profile" - # "dovecotprofile" - # "groups" - ]; - }; - services.kanidm.provision.systems.oauth2.roundcube = - lib.mkIf auth-module.enable { - displayName = "Roundcube"; - originUrl = "https://${cfg.subdomain}.${domain}/"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; - # when true, name is passed to a service instead of name@domain - preferShortUsername = false; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps.roundcube_users = [ - "email" - # "groups" - "profile" - "openid" - # "dovecotprofile" - ]; - }; + + services.kanidm.provision = lib.mkIf auth-module.enable { + groups.roundcube_users.present = true; + systems.oauth2.roundcube = + { + displayName = "Roundcube"; + originUrl = "https://${cfg.subdomain}.${domain}/"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; + # when true, name is passed to a service instead of name@domain + preferShortUsername = false; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps.roundcube_users = [ + "email" + # "groups" + "profile" + "openid" + # "dovecotprofile" + ]; + }; + }; }; } From ad6d3d697045dd2fea3af7464e964d6924814731 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 20 Dec 2024 16:13:59 +0400 Subject: [PATCH 003/115] WIP: LDAP: Dovecot&Postfix works, but Postfix sends to 25 port --- sp-modules/auth/config-paths-needed.json | 2 + sp-modules/auth/flake.nix | 26 +- sp-modules/auth/kanidm.nix | 1008 ++++++++++++++++++++++ sp-modules/auth/ldap-postfix.nix | 81 ++ sp-modules/auth/module.nix | 215 ++++- sp-modules/roundcube/module.nix | 190 +++- 6 files changed, 1472 insertions(+), 50 deletions(-) create mode 100644 sp-modules/auth/kanidm.nix create mode 100644 sp-modules/auth/ldap-postfix.nix diff --git a/sp-modules/auth/config-paths-needed.json b/sp-modules/auth/config-paths-needed.json index 47db4d4..7b922da 100644 --- a/sp-modules/auth/config-paths-needed.json +++ b/sp-modules/auth/config-paths-needed.json @@ -1,5 +1,7 @@ [ ["mailserver", "fqdn"], + ["mailserver", "ldap"], + ["mailserver", "vmailUID"], ["security", "acme", "certs"], ["selfprivacy", "domain"], ["selfprivacy", "modules"], diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix index 46b2d32..15cfc9c 100644 --- a/sp-modules/auth/flake.nix +++ b/sp-modules/auth/flake.nix @@ -2,8 +2,9 @@ description = "User authentication and authorization module"; # TODO remove when Kanidm provisioning without groups assertion lands in NixOS - inputs.nixos-unstable.url = github:alexoundos/nixpkgs/679fd3fd318ce2d57d0cabfbd7f4b8857d78ae95; - # inputs.nixos-unstable.url = git+file:/data/nixpkgs?ref=kanidm-1.4.0&rev=3feae1d8a2681b57c07d3a212a083988da6b96d2; + # inputs.nixos-unstable.url = github:alexoundos/nixpkgs/679fd3fd318ce2d57d0cabfbd7f4b8857d78ae95; + # inputs.nixos-unstable.url = git+file:/data/nixpkgs?ref=kanidm-1.4.0&rev=1bac99358baea6a3268027b4e585c68cd4ef107d; + inputs.nixos-unstable.url = github:nixos/nixpkgs/7ffd9ae656aec493492b44d0ddfb28e79a1ea25d; outputs = { self, nixos-unstable }: { overlays.default = _final: prev: { @@ -11,13 +12,13 @@ kanidm oauth2-proxy; kanidm-provision = nixos-unstable.legacyPackages.${prev.system}.kanidm-provision.overrideAttrs (_: { - # version = "git"; - # src = prev.fetchFromGitHub { - # owner = "oddlama"; - # repo = "kanidm-provision"; - # rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; - # hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; - # }; + version = "git"; + src = prev.fetchFromGitHub { + owner = "oddlama"; + repo = "kanidm-provision"; + rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; + hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; + }; }); }; @@ -28,15 +29,18 @@ "services/security/oauth2-proxy-nginx.nix" ]; imports = [ - (nixos-unstable.legacyPackages.x86_64-linux.path - + /nixos/modules/services/security/kanidm.nix) + ./kanidm.nix (nixos-unstable.legacyPackages.x86_64-linux.path + /nixos/modules/services/security/oauth2-proxy.nix) (nixos-unstable.legacyPackages.x86_64-linux.path + /nixos/modules/services/security/oauth2-proxy-nginx.nix) ./module.nix + ./ldap-postfix.nix ]; nixpkgs.overlays = [ self.overlays.default ]; + + selfprivacy.modules.auth.enable = true; + selfprivacy.modules.auth.debug = true; }; configPathsNeeded = diff --git a/sp-modules/auth/kanidm.nix b/sp-modules/auth/kanidm.nix new file mode 100644 index 0000000..6e33eb5 --- /dev/null +++ b/sp-modules/auth/kanidm.nix @@ -0,0 +1,1008 @@ +{ + config, + lib, + options, + pkgs, + ... +}: +let + inherit (lib) + any + attrNames + attrValues + concatLines + concatLists + converge + filter + filterAttrs + filterAttrsRecursive + flip + foldl' + getExe + hasInfix + hasPrefix + isStorePath + last + mapAttrsToList + mkEnableOption + mkForce + mkIf + mkMerge + mkOption + mkPackageOption + optional + optionalString + splitString + subtractLists + types + unique + ; + + cfg = config.services.kanidm; + settingsFormat = pkgs.formats.toml { }; + # Remove null values, so we can document optional values that don't end up in the generated TOML file. + filterConfig = converge (a: filterAttrsRecursive (_: v: v != null) (builtins.removeAttrs a [ "provision" ])); + serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); + clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); + unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); + certPaths = builtins.map builtins.dirOf [ + cfg.serverSettings.tls_chain + cfg.serverSettings.tls_key + ]; + + # Merge bind mount paths and remove paths where a prefix is already mounted. + # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount + # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. + hasPrefixInList = + list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list; + mergePaths = foldl' ( + merged: newPath: + let + # If the new path is a prefix to some existing path, we need to filter it out + filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; + # If a prefix of the new path is already in the list, do not add it + filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; + in + filteredPaths ++ filteredNew + ) [ ]; + + defaultServiceConfig = { + # Setting the type to notify enables additional healthchecks, ensuring units + # after and requiring kanidm-* wait for it to complete startup + Type = "notify"; + BindReadOnlyPaths = [ + "/nix/store" + # For healthcheck notifications + "/run/systemd/notify" + "-/etc/resolv.conf" + "-/etc/nsswitch.conf" + "-/etc/hosts" + "-/etc/localtime" + ]; + CapabilityBoundingSet = [ ]; + # ProtectClock= adds DeviceAllow=char-rtc r + DeviceAllow = ""; + # Implies ProtectSystem=strict, which re-mounts all paths + # DynamicUser = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateNetwork = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectHome = true; + ProtectHostname = true; + # Would re-mount paths ignored by temporary root + #ProtectSystem = "strict"; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged @resources @setuid @keyring" + ]; + # Does not work well with the temporary root + #UMask = "0066"; + }; + + mkPresentOption = + what: + mkOption { + description = "Whether to ensure that this ${what} is present or absent."; + type = types.bool; + default = true; + }; + + filterPresent = filterAttrs (_: v: v.present); + + isGroupEmpty = g: ! g ? members || g ? members && g.members == [ ]; + + provisionStateJson = pkgs.writeText "provision-state.json" ( + builtins.toJSON { + inherit (cfg.provision) persons systems; + groups = + lib.attrsets.filterAttrs (_n: v: ! isGroupEmpty v) cfg.provision.groups; + } + ); + + # Only recover the admin account if a password should explicitly be provisioned + # for the account. Otherwise it is not needed for provisioning. + maybeRecoverAdmin = optionalString (cfg.provision.adminPasswordFile != null) '' + KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile}) + # We always reset the admin account password if a desired password was specified. + if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then + echo "Failed to recover admin account" >&2 + exit 1 + fi + ''; + + # Recover the idm_admin account. If a password should explicitly be provisioned + # for the account we set it, otherwise we generate a new one because it is required + # for provisioning. + recoverIdmAdmin = + if cfg.provision.idmAdminPasswordFile != null then + '' + KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile}) + # We always reset the idm_admin account password if a desired password was specified. + if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then + echo "Failed to recover idm_admin account" >&2 + exit 1 + fi + '' + else + '' + # Recover idm_admin account + if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then + echo "$recover_out" >&2 + echo "kanidm provision: Failed to recover admin account" >&2 + exit 1 + fi + if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${getExe pkgs.jq} -r .password); then + echo "$recover_out" >&2 + echo "kanidm provision: Failed to parse password for idm_admin account" >&2 + exit 1 + fi + ''; + + emptyGroupsNames = + builtins.attrNames + (lib.attrsets.filterAttrs (_n: isGroupEmpty) cfg.provision.groups); + + createEmptyGroups = '' + readonly CLIENT_HOME=$RUNTIME_DIRECTORY/client_home + mkdir -p $CLIENT_HOME + HOME=$CLIENT_HOME KANIDM_PASSWORD="$KANIDM_IDM_ADMIN_PASSWORD" ${cfg.package}/bin/kanidm login --name idm_admin --url "${cfg.provision.instanceUrl}" --skip-hostname-verification + for group_name in ${lib.strings.concatLines emptyGroupsNames} + do + HOME=$CLIENT_HOME ${cfg.package}/bin/kanidm group create "$group_name" --name idm_admin --url "${cfg.provision.instanceUrl}" --skip-hostname-verification + done + # rm -r $CLIENT_HOME + ''; + + postStartScript = pkgs.writeShellScript "post-start" '' + set -euo pipefail + + # Wait for the kanidm server to come online + count=0 + while ! ${getExe pkgs.curl} -L --silent --max-time 1 --connect-timeout 1 --fail \ + ${optionalString cfg.provision.acceptInvalidCerts "--insecure"} \ + ${cfg.provision.instanceUrl} >/dev/null + do + sleep 1 + if [[ "$count" -eq 30 ]]; then + echo "Tried for at least 30 seconds, giving up..." + exit 1 + fi + count=$((count++)) + done + + ${recoverIdmAdmin} + ${maybeRecoverAdmin} + ${createEmptyGroups} + + KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \ + ${getExe pkgs.kanidm-provision} \ + ${optionalString (!cfg.provision.autoRemove) "--no-auto-remove"} \ + ${optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} \ + --url "${cfg.provision.instanceUrl}" \ + --state ${provisionStateJson} + ''; + + serverPort = + # ipv6: + if hasInfix "]:" cfg.serverSettings.bindaddress then + last (splitString "]:" cfg.serverSettings.bindaddress) + else + # ipv4: + if hasInfix "." cfg.serverSettings.bindaddress then + last (splitString ":" cfg.serverSettings.bindaddress) + # default is 8443 + else + "8443"; +in +{ + options.services.kanidm = { + enableClient = mkEnableOption "the Kanidm client"; + enableServer = mkEnableOption "the Kanidm server"; + enablePam = mkEnableOption "the Kanidm PAM and NSS integration"; + + package = mkPackageOption pkgs "kanidm" { }; + + serverSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + + options = { + bindaddress = mkOption { + description = "Address/port combination the webserver binds to."; + example = "[::1]:8443"; + type = types.str; + }; + # Should be optional but toml does not accept null + ldapbindaddress = mkOption { + description = '' + Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface. + ''; + example = "[::1]:636"; + default = null; + type = types.nullOr types.str; + }; + origin = mkOption { + description = "The origin of your Kanidm instance. Must have https as protocol."; + example = "https://idm.example.org"; + type = types.strMatching "^https://.*"; + }; + domain = mkOption { + description = '' + The `domain` that Kanidm manages. Must be below or equal to the domain + specified in `serverSettings.origin`. + This can be left at `null`, only if your instance has the role `ReadOnlyReplica`. + While it is possible to change the domain later on, it requires extra steps! + Please consider the warnings and execute the steps described + [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain). + ''; + example = "example.org"; + default = null; + type = types.nullOr types.str; + }; + db_path = mkOption { + description = "Path to Kanidm database."; + default = "/var/lib/kanidm/kanidm.db"; + readOnly = true; + type = types.path; + }; + tls_chain = mkOption { + description = "TLS chain in pem format."; + type = types.path; + }; + tls_key = mkOption { + description = "TLS key in pem format."; + type = types.path; + }; + log_level = mkOption { + description = "Log level of the server."; + default = "info"; + type = types.enum [ + "info" + "debug" + "trace" + ]; + }; + role = mkOption { + description = "The role of this server. This affects the replication relationship and thereby available features."; + default = "WriteReplica"; + type = types.enum [ + "WriteReplica" + "WriteReplicaNoUI" + "ReadOnlyReplica" + ]; + }; + online_backup = { + path = mkOption { + description = "Path to the output directory for backups."; + type = types.path; + default = "/var/lib/kanidm/backups"; + }; + schedule = mkOption { + description = "The schedule for backups in cron format."; + type = types.str; + default = "00 22 * * *"; + }; + versions = mkOption { + description = '' + Number of backups to keep. + + The default is set to `0`, in order to disable backups by default. + ''; + type = types.ints.unsigned; + default = 0; + example = 7; + }; + }; + }; + }; + default = { }; + description = '' + Settings for Kanidm, see + [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html) + and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml) + for possible values. + ''; + }; + + clientSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + + options.uri = mkOption { + description = "Address of the Kanidm server."; + example = "http://127.0.0.1:8080"; + type = types.str; + }; + }; + description = '' + Configure Kanidm clients, needed for the PAM daemon. See + [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration) + and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config) + for possible values. + ''; + }; + + unixSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + + options = { + pam_allowed_login_groups = mkOption { + description = "Kanidm groups that are allowed to login using PAM."; + example = "my_pam_group"; + type = types.listOf types.str; + }; + hsm_pin_path = mkOption { + description = "Path to a HSM pin."; + default = "/var/cache/kanidm-unixd/hsm-pin"; + type = types.path; + }; + }; + }; + description = '' + Configure Kanidm unix daemon. + See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon) + and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd) + for possible values. + ''; + }; + + provision = { + enable = mkEnableOption "provisioning of groups, users and oauth2 resource servers"; + + instanceUrl = mkOption { + description = "The instance url to which the provisioning tool should connect."; + default = "https://localhost:${serverPort}"; + defaultText = ''"https://localhost:"''; + type = types.str; + }; + + acceptInvalidCerts = mkOption { + description = '' + Whether to allow invalid certificates when provisioning the target instance. + By default this is only allowed when the instanceUrl is localhost. This is + dangerous when used with an external URL. + ''; + type = types.bool; + default = hasPrefix "https://localhost:" cfg.provision.instanceUrl; + defaultText = ''hasPrefix "https://localhost:" cfg.provision.instanceUrl''; + }; + + adminPasswordFile = mkOption { + description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!"; + example = "/run/secrets/kanidm-admin-password"; + default = null; + type = types.nullOr types.path; + }; + + idmAdminPasswordFile = mkOption { + description = '' + Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here! + If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart. + ''; + example = "/run/secrets/kanidm-idm-admin-password"; + default = null; + type = types.nullOr types.path; + }; + + autoRemove = mkOption { + description = '' + Determines whether deleting an entity in this provisioning config should automatically + cause them to be removed from kanidm, too. This works because the provisioning tool tracks + all entities it has ever created. If this is set to false, you need to explicitly specify + `present = false` to delete an entity. + ''; + type = types.bool; + default = true; + }; + + groups = mkOption { + description = "Provisioning of kanidm groups"; + default = { }; + type = types.attrsOf ( + types.submodule (groupSubmod: { + options = { + present = mkPresentOption "group"; + + members = mkOption { + description = "List of kanidm entities (persons, groups, ...) which are part of this group."; + type = types.listOf types.str; + apply = unique; + default = [ ]; + }; + }; + config.members = concatLists ( + flip mapAttrsToList cfg.provision.persons ( + person: personCfg: + optional ( + personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups + ) person + ) + ); + }) + ); + }; + + persons = mkOption { + description = "Provisioning of kanidm persons"; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + present = mkPresentOption "person"; + + displayName = mkOption { + description = "Display name"; + type = types.str; + example = "My User"; + }; + + legalName = mkOption { + description = "Full legal name"; + type = types.nullOr types.str; + example = "Jane Doe"; + default = null; + }; + + mailAddresses = mkOption { + description = "Mail addresses. First given address is considered the primary address."; + type = types.listOf types.str; + example = [ "jane.doe@example.com" ]; + default = [ ]; + }; + + groups = mkOption { + description = "List of groups this person should belong to."; + type = types.listOf types.str; + apply = unique; + default = [ ]; + }; + }; + } + ); + }; + + systems.oauth2 = mkOption { + description = "Provisioning of oauth2 resource servers"; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + present = mkPresentOption "oauth2 resource server"; + + public = mkOption { + description = "Whether this is a public client (enforces PKCE, doesn't use a basic secret)"; + type = types.bool; + default = false; + }; + + displayName = mkOption { + description = "Display name"; + type = types.str; + example = "Some Service"; + }; + + originUrl = mkOption { + description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin."; + type = + let + originStrType = types.strMatching ".*://.*$"; + in + types.either originStrType (types.nonEmptyListOf originStrType); + example = "https://someservice.example.com/"; + }; + + originLanding = mkOption { + description = "When redirecting from the Kanidm Apps Listing page, some linked applications may need to land on a specific page to trigger oauth2/oidc interactions."; + type = types.str; + example = "https://someservice.example.com/home"; + }; + + basicSecretFile = mkOption { + description = '' + The basic secret to use for this service. If null, the random secret generated + by kanidm will not be touched. Do NOT use a path from the nix store here! + ''; + type = types.nullOr types.path; + example = "/run/secrets/some-oauth2-basic-secret"; + default = null; + }; + + enableLocalhostRedirects = mkOption { + description = "Allow localhost redirects. Only for public clients."; + type = types.bool; + default = false; + }; + + enableLegacyCrypto = mkOption { + description = "Enable legacy crypto on this client. Allows JWT signing algorthms like RS256."; + type = types.bool; + default = false; + }; + + allowInsecureClientDisablePkce = mkOption { + description = '' + Disable PKCE on this oauth2 resource server to work around insecure clients + that may not support it. You should request the client to enable PKCE! + Only for non-public clients. + ''; + type = types.bool; + default = false; + }; + + preferShortUsername = mkOption { + description = "Use 'name' instead of 'spn' in the preferred_username claim"; + type = types.bool; + default = false; + }; + + scopeMaps = mkOption { + description = '' + Maps kanidm groups to returned oauth scopes. + See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. + ''; + type = types.attrsOf (types.listOf types.str); + default = { }; + }; + + supplementaryScopeMaps = mkOption { + description = '' + Maps kanidm groups to additionally returned oauth scopes. + See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. + ''; + type = types.attrsOf (types.listOf types.str); + default = { }; + }; + + removeOrphanedClaimMaps = mkOption { + description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm."; + type = types.bool; + default = true; + }; + + claimMaps = mkOption { + description = '' + Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to. + See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. + ''; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + joinType = mkOption { + description = '' + Determines how multiple values are joined to create the claim value. + See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. + ''; + type = types.enum [ + "array" + "csv" + "ssv" + ]; + default = "array"; + }; + + valuesByGroup = mkOption { + description = "Maps kanidm groups to values for the claim."; + default = { }; + type = types.attrsOf (types.listOf types.str); + }; + }; + } + ); + }; + }; + } + ); + }; + }; + }; + + config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { + assertions = + let + entityList = + type: attrs: flip mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; }); + entities = + entityList "group" cfg.provision.groups + ++ entityList "person" cfg.provision.persons + ++ entityList "oauth2" cfg.provision.systems.oauth2; + + # Accumulate entities by name. Track corresponding entity types for later duplicate check. + entitiesByName = foldl' ( + acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } + ) { } entities; + + assertGroupsKnown = + opt: groups: + let + knownGroups = attrNames (filterPresent cfg.provision.groups); + unknownGroups = subtractLists knownGroups groups; + in + { + assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ]; + message = "${opt} refers to unknown groups: ${toString unknownGroups}"; + }; + + assertEntitiesKnown = + opt: entities: + let + unknownEntities = subtractLists (attrNames entitiesByName) entities; + in + { + assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [ ]; + message = "${opt} refers to unknown entities: ${toString unknownEntities}"; + }; + in + [ + { + assertion = + !cfg.enableServer + || ((cfg.serverSettings.tls_chain or null) == null) + || (!isStorePath cfg.serverSettings.tls_chain); + message = '' + points to + a file in the Nix store. You should use a quoted absolute path to + prevent this. + ''; + } + { + assertion = + !cfg.enableServer + || ((cfg.serverSettings.tls_key or null) == null) + || (!isStorePath cfg.serverSettings.tls_key); + message = '' + points to + a file in the Nix store. You should use a quoted absolute path to + prevent this. + ''; + } + { + assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined; + message = '' + needs to be configured + if the client is enabled. + ''; + } + { + assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined; + message = '' + needs to be configured + for the PAM daemon to connect to the Kanidm server. + ''; + } + { + assertion = + !cfg.enableServer + || ( + cfg.serverSettings.domain == null + -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI" + ); + message = '' + can only be set if this instance + is not a ReadOnlyReplica. Otherwise the db would inherit it from + the instance it follows. + ''; + } + { + assertion = cfg.provision.enable -> cfg.enableServer; + message = " requires to be true"; + } + # If any secret is provisioned, the kanidm package must have some required patches applied to it + { + assertion = + ( + cfg.provision.enable + && ( + cfg.provision.adminPasswordFile != null + || cfg.provision.idmAdminPasswordFile != null + || any (x: x.basicSecretFile != null) (attrValues (filterPresent cfg.provision.systems.oauth2)) + ) + ) + -> cfg.package.enableSecretProvisioning; + message = '' + Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches. + You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`. + ''; + } + # Entity names must be globally unique: + ( + let + # Filter all names that occurred in more than one entity type. + duplicateNames = filterAttrs (_: v: builtins.length v > 1) entitiesByName; + in + { + assertion = cfg.provision.enable -> duplicateNames == { }; + message = '' + services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique! + ${concatLines ( + mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames + )}''; + } + ) + ] + ++ flip mapAttrsToList (filterPresent cfg.provision.persons) ( + person: personCfg: + assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups + ) + ++ flip mapAttrsToList (filterPresent cfg.provision.groups) ( + group: groupCfg: + assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members + ) + ++ concatLists ( + flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( + oauth2: oauth2Cfg: + [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( + attrNames oauth2Cfg.scopeMaps + )) + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( + attrNames oauth2Cfg.supplementaryScopeMaps + )) + ] + ++ concatLists ( + flip mapAttrsToList oauth2Cfg.claimMaps ( + claim: claimCfg: [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( + attrNames claimCfg.valuesByGroup + )) + # At least one group must map to a value in each claim map + { + assertion = + (cfg.provision.enable && cfg.enableServer) + -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup); + message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; + } + # Public clients cannot define a basic secret + { + assertion = + (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; + } + # Public clients cannot disable PKCE + { + assertion = + (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) + -> !oauth2Cfg.allowInsecureClientDisablePkce; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE"; + } + # Non-public clients cannot enable localhost redirects + { + assertion = + (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) + -> !oauth2Cfg.enableLocalhostRedirects; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; + } + ] + ) + ) + ) + ); + + environment.systemPackages = mkIf cfg.enableClient [ cfg.package ]; + + systemd.tmpfiles.settings."10-kanidm" = { + ${cfg.serverSettings.online_backup.path}.d = { + mode = "0700"; + user = "kanidm"; + group = "kanidm"; + }; + }; + + systemd.services.kanidm = mkIf cfg.enableServer { + description = "kanidm identity management daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = mkMerge [ + # Merge paths and ignore existing prefixes needs to sidestep mkMerge + ( + defaultServiceConfig + // { + BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); + } + ) + { + StateDirectory = "kanidm"; + StateDirectoryMode = "0700"; + RuntimeDirectory = "kanidmd"; + ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}"; + ExecStartPost = mkIf cfg.provision.enable postStartScript; + User = "kanidm"; + Group = "kanidm"; + + BindPaths = [ + # To create the socket + "/run/kanidmd:/run/kanidmd" + # To store backups + cfg.serverSettings.online_backup.path + ]; + + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + # This would otherwise override the CAP_NET_BIND_SERVICE capability. + PrivateUsers = mkForce false; + # Port needs to be exposed to the host network + PrivateNetwork = mkForce false; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + #TemporaryFileSystem = "/:ro"; + } + ]; + environment.RUST_LOG = "info"; + }; + + systemd.services.kanidm-unixd = mkIf cfg.enablePam { + description = "Kanidm PAM daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + restartTriggers = [ + unixConfigFile + clientConfigFile + ]; + serviceConfig = mkMerge [ + defaultServiceConfig + { + CacheDirectory = "kanidm-unixd"; + CacheDirectoryMode = "0700"; + RuntimeDirectory = "kanidm-unixd"; + ExecStart = "${cfg.package}/bin/kanidm_unixd"; + User = "kanidm-unixd"; + Group = "kanidm-unixd"; + + BindReadOnlyPaths = [ + "-/etc/kanidm" + "-/etc/static/kanidm" + "-/etc/ssl" + "-/etc/static/ssl" + "-/etc/passwd" + "-/etc/group" + ]; + BindPaths = [ + # To create the socket + "/run/kanidm-unixd:/var/run/kanidm-unixd" + ]; + # Needs to connect to kanidmd + PrivateNetwork = mkForce false; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + TemporaryFileSystem = "/:ro"; + } + ]; + environment.RUST_LOG = "info"; + }; + + systemd.services.kanidm-unixd-tasks = mkIf cfg.enablePam { + description = "Kanidm PAM home management daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ + "network.target" + "kanidm-unixd.service" + ]; + partOf = [ "kanidm-unixd.service" ]; + restartTriggers = [ + unixConfigFile + clientConfigFile + ]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks"; + + BindReadOnlyPaths = [ + "/nix/store" + "-/etc/resolv.conf" + "-/etc/nsswitch.conf" + "-/etc/hosts" + "-/etc/localtime" + "-/etc/kanidm" + "-/etc/static/kanidm" + ]; + BindPaths = [ + # To manage home directories + "/home" + # To connect to kanidm-unixd + "/run/kanidm-unixd:/var/run/kanidm-unixd" + ]; + # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket + CapabilityBoundingSet = [ + "CAP_CHOWN" + "CAP_FOWNER" + "CAP_DAC_OVERRIDE" + "CAP_DAC_READ_SEARCH" + ]; + IPAddressDeny = "any"; + # Need access to users + PrivateUsers = false; + # Need access to home directories + ProtectHome = false; + RestrictAddressFamilies = [ "AF_UNIX" ]; + TemporaryFileSystem = "/:ro"; + Restart = "on-failure"; + }; + environment.RUST_LOG = "info"; + }; + + # These paths are hardcoded + environment.etc = mkMerge [ + (mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; }) + (mkIf options.services.kanidm.clientSettings.isDefined { + "kanidm/config".source = clientConfigFile; + }) + (mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; }) + ]; + + system.nssModules = mkIf cfg.enablePam [ cfg.package ]; + + system.nssDatabases.group = optional cfg.enablePam "kanidm"; + system.nssDatabases.passwd = optional cfg.enablePam "kanidm"; + + users.groups = mkMerge [ + (mkIf cfg.enableServer { kanidm = { }; }) + (mkIf cfg.enablePam { kanidm-unixd = { }; }) + ]; + users.users = mkMerge [ + (mkIf cfg.enableServer { + kanidm = { + description = "Kanidm server"; + isSystemUser = true; + group = "kanidm"; + packages = [ cfg.package ]; + }; + }) + (mkIf cfg.enablePam { + kanidm-unixd = { + description = "Kanidm PAM daemon"; + isSystemUser = true; + group = "kanidm-unixd"; + }; + }) + ]; + }; + + meta.maintainers = with lib.maintainers; [ + Flakebi + oddlama + ]; + meta.buildDocsInSandbox = false; +} diff --git a/sp-modules/auth/ldap-postfix.nix b/sp-modules/auth/ldap-postfix.nix new file mode 100644 index 0000000..3a0e905 --- /dev/null +++ b/sp-modules/auth/ldap-postfix.nix @@ -0,0 +1,81 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.mailserver; + + appendLdapBindPwd = + { name, file, prefix, suffix ? "", passwordFile, destination }: + pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' + #!${pkgs.stdenv.shell} + set -euo pipefail + + baseDir=$(dirname ${destination}) + if (! test -d "$baseDir"); then + mkdir -p $baseDir + chmod 755 $baseDir + fi + + cat ${file} > ${destination} + echo -n '${prefix}' >> ${destination} + cat ${passwordFile} >> ${destination} + echo -n '${suffix}' >> ${destination} + chmod 600 ${destination} + ''; + + ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf"; + submissionOptions.smtpd_sender_login_maps = + lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}"; + commonLdapConfig = '' + server_host = ${lib.concatStringsSep " " cfg.ldap.uris} + start_tls = ${if cfg.ldap.startTls then "yes" else "no"} + version = 3 + # tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + # tls_require_cert = yes + + search_base = ${cfg.ldap.searchBase} + scope = ${cfg.ldap.searchScope} + + bind = yes + bind_dn = ${cfg.ldap.bind.dn} + ''; + ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.mailAttribute} + ''; + appendPwdInSenderLoginMap = appendLdapBindPwd { + name = "ldap-sender-login-map"; + file = ldapSenderLoginMap; + prefix = "bind_pw = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapSenderLoginMapFile; + }; + + ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.uidAttribute} + ''; + ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf"; + appendPwdInVirtualMailboxMap = appendLdapBindPwd { + name = "ldap-virtual-mailbox-map"; + file = ldapVirtualMailboxMap; + prefix = "bind_pw = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapVirtualMailboxMapFile; + }; +in +{ + systemd.services.postfix-setup = { + preStart = '' + ${appendPwdInVirtualMailboxMap} + ${appendPwdInSenderLoginMap} + ''; + restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; + }; + services.postfix = { + # the list should be merged with other options from nixos-mailserver + config.virtual_mailbox_maps = [ "ldap:${ldapVirtualMailboxMapFile}" ]; + submissionOptions = submissionOptions; + submissionsOptions = submissionOptions; + }; +} diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index d49fcce..d51335f 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -10,6 +10,12 @@ let kanidm-bind-address = "127.0.0.1:3013"; ldap_host = "127.0.0.1"; ldap_port = 3636; + # e.g. "dc=mydomain,dc=com" + ldap_base_dn = + lib.strings.concatMapStringsSep + "," + (x: "dc=" + x) + (lib.strings.splitString "." domain); dovecot-oauth2-conf-file = pkgs.writeTextFile { name = "dovecot-oauth2.conf.ext"; @@ -25,23 +31,81 @@ let active_attribute = active active_value = true openid_configuration_url = ${oauth2-discovery-url "roundcube"} - debug = yes # FIXME + debug = ${if cfg.debug then "yes" else "no"} ''; }; - provisionAdminPassword = "abcd1234"; - provisionIdmAdminPassword = "abcd1234"; # FIXME + lua_core_path = "${pkgs.luajitPackages.lua-resty-core}/lib/lua/5.1/?.lua"; + lua_lrucache_path = "${pkgs.luajitPackages.lua-resty-lrucache}/lib/lua/5.1/?.lua"; + lua_path = "${lua_core_path};${lua_lrucache_path};"; + ldapConfFile = "/run/dovecot2/dovecot-ldap.conf.ext"; # FIXME get "dovecot2" from `config` + mkLdapSearchScope = scope: ( + if scope == "sub" then "subtree" + else if scope == "one" then "onelevel" + else scope + ); + appendLdapBindPwd = + { name, file, prefix, suffix ? "", passwordFile, destination }: + pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' + #!${pkgs.stdenv.shell} + set -euo pipefail + + baseDir=$(dirname ${destination}) + if (! test -d "$baseDir"); then + mkdir -p $baseDir + chmod 755 $baseDir + fi + + cat ${file} > ${destination} + echo -n '${prefix}' >> ${destination} + cat ${passwordFile} >> ${destination} + echo -n '${suffix}' >> ${destination} + chmod 600 ${destination} + ''; + dovecot-ldap-config = pkgs.writeTextFile { + name = "dovecot-ldap.conf.ext.template"; + text = '' + ldap_version = 3 + uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris} + ${lib.optionalString config.mailserver.ldap.startTls '' + tls = yes + ''} + # tls_require_cert = hard + # tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile} + dn = ${config.mailserver.ldap.bind.dn} + sasl_bind = no + auth_bind = no + base = ${config.mailserver.ldap.searchBase} + scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope} + ${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) '' + user_attrs = ${config.mailserver.ldap.dovecot.userAttrs} + ''} + user_filter = ${config.mailserver.ldap.dovecot.userFilter} + ''; + }; + setPwdInLdapConfFile = appendLdapBindPwd { + name = "ldap-conf-file"; + file = dovecot-ldap-config; + prefix = ''dnpass = "''; + suffix = ''"''; + passwordFile = config.mailserver.ldap.bind.passwordFile; + destination = ldapConfFile; + }; in { options.selfprivacy.modules.auth = { enable = lib.mkOption { - default = true; + default = false; type = lib.types.bool; }; subdomain = lib.mkOption { default = "auth"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; }; + debug = lib.mkOption { + default = false; + type = lib.types.bool; + }; }; config = lib.mkIf cfg.enable { @@ -54,7 +118,6 @@ in # kanidm with Rust code patches for OAuth and admin passwords provisioning package = pkgs.kanidm.withSecretProvisioning; - # FIXME # package = pkgs.kanidm.withSecretProvisioning.overrideAttrs (_: { # version = "git"; # src = pkgs.fetchFromGitHub { @@ -91,10 +154,6 @@ in provision = { enable = true; autoRemove = false; - - # FIXME read randomly generated password from ? - # adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword; - # idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword; }; enableClient = true; clientSettings = { @@ -103,22 +162,113 @@ in verify_hostnames = false; # FIXME }; }; + # systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkBefore '' + # # check kanidm online here with curl again? + # # use API key for group creation? + # ''; + # services.phpfpm.pools.roundcube.settings = { + # catch_workers_output = true; + # "php_admin_value[error_log]" = "stdout"; + # "php_admin_flag[log_errors]" = true; + # "php_admin_value[log_level]" = "debug"; + # }; + services.phpfpm.phpOptions = '' + error_reporting = E_ALL + display_errors = on; + ''; + systemd.services.phpfpm-roundcube.serviceConfig = { + StandardError = "journal"; + StandardOutput = "journal"; + }; services.nginx = { enable = true; + additionalModules = + lib.lists.optional cfg.debug pkgs.nginxModules.lua; + commonHttpConfig = lib.strings.optionalString cfg.debug '' + log_format kanidm escape=none '$request $status\n' + '[Request body]: $request_body\n' + '[Header]: $resp_header\n' + '[Response Body]: $resp_body\n\n'; + lua_package_path "${lua_path}"; + ''; virtualHosts.${auth-fqdn} = { useACMEHost = domain; forceSSL = true; locations."/" = { - proxyPass = - "https://${kanidm-bind-address}"; + # extraConfig = '' + # if ($args != $new_args) { + # rewrite ^ /ui/oauth2?$new_args? last; + # } + # ''; + extraConfig = lib.strings.optionalString cfg.debug '' + access_log /var/log/nginx/kanidm.log kanidm; + + lua_need_request_body on; + + # log header + set $req_header ""; + set $resp_header ""; + header_filter_by_lua ' + local h = ngx.req.get_headers() + for k, v in pairs(h) do + ngx.var.req_header = ngx.var.req_header .. k.."="..v.." " + end + local rh = ngx.resp.get_headers() + for k, v in pairs(rh) do + ngx.var.resp_header = ngx.var.resp_header .. k.."="..v.." " + end + '; + + # log body + set $resp_body ""; + body_filter_by_lua ' + local resp_body = string.sub(ngx.arg[1], 1, 4000) + ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body + if ngx.arg[2] then + ngx.var.resp_body = ngx.ctx.buffered + end + '; + ''; + proxyPass = "https://${kanidm-bind-address}"; }; }; + # appendHttpConfig = '' + # # Define a map to modify redirect_uri and append %2F if missing + # map $args $new_args { + # ~^((.*)(redirect_uri=[^&]+)(?!%2F)(.*))$ $2$3%2F$4; + # default $args; + # } + # ''; }; # TODO move to mailserver module everything below - mailserver.debug = true; # FIXME + mailserver.debug = cfg.debug; # FIXME mailserver.mailDirectory = "/var/vmail"; + + mailserver.loginAccounts = lib.mkForce { }; + mailserver.extraVirtualAliases = lib.mkForce { }; + # LDAP is needed for Postfix to query Kanidm about email address ownership + # LDAP is needed for Dovecot also. + mailserver.ldap = { + enable = false; + # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; + bind.dn = "dn=token"; + # TODO change in this file should trigger system restart dovecot + bind.passwordFile = "/run/keys/dovecot/kanidm-service-account-token"; # FIXME + # searchBase = "ou=persons," + ldap_base_dn; + searchBase = ldap_base_dn; + # searchScope = "sub"; + uris = [ "ldaps://localhost:${toString ldap_port}" ]; + + # note: in `ldapsearch` first comes filter, then attributes + dovecot.userAttrs = "+"; # all operational attributes + # TODO: investigate whether "mail=%u" is better than: + # dovecot.userFilter = "(&(class=person)(uid=%n))"; + postfix.mailAttribute = "mail"; + postfix.uidAttribute = "uid"; + }; + services.dovecot2.extraConfig = '' auth_mechanisms = xoauth2 oauthbearer @@ -130,12 +280,12 @@ in userdb { driver = static - args = uid=virtualMail gid=virtualMail home=/var/vmail/%u + args = uid=virtualMail gid=virtualMail home=/var/vmail/${domain}/%u } # provide SASL via unix socket to postfix service auth { - unix_listener /var/lib/postfix/private/auth { + unix_listener /var/lib/postfix/private-auth { mode = 0660 user = postfix group = postfix @@ -154,7 +304,15 @@ in } } + userdb { + driver = ldap + args = ${ldapConfFile} + default_fields = home=/var/vmail/${domain}/%u uid=${toString config.mailserver.vmailUID} gid=${toString config.mailserver.vmailUID} + } + #auth_username_format = %Ln + + # FIXME auth_debug = yes auth_debug_passwords = yes # Be cautious with this in production as it logs passwords auth_verbose = yes @@ -162,11 +320,30 @@ in ''; services.dovecot2.enablePAM = false; services.postfix.extraConfig = '' - smtpd_sasl_local_domain = ${domain} - smtpd_relay_restrictions = permit_sasl_authenticated, reject - smtpd_sasl_type = dovecot - smtpd_sasl_path = private/auth - smtpd_sasl_auth_enable = yes + debug_peer_list = 94.43.135.210, 134.209.202.195 + debug_peer_level = 3 + smtp_use_tls = yes + # these below are already set in nixos-mailserver/mail-server/postfix.nix + # smtpd_sasl_local_domain = ${domain} + # smtpd_relay_restrictions = permit_sasl_authenticated, reject + # smtpd_sender_restrictions = + # smtpd_sender_login_maps = + # smtpd_sasl_type = dovecot + # smtpd_sasl_path = private-auth + # smtpd_sasl_auth_enable = yes ''; + + systemd.services.dovecot2 = { + # TODO does it merge with existing preStart? + preStart = setPwdInLdapConfFile + "\n"; + }; + + # does it merge with existing restartTriggers? + systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile ]; + + environment.systemPackages = lib.lists.optionals cfg.debug [ + pkgs.shelldap + pkgs.openldap + ]; }; } diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 5d5ecc0..a2b60df 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -5,6 +5,79 @@ let auth-module = config.selfprivacy.modules.auth; auth-fqdn = auth-module.subdomain + "." + domain; oauth-client-id = "roundcube"; + dovecot-service-account-name = "dovecot-service-account"; + postfix-service-account-name = "postfix-service-account"; + dovecot-service-account-token-name = "dovecot-service-account-token"; + postfix-service-account-token-name = "postfix-service-account-token"; + # dovecot-service-account-token-fp = "/run/kanidm/token/dovecot"; + dovecot-service-account-token-fp = + "/run/keys/dovecot/kanidm-service-account-token"; + postfix-service-account-token-fp = + "/run/keys/postfix/kanidm-service-account-token"; + dovecot-group = "dovecot2"; # FIXME + postfix-group = "postfix"; # FIXME + # FIXME use usernames and groups from `config` + # FIXME dependency on dovecot2 and postfix + # set-group-ID bit allows for kanidm user to create files, + # which inherit directory group (.e.g dovecot, postfix) + kanidmExecStartPostScriptRoot = pkgs.writeShellScript + "roundcube-kanidm-ExecStartPost-root-script.sh" + '' + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/dovecot + chown kanidm:dovecot2 /run/keys/dovecot + + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/postfix + chown kanidm:postfix /run/keys/postfix + ''; + # FIXME parameterize names like "dovecot2" group + kanidmExecStartPostScript = pkgs.writeShellScript + "roundcube-kanidm-ExecStartPost-script.sh" + '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + + # get Kanidm service account for Dovecot + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${dovecot-service-account-name}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${dovecot-service-account-name}\" is found" + else + echo "kanidm service account \"${dovecot-service-account-name}\" is not found" + echo "creating new kanidm service account \"${dovecot-service-account-name}\"" + if $KANIDM service-account create --name idm_admin ${dovecot-service-account-name} ${dovecot-service-account-name} idm_admin + then + "kanidm service account \"${dovecot-service-account-name}\" created" + else + echo "error: cannot create kanidm service account \"${dovecot-service-account-name}\"" + exit 1 + fi + fi + + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers ${dovecot-service-account-name} + + # create a new read-only token for Dovecot + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${dovecot-service-account-name} ${dovecot-service-account-token-name} --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi + + # if ! printf "%s\n" "$KANIDM_SERVICE_ACCOUNT_TOKEN" > ${dovecot-service-account-token-fp} + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${dovecot-service-account-token-fp} + then + echo "error: cannot write token to \"${dovecot-service-account-token-fp}\"" + exit 1 + fi + ''; in { options.selfprivacy.modules.roundcube = { @@ -32,15 +105,40 @@ in }; config = lib.mkIf cfg.enable { + # FIXME get user names from `config` + # in order to allow access below /run/keys + users.groups.keys.members = [ "kanidm" "dovecot2" "postfix" ]; services.roundcube = { enable = true; + # package = pkgs.roundcube.overrideAttrs (_: rec { + # version = "1.6.9"; + # src = pkgs.fetchurl { + # url = "https://github.com/roundcube/roundcubemail/releases/download/${version}/roundcubemail-${version}-complete.tar.gz"; + # sha256 = "sha256-thpfXCL4kMKZ6TWqz88IcGdpkNiuv/DWzf8HW/F8708="; + # }; + # # src = pkgs.fetchurl { + # # url = "https://github.com/roundcube/roundcubemail/archive/master/3a6e25a5b386e0d87427b934ccd2e0e282e0a74e.tar.gz"; + # # sha256 = "sha256-EpEI4E+r3reYbI/5rquia+zgz1+6k49lPChlp4QiZTE="; + # # }; + # postFixup = '' + # cp -v ${/data/sp/roundcubemail-1.6.9/program/include/rcmail_oauth.php} $out/program/include/rcmail_oauth.php + # cp -v ${/data/sp/roundcubemail-1.6.9/program/actions/login/oauth.php} $out/program/actions/login/oauth.php + # rm -r $out/program/localization/* + # ''; + # }); + # package = pkgs.runCommandNoCCLocal "roundcube-debug" {} '' + # cp -r --no-preserve=all ${pkgs.roundcube} $out + # cp -v ${/data/sp/roundcubemail-1.6.8/plugins/debug_logger/debug_logger.php} $out/plugins/debug_logger/debug_logger.php + # cp -v ${/data/sp/roundcubemail-1.6.8/program/include/rcmail_oauth.php} $out/program/include/rcmail_oauth.php + # ''; # this is the url of the vhost, not necessarily the same as the fqdn of # the mailserver hostName = "${cfg.subdomain}.${config.selfprivacy.domain}"; + # plugins = [ "debug_logger" ]; extraConfig = '' # starttls needed for authentication, so the fqdn required to match # the certificate - $config['smtp_server'] = "tls://${config.mailserver.fqdn}"; + $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; # $config['smtp_user'] = "%u"; # $config['smtp_pass'] = "%p"; '' + lib.strings.optionalString auth-module.enable '' @@ -53,20 +151,62 @@ in $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; $config['oauth_scope'] = 'email profile openid'; + # $config['oauth_scope'] = 'email openid dovecotprofile'; $config['oauth_auth_parameters'] = []; $config['oauth_identity_fields'] = ['email']; - $config['oauth_login_redirect'] = true; + $config['oauth_login_redirect'] = false; $config['auto_create_user'] = true; + + $config['log_dir'] = '/tmp/roundcube'; + $config['log_driver'] = 'stdout'; + $config['log_errors'] = 1; + // Log SQL queries to /sql or to syslog + $config['sql_debug'] = true; + + // Log IMAP conversation to /imap or to syslog + $config['imap_debug'] = true; + $config['log_debug'] = true; + $config['oauth_debug'] = true; + + // Log LDAP conversation to /ldap or to syslog + $config['ldap_debug'] = true; + + // Log SMTP conversation to /smtp or to syslog + $config['smtp_debug'] = true; + + $config['debug_logger']['master'] = 'master'; + $config['debug_logger']['oauth'] = 'oauth'; + $config['debug_logger']['imap'] = 'imap'; + $config['debug_logger']['log'] = 'log'; + $config['debug_logger']['smtp'] = 'smtp'; + + $config['oauth_verify_peer'] = false; + $config['log_logins'] = true; + $config['log_session'] = true; + # $config['oauth_pkce'] = 'S256'; ''; }; services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { forceSSL = true; useACMEHost = domain; enableACME = false; + # extraConfig = '' + # add_header X-Frame-Options DENY; + # add_header X-Content-Type-Options nosniff; + # add_header X-XSS-Protection "1; mode=block"; + # ''; }; systemd = { services = { - phpfpm-roundcube.serviceConfig.Slice = lib.mkForce "roundcube.slice"; + phpfpm-roundcube.serviceConfig = { + Slice = lib.mkForce "roundcube.slice"; + StandardError = "journal"; + StandardOutput = "journal"; + }; + kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ + ("+" + kanidmExecStartPostScriptRoot) + kanidmExecStartPostScript + ]; }; slices.roundcube = { description = "Roundcube service slice"; @@ -75,23 +215,33 @@ in services.kanidm.provision = lib.mkIf auth-module.enable { groups.roundcube_users.present = true; - systems.oauth2.roundcube = - { - displayName = "Roundcube"; - originUrl = "https://${cfg.subdomain}.${domain}/"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; - # when true, name is passed to a service instead of name@domain - preferShortUsername = false; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps.roundcube_users = [ - "email" - # "groups" - "profile" - "openid" - # "dovecotprofile" - ]; - }; + systems.oauth2.roundcube = { + displayName = "Roundcube"; + originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; + # when true, name is passed to a service instead of name@domain + preferShortUsername = false; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps.roundcube_users = [ + "email" + "profile" + "openid" + ]; + # scopeMaps.roundcube_users = [ + # "email" + # "openid" + # "dovecotprofile" + # ]; + + # add more scopes when a user is a member of specific group + # claimMaps.groups = { + # joinType = "array"; + # valuesByGroup = { + # "sp.roundcube.admin" = [ "admin" ]; + # }; + # }; + }; }; }; } From 5d76f456c11fc3dca2f0e1dff7e5a39473696506 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 20 Dec 2024 18:41:07 +0400 Subject: [PATCH 004/115] auth: ldap-dovecot.nix, clean code --- sp-modules/auth/common.nix | 35 ++++ sp-modules/auth/config-paths-needed.json | 1 + sp-modules/auth/flake.nix | 3 +- sp-modules/auth/ldap-dovecot.nix | 129 ++++++++++++ sp-modules/auth/ldap-postfix.nix | 36 ++-- sp-modules/auth/module.nix | 241 +++-------------------- 6 files changed, 209 insertions(+), 236 deletions(-) create mode 100644 sp-modules/auth/common.nix create mode 100644 sp-modules/auth/ldap-dovecot.nix diff --git a/sp-modules/auth/common.nix b/sp-modules/auth/common.nix new file mode 100644 index 0000000..91f0f0c --- /dev/null +++ b/sp-modules/auth/common.nix @@ -0,0 +1,35 @@ +{ config, lib, pkgs, ... }: +rec { + domain = config.selfprivacy.domain; + cfg = config.selfprivacy.modules.auth; + passthru = config.passthru.selfprivacy.auth; + auth-fqdn = cfg.subdomain + "." + domain; + + kanidm_ldap_port = 3636; + + # e.g. "dc=mydomain,dc=com" + ldap_base_dn = + lib.strings.concatMapStringsSep + "," + (x: "dc=" + x) + (lib.strings.splitString "." domain); + + appendLdapBindPwd = + { name, file, prefix, suffix ? "", passwordFile, destination }: + pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' + #!${pkgs.stdenv.shell} + set -euo pipefail + + baseDir=$(dirname ${destination}) + if (! test -d "$baseDir"); then + mkdir -p $baseDir + chmod 755 $baseDir + fi + + cat ${file} > ${destination} + echo -n '${prefix}' >> ${destination} + cat ${passwordFile} >> ${destination} + echo -n '${suffix}' >> ${destination} + chmod 600 ${destination} + ''; +} diff --git a/sp-modules/auth/config-paths-needed.json b/sp-modules/auth/config-paths-needed.json index 7b922da..2253194 100644 --- a/sp-modules/auth/config-paths-needed.json +++ b/sp-modules/auth/config-paths-needed.json @@ -2,6 +2,7 @@ ["mailserver", "fqdn"], ["mailserver", "ldap"], ["mailserver", "vmailUID"], + ["passthru", "selfprivacy", "auth"], ["security", "acme", "certs"], ["selfprivacy", "domain"], ["selfprivacy", "modules"], diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix index 15cfc9c..29d43bf 100644 --- a/sp-modules/auth/flake.nix +++ b/sp-modules/auth/flake.nix @@ -36,11 +36,12 @@ + /nixos/modules/services/security/oauth2-proxy-nginx.nix) ./module.nix ./ldap-postfix.nix + ./ldap-dovecot.nix ]; nixpkgs.overlays = [ self.overlays.default ]; selfprivacy.modules.auth.enable = true; - selfprivacy.modules.auth.debug = true; + selfprivacy.modules.auth.debug = false; }; configPathsNeeded = diff --git a/sp-modules/auth/ldap-dovecot.nix b/sp-modules/auth/ldap-dovecot.nix new file mode 100644 index 0000000..ee6ae92 --- /dev/null +++ b/sp-modules/auth/ldap-dovecot.nix @@ -0,0 +1,129 @@ +{ config, lib, pkgs, ... }@nixos-args: +let + inherit (import ./common.nix nixos-args) + appendLdapBindPwd + cfg + domain + passthru + ; + + ldapConfFile = "/run/dovecot2/dovecot-ldap.conf.ext"; # FIXME get "dovecot2" from `config` + mkLdapSearchScope = scope: ( + if scope == "sub" then "subtree" + else if scope == "one" then "onelevel" + else scope + ); + dovecot-ldap-config = pkgs.writeTextFile { + name = "dovecot-ldap.conf.ext.template"; + text = '' + ldap_version = 3 + uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris} + ${lib.optionalString config.mailserver.ldap.startTls '' + tls = yes + ''} + # tls_require_cert = hard + # tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile} + dn = ${config.mailserver.ldap.bind.dn} + sasl_bind = no + auth_bind = no + base = ${config.mailserver.ldap.searchBase} + scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope} + ${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) '' + user_attrs = ${config.mailserver.ldap.dovecot.userAttrs} + ''} + user_filter = ${config.mailserver.ldap.dovecot.userFilter} + ''; + }; + setPwdInLdapConfFile = appendLdapBindPwd { + name = "ldap-conf-file"; + file = dovecot-ldap-config; + prefix = ''dnpass = "''; + suffix = ''"''; + passwordFile = config.mailserver.ldap.bind.passwordFile; + destination = ldapConfFile; + }; + dovecot-oauth2-conf-file = pkgs.writeTextFile { + name = "dovecot-oauth2.conf.ext"; + text = '' + introspection_mode = post + introspection_url = ${passthru.oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"} + client_id = roundcube + client_secret = VERYSTRONGSECRETFORROUNDCUBE # FIXME + username_attribute = username + # scope = email groups profile openid dovecotprofile + scope = email profile openid + tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt + active_attribute = active + active_value = true + openid_configuration_url = ${passthru.oauth2-discovery-url "roundcube"} + debug = ${if cfg.debug then "yes" else "no"} + ''; + }; +in +{ + mailserver.ldap = { + # note: in `ldapsearch` first comes filter, then attributes + dovecot.userAttrs = "+"; # all operational attributes + # TODO: investigate whether "mail=%u" is better than: + # dovecot.userFilter = "(&(class=person)(uid=%n))"; + }; + + services.dovecot2.extraConfig = '' + auth_mechanisms = xoauth2 oauthbearer + + passdb { + driver = oauth2 + mechanisms = xoauth2 oauthbearer + args = ${dovecot-oauth2-conf-file} + } + + userdb { + driver = static + args = uid=virtualMail gid=virtualMail home=/var/vmail/${domain}/%u + } + + # provide SASL via unix socket to postfix + service auth { + unix_listener /var/lib/postfix/private-auth { + mode = 0660 + user = postfix + group = postfix + } + } + service auth { + unix_listener auth-userdb { + mode = 0660 + user = dovecot2 + } + unix_listener dovecot-auth { + mode = 0660 + # Assuming the default Postfix user and group + user = postfix + group = postfix + } + } + + userdb { + driver = ldap + args = ${ldapConfFile} + default_fields = home=/var/vmail/${domain}/%u uid=${toString config.mailserver.vmailUID} gid=${toString config.mailserver.vmailUID} + } + + #auth_username_format = %Ln + + # FIXME + auth_debug = yes + auth_debug_passwords = yes # Be cautious with this in production as it logs passwords + auth_verbose = yes + mail_debug = yes + ''; + services.dovecot2.enablePAM = false; + systemd.services.dovecot2 = { + # TODO does it merge with existing preStart? + preStart = setPwdInLdapConfFile + "\n"; + }; + + # does it merge with existing restartTriggers? + systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile ]; + +} diff --git a/sp-modules/auth/ldap-postfix.nix b/sp-modules/auth/ldap-postfix.nix index 3a0e905..0739f44 100644 --- a/sp-modules/auth/ldap-postfix.nix +++ b/sp-modules/auth/ldap-postfix.nix @@ -1,26 +1,11 @@ -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, ... }@nixos-args: let + inherit (import ./common.nix nixos-args) + appendLdapBindPwd + ; + cfg = config.mailserver; - appendLdapBindPwd = - { name, file, prefix, suffix ? "", passwordFile, destination }: - pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' - #!${pkgs.stdenv.shell} - set -euo pipefail - - baseDir=$(dirname ${destination}) - if (! test -d "$baseDir"); then - mkdir -p $baseDir - chmod 755 $baseDir - fi - - cat ${file} > ${destination} - echo -n '${prefix}' >> ${destination} - cat ${passwordFile} >> ${destination} - echo -n '${suffix}' >> ${destination} - chmod 600 ${destination} - ''; - ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf"; submissionOptions.smtpd_sender_login_maps = lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}"; @@ -65,6 +50,10 @@ let }; in { + mailserver.ldap = { + postfix.mailAttribute = "mail"; + postfix.uidAttribute = "uid"; + }; systemd.services.postfix-setup = { preStart = '' ${appendPwdInVirtualMailboxMap} @@ -75,7 +64,12 @@ in services.postfix = { # the list should be merged with other options from nixos-mailserver config.virtual_mailbox_maps = [ "ldap:${ldapVirtualMailboxMapFile}" ]; - submissionOptions = submissionOptions; + inherit submissionOptions; submissionsOptions = submissionOptions; + # extraConfig = '' + # debug_peer_list = + # debug_peer_level = 3 + # smtp_tls_security_level = encrypt + # ''; }; } diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index d51335f..063dbfd 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -1,96 +1,17 @@ -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, ... }@nixos-args: let - domain = config.selfprivacy.domain; - cfg = config.selfprivacy.modules.auth; - auth-fqdn = cfg.subdomain + "." + domain; - oauth2-introspection-url = client_id: client_secret: - "https://${client_id}:${client_secret}@${auth-fqdn}/oauth2/token/introspect"; - oauth2-discovery-url = client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; - - kanidm-bind-address = "127.0.0.1:3013"; - ldap_host = "127.0.0.1"; - ldap_port = 3636; - # e.g. "dc=mydomain,dc=com" - ldap_base_dn = - lib.strings.concatMapStringsSep - "," - (x: "dc=" + x) - (lib.strings.splitString "." domain); - - dovecot-oauth2-conf-file = pkgs.writeTextFile { - name = "dovecot-oauth2.conf.ext"; - text = '' - introspection_mode = post - introspection_url = ${oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"} - client_id = roundcube - client_secret = VERYSTRONGSECRETFORROUNDCUBE # FIXME - username_attribute = username - # scope = email groups profile openid dovecotprofile - scope = email profile openid - tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt - active_attribute = active - active_value = true - openid_configuration_url = ${oauth2-discovery-url "roundcube"} - debug = ${if cfg.debug then "yes" else "no"} - ''; - }; + inherit (import ./common.nix nixos-args) + auth-fqdn + cfg + domain + kanidm_ldap_port + ldap_base_dn + passthru + ; lua_core_path = "${pkgs.luajitPackages.lua-resty-core}/lib/lua/5.1/?.lua"; lua_lrucache_path = "${pkgs.luajitPackages.lua-resty-lrucache}/lib/lua/5.1/?.lua"; lua_path = "${lua_core_path};${lua_lrucache_path};"; - ldapConfFile = "/run/dovecot2/dovecot-ldap.conf.ext"; # FIXME get "dovecot2" from `config` - mkLdapSearchScope = scope: ( - if scope == "sub" then "subtree" - else if scope == "one" then "onelevel" - else scope - ); - appendLdapBindPwd = - { name, file, prefix, suffix ? "", passwordFile, destination }: - pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' - #!${pkgs.stdenv.shell} - set -euo pipefail - - baseDir=$(dirname ${destination}) - if (! test -d "$baseDir"); then - mkdir -p $baseDir - chmod 755 $baseDir - fi - - cat ${file} > ${destination} - echo -n '${prefix}' >> ${destination} - cat ${passwordFile} >> ${destination} - echo -n '${suffix}' >> ${destination} - chmod 600 ${destination} - ''; - dovecot-ldap-config = pkgs.writeTextFile { - name = "dovecot-ldap.conf.ext.template"; - text = '' - ldap_version = 3 - uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris} - ${lib.optionalString config.mailserver.ldap.startTls '' - tls = yes - ''} - # tls_require_cert = hard - # tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile} - dn = ${config.mailserver.ldap.bind.dn} - sasl_bind = no - auth_bind = no - base = ${config.mailserver.ldap.searchBase} - scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope} - ${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) '' - user_attrs = ${config.mailserver.ldap.dovecot.userAttrs} - ''} - user_filter = ${config.mailserver.ldap.dovecot.userFilter} - ''; - }; - setPwdInLdapConfFile = appendLdapBindPwd { - name = "ldap-conf-file"; - file = dovecot-ldap-config; - prefix = ''dnpass = "''; - suffix = ''"''; - passwordFile = config.mailserver.ldap.bind.passwordFile; - destination = ldapConfFile; - }; in { options.selfprivacy.modules.auth = { @@ -118,16 +39,6 @@ in # kanidm with Rust code patches for OAuth and admin passwords provisioning package = pkgs.kanidm.withSecretProvisioning; - # package = pkgs.kanidm.withSecretProvisioning.overrideAttrs (_: { - # version = "git"; - # src = pkgs.fetchFromGitHub { - # owner = "AleXoundOS"; - # repo = "kanidm"; - # rev = "a1a55f2e53facbfa504c7d64c44c3b5d0eb796c2"; - # hash = "sha256-ADh4Zwn6EMt4CiOrvgG0RbmNMeR5i0ilVTxF46t/wm8="; - # }; - # doCheck = false; - # }); serverSettings = { inherit domain; @@ -143,13 +54,15 @@ in tls_key = "${config.security.acme.certs.${domain}.directory}/key.pem"; - bindaddress = kanidm-bind-address; # nginx should connect to it - ldapbindaddress = "${ldap_host}:${toString ldap_port}"; + # nginx should proxy requests to it + bindaddress = passthru.kanidm-bind-address; + + ldapbindaddress = "127.0.0.1:${toString kanidm_ldap_port}"; # kanidm is behind a proxy trust_x_forward_for = true; - log_level = "trace"; # FIXME + log_level = if cfg.debug then "trace" else "info"; }; provision = { enable = true; @@ -162,24 +75,6 @@ in verify_hostnames = false; # FIXME }; }; - # systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkBefore '' - # # check kanidm online here with curl again? - # # use API key for group creation? - # ''; - # services.phpfpm.pools.roundcube.settings = { - # catch_workers_output = true; - # "php_admin_value[error_log]" = "stdout"; - # "php_admin_flag[log_errors]" = true; - # "php_admin_value[log_level]" = "debug"; - # }; - services.phpfpm.phpOptions = '' - error_reporting = E_ALL - display_errors = on; - ''; - systemd.services.phpfpm-roundcube.serviceConfig = { - StandardError = "journal"; - StandardOutput = "journal"; - }; services.nginx = { enable = true; @@ -196,11 +91,6 @@ in useACMEHost = domain; forceSSL = true; locations."/" = { - # extraConfig = '' - # if ($args != $new_args) { - # rewrite ^ /ui/oauth2?$new_args? last; - # } - # ''; extraConfig = lib.strings.optionalString cfg.debug '' access_log /var/log/nginx/kanidm.log kanidm; @@ -230,16 +120,9 @@ in end '; ''; - proxyPass = "https://${kanidm-bind-address}"; + proxyPass = "https://${passthru.kanidm-bind-address}"; }; }; - # appendHttpConfig = '' - # # Define a map to modify redirect_uri and append %2F if missing - # map $args $new_args { - # ~^((.*)(redirect_uri=[^&]+)(?!%2F)(.*))$ $2$3%2F$4; - # default $args; - # } - # ''; }; # TODO move to mailserver module everything below @@ -248,7 +131,7 @@ in mailserver.loginAccounts = lib.mkForce { }; mailserver.extraVirtualAliases = lib.mkForce { }; - # LDAP is needed for Postfix to query Kanidm about email address ownership + # LDAP is needed for Postfix to query Kanidm about email address ownership. # LDAP is needed for Dovecot also. mailserver.ldap = { enable = false; @@ -256,94 +139,24 @@ in bind.dn = "dn=token"; # TODO change in this file should trigger system restart dovecot bind.passwordFile = "/run/keys/dovecot/kanidm-service-account-token"; # FIXME + # searchBase = "ou=persons," + ldap_base_dn; - searchBase = ldap_base_dn; - # searchScope = "sub"; - uris = [ "ldaps://localhost:${toString ldap_port}" ]; + searchBase = ldap_base_dn; # TODO refine this - # note: in `ldapsearch` first comes filter, then attributes - dovecot.userAttrs = "+"; # all operational attributes - # TODO: investigate whether "mail=%u" is better than: - # dovecot.userFilter = "(&(class=person)(uid=%n))"; - postfix.mailAttribute = "mail"; - postfix.uidAttribute = "uid"; + # NOTE: 127.0.0.1 instead of localhost does not work for unknown reason + uris = [ "ldaps://localhost:${toString kanidm_ldap_port}" ]; }; - services.dovecot2.extraConfig = '' - auth_mechanisms = xoauth2 oauthbearer - - passdb { - driver = oauth2 - mechanisms = xoauth2 oauthbearer - args = ${dovecot-oauth2-conf-file} - } - - userdb { - driver = static - args = uid=virtualMail gid=virtualMail home=/var/vmail/${domain}/%u - } - - # provide SASL via unix socket to postfix - service auth { - unix_listener /var/lib/postfix/private-auth { - mode = 0660 - user = postfix - group = postfix - } - } - service auth { - unix_listener auth-userdb { - mode = 0660 - user = dovecot2 - } - unix_listener dovecot-auth { - mode = 0660 - # Assuming the default Postfix user and group - user = postfix - group = postfix - } - } - - userdb { - driver = ldap - args = ${ldapConfFile} - default_fields = home=/var/vmail/${domain}/%u uid=${toString config.mailserver.vmailUID} gid=${toString config.mailserver.vmailUID} - } - - #auth_username_format = %Ln - - # FIXME - auth_debug = yes - auth_debug_passwords = yes # Be cautious with this in production as it logs passwords - auth_verbose = yes - mail_debug = yes - ''; - services.dovecot2.enablePAM = false; - services.postfix.extraConfig = '' - debug_peer_list = 94.43.135.210, 134.209.202.195 - debug_peer_level = 3 - smtp_use_tls = yes - # these below are already set in nixos-mailserver/mail-server/postfix.nix - # smtpd_sasl_local_domain = ${domain} - # smtpd_relay_restrictions = permit_sasl_authenticated, reject - # smtpd_sender_restrictions = - # smtpd_sender_login_maps = - # smtpd_sasl_type = dovecot - # smtpd_sasl_path = private-auth - # smtpd_sasl_auth_enable = yes - ''; - - systemd.services.dovecot2 = { - # TODO does it merge with existing preStart? - preStart = setPwdInLdapConfFile + "\n"; - }; - - # does it merge with existing restartTriggers? - systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile ]; - environment.systemPackages = lib.lists.optionals cfg.debug [ pkgs.shelldap pkgs.openldap ]; + + passthru.selfprivacy.auth = { + kanidm-bind-address = "127.0.0.1:3013"; + oauth2-introspection-url = client_id: client_secret: + "https://${client_id}:${client_secret}@${auth-fqdn}/oauth2/token/introspect"; + oauth2-discovery-url = client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; + }; }; } From 3a904f599e6a73a8cfb4cea0cb411a9f7bc497eb Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 26 Dec 2024 18:27:25 +0400 Subject: [PATCH 005/115] chore: restructure LDAP related nix files --- sp-modules/auth/flake.nix | 2 - sp-modules/auth/module.nix | 83 ++++---- sp-modules/roundcube/config-paths-needed.json | 8 +- sp-modules/roundcube/module.nix | 177 ++---------------- .../common.nix | 16 +- .../config-paths-needed.json | 6 +- sp-modules/simple-nixos-mailserver/config.nix | 102 +++++++++- sp-modules/simple-nixos-mailserver/flake.nix | 2 + .../ldap-dovecot.nix | 14 +- .../ldap-postfix.nix | 8 +- 10 files changed, 178 insertions(+), 240 deletions(-) rename sp-modules/{auth => simple-nixos-mailserver}/common.nix (63%) rename sp-modules/{auth => simple-nixos-mailserver}/ldap-dovecot.nix (89%) rename sp-modules/{auth => simple-nixos-mailserver}/ldap-postfix.nix (90%) diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix index 29d43bf..f0a901a 100644 --- a/sp-modules/auth/flake.nix +++ b/sp-modules/auth/flake.nix @@ -35,8 +35,6 @@ (nixos-unstable.legacyPackages.x86_64-linux.path + /nixos/modules/services/security/oauth2-proxy-nginx.nix) ./module.nix - ./ldap-postfix.nix - ./ldap-dovecot.nix ]; nixpkgs.overlays = [ self.overlays.default ]; diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index 063dbfd..1e32ce4 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -1,14 +1,12 @@ -{ config, lib, pkgs, ... }@nixos-args: +{ config, lib, pkgs, ... }: let - inherit (import ./common.nix nixos-args) - auth-fqdn - cfg - domain - kanidm_ldap_port - ldap_base_dn - passthru - ; + passthru = config.passthru.selfprivacy.auth; + cfg = config.selfprivacy.modules.auth; + domain = config.selfprivacy.domain; + kanidm-bind-address = "127.0.0.1:3013"; + + # lua stuff for debugging only lua_core_path = "${pkgs.luajitPackages.lua-resty-core}/lib/lua/5.1/?.lua"; lua_lrucache_path = "${pkgs.luajitPackages.lua-resty-lrucache}/lib/lua/5.1/?.lua"; lua_path = "${lua_core_path};${lua_lrucache_path};"; @@ -34,6 +32,9 @@ in # FIXME revise this: maybe kanidm must not have access to a public TLS users.groups."acmereceivers".members = [ "kanidm" ]; + # for ExecStartPost scripts to have access to /run/keys/* + users.groups.keys.members = [ "kanidm" ]; + services.kanidm = { enableServer = true; @@ -46,7 +47,7 @@ in # included if it is non-standard (any port except 443). This must match or # be a descendent of the domain name you configure above. If these two # items are not consistent, the server WILL refuse to start! - origin = "https://" + auth-fqdn; + origin = "https://" + passthru.auth-fqdn; # TODO revise this: maybe kanidm must not have access to a public TLS tls_chain = @@ -55,9 +56,9 @@ in "${config.security.acme.certs.${domain}.directory}/key.pem"; # nginx should proxy requests to it - bindaddress = passthru.kanidm-bind-address; + bindaddress = kanidm-bind-address; - ldapbindaddress = "127.0.0.1:${toString kanidm_ldap_port}"; + ldapbindaddress = "127.0.0.1:${toString passthru.ldap-port}"; # kanidm is behind a proxy trust_x_forward_for = true; @@ -70,7 +71,7 @@ in }; enableClient = true; clientSettings = { - uri = "https://" + auth-fqdn; + uri = "https://" + passthru.auth-fqdn; verify_ca = false; # FIXME verify_hostnames = false; # FIXME }; @@ -79,19 +80,21 @@ in services.nginx = { enable = true; additionalModules = - lib.lists.optional cfg.debug pkgs.nginxModules.lua; - commonHttpConfig = lib.strings.optionalString cfg.debug '' + lib.mkIf cfg.debug pkgs.nginxModules.lua; + commonHttpConfig = lib.mkIf cfg.debug '' log_format kanidm escape=none '$request $status\n' '[Request body]: $request_body\n' '[Header]: $resp_header\n' '[Response Body]: $resp_body\n\n'; lua_package_path "${lua_path}"; ''; - virtualHosts.${auth-fqdn} = { + virtualHosts.${passthru.auth-fqdn} = { useACMEHost = domain; forceSSL = true; locations."/" = { - extraConfig = lib.strings.optionalString cfg.debug '' + # be aware that such logging mechanism breaks Kanidm authentication + # (but authorization works) + extraConfig = lib.mkIf cfg.debug '' access_log /var/log/nginx/kanidm.log kanidm; lua_need_request_body on; @@ -120,43 +123,27 @@ in end '; ''; - proxyPass = "https://${passthru.kanidm-bind-address}"; + proxyPass = "https://${kanidm-bind-address}"; }; }; }; - # TODO move to mailserver module everything below - mailserver.debug = cfg.debug; # FIXME - mailserver.mailDirectory = "/var/vmail"; - - mailserver.loginAccounts = lib.mkForce { }; - mailserver.extraVirtualAliases = lib.mkForce { }; - # LDAP is needed for Postfix to query Kanidm about email address ownership. - # LDAP is needed for Dovecot also. - mailserver.ldap = { - enable = false; - # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; - bind.dn = "dn=token"; - # TODO change in this file should trigger system restart dovecot - bind.passwordFile = "/run/keys/dovecot/kanidm-service-account-token"; # FIXME - - # searchBase = "ou=persons," + ldap_base_dn; - searchBase = ldap_base_dn; # TODO refine this - - # NOTE: 127.0.0.1 instead of localhost does not work for unknown reason - uris = [ "ldaps://localhost:${toString kanidm_ldap_port}" ]; - }; - - environment.systemPackages = lib.lists.optionals cfg.debug [ - pkgs.shelldap - pkgs.openldap - ]; - - passthru.selfprivacy.auth = { - kanidm-bind-address = "127.0.0.1:3013"; + passthru.selfprivacy.auth = rec { + auth-fqdn = cfg.subdomain + "." + domain; oauth2-introspection-url = client_id: client_secret: "https://${client_id}:${client_secret}@${auth-fqdn}/oauth2/token/introspect"; - oauth2-discovery-url = client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; + oauth2-discovery-url = client_id: + "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; + oauth2-provider-name = "kanidm"; + oauth2-systemd-service = "kanidm.service"; + + # e.g. "dc=mydomain,dc=com" + ldap-base-dn = + lib.strings.concatMapStringsSep + "," + (x: "dc=" + x) + (lib.strings.splitString "." domain); + ldap-port = 3636; }; }; } diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index f017fdd..f40e8f4 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -1,7 +1,9 @@ [ + ["mailserver", "fqdn"], + ["passthru", "selfprivacy", "auth", "auth-fqdn"], + ["passthru", "selfprivacy", "auth", "oauth2-provider-name"], ["selfprivacy", "domain"], - ["selfprivacy", "modules", "roundcube"], ["selfprivacy", "modules", "auth"], - ["service", "kanidm"], - ["mailserver", "fqdn"] + ["selfprivacy", "modules", "roundcube"], + ["service", "kanidm"] ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index a2b60df..a2ca800 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -2,82 +2,10 @@ let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; - auth-module = config.selfprivacy.modules.auth; - auth-fqdn = auth-module.subdomain + "." + domain; + is-auth-enabled = config.selfprivacy.modules.auth.enable; + auth-passthru = config.passthru.selfprivacy.auth; + auth-fqdn = auth-passthru.auth-fqdn; oauth-client-id = "roundcube"; - dovecot-service-account-name = "dovecot-service-account"; - postfix-service-account-name = "postfix-service-account"; - dovecot-service-account-token-name = "dovecot-service-account-token"; - postfix-service-account-token-name = "postfix-service-account-token"; - # dovecot-service-account-token-fp = "/run/kanidm/token/dovecot"; - dovecot-service-account-token-fp = - "/run/keys/dovecot/kanidm-service-account-token"; - postfix-service-account-token-fp = - "/run/keys/postfix/kanidm-service-account-token"; - dovecot-group = "dovecot2"; # FIXME - postfix-group = "postfix"; # FIXME - # FIXME use usernames and groups from `config` - # FIXME dependency on dovecot2 and postfix - # set-group-ID bit allows for kanidm user to create files, - # which inherit directory group (.e.g dovecot, postfix) - kanidmExecStartPostScriptRoot = pkgs.writeShellScript - "roundcube-kanidm-ExecStartPost-root-script.sh" - '' - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/dovecot - chown kanidm:dovecot2 /run/keys/dovecot - - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/postfix - chown kanidm:postfix /run/keys/postfix - ''; - # FIXME parameterize names like "dovecot2" group - kanidmExecStartPostScript = pkgs.writeShellScript - "roundcube-kanidm-ExecStartPost-script.sh" - '' - export HOME=$RUNTIME_DIRECTORY/client_home - readonly KANIDM="${pkgs.kanidm}/bin/kanidm" - - # get Kanidm service account for Dovecot - KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${dovecot-service-account-name}$")" - echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" - if [ -n "$KANIDM_SERVICE_ACCOUNT" ] - then - echo "kanidm service account \"${dovecot-service-account-name}\" is found" - else - echo "kanidm service account \"${dovecot-service-account-name}\" is not found" - echo "creating new kanidm service account \"${dovecot-service-account-name}\"" - if $KANIDM service-account create --name idm_admin ${dovecot-service-account-name} ${dovecot-service-account-name} idm_admin - then - "kanidm service account \"${dovecot-service-account-name}\" created" - else - echo "error: cannot create kanidm service account \"${dovecot-service-account-name}\"" - exit 1 - fi - fi - - # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers ${dovecot-service-account-name} - - # create a new read-only token for Dovecot - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${dovecot-service-account-name} ${dovecot-service-account-token-name} --output json)" - then - echo "error: kanidm CLI returns an error when trying to generate service-account api-token" - exit 1 - fi - if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" - then - echo "error: cannot get service-account API token from JSON" - exit 1 - fi - - # if ! printf "%s\n" "$KANIDM_SERVICE_ACCOUNT_TOKEN" > ${dovecot-service-account-token-fp} - if ! install --mode=640 \ - <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ - ${dovecot-service-account-token-fp} - then - echo "error: cannot write token to \"${dovecot-service-account-token-fp}\"" - exit 1 - fi - ''; in { options.selfprivacy.modules.roundcube = { @@ -105,115 +33,44 @@ in }; config = lib.mkIf cfg.enable { - # FIXME get user names from `config` - # in order to allow access below /run/keys - users.groups.keys.members = [ "kanidm" "dovecot2" "postfix" ]; services.roundcube = { enable = true; - # package = pkgs.roundcube.overrideAttrs (_: rec { - # version = "1.6.9"; - # src = pkgs.fetchurl { - # url = "https://github.com/roundcube/roundcubemail/releases/download/${version}/roundcubemail-${version}-complete.tar.gz"; - # sha256 = "sha256-thpfXCL4kMKZ6TWqz88IcGdpkNiuv/DWzf8HW/F8708="; - # }; - # # src = pkgs.fetchurl { - # # url = "https://github.com/roundcube/roundcubemail/archive/master/3a6e25a5b386e0d87427b934ccd2e0e282e0a74e.tar.gz"; - # # sha256 = "sha256-EpEI4E+r3reYbI/5rquia+zgz1+6k49lPChlp4QiZTE="; - # # }; - # postFixup = '' - # cp -v ${/data/sp/roundcubemail-1.6.9/program/include/rcmail_oauth.php} $out/program/include/rcmail_oauth.php - # cp -v ${/data/sp/roundcubemail-1.6.9/program/actions/login/oauth.php} $out/program/actions/login/oauth.php - # rm -r $out/program/localization/* - # ''; - # }); - # package = pkgs.runCommandNoCCLocal "roundcube-debug" {} '' - # cp -r --no-preserve=all ${pkgs.roundcube} $out - # cp -v ${/data/sp/roundcubemail-1.6.8/plugins/debug_logger/debug_logger.php} $out/plugins/debug_logger/debug_logger.php - # cp -v ${/data/sp/roundcubemail-1.6.8/program/include/rcmail_oauth.php} $out/program/include/rcmail_oauth.php - # ''; # this is the url of the vhost, not necessarily the same as the fqdn of # the mailserver hostName = "${cfg.subdomain}.${config.selfprivacy.domain}"; - # plugins = [ "debug_logger" ]; extraConfig = '' # starttls needed for authentication, so the fqdn required to match # the certificate $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; - # $config['smtp_user'] = "%u"; - # $config['smtp_pass'] = "%p"; - '' + lib.strings.optionalString auth-module.enable '' + $config['smtp_user'] = "%u"; + $config['smtp_pass'] = "%p"; + '' + lib.strings.optionalString is-auth-enabled '' $config['oauth_provider'] = 'generic'; - $config['oauth_provider_name'] = 'kanidm'; # FIXME + $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; $config['oauth_client_id'] = '${oauth-client-id}'; $config['oauth_client_secret'] = 'VERYSTRONGSECRETFORROUNDCUBE'; # FIXME - $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; - $config['oauth_scope'] = 'email profile openid'; - # $config['oauth_scope'] = 'email openid dovecotprofile'; + $config['oauth_scope'] = 'email profile openid'; # FIXME $config['oauth_auth_parameters'] = []; $config['oauth_identity_fields'] = ['email']; - $config['oauth_login_redirect'] = false; + $config['oauth_login_redirect'] = true; $config['auto_create_user'] = true; - - $config['log_dir'] = '/tmp/roundcube'; - $config['log_driver'] = 'stdout'; - $config['log_errors'] = 1; - // Log SQL queries to /sql or to syslog - $config['sql_debug'] = true; - - // Log IMAP conversation to /imap or to syslog - $config['imap_debug'] = true; - $config['log_debug'] = true; - $config['oauth_debug'] = true; - - // Log LDAP conversation to /ldap or to syslog - $config['ldap_debug'] = true; - - // Log SMTP conversation to /smtp or to syslog - $config['smtp_debug'] = true; - - $config['debug_logger']['master'] = 'master'; - $config['debug_logger']['oauth'] = 'oauth'; - $config['debug_logger']['imap'] = 'imap'; - $config['debug_logger']['log'] = 'log'; - $config['debug_logger']['smtp'] = 'smtp'; - - $config['oauth_verify_peer'] = false; - $config['log_logins'] = true; - $config['log_session'] = true; - # $config['oauth_pkce'] = 'S256'; + $config['oauth_verify_peer'] = false; # FIXME + # $config['oauth_pkce'] = 'S256'; # FIXME ''; }; + services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { forceSSL = true; useACMEHost = domain; enableACME = false; - # extraConfig = '' - # add_header X-Frame-Options DENY; - # add_header X-Content-Type-Options nosniff; - # add_header X-XSS-Protection "1; mode=block"; - # ''; - }; - systemd = { - services = { - phpfpm-roundcube.serviceConfig = { - Slice = lib.mkForce "roundcube.slice"; - StandardError = "journal"; - StandardOutput = "journal"; - }; - kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ - ("+" + kanidmExecStartPostScriptRoot) - kanidmExecStartPostScript - ]; - }; - slices.roundcube = { - description = "Roundcube service slice"; - }; }; - services.kanidm.provision = lib.mkIf auth-module.enable { + systemd.slices.roundcube.description = "Roundcube service slice"; + + services.kanidm.provision = lib.mkIf is-auth-enabled { groups.roundcube_users.present = true; systems.oauth2.roundcube = { displayName = "Roundcube"; @@ -228,7 +85,7 @@ in "profile" "openid" ]; - # scopeMaps.roundcube_users = [ + # scopeMaps."sp.roundcube.users" = [ # "email" # "openid" # "dovecotprofile" @@ -238,7 +95,7 @@ in # claimMaps.groups = { # joinType = "array"; # valuesByGroup = { - # "sp.roundcube.admin" = [ "admin" ]; + # "sp.roundcube.admins" = [ "admin" ]; # }; # }; }; diff --git a/sp-modules/auth/common.nix b/sp-modules/simple-nixos-mailserver/common.nix similarity index 63% rename from sp-modules/auth/common.nix rename to sp-modules/simple-nixos-mailserver/common.nix index 91f0f0c..d7544e6 100644 --- a/sp-modules/auth/common.nix +++ b/sp-modules/simple-nixos-mailserver/common.nix @@ -1,18 +1,8 @@ -{ config, lib, pkgs, ... }: +{ config, pkgs, ... }: rec { + auth-passthru = config.passthru.selfprivacy.auth; domain = config.selfprivacy.domain; - cfg = config.selfprivacy.modules.auth; - passthru = config.passthru.selfprivacy.auth; - auth-fqdn = cfg.subdomain + "." + domain; - - kanidm_ldap_port = 3636; - - # e.g. "dc=mydomain,dc=com" - ldap_base_dn = - lib.strings.concatMapStringsSep - "," - (x: "dc=" + x) - (lib.strings.splitString "." domain); + is-auth-enabled = config.selfprivacy.modules.auth.enable; appendLdapBindPwd = { name, file, prefix, suffix ? "", passwordFile, destination }: diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index 9341829..1c1df79 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -1,8 +1,11 @@ [ [ "mailserver" ], + [ "passthru", "selfprivacy", "auth" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "hashedMasterPassword" ], + [ "selfprivacy", "modules", "auth", "enable" ], + [ "selfprivacy", "modules", "simple-nixos-mailserver" ], [ "selfprivacy", "useBinds" ], [ "selfprivacy", "username" ], [ "selfprivacy", "users" ], @@ -11,6 +14,5 @@ [ "services", "postfix", "group" ], [ "services", "postfix", "user" ], [ "services", "redis" ], - [ "services", "rspamd" ], - [ "selfprivacy", "modules", "simple-nixos-mailserver" ] + [ "services", "rspamd" ] ] diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 984289f..c7da54d 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -1,6 +1,71 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: let sp = config.selfprivacy; + + inherit (import ./common.nix {inherit config pkgs;}) + auth-passthru + domain + is-auth-enabled + ; + + mailserver-service-account-name = "sp.mailserver.service-account"; + mailserver-service-account-token-name = "mailserver-service-account-token"; + mailserver-service-account-token-fp = + "/run/keys/mailserver/kanidm-service-account-token"; # FIXME sync with auth module + kanidmExecStartPostScriptRoot = pkgs.writeShellScript + "mailserver-kanidm-ExecStartPost-root-script.sh" + '' + # set-group-ID bit allows for kanidm user to create files, + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/mailserver + chown kanidm:kanidm /run/keys/mailserver + ''; + kanidmExecStartPostScript = pkgs.writeShellScript + "mailserver-kanidm-ExecStartPost-script.sh" + '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + + # get Kanidm service account for mailserver + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${mailserver-service-account-name}\" is found" + else + echo "kanidm service account \"${mailserver-service-account-name}\" is not found" + echo "creating new kanidm service account \"${mailserver-service-account-name}\"" + if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin + then + "kanidm service account \"${mailserver-service-account-name}\" created" + else + echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\"" + exit 1 + fi + fi + + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers ${mailserver-service-account-name} + + # create a new read-only token for mailserver + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi + + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${mailserver-service-account-token-fp} + then + echo "error: cannot write token to \"${mailserver-service-account-token-fp}\"" + exit 1 + fi + ''; in lib.mkIf sp.modules.simple-nixos-mailserver.enable { @@ -42,7 +107,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable # A list of all login accounts. To create the password hashes, use # mkpasswd -m sha-512 "super secret password" - loginAccounts = { + loginAccounts = lib.mkIf (!is-auth-enabled) ({ "${sp.username}@${sp.domain}" = { hashedPassword = sp.hashedMasterPassword; sieveScript = '' @@ -69,9 +134,9 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable ''; }; }) - sp.users); + sp.users)); - extraVirtualAliases = { + extraVirtualAliases = lib.mkIf (!is-auth-enabled) { "admin@${sp.domain}" = "${sp.username}@${sp.domain}"; }; @@ -90,6 +155,26 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable enableManageSieve = true; virusScanning = false; + + mailDirectory = "/var/vmail"; + + # LDAP is needed for Postfix to query Kanidm about email address ownership. + # LDAP is needed for Dovecot also. + ldap = lib.mkIf is-auth-enabled { + # false; otherwise, simple-nixos-mailserver enables auth via LDAP + enable = false; + + # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; + bind.dn = "dn=token"; + # TODO change in this file should trigger system restart dovecot + bind.passwordFile = "/run/keys/mailserver/kanidm-service-account-token"; # FIXME + + # searchBase = "ou=persons," + ldap_base_dn; + searchBase = auth-passthru.ldap-base-dn; # TODO refine this + + # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) + uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; + }; }; systemd = { @@ -99,6 +184,15 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + # FIXME set auth module option instead + kanidm.serviceConfig.ExecStartPost = + lib.mkIf is-auth-enabled + (lib.mkAfter + [ + ("+" + kanidmExecStartPostScriptRoot) + kanidmExecStartPostScript + ] + ); }; slices."simple_nixos_mailserver" = { name = "simple_nixos_mailserver.slice"; diff --git a/sp-modules/simple-nixos-mailserver/flake.nix b/sp-modules/simple-nixos-mailserver/flake.nix index abff9aa..ebc9a0e 100644 --- a/sp-modules/simple-nixos-mailserver/flake.nix +++ b/sp-modules/simple-nixos-mailserver/flake.nix @@ -10,6 +10,8 @@ mailserver.nixosModules.default ./options.nix ./config.nix + ./ldap-postfix.nix + ./ldap-dovecot.nix ]; }; configPathsNeeded = diff --git a/sp-modules/auth/ldap-dovecot.nix b/sp-modules/simple-nixos-mailserver/ldap-dovecot.nix similarity index 89% rename from sp-modules/auth/ldap-dovecot.nix rename to sp-modules/simple-nixos-mailserver/ldap-dovecot.nix index ee6ae92..4a22442 100644 --- a/sp-modules/auth/ldap-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/ldap-dovecot.nix @@ -4,7 +4,7 @@ let appendLdapBindPwd cfg domain - passthru + auth-passthru ; ldapConfFile = "/run/dovecot2/dovecot-ldap.conf.ext"; # FIXME get "dovecot2" from `config` @@ -46,21 +46,20 @@ let name = "dovecot-oauth2.conf.ext"; text = '' introspection_mode = post - introspection_url = ${passthru.oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"} + introspection_url = ${auth-passthru.oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"} client_id = roundcube client_secret = VERYSTRONGSECRETFORROUNDCUBE # FIXME username_attribute = username - # scope = email groups profile openid dovecotprofile scope = email profile openid tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt active_attribute = active active_value = true - openid_configuration_url = ${passthru.oauth2-discovery-url "roundcube"} - debug = ${if cfg.debug then "yes" else "no"} + openid_configuration_url = ${auth-passthru.oauth2-discovery-url "roundcube"} + debug = "no" ''; }; in -{ +lib.mkIf config.selfprivacy.modules.auth.enable { mailserver.ldap = { # note: in `ldapsearch` first comes filter, then attributes dovecot.userAttrs = "+"; # all operational attributes @@ -121,6 +120,9 @@ in systemd.services.dovecot2 = { # TODO does it merge with existing preStart? preStart = setPwdInLdapConfFile + "\n"; + # FIXME pass dependant services to auth module option instead + wants = [ "kanidm.service" ]; + after = [ "kanidm.service" ]; }; # does it merge with existing restartTriggers? diff --git a/sp-modules/auth/ldap-postfix.nix b/sp-modules/simple-nixos-mailserver/ldap-postfix.nix similarity index 90% rename from sp-modules/auth/ldap-postfix.nix rename to sp-modules/simple-nixos-mailserver/ldap-postfix.nix index 0739f44..217f9bf 100644 --- a/sp-modules/auth/ldap-postfix.nix +++ b/sp-modules/simple-nixos-mailserver/ldap-postfix.nix @@ -2,6 +2,7 @@ let inherit (import ./common.nix nixos-args) appendLdapBindPwd + auth-passthru ; cfg = config.mailserver; @@ -49,7 +50,7 @@ let destination = ldapVirtualMailboxMapFile; }; in -{ +lib.mkIf config.selfprivacy.modules.auth.enable { mailserver.ldap = { postfix.mailAttribute = "mail"; postfix.uidAttribute = "uid"; @@ -59,7 +60,10 @@ in ${appendPwdInVirtualMailboxMap} ${appendPwdInSenderLoginMap} ''; - restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; + restartTriggers = + [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; + wants = [ auth-passthru.oauth2-systemd-service ]; + after = [ "kanidm.service" ]; }; services.postfix = { # the list should be merged with other options from nixos-mailserver From f07b867af2e9b491ea0f93518d8b349f1d0965d7 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 26 Dec 2024 18:42:41 +0400 Subject: [PATCH 006/115] security: harden some SP modules NixOS config evaluation permissions --- sp-modules/auth/config-paths-needed.json | 17 ++++++++--------- sp-modules/roundcube/config-paths-needed.json | 13 ++++++------- .../config-paths-needed.json | 4 +++- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sp-modules/auth/config-paths-needed.json b/sp-modules/auth/config-paths-needed.json index 2253194..f42cfd1 100644 --- a/sp-modules/auth/config-paths-needed.json +++ b/sp-modules/auth/config-paths-needed.json @@ -1,11 +1,10 @@ [ - ["mailserver", "fqdn"], - ["mailserver", "ldap"], - ["mailserver", "vmailUID"], - ["passthru", "selfprivacy", "auth"], - ["security", "acme", "certs"], - ["selfprivacy", "domain"], - ["selfprivacy", "modules"], - ["services"], - ["systemd", "services", "kanidm"] + [ "passthru", "selfprivacy", "auth" ], + [ "security", "acme", "certs" ], + [ "selfprivacy", "domain" ], + [ "selfprivacy", "modules", "auth" ], + [ "services", "kanidm" ], + [ "services", "oauth2-proxy", "enable" ], + [ "services", "oauth2-proxy", "nginx" ], + [ "systemd", "services", "kanidm" ] ] diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index f40e8f4..31c78d0 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -1,9 +1,8 @@ [ - ["mailserver", "fqdn"], - ["passthru", "selfprivacy", "auth", "auth-fqdn"], - ["passthru", "selfprivacy", "auth", "oauth2-provider-name"], - ["selfprivacy", "domain"], - ["selfprivacy", "modules", "auth"], - ["selfprivacy", "modules", "roundcube"], - ["service", "kanidm"] + [ "mailserver", "fqdn" ], + [ "passthru", "selfprivacy", "auth", "auth-fqdn" ], + [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], + [ "selfprivacy", "domain" ], + [ "selfprivacy", "modules", "auth" ], + [ "selfprivacy", "modules", "roundcube" ] ] diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index 1c1df79..e717a3d 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -13,6 +13,8 @@ [ "services", "opendkim" ], [ "services", "postfix", "group" ], [ "services", "postfix", "user" ], - [ "services", "redis" ], + [ "services", "redis", "servers", "rspamd", "bind" ], + [ "services", "redis", "servers", "rspamd", "port" ], + [ "services", "redis", "servers", "rspamd", "requirePass" ], [ "services", "rspamd" ] ] From 69c69dfb4613e6c4caa893cfe3fd714fc3e4470d Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 27 Dec 2024 07:46:36 +0400 Subject: [PATCH 007/115] chore dovecot&postfix: rename nix files, disable debug --- .../{ldap-dovecot.nix => auth-dovecot.nix} | 23 ++++++++----------- .../{ldap-postfix.nix => auth-postfix.nix} | 11 +++------ sp-modules/simple-nixos-mailserver/flake.nix | 4 ++-- 3 files changed, 15 insertions(+), 23 deletions(-) rename sp-modules/simple-nixos-mailserver/{ldap-dovecot.nix => auth-dovecot.nix} (90%) rename sp-modules/simple-nixos-mailserver/{ldap-postfix.nix => auth-postfix.nix} (90%) diff --git a/sp-modules/simple-nixos-mailserver/ldap-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix similarity index 90% rename from sp-modules/simple-nixos-mailserver/ldap-dovecot.nix rename to sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 4a22442..55f3197 100644 --- a/sp-modules/simple-nixos-mailserver/ldap-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -21,8 +21,8 @@ let ${lib.optionalString config.mailserver.ldap.startTls '' tls = yes ''} - # tls_require_cert = hard - # tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile} + tls_require_cert = hard + tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile} dn = ${config.mailserver.ldap.bind.dn} sasl_bind = no auth_bind = no @@ -108,24 +108,21 @@ lib.mkIf config.selfprivacy.modules.auth.enable { default_fields = home=/var/vmail/${domain}/%u uid=${toString config.mailserver.vmailUID} gid=${toString config.mailserver.vmailUID} } - #auth_username_format = %Ln - - # FIXME - auth_debug = yes - auth_debug_passwords = yes # Be cautious with this in production as it logs passwords - auth_verbose = yes - mail_debug = yes + # with debugging OAuth2 token gets printed in logs + # auth_debug = yes + # auth_debug_passwords = yes + # auth_verbose = yes + # mail_debug = yes ''; services.dovecot2.enablePAM = false; systemd.services.dovecot2 = { # TODO does it merge with existing preStart? preStart = setPwdInLdapConfFile + "\n"; - # FIXME pass dependant services to auth module option instead - wants = [ "kanidm.service" ]; - after = [ "kanidm.service" ]; + # FIXME pass dependant services to auth module option instead? + wants = [ auth-passthru.oauth2-systemd-service ]; + after = [ auth-passthru.oauth2-systemd-service ]; }; # does it merge with existing restartTriggers? systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile ]; - } diff --git a/sp-modules/simple-nixos-mailserver/ldap-postfix.nix b/sp-modules/simple-nixos-mailserver/auth-postfix.nix similarity index 90% rename from sp-modules/simple-nixos-mailserver/ldap-postfix.nix rename to sp-modules/simple-nixos-mailserver/auth-postfix.nix index 217f9bf..6404380 100644 --- a/sp-modules/simple-nixos-mailserver/ldap-postfix.nix +++ b/sp-modules/simple-nixos-mailserver/auth-postfix.nix @@ -14,8 +14,8 @@ let server_host = ${lib.concatStringsSep " " cfg.ldap.uris} start_tls = ${if cfg.ldap.startTls then "yes" else "no"} version = 3 - # tls_ca_cert_file = ${cfg.ldap.tlsCAFile} - # tls_require_cert = yes + tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + tls_require_cert = yes search_base = ${cfg.ldap.searchBase} scope = ${cfg.ldap.searchScope} @@ -63,17 +63,12 @@ lib.mkIf config.selfprivacy.modules.auth.enable { restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; wants = [ auth-passthru.oauth2-systemd-service ]; - after = [ "kanidm.service" ]; + after = [ auth-passthru.oauth2-systemd-service ]; }; services.postfix = { # the list should be merged with other options from nixos-mailserver config.virtual_mailbox_maps = [ "ldap:${ldapVirtualMailboxMapFile}" ]; inherit submissionOptions; submissionsOptions = submissionOptions; - # extraConfig = '' - # debug_peer_list = - # debug_peer_level = 3 - # smtp_tls_security_level = encrypt - # ''; }; } diff --git a/sp-modules/simple-nixos-mailserver/flake.nix b/sp-modules/simple-nixos-mailserver/flake.nix index ebc9a0e..333e097 100644 --- a/sp-modules/simple-nixos-mailserver/flake.nix +++ b/sp-modules/simple-nixos-mailserver/flake.nix @@ -10,8 +10,8 @@ mailserver.nixosModules.default ./options.nix ./config.nix - ./ldap-postfix.nix - ./ldap-dovecot.nix + ./auth-postfix.nix + ./auth-dovecot.nix ]; }; configPathsNeeded = From c127145425a7f9c79d4dea249ed7a1b73248b546 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 27 Dec 2024 07:49:31 +0400 Subject: [PATCH 008/115] feat(auth,roundcube): members of sp.admins group become admins --- sp-modules/auth/kanidm.nix | 196 ++++++++++++++++++-------------- sp-modules/auth/module.nix | 3 +- sp-modules/roundcube/module.nix | 39 +++---- 3 files changed, 133 insertions(+), 105 deletions(-) diff --git a/sp-modules/auth/kanidm.nix b/sp-modules/auth/kanidm.nix index 6e33eb5..2d2ecc2 100644 --- a/sp-modules/auth/kanidm.nix +++ b/sp-modules/auth/kanidm.nix @@ -1,9 +1,8 @@ -{ - config, - lib, - options, - pkgs, - ... +{ config +, lib +, options +, pkgs +, ... }: let inherit (lib) @@ -55,16 +54,17 @@ let # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. hasPrefixInList = list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list; - mergePaths = foldl' ( - merged: newPath: - let - # If the new path is a prefix to some existing path, we need to filter it out - filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; - # If a prefix of the new path is already in the list, do not add it - filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; - in - filteredPaths ++ filteredNew - ) [ ]; + mergePaths = foldl' + ( + merged: newPath: + let + # If the new path is a prefix to some existing path, we need to filter it out + filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; + # If a prefix of the new path is already in the list, do not add it + filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; + in + filteredPaths ++ filteredNew + ) [ ]; defaultServiceConfig = { # Setting the type to notify enables additional healthchecks, ensuring units @@ -126,13 +126,16 @@ let filterPresent = filterAttrs (_: v: v.present); - isGroupEmpty = g: ! g ? members || g ? members && g.members == [ ]; + isGroupNonOverwritable = g: false + || ! g ? members + || g ? members && g.members == [ ] + || g ? members && g.members == [ "sp.admins" ]; provisionStateJson = pkgs.writeText "provision-state.json" ( builtins.toJSON { inherit (cfg.provision) persons systems; groups = - lib.attrsets.filterAttrs (_n: v: ! isGroupEmpty v) cfg.provision.groups; + lib.attrsets.filterAttrs (_n: v: ! isGroupNonOverwritable v) cfg.provision.groups; } ); @@ -175,21 +178,27 @@ let fi ''; - emptyGroupsNames = - builtins.attrNames - (lib.attrsets.filterAttrs (_n: isGroupEmpty) cfg.provision.groups); + groupsToCreateAndPopulate = + lib.attrsets.filterAttrs (_n: isGroupNonOverwritable) cfg.provision.groups; - createEmptyGroups = '' - readonly CLIENT_HOME=$RUNTIME_DIRECTORY/client_home - mkdir -p $CLIENT_HOME - HOME=$CLIENT_HOME KANIDM_PASSWORD="$KANIDM_IDM_ADMIN_PASSWORD" ${cfg.package}/bin/kanidm login --name idm_admin --url "${cfg.provision.instanceUrl}" --skip-hostname-verification - for group_name in ${lib.strings.concatLines emptyGroupsNames} + createGroups = '' + for group_name in ${lib.strings.concatStringsSep " " (builtins.attrNames groupsToCreateAndPopulate)} do - HOME=$CLIENT_HOME ${cfg.package}/bin/kanidm group create "$group_name" --name idm_admin --url "${cfg.provision.instanceUrl}" --skip-hostname-verification + ${cfg.package}/bin/kanidm group create "$group_name" done - # rm -r $CLIENT_HOME ''; + populateGroup = group_name: group: '' + for member_name in ${lib.strings.concatStringsSep " " group.members} + do + ${cfg.package}/bin/kanidm group add-members "${group_name}" "$member_name" + done + ''; + + createAndPopulateGroups = + lib.concatLines ([ createGroups ] + ++ (lib.mapAttrsToList populateGroup groupsToCreateAndPopulate)); + postStartScript = pkgs.writeShellScript "post-start" '' set -euo pipefail @@ -209,7 +218,19 @@ let ${recoverIdmAdmin} ${maybeRecoverAdmin} - ${createEmptyGroups} + + readonly CLIENT_HOME=$RUNTIME_DIRECTORY/client_home + mkdir -p "$CLIENT_HOME" + export HOME="$CLIENT_HOME" + export KANIDM_NAME=idm_admin + export KANIDM_URL="${cfg.provision.instanceUrl}" + export KANIDM_SKIP_HOSTNAME_VERIFICATION="true" + KANIDM_PASSWORD="$KANIDM_IDM_ADMIN_PASSWORD" ${cfg.package}/bin/kanidm login + ${createAndPopulateGroups} + unset HOME + unset KANIDM_NAME + unset KANIDM_URL + unset KANIDM_SKIP_HOSTNAME_VERIFICATION KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \ ${getExe pkgs.kanidm-provision} \ @@ -225,11 +246,11 @@ let last (splitString "]:" cfg.serverSettings.bindaddress) else # ipv4: - if hasInfix "." cfg.serverSettings.bindaddress then - last (splitString ":" cfg.serverSettings.bindaddress) - # default is 8443 - else - "8443"; + if hasInfix "." cfg.serverSettings.bindaddress then + last (splitString ":" cfg.serverSettings.bindaddress) + # default is 8443 + else + "8443"; in { options.services.kanidm = { @@ -451,9 +472,11 @@ in config.members = concatLists ( flip mapAttrsToList cfg.provision.persons ( person: personCfg: - optional ( - personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups - ) person + optional + ( + personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups + ) + person ) ); }) @@ -646,9 +669,12 @@ in ++ entityList "oauth2" cfg.provision.systems.oauth2; # Accumulate entities by name. Track corresponding entity types for later duplicate check. - entitiesByName = foldl' ( - acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } - ) { } entities; + entitiesByName = foldl' + ( + acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } + ) + { } + entities; assertGroupsKnown = opt: groups: @@ -760,59 +786,59 @@ in ] ++ flip mapAttrsToList (filterPresent cfg.provision.persons) ( person: personCfg: - assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups + assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups ) ++ flip mapAttrsToList (filterPresent cfg.provision.groups) ( group: groupCfg: - assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members + assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members ) ++ concatLists ( flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( oauth2: oauth2Cfg: - [ - (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( - attrNames oauth2Cfg.scopeMaps - )) - (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( - attrNames oauth2Cfg.supplementaryScopeMaps - )) - ] - ++ concatLists ( - flip mapAttrsToList oauth2Cfg.claimMaps ( - claim: claimCfg: [ - (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( - attrNames claimCfg.valuesByGroup - )) - # At least one group must map to a value in each claim map - { - assertion = - (cfg.provision.enable && cfg.enableServer) - -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup); - message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; - } - # Public clients cannot define a basic secret - { - assertion = - (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; - message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; - } - # Public clients cannot disable PKCE - { - assertion = - (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) - -> !oauth2Cfg.allowInsecureClientDisablePkce; - message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE"; - } - # Non-public clients cannot enable localhost redirects - { - assertion = - (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) - -> !oauth2Cfg.enableLocalhostRedirects; - message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; - } - ] + [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( + attrNames oauth2Cfg.scopeMaps + )) + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( + attrNames oauth2Cfg.supplementaryScopeMaps + )) + ] + ++ concatLists ( + flip mapAttrsToList oauth2Cfg.claimMaps ( + claim: claimCfg: [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( + attrNames claimCfg.valuesByGroup + )) + # At least one group must map to a value in each claim map + { + assertion = + (cfg.provision.enable && cfg.enableServer) + -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup); + message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; + } + # Public clients cannot define a basic secret + { + assertion = + (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; + } + # Public clients cannot disable PKCE + { + assertion = + (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) + -> !oauth2Cfg.allowInsecureClientDisablePkce; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE"; + } + # Non-public clients cannot enable localhost redirects + { + assertion = + (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) + -> !oauth2Cfg.enableLocalhostRedirects; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; + } + ] + ) ) - ) ) ); diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index 1e32ce4..d786570 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -67,7 +67,8 @@ in }; provision = { enable = true; - autoRemove = false; + autoRemove = true; # if false, obsolete oauth2 scopeMaps remain + groups."sp.admins".present = true; }; enableClient = true; clientSettings = { diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index a2ca800..ec3eafa 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -71,33 +71,34 @@ in systemd.slices.roundcube.description = "Roundcube service slice"; services.kanidm.provision = lib.mkIf is-auth-enabled { - groups.roundcube_users.present = true; + groups = { + "sp.roundcube.admins".members = [ "sp.admins" ]; + "sp.roundcube.users".present = true; + }; systems.oauth2.roundcube = { displayName = "Roundcube"; originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; + basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; # FIXME # when true, name is passed to a service instead of name@domain preferShortUsername = false; allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps.roundcube_users = [ - "email" - "profile" - "openid" - ]; - # scopeMaps."sp.roundcube.users" = [ - # "email" - # "openid" - # "dovecotprofile" - # ]; - + scopeMaps = { + "sp.roundcube.users" = [ + "email" + "openid" + "profile" + ]; + }; + removeOrphanedClaimMaps = true; # add more scopes when a user is a member of specific group - # claimMaps.groups = { - # joinType = "array"; - # valuesByGroup = { - # "sp.roundcube.admins" = [ "admin" ]; - # }; - # }; + supplementaryScopeMaps."sp.roundcube.admins" = [ "admin" ]; + claimMaps.groups = { + joinType = "array"; + valuesByGroup = { + "sp.roundcube.admins" = [ "admin" "test" ]; + }; + }; }; }; }; From dd4a356ae7fbd81a705210f07d1b1d36eaab8b53 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 27 Dec 2024 08:07:45 +0400 Subject: [PATCH 009/115] feat(auth,roundcube): sp.roundcube.admins inherits sp.roundcube.users --- sp-modules/roundcube/module.nix | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index ec3eafa..78747b9 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -73,7 +73,7 @@ in services.kanidm.provision = lib.mkIf is-auth-enabled { groups = { "sp.roundcube.admins".members = [ "sp.admins" ]; - "sp.roundcube.users".present = true; + "sp.roundcube.users".members = [ "sp.roundcube.admins" ]; }; systems.oauth2.roundcube = { displayName = "Roundcube"; @@ -91,14 +91,6 @@ in ]; }; removeOrphanedClaimMaps = true; - # add more scopes when a user is a member of specific group - supplementaryScopeMaps."sp.roundcube.admins" = [ "admin" ]; - claimMaps.groups = { - joinType = "array"; - valuesByGroup = { - "sp.roundcube.admins" = [ "admin" "test" ]; - }; - }; }; }; }; From bc8f998176ed7b9713e20c6d44a25a308c013689 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 28 Dec 2024 20:52:50 +0400 Subject: [PATCH 010/115] fix(auth): debug and enable options --- sp-modules/auth/flake.nix | 3 --- sp-modules/auth/module.nix | 20 ++++++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix index f0a901a..fa5f5a7 100644 --- a/sp-modules/auth/flake.nix +++ b/sp-modules/auth/flake.nix @@ -37,9 +37,6 @@ ./module.nix ]; nixpkgs.overlays = [ self.overlays.default ]; - - selfprivacy.modules.auth.enable = true; - selfprivacy.modules.auth.debug = false; }; configPathsNeeded = diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index d786570..56a4a25 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -58,7 +58,8 @@ in # nginx should proxy requests to it bindaddress = kanidm-bind-address; - ldapbindaddress = "127.0.0.1:${toString passthru.ldap-port}"; + ldapbindaddress = + "${passthru.ldap-host}:${toString passthru.ldap-port}"; # kanidm is behind a proxy trust_x_forward_for = true; @@ -81,7 +82,7 @@ in services.nginx = { enable = true; additionalModules = - lib.mkIf cfg.debug pkgs.nginxModules.lua; + lib.mkIf cfg.debug [ pkgs.nginxModules.lua ]; commonHttpConfig = lib.mkIf cfg.debug '' log_format kanidm escape=none '$request $status\n' '[Request body]: $request_body\n' @@ -93,8 +94,6 @@ in useACMEHost = domain; forceSSL = true; locations."/" = { - # be aware that such logging mechanism breaks Kanidm authentication - # (but authorization works) extraConfig = lib.mkIf cfg.debug '' access_log /var/log/nginx/kanidm.log kanidm; @@ -106,11 +105,19 @@ in header_filter_by_lua ' local h = ngx.req.get_headers() for k, v in pairs(h) do - ngx.var.req_header = ngx.var.req_header .. k.."="..v.." " + if type(v) == "table" then + ngx.var.req_header = ngx.var.req_header .. k .. "=" .. table.concat(v, ", ") .. " " + else + ngx.var.req_header = ngx.var.req_header .. k .. "=" .. v .. " " + end end local rh = ngx.resp.get_headers() for k, v in pairs(rh) do - ngx.var.resp_header = ngx.var.resp_header .. k.."="..v.." " + if type(v) == "table" then + ngx.var.resp_header = ngx.var.resp_header .. k .. "=" .. table.concat(v, ", ") .. " " + else + ngx.var.resp_header = ngx.var.resp_header .. k .. "=" .. v .. " " + end end '; @@ -144,6 +151,7 @@ in "," (x: "dc=" + x) (lib.strings.splitString "." domain); + ldap-host = "127.0.0.1"; ldap-port = 3636; }; }; From 7f9f7a4db2367c079d64f73bd562aa0d3ab2c970 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sun, 29 Dec 2024 02:20:54 +0400 Subject: [PATCH 011/115] fix auth: sp.{service}.admins groups provisioning --- sp-modules/auth/kanidm.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sp-modules/auth/kanidm.nix b/sp-modules/auth/kanidm.nix index 2d2ecc2..13360d8 100644 --- a/sp-modules/auth/kanidm.nix +++ b/sp-modules/auth/kanidm.nix @@ -126,10 +126,14 @@ let filterPresent = filterAttrs (_: v: v.present); + selfprivacy-admin-groups-regex = "^sp\.([[:alnum:]]+\.|)admins$"; + is-selfprivacy-admin-group = name: + ! builtins.isNull (builtins.match selfprivacy-admin-groups-regex name); + isGroupNonOverwritable = g: false || ! g ? members || g ? members && g.members == [ ] - || g ? members && g.members == [ "sp.admins" ]; + || g ? members && builtins.any is-selfprivacy-admin-group g.members; provisionStateJson = pkgs.writeText "provision-state.json" ( builtins.toJSON { From 8db13dfccf8829b204faa61a4923be190f023984 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sun, 29 Dec 2024 02:21:57 +0400 Subject: [PATCH 012/115] feat auth,forgejo: OAuth2 and LDAP integration --- sp-modules/gitea/config-paths-needed.json | 12 +- sp-modules/gitea/module.nix | 208 +++++++++++++++++++++- 2 files changed, 218 insertions(+), 2 deletions(-) diff --git a/sp-modules/gitea/config-paths-needed.json b/sp-modules/gitea/config-paths-needed.json index cdbf856..44e2729 100644 --- a/sp-modules/gitea/config-paths-needed.json +++ b/sp-modules/gitea/config-paths-needed.json @@ -1,5 +1,15 @@ [ + [ "passthru", "selfprivacy", "auth", "auth-fqdn" ], + [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], + [ "passthru", "selfprivacy", "auth", "ldap-host" ], + [ "passthru", "selfprivacy", "auth", "ldap-port" ], + [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], + [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], + [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "domain" ], + [ "selfprivacy", "modules", "auth", "enable" ], + [ "selfprivacy", "modules", "gitea" ], [ "selfprivacy", "useBinds" ], - [ "selfprivacy", "modules", "gitea" ] + [ "services", "forgejo", "group" ], + [ "services", "forgejo", "package" ] ] diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 8fc063c..cfa3ce6 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -14,6 +14,72 @@ let "gitea-light" "gitea-dark" ]; + is-auth-enabled = config.selfprivacy.modules.auth.enable; + oauth-client-id = "forgejo"; + auth-passthru = config.passthru.selfprivacy.auth; + redirect-uri = "https://git.${sp.domain}/user/oauth2/OAUTH/callback"; + + admins-group = "sp.forgejo.admins"; + users-group = "sp.forgejo.users"; + + kanidm-service-account-name = "sp.${oauth-client-id}.service-account"; + kanidm-service-account-token-name = "${oauth-client-id}-service-account-token"; + kanidm-service-account-token-fp = + "/run/keys/${oauth-client-id}/kanidm-service-account-token"; # FIXME sync with auth module + kanidmExecStartPostScriptRoot = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPost-root-script.sh" + '' + # set-group-ID bit allows for kanidm user to create files, + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} + chown kanidm:${config.services.forgejo.group} /run/keys/${oauth-client-id} + ''; + kanidmExecStartPostScript = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPost-script.sh" + '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + + # get Kanidm service account for mailserver + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${kanidm-service-account-name}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${kanidm-service-account-name}\" is found" + else + echo "kanidm service account \"${kanidm-service-account-name}\" is not found" + echo "creating new kanidm service account \"${kanidm-service-account-name}\"" + if $KANIDM service-account create --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-name} idm_admin + then + "kanidm service account \"${kanidm-service-account-name}\" created" + else + echo "error: cannot create kanidm service account \"${kanidm-service-account-name}\"" + exit 1 + fi + fi + + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers ${kanidm-service-account-name} + + # create a new read-only token for kanidm + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-token-name} --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi + + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${kanidm-service-account-token-fp} + then + echo "error: cannot write token to \"${kanidm-service-account-token-fp}\"" + exit 1 + fi + ''; in { options.selfprivacy.modules.gitea = { @@ -107,6 +173,10 @@ in weight = 6; }; }; + debug = lib.mkOption { + default = false; + type = lib.types.bool; + }; }; config = lib.mkIf cfg.enable { @@ -172,14 +242,45 @@ in }; log = { ROOT_PATH = "${stateDir}/log"; - LEVEL = "Warn"; + LEVEL = if cfg.debug then "Warn" else "Trace"; }; service = { DISABLE_REGISTRATION = cfg.disableRegistration; REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; }; + } // lib.attrsets.optionalAttrs is-auth-enabled { + auth.DISABLE_LOGIN_FORM = true; + service = { + DISABLE_REGISTRATION = cfg.disableRegistration; + REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; + ALLOW_ONLY_EXTERNAL_REGISTRATION = true; + SHOW_REGISTRATION_BUTTON = false; + ENABLE_BASIC_AUTHENTICATION = false; + DEFAULT_USER_VISIBILITY = "limited"; + DEFAULT_ORG_VISIBILITY = "limited"; + }; + + # disallow explore page and access to private repositories, but allow public + "service.explore".REQUIRE_SIGNIN_VIEW = true; + + # TODO control via selfprivacy parameter + # "service.explore".DISABLE_USERS_PAGE = true; + + oauth2_client = { + REDIRECT_URI = redirect-uri; + ACCOUNT_LINKING = "auto"; + ENABLE_AUTO_REGISTRATION = true; + OPENID_CONNECT_SCOPES = "email openid profile"; + }; + # doesn't work if LDAP auth source is not active! + "cron.sync_external_users" = { + ENABLED = true; + RUN_AT_START = true; + NOTICE_ON_SUCCESS = true; + }; }; }; + users.users.gitea = { home = "${stateDir}"; useDefaultShell = true; @@ -198,6 +299,10 @@ in add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + '' + lib.strings.optionalString is-auth-enabled '' + rewrite ^/user/login.*$ /user/oauth2/OAUTH last; + # FIXME is it needed? + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ''; locations = { "/" = { @@ -211,11 +316,112 @@ in serviceConfig = { Slice = "gitea.slice"; }; + preStart = + let + exe = lib.getExe config.services.forgejo.package; + # FIXME skip-tls-verify, bind-password + ldapConfigArgs = '' + --name LDAP \ + --active \ + --security-protocol LDAPS \ + --skip-tls-verify \ + --host '${auth-passthru.ldap-host}' \ + --port '${toString auth-passthru.ldap-port}' \ + --user-search-base '${auth-passthru.ldap-base-dn}' \ + --user-filter '(&(class=person)(memberof=${users-group})(name=%s))' \ + --admin-filter '(&(class=person)(memberof=${admins-group}))' \ + --username-attribute name \ + --firstname-attribute name \ + --surname-attribute displayname \ + --email-attribute mail \ + --public-ssh-key-attribute sshPublicKey \ + --bind-dn 'dn=token' \ + --bind-password "$(cat ${kanidm-service-account-token-fp})" \ + --synchronize-users + ''; + # FIXME secret + oauthConfigArgs = '' + --name OAUTH \ + --provider openidConnect \ + --key forgejo \ + --secret VERYSTRONGSECRETFORFORGEJO \ + --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' + ''; + in + lib.mkIf is-auth-enabled (lib.mkAfter '' + set -o xtrace + + # Check if LDAP is already configured + ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" + + if [[ -n "$ldap_line" ]]; then + # update ldap config + id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} + else + # initially configure ldap + ${exe} admin auth add-ldap ${ldapConfigArgs} + fi + + oauth_line="$(${exe} admin auth list | grep OAUTH | head -n 1)" + if [[ -n "$oauth_line" ]]; then + id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} + else + ${exe} admin auth add-oauth ${oauthConfigArgs} + fi + '' + ); + # TODO consider passing oauth consumer service to auth module instead + wants = lib.mkIf is-auth-enabled + [ auth-passthru.oauth2-systemd-service ]; + after = lib.mkIf is-auth-enabled + [ auth-passthru.oauth2-systemd-service ]; }; slices.gitea = { description = "Forgejo service slice"; }; }; + # for ExecStartPre script to have access to /run/keys/* + users.groups.keys.members = + lib.mkIf is-auth-enabled [ config.services.forgejo.group ]; + + systemd.services.kanidm.serviceConfig.ExecStartPost = + lib.mkIf is-auth-enabled + (lib.mkAfter + [ + ("+" + kanidmExecStartPostScriptRoot) + kanidmExecStartPostScript + ] + ); + services.kanidm.provision = lib.mkIf is-auth-enabled { + groups = { + "${admins-group}".members = [ "sp.admins" ]; + "${users-group}".members = [ admins-group ]; + }; + systems.oauth2.forgejo = { + displayName = "Forgejo"; + originUrl = redirect-uri; + originLanding = "https://${cfg.subdomain}.${sp.domain}/"; + basicSecretFile = pkgs.writeText "bs-forgejo" "VERYSTRONGSECRETFORFORGEJO"; # FIXME + # when true, name is passed to a service instead of name@domain + preferShortUsername = true; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps = { + "${users-group}" = [ + "email" + "openid" + "profile" + ]; + }; + removeOrphanedClaimMaps = true; + # NOTE https://github.com/oddlama/kanidm-provision/issues/15 + # add more scopes when a user is a member of specific group + # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 + # supplementaryScopeMaps."${admins-group}" = + # [ "read:admin" "write:admin" ]; + }; + }; }; } From a45cf792e51d6b45e0f9f44cd1f43e9f7329ee49 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 30 Dec 2024 00:39:08 +0400 Subject: [PATCH 013/115] fix(auth): rename oauth2-provider-name --- sp-modules/auth/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index 56a4a25..1d33ac5 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -142,7 +142,7 @@ in "https://${client_id}:${client_secret}@${auth-fqdn}/oauth2/token/introspect"; oauth2-discovery-url = client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; - oauth2-provider-name = "kanidm"; + oauth2-provider-name = "Kanidm"; oauth2-systemd-service = "kanidm.service"; # e.g. "dc=mydomain,dc=com" From 153e1c12d54b8cbe9123c4b5160ad579dc79904a Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 30 Dec 2024 04:22:50 +0400 Subject: [PATCH 014/115] feat(auth,nextcloud): OAuth2 and LDAP integration --- sp-modules/nextcloud/cleanup-module.nix | 11 +- sp-modules/nextcloud/common.nix | 2 + sp-modules/nextcloud/config-paths-needed.json | 8 +- sp-modules/nextcloud/module.nix | 415 ++++++++++++++---- 4 files changed, 351 insertions(+), 85 deletions(-) diff --git a/sp-modules/nextcloud/cleanup-module.nix b/sp-modules/nextcloud/cleanup-module.nix index 24d8fd5..8e46568 100644 --- a/sp-modules/nextcloud/cleanup-module.nix +++ b/sp-modules/nextcloud/cleanup-module.nix @@ -1,6 +1,11 @@ { config, lib, ... }: let - inherit (import ./common.nix config) sp db-pass-filepath admin-pass-filepath; + inherit (import ./common.nix config) + admin-pass-filepath + db-pass-filepath + override-config-fp + sp + ; in # FIXME do we really want to delete passwords on module deactivation!? { @@ -9,11 +14,13 @@ in lib.trivial.warn ( "nextcloud service is disabled, " + - "${db-pass-filepath} and ${admin-pass-filepath} will be removed!" + "${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!" ) '' rm -f -v ${db-pass-filepath} rm -f -v ${admin-pass-filepath} + [ ! -f "${override-config-fp}" && -L "${override-config-fp}" ] && \ + rm -v "${override-config-fp}" ''; }; } diff --git a/sp-modules/nextcloud/common.nix b/sp-modules/nextcloud/common.nix index 30f47e6..3fb4709 100644 --- a/sp-modules/nextcloud/common.nix +++ b/sp-modules/nextcloud/common.nix @@ -1,6 +1,8 @@ config: rec { sp = config.selfprivacy; + domain= sp.domain; secrets-filepath = "/etc/selfprivacy/secrets.json"; db-pass-filepath = "/var/lib/nextcloud/db-pass"; admin-pass-filepath = "/var/lib/nextcloud/admin-pass"; + override-config-fp = "/var/lib/nextcloud/config/override.config.php"; } diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index 5c2a9fc..5b2cd04 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -1,5 +1,11 @@ [ + [ "passthru", "selfprivacy", "auth" ], [ "selfprivacy", "domain" ], + [ "selfprivacy", "modules", "auth", "enable" ], + [ "selfprivacy", "modules", "nextcloud" ], [ "selfprivacy", "useBinds" ], - [ "selfprivacy", "modules", "nextcloud" ] + [ "services", "nextcloud" ], + [ "services", "phpfpm", "pools", "nextcloud", "group" ], + [ "systemd", "services", "nextcloud" ], + [ "systemd", "services", "nextcloud-setup" ] ] diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 12ad007..e2411d9 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -1,4 +1,89 @@ { config, lib, pkgs, ... }: +let + inherit (import ./common.nix config) + admin-pass-filepath + db-pass-filepath + domain + override-config-fp + secrets-filepath + sp + ; + hostName = "${cfg.subdomain}.${sp.domain}"; + + auth-passthru = config.passthru.selfprivacy.auth; + + is-auth-enabled = config.selfprivacy.modules.auth.enable; + + occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; + cfg = sp.modules.nextcloud; + nextcloud-secret-file = "/var/lib/nextcloud/secrets.json"; + nextcloud-setup-group = + config.systemd.services.nextcloud-setup.serviceConfig.Group; + + admins-group = "sp.nextcloud.admins"; + users-group = "sp.nextcloud.users"; + wildcard-group = "sp.nextcloud.*"; + + oauth-client-id = "nextcloud"; + kanidm-service-account-name = "sp.${oauth-client-id}.service-account"; + kanidm-service-account-token-name = "${oauth-client-id}-service-account-token"; + kanidm-service-account-token-fp = + "/run/keys/${oauth-client-id}/kanidm-service-account-token"; # FIXME sync with auth module + kanidmExecStartPostScriptRoot = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPost-root-script.sh" + '' + # set-group-ID bit allows for kanidm user to create files, + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} + chown kanidm:${nextcloud-setup-group} /run/keys/${oauth-client-id} + ''; + kanidmExecStartPostScript = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPost-script.sh" + '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + + # get Kanidm service account for mailserver + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${kanidm-service-account-name}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${kanidm-service-account-name}\" is found" + else + echo "kanidm service account \"${kanidm-service-account-name}\" is not found" + echo "creating new kanidm service account \"${kanidm-service-account-name}\"" + if $KANIDM service-account create --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-name} idm_admin + then + "kanidm service account \"${kanidm-service-account-name}\" created" + else + echo "error: cannot create kanidm service account \"${kanidm-service-account-name}\"" + exit 1 + fi + fi + + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers ${kanidm-service-account-name} + + # create a new read-only token for kanidm + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-token-name} --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi + + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${kanidm-service-account-token-fp} + then + echo "error: cannot write token to \"${kanidm-service-account-token-fp}\"" + exit 1 + fi + ''; +in { options.selfprivacy.modules.nextcloud = with lib; { enable = (lib.mkOption { @@ -40,90 +125,256 @@ weight = 1; }; }; + debug = lib.mkOption { + default = false; + type = lib.types.bool; + }; }; - config = - let - inherit (import ./common.nix config) - sp secrets-filepath db-pass-filepath admin-pass-filepath; - cfg = sp.modules.nextcloud; - hostName = "${cfg.subdomain}.${sp.domain}"; - in - lib.mkIf sp.modules.nextcloud.enable { - fileSystems = lib.mkIf sp.useBinds { - "/var/lib/nextcloud" = { - device = "/volumes/${cfg.location}/nextcloud"; - options = [ - "bind" - "x-systemd.required-by=nextcloud-setup.service" - "x-systemd.required-by=nextcloud-secrets.service" - "x-systemd.before=nextcloud-setup.service" - "x-systemd.before=nextcloud-secrets.service" - ]; - }; - }; - systemd = { - services = { - phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice"; - nextcloud-setup.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-secrets = { - before = [ "nextcloud-setup.service" ]; - requiredBy = [ "nextcloud-setup.service" ]; - serviceConfig.Type = "oneshot"; - path = with pkgs; [ coreutils jq ]; - script = '' - databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath}) - adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath}) - - install -C -m 0440 -o nextcloud -g nextcloud -DT \ - <(printf "%s\n" "$databasePassword") \ - ${db-pass-filepath} - - install -C -m 0440 -o nextcloud -g nextcloud -DT \ - <(printf "%s\n" "$adminPassword") \ - ${admin-pass-filepath} - ''; - }; - }; - slices.nextcloud = { - description = "Nextcloud service slice"; - }; - }; - services.nextcloud = { - enable = true; - package = pkgs.nextcloud29; - inherit hostName; - - # Use HTTPS for links - https = true; - - # auto-update Nextcloud Apps - autoUpdateApps.enable = true; - # set what time makes sense for you - autoUpdateApps.startAt = "05:00:00"; - - settings = { - # further forces Nextcloud to use HTTPS - overwriteprotocol = "https"; - }; - - config = { - dbtype = "sqlite"; - dbuser = "nextcloud"; - dbname = "nextcloud"; - dbpassFile = db-pass-filepath; - adminpassFile = admin-pass-filepath; - adminuser = "admin"; - }; - - enableImagemagick = cfg.enableImagemagick; - }; - services.nginx.virtualHosts.${hostName} = { - useACMEHost = sp.domain; - forceSSL = true; + config = lib.mkIf sp.modules.nextcloud.enable { + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/nextcloud" = { + device = "/volumes/${cfg.location}/nextcloud"; + options = [ + "bind" + "x-systemd.required-by=nextcloud-setup.service" + "x-systemd.required-by=nextcloud-secrets.service" + "x-systemd.before=nextcloud-setup.service" + "x-systemd.before=nextcloud-secrets.service" + ]; }; }; + # for ExecStartPost script to have access to /run/keys/* + users.groups.keys.members = + lib.mkIf is-auth-enabled [ nextcloud-setup-group ]; + systemd = { + services = { + phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice"; + nextcloud-setup = { + serviceConfig.Slice = "nextcloud.slice"; + serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; + # FIXME secret + preStart = lib.mkIf is-auth-enabled '' + cat < "${nextcloud-secret-file}" + { + "oidc_login_client_secret": "VERY-STRONG-SECRET-FOR-NEXTCLOUD" + } + EOF + ''; + path = lib.mkIf is-auth-enabled [ pkgs.jq ]; + script = lib.mkIf is-auth-enabled '' + ${lib.strings.optionalString cfg.debug "set -o xtrace"} + + ${occ} app:install user_ldap || : + ${occ} app:enable user_ldap + + # The following code tries to match an existing config or creates a new one. + # The criteria for matching is the ldapHost value. + + # remove broken link after previous nextcloud (un)installation + [ ! -f "${override-config-fp}" && -L "${override-config-fp}" ] && \ + rm -v "${override-config-fp}" + + ALL_CONFIG="$(${occ} ldap:show-config --output=json --show-password)" + + # TODO investigate this! + MATCHING_CONFIG_IDs="$(echo "$ALL_CONFIG" | jq '[to_entries[] | select(.value.ldapHost=="${auth-passthru.ldap-host}") | .key]')" + if [[ $(echo "$MATCHING_CONFIG_IDs" | jq 'length') > 0 ]]; then + CONFIG_ID="$(echo "$MATCHING_CONFIG_IDs" | jq --raw-output '.[0]')" + else + CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)" + fi + + echo "Using configId $CONFIG_ID" + + # The following CLI commands follow + # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way + + # FIXME + ${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1' + + ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' 'ldaps://${auth-passthru.ldap-host}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${kanidm-service-account-token-fp})" + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \ + '(&(class=group)(${wildcard-group}))' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \ + '(&(class=group)(${wildcard-group}))' + # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \ + # 'groupOfUniqueNames' + # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \ + # 'uniqueMember' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \ + '(&(class=person)(memberof=${users-group})(uid=%uid))' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \ + 'uid' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \ + 'displayname' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \ + '(&(class=person)(memberof=${users-group})(name=%s))' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \ + '1' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \ + 'person' + + ${occ} ldap:test-config -- "$CONFIG_ID" + + # Only one active at the same time + + # TODO investigate this! It takes a minute to deactivate all. + for configid in $(echo "$ALL_CONFIG" | jq --raw-output "keys[]"); do + echo "Deactivating $configid" + ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' \ + '0' + echo "Deactivated $configid" + done + + ${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' \ + '1' + + ############################################################################ + # OIDC app + ############################################################################ + ${occ} app:install user_oidc || : + ${occ} app:enable user_oidc + + # FIXME clientsecret + ${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \ + --clientid="${oauth-client-id}" \ + --clientsecret="VERY-STRONG-SECRET-FOR-NEXTCLOUD" \ + --discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \ + --unique-uid=0 \ + --scope="email openid profile" \ + --mapping-uid=preferred_username \ + --no-interaction \ + --mapping-uid=name \ + --mapping-groups=groups \ + --group-provisioning=1 \ + -vvv + ''; + # TODO consider passing oauth consumer service to auth module instead + wants = lib.mkIf is-auth-enabled + [ auth-passthru.oauth2-systemd-service ]; + after = lib.mkIf is-auth-enabled + [ auth-passthru.oauth2-systemd-service ]; + }; + kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled + (lib.mkAfter [ + ("+" + kanidmExecStartPostScriptRoot) + kanidmExecStartPostScript + ]); + nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-secrets = { + before = [ "nextcloud-setup.service" ]; + requiredBy = [ "nextcloud-setup.service" ]; + serviceConfig.Type = "oneshot"; + path = with pkgs; [ coreutils jq ]; + script = '' + databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath}) + adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath}) + + install -C -m 0440 -o nextcloud -g nextcloud -DT \ + <(printf "%s\n" "$databasePassword") \ + ${db-pass-filepath} + + install -C -m 0440 -o nextcloud -g nextcloud -DT \ + <(printf "%s\n" "$adminPassword") \ + ${admin-pass-filepath} + ''; + }; + }; + slices.nextcloud = { + description = "Nextcloud service slice"; + }; + }; + services.nextcloud = { + enable = true; + package = pkgs.nextcloud29; + inherit hostName; + + # Use HTTPS for links + https = true; + + # auto-update Nextcloud Apps + autoUpdateApps.enable = true; + # set what time makes sense for you + autoUpdateApps.startAt = "05:00:00"; + + phpOptions.display_errors = "Off"; + + settings = { + # further forces Nextcloud to use HTTPS + overwriteprotocol = "https"; + } // lib.attrsets.optionalAttrs is-auth-enabled { + loglevel = 0; + # log_type = "file"; + social_login_auto_redirect = false; + + allow_local_remote_servers = true; + allow_user_to_change_display_name = false; + lost_password_link = "disabled"; + allow_multiple_user_backends = false; + + user_oidc = { + single_logout = true; + use_pkce = true; + auto_provision = true; + soft_auto_provision = true; + disable_account_creation = false; + }; + }; + + config = { + dbtype = "sqlite"; + dbuser = "nextcloud"; + dbname = "nextcloud"; + dbpassFile = db-pass-filepath; + # TODO review whether admin user is needed at all - admin group works + adminpassFile = admin-pass-filepath; + adminuser = "admin"; + }; + + secretFile = lib.mkIf is-auth-enabled nextcloud-secret-file; + }; + services.nginx.virtualHosts.${hostName} = { + useACMEHost = sp.domain; + forceSSL = true; + locations."/".extraConfig = lib.mkIf is-auth-enabled '' + # FIXME does not work + rewrite ^/login$ /apps/user_oidc/login/1 last; + ''; + # show an error instead of a blank page on Nextcloud PHP/FastCGI error + locations."~ \\.php(?:$|/)".extraConfig = '' + error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html; + ''; + }; + services.kanidm.provision = lib.mkIf is-auth-enabled { + groups = { + "${admins-group}".members = [ "sp.admins" ]; + "${users-group}".members = [ admins-group ]; + }; + systems.oauth2.${oauth-client-id} = { + displayName = "Nextcloud"; + originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = pkgs.writeText "bs-nextcloud" "VERY-STRONG-SECRET-FOR-NEXTCLOUD"; # FIXME + # when true, name is passed to a service instead of name@domain + preferShortUsername = true; + allowInsecureClientDisablePkce = false; + scopeMaps.${users-group} = [ "email" "openid" "profile" ]; + removeOrphanedClaimMaps = true; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${admins-group} = [ "admin" ]; + }; + }; + }; + }; } From 041479a48bb2ee565655ae900dcefb0bc314a41c Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 30 Dec 2024 05:13:28 +0400 Subject: [PATCH 015/115] fix(auth,forgejo): recognize admins --- sp-modules/gitea/module.nix | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index cfa3ce6..0ccecfd 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -17,7 +17,9 @@ let is-auth-enabled = config.selfprivacy.modules.auth.enable; oauth-client-id = "forgejo"; auth-passthru = config.passthru.selfprivacy.auth; - redirect-uri = "https://git.${sp.domain}/user/oauth2/OAUTH/callback"; + oauth2-provider-name = auth-passthru.oauth2-provider-name; + redirect-uri = + "https://git.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; admins-group = "sp.forgejo.admins"; users-group = "sp.forgejo.users"; @@ -256,8 +258,6 @@ in ALLOW_ONLY_EXTERNAL_REGISTRATION = true; SHOW_REGISTRATION_BUTTON = false; ENABLE_BASIC_AUTHENTICATION = false; - DEFAULT_USER_VISIBILITY = "limited"; - DEFAULT_ORG_VISIBILITY = "limited"; }; # disallow explore page and access to private repositories, but allow public @@ -300,7 +300,7 @@ in add_header X-XSS-Protection "1; mode=block"; proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; '' + lib.strings.optionalString is-auth-enabled '' - rewrite ^/user/login.*$ /user/oauth2/OAUTH last; + rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; # FIXME is it needed? proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ''; @@ -341,10 +341,12 @@ in ''; # FIXME secret oauthConfigArgs = '' - --name OAUTH \ + --name "${oauth2-provider-name}" \ --provider openidConnect \ --key forgejo \ --secret VERYSTRONGSECRETFORFORGEJO \ + --group-claim-name groups \ + --admin-group admins \ --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' ''; in @@ -363,7 +365,7 @@ in ${exe} admin auth add-ldap ${ldapConfigArgs} fi - oauth_line="$(${exe} admin auth list | grep OAUTH | head -n 1)" + oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" if [[ -n "$oauth_line" ]]; then id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} @@ -383,7 +385,7 @@ in }; }; - # for ExecStartPre script to have access to /run/keys/* + # for ExecStartPost script to have access to /run/keys/* users.groups.keys.members = lib.mkIf is-auth-enabled [ config.services.forgejo.group ]; @@ -421,6 +423,10 @@ in # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 # supplementaryScopeMaps."${admins-group}" = # [ "read:admin" "write:admin" ]; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${admins-group} = [ "admins" ]; + }; }; }; }; From bf8fb31065f3a283947a3359f872bbcee8b7a317 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 30 Dec 2024 05:44:47 +0400 Subject: [PATCH 016/115] chore(mailserver): less hardcode --- sp-modules/simple-nixos-mailserver/auth-dovecot.nix | 13 +++++-------- .../config-paths-needed.json | 2 +- sp-modules/simple-nixos-mailserver/config.nix | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 55f3197..0c2677d 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -7,7 +7,9 @@ let auth-passthru ; - ldapConfFile = "/run/dovecot2/dovecot-ldap.conf.ext"; # FIXME get "dovecot2" from `config` + runtime-directory = "dovecot2"; + + ldapConfFile = "/run/${runtime-directory}/dovecot-ldap.conf.ext"; mkLdapSearchScope = scope: ( if scope == "sub" then "subtree" else if scope == "one" then "onelevel" @@ -92,7 +94,7 @@ lib.mkIf config.selfprivacy.modules.auth.enable { service auth { unix_listener auth-userdb { mode = 0660 - user = dovecot2 + user = ${config.services.dovecot2.user} } unix_listener dovecot-auth { mode = 0660 @@ -107,12 +109,6 @@ lib.mkIf config.selfprivacy.modules.auth.enable { args = ${ldapConfFile} default_fields = home=/var/vmail/${domain}/%u uid=${toString config.mailserver.vmailUID} gid=${toString config.mailserver.vmailUID} } - - # with debugging OAuth2 token gets printed in logs - # auth_debug = yes - # auth_debug_passwords = yes - # auth_verbose = yes - # mail_debug = yes ''; services.dovecot2.enablePAM = false; systemd.services.dovecot2 = { @@ -121,6 +117,7 @@ lib.mkIf config.selfprivacy.modules.auth.enable { # FIXME pass dependant services to auth module option instead? wants = [ auth-passthru.oauth2-systemd-service ]; after = [ auth-passthru.oauth2-systemd-service ]; + serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ]; }; # does it merge with existing restartTriggers? diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index e717a3d..a057322 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -9,7 +9,7 @@ [ "selfprivacy", "useBinds" ], [ "selfprivacy", "username" ], [ "selfprivacy", "users" ], - [ "services", "dovecot2" ], + [ "services", "dovecot2", "user" ], [ "services", "opendkim" ], [ "services", "postfix", "group" ], [ "services", "postfix", "user" ], diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index c7da54d..2d830c0 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -167,7 +167,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; bind.dn = "dn=token"; # TODO change in this file should trigger system restart dovecot - bind.passwordFile = "/run/keys/mailserver/kanidm-service-account-token"; # FIXME + bind.passwordFile = mailserver-service-account-token-fp; # searchBase = "ou=persons," + ldap_base_dn; searchBase = auth-passthru.ldap-base-dn; # TODO refine this From 0e7b113ce01da018e186cd19921953e3e27ef9cb Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 10 Jan 2025 20:39:07 +0400 Subject: [PATCH 017/115] fix(nextcloud): user_oidc mapping-uid is preferred_username --- sp-modules/nextcloud/module.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index e2411d9..3c13528 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -252,7 +252,6 @@ in --scope="email openid profile" \ --mapping-uid=preferred_username \ --no-interaction \ - --mapping-uid=name \ --mapping-groups=groups \ --group-provisioning=1 \ -vvv From ed10508ed9e6d66326dc3679f798ea2fa6f0d52b Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 15 Jan 2025 14:53:58 +0400 Subject: [PATCH 018/115] auth: create sp.selfprivacy-api.service-account --- sp-modules/auth/module.nix | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index 1d33ac5..80850ad 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -6,6 +6,34 @@ let kanidm-bind-address = "127.0.0.1:3013"; + selfprivacy-service-account-name = "sp.selfprivacy-api.service-account"; + + spApiUserExecStartPostScript = + pkgs.writeShellScript "spApiUserExecStartPostScript" '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + + # get Kanidm service account for SelfPrivacyAPI + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${selfprivacy-service-account-name}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${selfprivacy-service-account-name}\" is found" + else + echo "kanidm service account \"${selfprivacy-service-account-name}\" is not found" + echo "creating new kanidm service account \"${selfprivacy-service-account-name}\"" + if $KANIDM service-account create --name idm_admin "${selfprivacy-service-account-name}" "SelfPrivacy API service account" idm_admin + then + echo "kanidm service account \"${selfprivacy-service-account-name}\" created" + else + echo "error: cannot create kanidm service account \"${selfprivacy-service-account-name}\"" + exit 1 + fi + fi + + $KANIDM group add-members idm_admins "${selfprivacy-service-account-name}" + ''; + # lua stuff for debugging only lua_core_path = "${pkgs.luajitPackages.lua-resty-core}/lib/lua/5.1/?.lua"; lua_lrucache_path = "${pkgs.luajitPackages.lua-resty-lrucache}/lib/lua/5.1/?.lua"; @@ -136,6 +164,9 @@ in }; }; + systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter + [ spApiUserExecStartPostScript ]; + passthru.selfprivacy.auth = rec { auth-fqdn = cfg.subdomain + "." + domain; oauth2-introspection-url = client_id: client_secret: From 5cb3be9a36a8b25ae1468c56ac8f3410e87e76e4 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 15 Jan 2025 14:57:23 +0400 Subject: [PATCH 019/115] fix forgejo: OAuth secret, ExecStartPost ignore failure, subdomain --- sp-modules/gitea/module.nix | 45 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 0ccecfd..03bfa07 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -19,7 +19,7 @@ let auth-passthru = config.passthru.selfprivacy.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; redirect-uri = - "https://git.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; + "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; admins-group = "sp.forgejo.admins"; users-group = "sp.forgejo.users"; @@ -28,13 +28,21 @@ let kanidm-service-account-token-name = "${oauth-client-id}-service-account-token"; kanidm-service-account-token-fp = "/run/keys/${oauth-client-id}/kanidm-service-account-token"; # FIXME sync with auth module - kanidmExecStartPostScriptRoot = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPost-root-script.sh" + # TODO rewrite to tmpfiles.d + kanidmExecStartPreScriptRoot = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPre-root-script.sh" '' - # set-group-ID bit allows for kanidm user to create files, + # set-group-ID bit allows kanidm user to create files with another group mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} chown kanidm:${config.services.forgejo.group} /run/keys/${oauth-client-id} ''; + kanidm-oauth-client-secret-fp = + "/run/keys/${oauth-client-id}/kanidm-oauth-client-secret"; + kanidmExecStartPreScript = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' + [ -f "${kanidm-oauth-client-secret-fp}" ] || \ + "${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32 + ''; kanidmExecStartPostScript = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPost-script.sh" '' @@ -50,9 +58,9 @@ let else echo "kanidm service account \"${kanidm-service-account-name}\" is not found" echo "creating new kanidm service account \"${kanidm-service-account-name}\"" - if $KANIDM service-account create --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-name} idm_admin + if $KANIDM service-account create --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-name}" idm_admin then - "kanidm service account \"${kanidm-service-account-name}\" created" + echo "kanidm service account \"${kanidm-service-account-name}\" created" else echo "error: cannot create kanidm service account \"${kanidm-service-account-name}\"" exit 1 @@ -60,10 +68,10 @@ let fi # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers ${kanidm-service-account-name} + $KANIDM group add-members idm_mail_servers "${kanidm-service-account-name}" # create a new read-only token for kanidm - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-token-name} --output json)" + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-token-name}" --output json)" then echo "error: kanidm CLI returns an error when trying to generate service-account api-token" exit 1 @@ -339,12 +347,11 @@ in --bind-password "$(cat ${kanidm-service-account-token-fp})" \ --synchronize-users ''; - # FIXME secret oauthConfigArgs = '' --name "${oauth2-provider-name}" \ --provider openidConnect \ --key forgejo \ - --secret VERYSTRONGSECRETFORFORGEJO \ + --secret "$(<${kanidm-oauth-client-secret-fp})" \ --group-claim-name groups \ --admin-group admins \ --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' @@ -375,9 +382,7 @@ in '' ); # TODO consider passing oauth consumer service to auth module instead - wants = lib.mkIf is-auth-enabled - [ auth-passthru.oauth2-systemd-service ]; - after = lib.mkIf is-auth-enabled + requires = lib.mkIf is-auth-enabled [ auth-passthru.oauth2-systemd-service ]; }; slices.gitea = { @@ -389,14 +394,14 @@ in users.groups.keys.members = lib.mkIf is-auth-enabled [ config.services.forgejo.group ]; + systemd.services.kanidm.serviceConfig.ExecStartPre = + lib.mkIf is-auth-enabled [ + ("-+" + kanidmExecStartPreScriptRoot) + ("-" + kanidmExecStartPreScript) + ]; systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled - (lib.mkAfter - [ - ("+" + kanidmExecStartPostScriptRoot) - kanidmExecStartPostScript - ] - ); + (lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]); services.kanidm.provision = lib.mkIf is-auth-enabled { groups = { "${admins-group}".members = [ "sp.admins" ]; @@ -406,7 +411,7 @@ in displayName = "Forgejo"; originUrl = redirect-uri; originLanding = "https://${cfg.subdomain}.${sp.domain}/"; - basicSecretFile = pkgs.writeText "bs-forgejo" "VERYSTRONGSECRETFORFORGEJO"; # FIXME + basicSecretFile = kanidm-oauth-client-secret-fp; # when true, name is passed to a service instead of name@domain preferShortUsername = true; allowInsecureClientDisablePkce = true; # FIXME is it needed? From 89d788aab2b2d95aa75b3a8ddd7c197c40f80955 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 15 Jan 2025 15:15:46 +0400 Subject: [PATCH 020/115] fix nextcloud: OAuth secret, ExecStartPost ignore failure --- sp-modules/nextcloud/module.nix | 53 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 3c13528..7e689ea 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -29,13 +29,21 @@ let kanidm-service-account-token-name = "${oauth-client-id}-service-account-token"; kanidm-service-account-token-fp = "/run/keys/${oauth-client-id}/kanidm-service-account-token"; # FIXME sync with auth module - kanidmExecStartPostScriptRoot = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPost-root-script.sh" + # TODO rewrite to tmpfiles.d, but make sure the group exists first! + kanidmExecStartPreScriptRoot = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPre-root-script.sh" '' # set-group-ID bit allows for kanidm user to create files, mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} chown kanidm:${nextcloud-setup-group} /run/keys/${oauth-client-id} ''; + kanidm-oauth-client-secret-fp = + "/run/keys/${oauth-client-id}/kanidm-oauth-client-secret"; + kanidmExecStartPreScript = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' + [ -f "${kanidm-oauth-client-secret-fp}" ] || \ + "${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32 + ''; kanidmExecStartPostScript = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPost-script.sh" '' @@ -51,9 +59,9 @@ let else echo "kanidm service account \"${kanidm-service-account-name}\" is not found" echo "creating new kanidm service account \"${kanidm-service-account-name}\"" - if $KANIDM service-account create --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-name} idm_admin + if $KANIDM service-account create --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-name}" idm_admin then - "kanidm service account \"${kanidm-service-account-name}\" created" + echo "kanidm service account \"${kanidm-service-account-name}\" created" else echo "error: cannot create kanidm service account \"${kanidm-service-account-name}\"" exit 1 @@ -61,10 +69,10 @@ let fi # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers ${kanidm-service-account-name} + $KANIDM group add-members idm_mail_servers "${kanidm-service-account-name}" # create a new read-only token for kanidm - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-token-name} --output json)" + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-token-name}" --output json)" then echo "error: kanidm CLI returns an error when trying to generate service-account api-token" exit 1 @@ -153,14 +161,6 @@ in nextcloud-setup = { serviceConfig.Slice = "nextcloud.slice"; serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; - # FIXME secret - preStart = lib.mkIf is-auth-enabled '' - cat < "${nextcloud-secret-file}" - { - "oidc_login_client_secret": "VERY-STRONG-SECRET-FOR-NEXTCLOUD" - } - EOF - ''; path = lib.mkIf is-auth-enabled [ pkgs.jq ]; script = lib.mkIf is-auth-enabled '' ${lib.strings.optionalString cfg.debug "set -o xtrace"} @@ -243,10 +243,9 @@ in ${occ} app:install user_oidc || : ${occ} app:enable user_oidc - # FIXME clientsecret ${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \ --clientid="${oauth-client-id}" \ - --clientsecret="VERY-STRONG-SECRET-FOR-NEXTCLOUD" \ + --clientsecret="$(<${kanidm-oauth-client-secret-fp})" \ --discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \ --unique-uid=0 \ --scope="email openid profile" \ @@ -257,16 +256,16 @@ in -vvv ''; # TODO consider passing oauth consumer service to auth module instead - wants = lib.mkIf is-auth-enabled - [ auth-passthru.oauth2-systemd-service ]; - after = lib.mkIf is-auth-enabled + requires = lib.mkIf is-auth-enabled [ auth-passthru.oauth2-systemd-service ]; }; - kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled + kanidm.serviceConfig.ExecStartPre = lib.mkIf is-auth-enabled (lib.mkAfter [ - ("+" + kanidmExecStartPostScriptRoot) - kanidmExecStartPostScript + ("-+" + kanidmExecStartPreScriptRoot) + ("-" + kanidmExecStartPreScript) ]); + kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled + (lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]); nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; @@ -345,10 +344,10 @@ in services.nginx.virtualHosts.${hostName} = { useACMEHost = sp.domain; forceSSL = true; - locations."/".extraConfig = lib.mkIf is-auth-enabled '' - # FIXME does not work - rewrite ^/login$ /apps/user_oidc/login/1 last; - ''; + #locations."/".extraConfig = lib.mkIf is-auth-enabled '' + # # FIXME does not work + # rewrite ^/login$ /apps/user_oidc/login/1 last; + #''; # show an error instead of a blank page on Nextcloud PHP/FastCGI error locations."~ \\.php(?:$|/)".extraConfig = '' error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html; @@ -363,7 +362,7 @@ in displayName = "Nextcloud"; originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = pkgs.writeText "bs-nextcloud" "VERY-STRONG-SECRET-FOR-NEXTCLOUD"; # FIXME + basicSecretFile = kanidm-oauth-client-secret-fp; # when true, name is passed to a service instead of name@domain preferShortUsername = true; allowInsecureClientDisablePkce = false; From 56fe5690c182002a9fe541e91b49a24354b8d8c2 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 15 Jan 2025 18:03:19 +0400 Subject: [PATCH 021/115] fix roundcube: OAuth secret, ExecStartPost ignore failure --- sp-modules/roundcube/config-paths-needed.json | 1 + sp-modules/roundcube/module.nix | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index 31c78d0..dd62508 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -2,6 +2,7 @@ [ "mailserver", "fqdn" ], [ "passthru", "selfprivacy", "auth", "auth-fqdn" ], [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], + [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth" ], [ "selfprivacy", "modules", "roundcube" ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 78747b9..45b160d 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -6,6 +6,22 @@ let auth-passthru = config.passthru.selfprivacy.auth; auth-fqdn = auth-passthru.auth-fqdn; oauth-client-id = "roundcube"; + roundcube-group = "roundcube"; + kanidmExecStartPreScriptRoot = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPre-root-script.sh" + '' + # set-group-ID bit allows for kanidm user to create files, + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} + chown kanidm:${roundcube-group} /run/keys/${oauth-client-id} + ''; + kanidm-oauth-client-secret-fp = + "/run/keys/${oauth-client-id}/kanidm-oauth-client-secret"; + kanidmExecStartPreScript = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' + set -o xtrace + [ -f "${kanidm-oauth-client-secret-fp}" ] || \ + "${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32 + ''; in { options.selfprivacy.modules.roundcube = { @@ -48,7 +64,7 @@ in $config['oauth_provider'] = 'generic'; $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; $config['oauth_client_id'] = '${oauth-client-id}'; - $config['oauth_client_secret'] = 'VERYSTRONGSECRETFORROUNDCUBE'; # FIXME + $config['oauth_client_secret'] = "$(<${kanidm-oauth-client-secret-fp})"; $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; @@ -70,6 +86,14 @@ in systemd.slices.roundcube.description = "Roundcube service slice"; + systemd.services.kanidm = lib.mkIf is-auth-enabled { + serviceConfig.ExecStartPre = lib.mkAfter [ + ("-+" + kanidmExecStartPreScriptRoot) + ("-" + kanidmExecStartPreScript) + ]; + requires = [ auth-passthru.oauth2-systemd-service ]; + }; + services.kanidm.provision = lib.mkIf is-auth-enabled { groups = { "sp.roundcube.admins".members = [ "sp.admins" ]; @@ -79,7 +103,7 @@ in displayName = "Roundcube"; originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = pkgs.writeText "bs-roundcube" "VERYSTRONGSECRETFORROUNDCUBE"; # FIXME + basicSecretFile = kanidm-oauth-client-secret-fp; # when true, name is passed to a service instead of name@domain preferShortUsername = false; allowInsecureClientDisablePkce = true; # FIXME is it needed? From f43ec2686d659eb7f3a482feecc584941f888e25 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 17 Jan 2025 15:04:08 +0400 Subject: [PATCH 022/115] fix nextcloud: get rid of extra user_ldap configs; other fixes --- sp-modules/nextcloud/cleanup-module.nix | 2 +- sp-modules/nextcloud/config-paths-needed.json | 2 + sp-modules/nextcloud/module.nix | 48 +++++++++++-------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/sp-modules/nextcloud/cleanup-module.nix b/sp-modules/nextcloud/cleanup-module.nix index 8e46568..0b3b3ae 100644 --- a/sp-modules/nextcloud/cleanup-module.nix +++ b/sp-modules/nextcloud/cleanup-module.nix @@ -19,7 +19,7 @@ in '' rm -f -v ${db-pass-filepath} rm -f -v ${admin-pass-filepath} - [ ! -f "${override-config-fp}" && -L "${override-config-fp}" ] && \ + [[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \ rm -v "${override-config-fp}" ''; }; diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index 5b2cd04..bd46940 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -1,11 +1,13 @@ [ [ "passthru", "selfprivacy", "auth" ], + [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth", "enable" ], [ "selfprivacy", "modules", "nextcloud" ], [ "selfprivacy", "useBinds" ], [ "services", "nextcloud" ], [ "services", "phpfpm", "pools", "nextcloud", "group" ], + [ "services", "phpfpm", "pools", "nextcloud", "user" ], [ "systemd", "services", "nextcloud" ], [ "systemd", "services", "nextcloud-setup" ] ] diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 7e689ea..671809d 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -8,14 +8,15 @@ let secrets-filepath sp ; + hostName = "${cfg.subdomain}.${sp.domain}"; - auth-passthru = config.passthru.selfprivacy.auth; - is-auth-enabled = config.selfprivacy.modules.auth.enable; + cfg = sp.modules.nextcloud; + ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; - cfg = sp.modules.nextcloud; + nextcloud-secret-file = "/var/lib/nextcloud/secrets.json"; nextcloud-setup-group = config.systemd.services.nextcloud-setup.serviceConfig.Group; @@ -152,9 +153,15 @@ in ]; }; }; + # for ExecStartPost script to have access to /run/keys/* users.groups.keys.members = lib.mkIf is-auth-enabled [ nextcloud-setup-group ]; + + # not needed, due to turnOffCertCheck=1 in used_ldap + # users.groups.${config.security.acme.certs.${domain}.group}.members = + # [ config.services.phpfpm.pools.nextcloud.user ]; + systemd = { services = { phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice"; @@ -163,6 +170,8 @@ in serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; path = lib.mkIf is-auth-enabled [ pkgs.jq ]; script = lib.mkIf is-auth-enabled '' + set -o errexit + set -o nounset ${lib.strings.optionalString cfg.debug "set -o xtrace"} ${occ} app:install user_ldap || : @@ -172,15 +181,14 @@ in # The criteria for matching is the ldapHost value. # remove broken link after previous nextcloud (un)installation - [ ! -f "${override-config-fp}" && -L "${override-config-fp}" ] && \ + [[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \ rm -v "${override-config-fp}" - ALL_CONFIG="$(${occ} ldap:show-config --output=json --show-password)" + ALL_CONFIG="$(${occ} ldap:show-config --output=json)" - # TODO investigate this! - MATCHING_CONFIG_IDs="$(echo "$ALL_CONFIG" | jq '[to_entries[] | select(.value.ldapHost=="${auth-passthru.ldap-host}") | .key]')" - if [[ $(echo "$MATCHING_CONFIG_IDs" | jq 'length') > 0 ]]; then - CONFIG_ID="$(echo "$MATCHING_CONFIG_IDs" | jq --raw-output '.[0]')" + MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")" + if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then + CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")" else CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)" fi @@ -190,10 +198,13 @@ in # The following CLI commands follow # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way - # FIXME + # StartTLS is not supported in Kanidm due to security risks, whereas + # user_ldap doesn't support SASL. Importing certificate doesn't + # help: + # ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem" ${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' 'ldaps://${auth-passthru.ldap-host}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}' ${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}' ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token' ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${kanidm-service-account-token-fp})" @@ -224,18 +235,17 @@ in ${occ} ldap:test-config -- "$CONFIG_ID" - # Only one active at the same time - - # TODO investigate this! It takes a minute to deactivate all. - for configid in $(echo "$ALL_CONFIG" | jq --raw-output "keys[]"); do + # delete all configs except "$CONFIG_ID" + for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do echo "Deactivating $configid" - ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' \ - '0' + ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0' echo "Deactivated $configid" + echo "Deleting $configid" + ${occ} ldap:delete-config "$configid" + echo "Deleted $configid" done - ${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' \ - '1' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1' ############################################################################ # OIDC app From f795bc977f03de64c10a62528bfa04a88f2611ca Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 17 Jan 2025 15:53:21 +0400 Subject: [PATCH 023/115] fix auth: config.selfprivacy.modules.auth.enable or false --- sp-modules/gitea/module.nix | 2 +- sp-modules/nextcloud/module.nix | 2 +- sp-modules/roundcube/module.nix | 2 +- sp-modules/simple-nixos-mailserver/auth-dovecot.nix | 5 +++-- sp-modules/simple-nixos-mailserver/auth-postfix.nix | 3 ++- sp-modules/simple-nixos-mailserver/common.nix | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 03bfa07..d944885 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -14,7 +14,7 @@ let "gitea-light" "gitea-dark" ]; - is-auth-enabled = config.selfprivacy.modules.auth.enable; + is-auth-enabled = config.selfprivacy.modules.auth.enable or false; oauth-client-id = "forgejo"; auth-passthru = config.passthru.selfprivacy.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 671809d..6cfc4f5 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -11,7 +11,7 @@ let hostName = "${cfg.subdomain}.${sp.domain}"; auth-passthru = config.passthru.selfprivacy.auth; - is-auth-enabled = config.selfprivacy.modules.auth.enable; + is-auth-enabled = config.selfprivacy.modules.auth.enable or false; cfg = sp.modules.nextcloud; ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 45b160d..7a389d8 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -2,7 +2,7 @@ let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; - is-auth-enabled = config.selfprivacy.modules.auth.enable; + is-auth-enabled = config.selfprivacy.modules.auth.enable or false; auth-passthru = config.passthru.selfprivacy.auth; auth-fqdn = auth-passthru.auth-fqdn; oauth-client-id = "roundcube"; diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 0c2677d..4b97545 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -2,9 +2,10 @@ let inherit (import ./common.nix nixos-args) appendLdapBindPwd + auth-passthru cfg domain - auth-passthru + is-auth-enabled ; runtime-directory = "dovecot2"; @@ -61,7 +62,7 @@ let ''; }; in -lib.mkIf config.selfprivacy.modules.auth.enable { +lib.mkIf is-auth-enabled { mailserver.ldap = { # note: in `ldapsearch` first comes filter, then attributes dovecot.userAttrs = "+"; # all operational attributes diff --git a/sp-modules/simple-nixos-mailserver/auth-postfix.nix b/sp-modules/simple-nixos-mailserver/auth-postfix.nix index 6404380..ca6ca68 100644 --- a/sp-modules/simple-nixos-mailserver/auth-postfix.nix +++ b/sp-modules/simple-nixos-mailserver/auth-postfix.nix @@ -3,6 +3,7 @@ let inherit (import ./common.nix nixos-args) appendLdapBindPwd auth-passthru + is-auth-enabled ; cfg = config.mailserver; @@ -50,7 +51,7 @@ let destination = ldapVirtualMailboxMapFile; }; in -lib.mkIf config.selfprivacy.modules.auth.enable { +lib.mkIf is-auth-enabled { mailserver.ldap = { postfix.mailAttribute = "mail"; postfix.uidAttribute = "uid"; diff --git a/sp-modules/simple-nixos-mailserver/common.nix b/sp-modules/simple-nixos-mailserver/common.nix index d7544e6..eba175b 100644 --- a/sp-modules/simple-nixos-mailserver/common.nix +++ b/sp-modules/simple-nixos-mailserver/common.nix @@ -2,7 +2,7 @@ rec { auth-passthru = config.passthru.selfprivacy.auth; domain = config.selfprivacy.domain; - is-auth-enabled = config.selfprivacy.modules.auth.enable; + is-auth-enabled = config.selfprivacy.modules.auth.enable or false; appendLdapBindPwd = { name, file, prefix, suffix ? "", passwordFile, destination }: From 0c7a8d51b06c5d4690d020ac641f7b76781a0c2e Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 24 Jan 2025 16:27:48 +0400 Subject: [PATCH 024/115] fix gitea,nextcloud,roundcube: evaluate without auth module --- sp-modules/gitea/module.nix | 493 ++++++++++++++++---------------- sp-modules/nextcloud/module.nix | 308 ++++++++++---------- sp-modules/roundcube/module.nix | 143 ++++----- 3 files changed, 490 insertions(+), 454 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index d944885..82e57ab 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }: let sp = config.selfprivacy; stateDir = @@ -14,7 +14,7 @@ let "gitea-light" "gitea-dark" ]; - is-auth-enabled = config.selfprivacy.modules.auth.enable or false; + is-auth-enabled = sp.modules.auth.enable or false; oauth-client-id = "forgejo"; auth-passthru = config.passthru.selfprivacy.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; @@ -189,250 +189,261 @@ in }; }; - config = lib.mkIf cfg.enable { - fileSystems = lib.mkIf sp.useBinds { - "/var/lib/gitea" = { - device = "/volumes/${cfg.location}/gitea"; - options = [ "bind" ]; - }; - }; - services.gitea.enable = false; - services.forgejo = { - enable = true; - package = pkgs.forgejo; - inherit stateDir; - user = "gitea"; - group = "gitea"; - database = { - type = "sqlite3"; - host = "127.0.0.1"; - name = "gitea"; - user = "gitea"; - path = "${stateDir}/data/gitea.db"; - createDatabase = true; - }; - # ssh = { - # enable = true; - # clonePort = 22; - # }; - lfs = { - enable = cfg.enableLfs; - contentDir = "${stateDir}/lfs"; - }; - repositoryRoot = "${stateDir}/repositories"; - # cookieSecure = true; - settings = { - DEFAULT = { - APP_NAME = "${cfg.appName}"; - }; - server = { - DOMAIN = "${cfg.subdomain}.${sp.domain}"; - ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/"; - HTTP_ADDR = "0.0.0.0"; - HTTP_PORT = 3000; - }; - mailer = { - ENABLED = false; - }; - ui = { - DEFAULT_THEME = cfg.defaultTheme; - SHOW_USER_EMAIL = false; - }; - picture = { - DISABLE_GRAVATAR = true; - }; - admin = { - ENABLE_KANBAN_BOARD = true; - }; - repository = { - FORCE_PRIVATE = cfg.forcePrivate; - }; - session = { - COOKIE_SECURE = true; - }; - log = { - ROOT_PATH = "${stateDir}/log"; - LEVEL = if cfg.debug then "Warn" else "Trace"; - }; - service = { - DISABLE_REGISTRATION = cfg.disableRegistration; - REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; - }; - } // lib.attrsets.optionalAttrs is-auth-enabled { - auth.DISABLE_LOGIN_FORM = true; - service = { - DISABLE_REGISTRATION = cfg.disableRegistration; - REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; - ALLOW_ONLY_EXTERNAL_REGISTRATION = true; - SHOW_REGISTRATION_BUTTON = false; - ENABLE_BASIC_AUTHENTICATION = false; - }; - - # disallow explore page and access to private repositories, but allow public - "service.explore".REQUIRE_SIGNIN_VIEW = true; - - # TODO control via selfprivacy parameter - # "service.explore".DISABLE_USERS_PAGE = true; - - oauth2_client = { - REDIRECT_URI = redirect-uri; - ACCOUNT_LINKING = "auto"; - ENABLE_AUTO_REGISTRATION = true; - OPENID_CONNECT_SCOPES = "email openid profile"; - }; - # doesn't work if LDAP auth source is not active! - "cron.sync_external_users" = { - ENABLED = true; - RUN_AT_START = true; - NOTICE_ON_SUCCESS = true; + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/gitea" = { + device = "/volumes/${cfg.location}/gitea"; + options = [ "bind" ]; }; }; - }; - - users.users.gitea = { - home = "${stateDir}"; - useDefaultShell = true; - group = "gitea"; - isSystemUser = true; - }; - users.groups.gitea = { }; - services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { - useACMEHost = sp.domain; - forceSSL = true; - extraConfig = '' - add_header Strict-Transport-Security $hsts_header; - #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; - add_header 'Referrer-Policy' 'origin-when-cross-origin'; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - '' + lib.strings.optionalString is-auth-enabled '' - rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; - # FIXME is it needed? - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - ''; - locations = { - "/" = { - proxyPass = "http://127.0.0.1:3000"; - }; - }; - }; - systemd = { + services.gitea.enable = false; services.forgejo = { - unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea"; - serviceConfig = { - Slice = "gitea.slice"; + enable = true; + package = pkgs.forgejo; + inherit stateDir; + user = "gitea"; + group = "gitea"; + database = { + type = "sqlite3"; + host = "127.0.0.1"; + name = "gitea"; + user = "gitea"; + path = "${stateDir}/data/gitea.db"; + createDatabase = true; }; - preStart = - let - exe = lib.getExe config.services.forgejo.package; - # FIXME skip-tls-verify, bind-password - ldapConfigArgs = '' - --name LDAP \ - --active \ - --security-protocol LDAPS \ - --skip-tls-verify \ - --host '${auth-passthru.ldap-host}' \ - --port '${toString auth-passthru.ldap-port}' \ - --user-search-base '${auth-passthru.ldap-base-dn}' \ - --user-filter '(&(class=person)(memberof=${users-group})(name=%s))' \ - --admin-filter '(&(class=person)(memberof=${admins-group}))' \ - --username-attribute name \ - --firstname-attribute name \ - --surname-attribute displayname \ - --email-attribute mail \ - --public-ssh-key-attribute sshPublicKey \ - --bind-dn 'dn=token' \ - --bind-password "$(cat ${kanidm-service-account-token-fp})" \ - --synchronize-users + # ssh = { + # enable = true; + # clonePort = 22; + # }; + lfs = { + enable = cfg.enableLfs; + contentDir = "${stateDir}/lfs"; + }; + repositoryRoot = "${stateDir}/repositories"; + # cookieSecure = true; + settings = { + DEFAULT = { + APP_NAME = "${cfg.appName}"; + }; + server = { + DOMAIN = "${cfg.subdomain}.${sp.domain}"; + ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/"; + HTTP_ADDR = "0.0.0.0"; + HTTP_PORT = 3000; + }; + mailer = { + ENABLED = false; + }; + ui = { + DEFAULT_THEME = cfg.defaultTheme; + SHOW_USER_EMAIL = false; + }; + picture = { + DISABLE_GRAVATAR = true; + }; + admin = { + ENABLE_KANBAN_BOARD = true; + }; + repository = { + FORCE_PRIVATE = cfg.forcePrivate; + }; + session = { + COOKIE_SECURE = true; + }; + log = { + ROOT_PATH = "${stateDir}/log"; + LEVEL = if cfg.debug then "Warn" else "Trace"; + }; + service = { + DISABLE_REGISTRATION = cfg.disableRegistration; + REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; + }; + }; + }; + + users.users.gitea = { + home = "${stateDir}"; + useDefaultShell = true; + group = "gitea"; + isSystemUser = true; + }; + users.groups.gitea = { }; + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + useACMEHost = sp.domain; + forceSSL = true; + extraConfig = '' + add_header Strict-Transport-Security $hsts_header; + #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + ''; + locations = { + "/" = { + proxyPass = "http://127.0.0.1:3000"; + }; + }; + }; + systemd = { + services.forgejo = { + unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea"; + serviceConfig = { + Slice = "gitea.slice"; + }; + }; + slices.gitea = { + description = "Forgejo service slice"; + }; + }; + } + # the following part is active only when "auth" module is enabled + (lib.attrsets.optionalAttrs + (options.selfprivacy.modules ? "auth") + (lib.mkIf is-auth-enabled { + services.forgejo.settings = { + auth.DISABLE_LOGIN_FORM = true; + service = { + DISABLE_REGISTRATION = cfg.disableRegistration; + REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; + ALLOW_ONLY_EXTERNAL_REGISTRATION = true; + SHOW_REGISTRATION_BUTTON = false; + ENABLE_BASIC_AUTHENTICATION = false; + }; + + # disallow explore page and access to private repositories, but allow public + "service.explore".REQUIRE_SIGNIN_VIEW = true; + + # TODO control via selfprivacy parameter + # "service.explore".DISABLE_USERS_PAGE = true; + + oauth2_client = { + REDIRECT_URI = redirect-uri; + ACCOUNT_LINKING = "auto"; + ENABLE_AUTO_REGISTRATION = true; + OPENID_CONNECT_SCOPES = "email openid profile"; + }; + # doesn't work if LDAP auth source is not active! + "cron.sync_external_users" = { + ENABLED = true; + RUN_AT_START = true; + NOTICE_ON_SUCCESS = true; + }; + }; + systemd.services.forgejo = { + preStart = + let + exe = lib.getExe config.services.forgejo.package; + # FIXME skip-tls-verify, bind-password + ldapConfigArgs = '' + --name LDAP \ + --active \ + --security-protocol LDAPS \ + --skip-tls-verify \ + --host '${auth-passthru.ldap-host}' \ + --port '${toString auth-passthru.ldap-port}' \ + --user-search-base '${auth-passthru.ldap-base-dn}' \ + --user-filter '(&(class=person)(memberof=${users-group})(name=%s))' \ + --admin-filter '(&(class=person)(memberof=${admins-group}))' \ + --username-attribute name \ + --firstname-attribute name \ + --surname-attribute displayname \ + --email-attribute mail \ + --public-ssh-key-attribute sshPublicKey \ + --bind-dn 'dn=token' \ + --bind-password "$(cat ${kanidm-service-account-token-fp})" \ + --synchronize-users + ''; + oauthConfigArgs = '' + --name "${oauth2-provider-name}" \ + --provider openidConnect \ + --key forgejo \ + --secret "$(<${kanidm-oauth-client-secret-fp})" \ + --group-claim-name groups \ + --admin-group admins \ + --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' + ''; + in + lib.mkAfter '' + set -o xtrace + + # Check if LDAP is already configured + ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" + + if [[ -n "$ldap_line" ]]; then + # update ldap config + id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} + else + # initially configure ldap + ${exe} admin auth add-ldap ${ldapConfigArgs} + fi + + oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" + if [[ -n "$oauth_line" ]]; then + id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} + else + ${exe} admin auth add-oauth ${oauthConfigArgs} + fi ''; - oauthConfigArgs = '' - --name "${oauth2-provider-name}" \ - --provider openidConnect \ - --key forgejo \ - --secret "$(<${kanidm-oauth-client-secret-fp})" \ - --group-claim-name groups \ - --admin-group admins \ - --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' - ''; - in - lib.mkIf is-auth-enabled (lib.mkAfter '' - set -o xtrace - - # Check if LDAP is already configured - ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" - - if [[ -n "$ldap_line" ]]; then - # update ldap config - id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} - else - # initially configure ldap - ${exe} admin auth add-ldap ${ldapConfigArgs} - fi - - oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" - if [[ -n "$oauth_line" ]]; then - id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} - else - ${exe} admin auth add-oauth ${oauthConfigArgs} - fi - '' - ); - # TODO consider passing oauth consumer service to auth module instead - requires = lib.mkIf is-auth-enabled - [ auth-passthru.oauth2-systemd-service ]; - }; - slices.gitea = { - description = "Forgejo service slice"; - }; - }; - - # for ExecStartPost script to have access to /run/keys/* - users.groups.keys.members = - lib.mkIf is-auth-enabled [ config.services.forgejo.group ]; - - systemd.services.kanidm.serviceConfig.ExecStartPre = - lib.mkIf is-auth-enabled [ - ("-+" + kanidmExecStartPreScriptRoot) - ("-" + kanidmExecStartPreScript) - ]; - systemd.services.kanidm.serviceConfig.ExecStartPost = - lib.mkIf is-auth-enabled - (lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]); - services.kanidm.provision = lib.mkIf is-auth-enabled { - groups = { - "${admins-group}".members = [ "sp.admins" ]; - "${users-group}".members = [ admins-group ]; - }; - systems.oauth2.forgejo = { - displayName = "Forgejo"; - originUrl = redirect-uri; - originLanding = "https://${cfg.subdomain}.${sp.domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = true; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps = { - "${users-group}" = [ - "email" - "openid" - "profile" - ]; + # TODO consider passing oauth consumer service to auth module instead + requires = [ auth-passthru.oauth2-systemd-service ]; }; - removeOrphanedClaimMaps = true; - # NOTE https://github.com/oddlama/kanidm-provision/issues/15 - # add more scopes when a user is a member of specific group - # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 - # supplementaryScopeMaps."${admins-group}" = - # [ "read:admin" "write:admin" ]; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${admins-group} = [ "admins" ]; + + # for ExecStartPost script to have access to /run/keys/* + users.groups.keys.members = [ config.services.forgejo.group ]; + + systemd.services.kanidm.serviceConfig.ExecStartPre = [ + ("-+" + kanidmExecStartPreScriptRoot) + ("-" + kanidmExecStartPreScript) + ]; + systemd.services.kanidm.serviceConfig.ExecStartPost = + lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]; + + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + extraConfig = lib.mkAfter '' + rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; + # FIXME is it needed? + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + ''; }; - }; - }; - }; + + services.kanidm.provision = { + groups = { + "${admins-group}".members = [ "sp.admins" ]; + "${users-group}".members = [ admins-group ]; + }; + systems.oauth2.forgejo = { + displayName = "Forgejo"; + originUrl = redirect-uri; + originLanding = "https://${cfg.subdomain}.${sp.domain}/"; + basicSecretFile = kanidm-oauth-client-secret-fp; + # when true, name is passed to a service instead of name@domain + preferShortUsername = true; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps = { + "${users-group}" = [ + "email" + "openid" + "profile" + ]; + }; + removeOrphanedClaimMaps = true; + # NOTE https://github.com/oddlama/kanidm-provision/issues/15 + # add more scopes when a user is a member of specific group + # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 + # supplementaryScopeMaps."${admins-group}" = + # [ "read:admin" "write:admin" ]; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${admins-group} = [ "admins" ]; + }; + }; + }; + }) + ) + ]); } diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 6cfc4f5..f8f9499 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }: let inherit (import ./common.nix config) admin-pass-filepath @@ -11,7 +11,7 @@ let hostName = "${cfg.subdomain}.${sp.domain}"; auth-passthru = config.passthru.selfprivacy.auth; - is-auth-enabled = config.selfprivacy.modules.auth.enable or false; + is-auth-enabled = sp.modules.nextcloud.enableSso; cfg = sp.modules.nextcloud; ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; @@ -104,6 +104,15 @@ in type = "enable"; }; }; + enableSso = (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable SSO for Nextcloud"; + }) // { + meta = { + type = "enable"; + }; + }; location = (lib.mkOption { type = lib.types.str; description = "Nextcloud location"; @@ -140,36 +149,139 @@ in }; }; - config = lib.mkIf sp.modules.nextcloud.enable { - fileSystems = lib.mkIf sp.useBinds { - "/var/lib/nextcloud" = { - device = "/volumes/${cfg.location}/nextcloud"; - options = [ - "bind" - "x-systemd.required-by=nextcloud-setup.service" - "x-systemd.required-by=nextcloud-secrets.service" - "x-systemd.before=nextcloud-setup.service" - "x-systemd.before=nextcloud-secrets.service" - ]; + # config = lib.mkIf sp.modules.nextcloud.enable + config = lib.mkIf sp.modules.nextcloud.enable (lib.mkMerge [ + { + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/nextcloud" = { + device = "/volumes/${cfg.location}/nextcloud"; + options = [ + "bind" + "x-systemd.required-by=nextcloud-setup.service" + "x-systemd.required-by=nextcloud-secrets.service" + "x-systemd.before=nextcloud-setup.service" + "x-systemd.before=nextcloud-secrets.service" + ]; + }; }; - }; - # for ExecStartPost script to have access to /run/keys/* - users.groups.keys.members = - lib.mkIf is-auth-enabled [ nextcloud-setup-group ]; + # for ExecStartPost script to have access to /run/keys/* + users.groups.keys.members = + lib.mkIf is-auth-enabled [ nextcloud-setup-group ]; - # not needed, due to turnOffCertCheck=1 in used_ldap - # users.groups.${config.security.acme.certs.${domain}.group}.members = - # [ config.services.phpfpm.pools.nextcloud.user ]; + # not needed, due to turnOffCertCheck=1 in used_ldap + # users.groups.${config.security.acme.certs.${domain}.group}.members = + # [ config.services.phpfpm.pools.nextcloud.user ]; - systemd = { - services = { - phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice"; - nextcloud-setup = { - serviceConfig.Slice = "nextcloud.slice"; - serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; - path = lib.mkIf is-auth-enabled [ pkgs.jq ]; - script = lib.mkIf is-auth-enabled '' + systemd = { + services = { + phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice"; + nextcloud-setup = { + serviceConfig.Slice = "nextcloud.slice"; + serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; + }; + kanidm.serviceConfig.ExecStartPre = lib.mkIf is-auth-enabled + (lib.mkAfter [ + ("-+" + kanidmExecStartPreScriptRoot) + ("-" + kanidmExecStartPreScript) + ]); + kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled + (lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]); + nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-secrets = { + before = [ "nextcloud-setup.service" ]; + requiredBy = [ "nextcloud-setup.service" ]; + serviceConfig.Type = "oneshot"; + path = with pkgs; [ coreutils jq ]; + script = '' + databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath}) + adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath}) + + install -C -m 0440 -o nextcloud -g nextcloud -DT \ + <(printf "%s\n" "$databasePassword") \ + ${db-pass-filepath} + + install -C -m 0440 -o nextcloud -g nextcloud -DT \ + <(printf "%s\n" "$adminPassword") \ + ${admin-pass-filepath} + ''; + }; + }; + slices.nextcloud = { + description = "Nextcloud service slice"; + }; + }; + services.nextcloud = { + enable = true; + package = pkgs.nextcloud29; + inherit hostName; + + # Use HTTPS for links + https = true; + + # auto-update Nextcloud Apps + autoUpdateApps.enable = true; + # set what time makes sense for you + autoUpdateApps.startAt = "05:00:00"; + + phpOptions.display_errors = "Off"; + + settings = { + # further forces Nextcloud to use HTTPS + overwriteprotocol = "https"; + } // lib.attrsets.optionalAttrs is-auth-enabled { + loglevel = 0; + # log_type = "file"; + social_login_auto_redirect = false; + + allow_local_remote_servers = true; + allow_user_to_change_display_name = false; + lost_password_link = "disabled"; + allow_multiple_user_backends = false; + + user_oidc = { + single_logout = true; + use_pkce = true; + auto_provision = true; + soft_auto_provision = true; + disable_account_creation = false; + }; + }; + + config = { + dbtype = "sqlite"; + dbuser = "nextcloud"; + dbname = "nextcloud"; + dbpassFile = db-pass-filepath; + # TODO review whether admin user is needed at all - admin group works + adminpassFile = admin-pass-filepath; + adminuser = "admin"; + }; + + secretFile = lib.mkIf is-auth-enabled nextcloud-secret-file; + }; + services.nginx.virtualHosts.${hostName} = { + useACMEHost = sp.domain; + forceSSL = true; + #locations."/".extraConfig = lib.mkIf is-auth-enabled '' + # # FIXME does not work + # rewrite ^/login$ /apps/user_oidc/login/1 last; + #''; + # show an error instead of a blank page on Nextcloud PHP/FastCGI error + locations."~ \\.php(?:$|/)".extraConfig = '' + error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html; + ''; + }; + } + # the following part is active only when "auth" module is enabled + (lib.attrsets.optionalAttrs + (options.selfprivacy.modules ? "auth") + (lib.mkIf is-auth-enabled { + systemd.services.nextcloud-setup = { + path = [ pkgs.jq ]; + script = '' set -o errexit set -o nounset ${lib.strings.optionalString cfg.debug "set -o xtrace"} @@ -266,123 +378,29 @@ in -vvv ''; # TODO consider passing oauth consumer service to auth module instead - requires = lib.mkIf is-auth-enabled - [ auth-passthru.oauth2-systemd-service ]; + requires = [ auth-passthru.oauth2-systemd-service ]; }; - kanidm.serviceConfig.ExecStartPre = lib.mkIf is-auth-enabled - (lib.mkAfter [ - ("-+" + kanidmExecStartPreScriptRoot) - ("-" + kanidmExecStartPreScript) - ]); - kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled - (lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]); - nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-secrets = { - before = [ "nextcloud-setup.service" ]; - requiredBy = [ "nextcloud-setup.service" ]; - serviceConfig.Type = "oneshot"; - path = with pkgs; [ coreutils jq ]; - script = '' - databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath}) - adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath}) - - install -C -m 0440 -o nextcloud -g nextcloud -DT \ - <(printf "%s\n" "$databasePassword") \ - ${db-pass-filepath} - - install -C -m 0440 -o nextcloud -g nextcloud -DT \ - <(printf "%s\n" "$adminPassword") \ - ${admin-pass-filepath} - ''; + services.kanidm.provision = { + groups = { + "${admins-group}".members = [ "sp.admins" ]; + "${users-group}".members = [ admins-group ]; + }; + systems.oauth2.${oauth-client-id} = { + displayName = "Nextcloud"; + originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = kanidm-oauth-client-secret-fp; + # when true, name is passed to a service instead of name@domain + preferShortUsername = true; + allowInsecureClientDisablePkce = false; + scopeMaps.${users-group} = [ "email" "openid" "profile" ]; + removeOrphanedClaimMaps = true; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${admins-group} = [ "admin" ]; + }; + }; }; - }; - slices.nextcloud = { - description = "Nextcloud service slice"; - }; - }; - services.nextcloud = { - enable = true; - package = pkgs.nextcloud29; - inherit hostName; - - # Use HTTPS for links - https = true; - - # auto-update Nextcloud Apps - autoUpdateApps.enable = true; - # set what time makes sense for you - autoUpdateApps.startAt = "05:00:00"; - - phpOptions.display_errors = "Off"; - - settings = { - # further forces Nextcloud to use HTTPS - overwriteprotocol = "https"; - } // lib.attrsets.optionalAttrs is-auth-enabled { - loglevel = 0; - # log_type = "file"; - social_login_auto_redirect = false; - - allow_local_remote_servers = true; - allow_user_to_change_display_name = false; - lost_password_link = "disabled"; - allow_multiple_user_backends = false; - - user_oidc = { - single_logout = true; - use_pkce = true; - auto_provision = true; - soft_auto_provision = true; - disable_account_creation = false; - }; - }; - - config = { - dbtype = "sqlite"; - dbuser = "nextcloud"; - dbname = "nextcloud"; - dbpassFile = db-pass-filepath; - # TODO review whether admin user is needed at all - admin group works - adminpassFile = admin-pass-filepath; - adminuser = "admin"; - }; - - secretFile = lib.mkIf is-auth-enabled nextcloud-secret-file; - }; - services.nginx.virtualHosts.${hostName} = { - useACMEHost = sp.domain; - forceSSL = true; - #locations."/".extraConfig = lib.mkIf is-auth-enabled '' - # # FIXME does not work - # rewrite ^/login$ /apps/user_oidc/login/1 last; - #''; - # show an error instead of a blank page on Nextcloud PHP/FastCGI error - locations."~ \\.php(?:$|/)".extraConfig = '' - error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html; - ''; - }; - services.kanidm.provision = lib.mkIf is-auth-enabled { - groups = { - "${admins-group}".members = [ "sp.admins" ]; - "${users-group}".members = [ admins-group ]; - }; - systems.oauth2.${oauth-client-id} = { - displayName = "Nextcloud"; - originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = true; - allowInsecureClientDisablePkce = false; - scopeMaps.${users-group} = [ "email" "openid" "profile" ]; - removeOrphanedClaimMaps = true; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${admins-group} = [ "admin" ]; - }; - }; - }; - }; + })) + ]); } diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 7a389d8..9ed07d4 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }: let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; @@ -48,74 +48,81 @@ in }; }; - config = lib.mkIf cfg.enable { - services.roundcube = { - enable = true; - # this is the url of the vhost, not necessarily the same as the fqdn of - # the mailserver - hostName = "${cfg.subdomain}.${config.selfprivacy.domain}"; - extraConfig = '' - # starttls needed for authentication, so the fqdn required to match - # the certificate - $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; - $config['smtp_user'] = "%u"; - $config['smtp_pass'] = "%p"; - '' + lib.strings.optionalString is-auth-enabled '' - $config['oauth_provider'] = 'generic'; - $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; - $config['oauth_client_id'] = '${oauth-client-id}'; - $config['oauth_client_secret'] = "$(<${kanidm-oauth-client-secret-fp})"; - $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; - $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; - $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; - $config['oauth_scope'] = 'email profile openid'; # FIXME - $config['oauth_auth_parameters'] = []; - $config['oauth_identity_fields'] = ['email']; - $config['oauth_login_redirect'] = true; - $config['auto_create_user'] = true; - $config['oauth_verify_peer'] = false; # FIXME - # $config['oauth_pkce'] = 'S256'; # FIXME - ''; - }; - - services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { - forceSSL = true; - useACMEHost = domain; - enableACME = false; - }; - - systemd.slices.roundcube.description = "Roundcube service slice"; - - systemd.services.kanidm = lib.mkIf is-auth-enabled { - serviceConfig.ExecStartPre = lib.mkAfter [ - ("-+" + kanidmExecStartPreScriptRoot) - ("-" + kanidmExecStartPreScript) - ]; - requires = [ auth-passthru.oauth2-systemd-service ]; - }; - - services.kanidm.provision = lib.mkIf is-auth-enabled { - groups = { - "sp.roundcube.admins".members = [ "sp.admins" ]; - "sp.roundcube.users".members = [ "sp.roundcube.admins" ]; + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + services.roundcube = { + enable = true; + # this is the url of the vhost, not necessarily the same as the fqdn of + # the mailserver + hostName = "${cfg.subdomain}.${config.selfprivacy.domain}"; + extraConfig = '' + # starttls needed for authentication, so the fqdn required to match + # the certificate + $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; + $config['smtp_user'] = "%u"; + $config['smtp_pass'] = "%p"; + ''; }; - systems.oauth2.roundcube = { - displayName = "Roundcube"; - originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = false; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps = { - "sp.roundcube.users" = [ - "email" - "openid" - "profile" + + services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { + forceSSL = true; + useACMEHost = domain; + enableACME = false; + }; + + systemd.slices.roundcube.description = "Roundcube service slice"; + } + # the following part is active only when "auth" module is enabled + (lib.attrsets.optionalAttrs + (options.selfprivacy.modules ? "auth") + (lib.mkIf is-auth-enabled { + services.roundcube.extraConfig = lib.mkAfter '' + $config['oauth_provider'] = 'generic'; + $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; + $config['oauth_client_id'] = '${oauth-client-id}'; + $config['oauth_client_secret'] = "$(<${kanidm-oauth-client-secret-fp})"; + $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; + $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; + $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; + $config['oauth_scope'] = 'email profile openid'; # FIXME + $config['oauth_auth_parameters'] = []; + $config['oauth_identity_fields'] = ['email']; + $config['oauth_login_redirect'] = true; + $config['auto_create_user'] = true; + $config['oauth_verify_peer'] = false; # FIXME + # $config['oauth_pkce'] = 'S256'; # FIXME + ''; + systemd.services.kanidm = { + serviceConfig.ExecStartPre = lib.mkAfter [ + ("-+" + kanidmExecStartPreScriptRoot) + ("-" + kanidmExecStartPreScript) ]; + requires = [ auth-passthru.oauth2-systemd-service ]; }; - removeOrphanedClaimMaps = true; - }; - }; - }; + services.kanidm.provision = { + groups = { + "sp.roundcube.admins".members = [ "sp.admins" ]; + "sp.roundcube.users".members = [ "sp.roundcube.admins" ]; + }; + systems.oauth2.roundcube = { + displayName = "Roundcube"; + originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = kanidm-oauth-client-secret-fp; + # when true, name is passed to a service instead of name@domain + preferShortUsername = false; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps = { + "sp.roundcube.users" = [ + "email" + "openid" + "profile" + ]; + }; + removeOrphanedClaimMaps = true; + }; + }; + }) + ) + ]); } From d8d1a1e86f8e7240a7835d70897e4ac54327d1d0 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 25 Jan 2025 01:08:41 +0400 Subject: [PATCH 025/115] fix mailserver: evaluate without auth module --- .../simple-nixos-mailserver/auth-dovecot.nix | 2 +- .../simple-nixos-mailserver/auth-postfix.nix | 2 +- sp-modules/simple-nixos-mailserver/config.nix | 239 +++++++++--------- sp-modules/simple-nixos-mailserver/flake.nix | 2 - 4 files changed, 127 insertions(+), 118 deletions(-) diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 4b97545..10dbb3d 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -62,7 +62,7 @@ let ''; }; in -lib.mkIf is-auth-enabled { +{ mailserver.ldap = { # note: in `ldapsearch` first comes filter, then attributes dovecot.userAttrs = "+"; # all operational attributes diff --git a/sp-modules/simple-nixos-mailserver/auth-postfix.nix b/sp-modules/simple-nixos-mailserver/auth-postfix.nix index ca6ca68..38b141c 100644 --- a/sp-modules/simple-nixos-mailserver/auth-postfix.nix +++ b/sp-modules/simple-nixos-mailserver/auth-postfix.nix @@ -51,7 +51,7 @@ let destination = ldapVirtualMailboxMapFile; }; in -lib.mkIf is-auth-enabled { +{ mailserver.ldap = { postfix.mailAttribute = "mail"; postfix.uidAttribute = "uid"; diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 2d830c0..6868666 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -1,8 +1,8 @@ -{ config, lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }@nixos-args: let sp = config.selfprivacy; - inherit (import ./common.nix {inherit config pkgs;}) + inherit (import ./common.nix { inherit config pkgs; }) auth-passthru domain is-auth-enabled @@ -67,63 +67,49 @@ let fi ''; in -lib.mkIf sp.modules.simple-nixos-mailserver.enable -{ - fileSystems = lib.mkIf sp.useBinds - { - "/var/vmail" = { - device = - "/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail"; - options = [ - "bind" - "x-systemd.required-by=postfix.service" - "x-systemd.before=postfix.service" - ]; +lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ + { + fileSystems = lib.mkIf sp.useBinds + { + "/var/vmail" = { + device = + "/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail"; + options = [ + "bind" + "x-systemd.required-by=postfix.service" + "x-systemd.before=postfix.service" + ]; + }; + "/var/sieve" = { + device = + "/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve"; + options = [ + "bind" + "x-systemd.required-by=dovecot2.service" + "x-systemd.before=dovecot2.service" + ]; + }; }; - "/var/sieve" = { - device = - "/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve"; - options = [ - "bind" - "x-systemd.required-by=dovecot2.service" - "x-systemd.before=dovecot2.service" - ]; + + users.users = { + virtualMail = { + isNormalUser = false; }; }; - users.users = { - virtualMail = { - isNormalUser = false; - }; - }; + users.groups.acmereceivers.members = [ "dovecot2" "postfix" "virtualMail" ]; - users.groups.acmereceivers.members = [ "dovecot2" "postfix" "virtualMail" ]; + mailserver = { + enable = true; + fqdn = sp.domain; + domains = [ sp.domain ]; + localDnsResolver = false; - mailserver = { - enable = true; - fqdn = sp.domain; - domains = [ sp.domain ]; - localDnsResolver = false; - - # A list of all login accounts. To create the password hashes, use - # mkpasswd -m sha-512 "super secret password" - loginAccounts = lib.mkIf (!is-auth-enabled) ({ - "${sp.username}@${sp.domain}" = { - hashedPassword = sp.hashedMasterPassword; - sieveScript = '' - require ["fileinto", "mailbox"]; - if header :contains "Chat-Version" "1.0" - { - fileinto :create "DeltaChat"; - stop; - } - ''; - }; - } // builtins.listToAttrs (builtins.map - (user: { - name = "${user.username}@${sp.domain}"; - value = { - hashedPassword = user.hashedPassword; + # A list of all login accounts. To create the password hashes, use + # mkpasswd -m sha-512 "super secret password" + loginAccounts = ({ + "${sp.username}@${sp.domain}" = { + hashedPassword = sp.hashedMasterPassword; sieveScript = '' require ["fileinto", "mailbox"]; if header :contains "Chat-Version" "1.0" @@ -133,70 +119,95 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable } ''; }; - }) - sp.users)); + } // builtins.listToAttrs (builtins.map + (user: { + name = "${user.username}@${sp.domain}"; + value = { + hashedPassword = user.hashedPassword; + sieveScript = '' + require ["fileinto", "mailbox"]; + if header :contains "Chat-Version" "1.0" + { + fileinto :create "DeltaChat"; + stop; + } + ''; + }; + }) + sp.users)); - extraVirtualAliases = lib.mkIf (!is-auth-enabled) { - "admin@${sp.domain}" = "${sp.username}@${sp.domain}"; + extraVirtualAliases = { + "admin@${sp.domain}" = "${sp.username}@${sp.domain}"; + }; + + certificateScheme = "manual"; + certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem"; + keyFile = "/var/lib/acme/root-${sp.domain}/key.pem"; + + # Enable IMAP and POP3 + enableImap = true; + enableImapSsl = true; + enablePop3 = false; + enablePop3Ssl = false; + dkimSelector = "selector"; + + # Enable the ManageSieve protocol + enableManageSieve = true; + + virusScanning = false; + + mailDirectory = "/var/vmail"; }; - certificateScheme = "manual"; - certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem"; - keyFile = "/var/lib/acme/root-${sp.domain}/key.pem"; - - # Enable IMAP and POP3 - enableImap = true; - enableImapSsl = true; - enablePop3 = false; - enablePop3Ssl = false; - dkimSelector = "selector"; - - # Enable the ManageSieve protocol - enableManageSieve = true; - - virusScanning = false; - - mailDirectory = "/var/vmail"; - - # LDAP is needed for Postfix to query Kanidm about email address ownership. - # LDAP is needed for Dovecot also. - ldap = lib.mkIf is-auth-enabled { - # false; otherwise, simple-nixos-mailserver enables auth via LDAP - enable = false; - - # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; - bind.dn = "dn=token"; - # TODO change in this file should trigger system restart dovecot - bind.passwordFile = mailserver-service-account-token-fp; - - # searchBase = "ou=persons," + ldap_base_dn; - searchBase = auth-passthru.ldap-base-dn; # TODO refine this - - # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) - uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; + systemd = { + services = { + dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + }; + slices."simple_nixos_mailserver" = { + name = "simple_nixos_mailserver.slice"; + description = "Simple NixOS Mailserver service slice"; + }; }; - }; + } + # the following part is active only when "auth" module is enabled + (lib.attrsets.optionalAttrs + (options.selfprivacy.modules ? "auth") + (lib.mkIf is-auth-enabled { + mailserver = { + extraVirtualAliases = lib.mkForce { }; + loginAccounts = lib.mkForce { }; + # LDAP is needed for Postfix to query Kanidm about email address ownership. + # LDAP is needed for Dovecot also. + ldap = { + # false; otherwise, simple-nixos-mailserver enables auth via LDAP + enable = false; - systemd = { - services = { - dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; + bind.dn = "dn=token"; + # TODO change in this file should trigger system restart dovecot + bind.passwordFile = mailserver-service-account-token-fp; + + # searchBase = "ou=persons," + ldap_base_dn; + searchBase = auth-passthru.ldap-base-dn; # TODO refine this + + # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) + uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; + }; + }; # FIXME set auth module option instead - kanidm.serviceConfig.ExecStartPost = - lib.mkIf is-auth-enabled - (lib.mkAfter - [ - ("+" + kanidmExecStartPostScriptRoot) - kanidmExecStartPostScript - ] - ); - }; - slices."simple_nixos_mailserver" = { - name = "simple_nixos_mailserver.slice"; - description = "Simple NixOS Mailserver service slice"; - }; - }; -} + systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ + ("+" + kanidmExecStartPostScriptRoot) + kanidmExecStartPostScript + ]; + })) + (lib.attrsets.optionalAttrs + (options.selfprivacy.modules ? "auth") + (lib.mkIf is-auth-enabled (import ./auth-dovecot.nix nixos-args))) + (lib.attrsets.optionalAttrs + (options.selfprivacy.modules ? "auth") + (lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args))) +]) diff --git a/sp-modules/simple-nixos-mailserver/flake.nix b/sp-modules/simple-nixos-mailserver/flake.nix index 333e097..abff9aa 100644 --- a/sp-modules/simple-nixos-mailserver/flake.nix +++ b/sp-modules/simple-nixos-mailserver/flake.nix @@ -10,8 +10,6 @@ mailserver.nixosModules.default ./options.nix ./config.nix - ./auth-postfix.nix - ./auth-dovecot.nix ]; }; configPathsNeeded = From d008fbcc178c562d51283c3d9c7fe942ccfc1155 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 25 Jan 2025 01:24:28 +0400 Subject: [PATCH 026/115] auth: sp.full_users group --- sp-modules/auth/module.nix | 3 +++ sp-modules/gitea/module.nix | 3 ++- sp-modules/nextcloud/module.nix | 3 ++- sp-modules/roundcube/module.nix | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index 80850ad..8670e81 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -98,6 +98,7 @@ in enable = true; autoRemove = true; # if false, obsolete oauth2 scopeMaps remain groups."sp.admins".present = true; + groups.${passthru.full-users-group}.present = true; }; enableClient = true; clientSettings = { @@ -184,6 +185,8 @@ in (lib.strings.splitString "." domain); ldap-host = "127.0.0.1"; ldap-port = 3636; + + full-users-group = "sp.full_users"; }; }; } diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 82e57ab..390c017 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -414,7 +414,8 @@ in services.kanidm.provision = { groups = { "${admins-group}".members = [ "sp.admins" ]; - "${users-group}".members = [ admins-group ]; + "${users-group}".members = + [ admins-group auth-passthru.full-users-group ]; }; systems.oauth2.forgejo = { displayName = "Forgejo"; diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index f8f9499..1ad01f5 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -383,7 +383,8 @@ in services.kanidm.provision = { groups = { "${admins-group}".members = [ "sp.admins" ]; - "${users-group}".members = [ admins-group ]; + "${users-group}".members = + [ admins-group auth-passthru.full-users-group ]; }; systems.oauth2.${oauth-client-id} = { displayName = "Nextcloud"; diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 9ed07d4..996abc2 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -102,7 +102,8 @@ in services.kanidm.provision = { groups = { "sp.roundcube.admins".members = [ "sp.admins" ]; - "sp.roundcube.users".members = [ "sp.roundcube.admins" ]; + "sp.roundcube.users".members = + [ "sp.roundcube.admins" auth-passthru.full-users-group ]; }; systems.oauth2.roundcube = { displayName = "Roundcube"; From 2ed4cc0dee9a931bd10ca547d95c4e52c5c4cd57 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 25 Jan 2025 23:20:00 +0400 Subject: [PATCH 027/115] passthru.selfprivacy.auth.admins-group = "sp.admins" --- sp-modules/auth/module.nix | 3 ++- sp-modules/gitea/module.nix | 2 +- sp-modules/nextcloud/module.nix | 2 +- sp-modules/roundcube/module.nix | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index 8670e81..c4644ca 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -97,7 +97,7 @@ in provision = { enable = true; autoRemove = true; # if false, obsolete oauth2 scopeMaps remain - groups."sp.admins".present = true; + groups.${passthru.admins-group}.present = true; groups.${passthru.full-users-group}.present = true; }; enableClient = true; @@ -186,6 +186,7 @@ in ldap-host = "127.0.0.1"; ldap-port = 3636; + admins-group = "sp.admins"; full-users-group = "sp.full_users"; }; }; diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 390c017..c992599 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -413,7 +413,7 @@ in services.kanidm.provision = { groups = { - "${admins-group}".members = [ "sp.admins" ]; + "${admins-group}".members = [ auth-passthru.admins-group ]; "${users-group}".members = [ admins-group auth-passthru.full-users-group ]; }; diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 1ad01f5..62104ac 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -382,7 +382,7 @@ in }; services.kanidm.provision = { groups = { - "${admins-group}".members = [ "sp.admins" ]; + "${admins-group}".members = [ auth-passthru.admins-group ]; "${users-group}".members = [ admins-group auth-passthru.full-users-group ]; }; diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 996abc2..3626cad 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -101,7 +101,7 @@ in }; services.kanidm.provision = { groups = { - "sp.roundcube.admins".members = [ "sp.admins" ]; + "sp.roundcube.admins".members = [ auth-passthru.admins-group ]; "sp.roundcube.users".members = [ "sp.roundcube.admins" auth-passthru.full-users-group ]; }; From 2cc57431527e0499e67c7db558e0a0af673da69f Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 29 Jan 2025 12:53:32 +0400 Subject: [PATCH 028/115] fix sp-modules: configPathsNeeded, requiring passthru.selfprivacy.auth --- sp-modules/gitea/config-paths-needed.json | 2 ++ sp-modules/nextcloud/config-paths-needed.json | 3 ++- sp-modules/roundcube/config-paths-needed.json | 2 ++ sp-modules/simple-nixos-mailserver/config-paths-needed.json | 6 +++++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sp-modules/gitea/config-paths-needed.json b/sp-modules/gitea/config-paths-needed.json index 44e2729..b681d62 100644 --- a/sp-modules/gitea/config-paths-needed.json +++ b/sp-modules/gitea/config-paths-needed.json @@ -1,5 +1,7 @@ [ + [ "passthru", "selfprivacy", "auth", "admins-group" ], [ "passthru", "selfprivacy", "auth", "auth-fqdn" ], + [ "passthru", "selfprivacy", "auth", "full-users-group" ], [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], [ "passthru", "selfprivacy", "auth", "ldap-host" ], [ "passthru", "selfprivacy", "auth", "ldap-port" ], diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index bd46940..1e846f9 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -1,5 +1,6 @@ [ - [ "passthru", "selfprivacy", "auth" ], + [ "passthru", "selfprivacy", "auth", "admins-group" ], + [ "passthru", "selfprivacy", "auth", "full-users-group" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth", "enable" ], diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index dd62508..aff2a53 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -1,6 +1,8 @@ [ [ "mailserver", "fqdn" ], + [ "passthru", "selfprivacy", "auth", "admins-group" ], [ "passthru", "selfprivacy", "auth", "auth-fqdn" ], + [ "passthru", "selfprivacy", "auth", "full-users-group" ], [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "domain" ], diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index a057322..3e0ce04 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -1,6 +1,10 @@ [ [ "mailserver" ], - [ "passthru", "selfprivacy", "auth" ], + [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], + [ "passthru", "selfprivacy", "auth", "ldap-port" ], + [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], + [ "passthru", "selfprivacy", "auth", "oauth2-introspection-url" ], + [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "hashedMasterPassword" ], From 857d6729ef3e678da7a936ecf2d8ec2ace950e33 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 29 Jan 2025 13:21:36 +0400 Subject: [PATCH 029/115] fix nextcloud when sp.modules.auth.enable is true --- sp-modules/nextcloud/config-paths-needed.json | 6 ++++++ sp-modules/nextcloud/module.nix | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index 1e846f9..a263bbd 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -1,6 +1,12 @@ [ [ "passthru", "selfprivacy", "auth", "admins-group" ], [ "passthru", "selfprivacy", "auth", "full-users-group" ], + [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], + [ "passthru", "selfprivacy", "auth", "ldap-host" ], + [ "passthru", "selfprivacy", "auth", "ldap-port" ], + [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], + [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], + [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth", "enable" ], diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 62104ac..f353e17 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -11,7 +11,7 @@ let hostName = "${cfg.subdomain}.${sp.domain}"; auth-passthru = config.passthru.selfprivacy.auth; - is-auth-enabled = sp.modules.nextcloud.enableSso; + is-auth-enabled = sp.modules.auth.enable or false; cfg = sp.modules.nextcloud; ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; From 67a943c829b3d18e547a82780fdbc78ff06f6dda Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 29 Jan 2025 14:30:18 +0400 Subject: [PATCH 030/115] fix roundcube: ['oauth_client_secret'] = file_get_contents... --- sp-modules/roundcube/module.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 3626cad..b5d6ddf 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -6,6 +6,7 @@ let auth-passthru = config.passthru.selfprivacy.auth; auth-fqdn = auth-passthru.auth-fqdn; oauth-client-id = "roundcube"; + roundcube-user = "roundcube"; roundcube-group = "roundcube"; kanidmExecStartPreScriptRoot = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPre-root-script.sh" @@ -76,11 +77,13 @@ in (lib.attrsets.optionalAttrs (options.selfprivacy.modules ? "auth") (lib.mkIf is-auth-enabled { + # for phpfpm-roundcube to have access to get through /run/keys directory + users.groups.keys.members = [ roundcube-user ]; services.roundcube.extraConfig = lib.mkAfter '' $config['oauth_provider'] = 'generic'; $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; $config['oauth_client_id'] = '${oauth-client-id}'; - $config['oauth_client_secret'] = "$(<${kanidm-oauth-client-secret-fp})"; + $config['oauth_client_secret'] = file_get_contents('${kanidm-oauth-client-secret-fp}'); $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; From f1d2119f62b7e4416790f02ac743418d6c75eddd Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 31 Jan 2025 14:24:05 +0400 Subject: [PATCH 031/115] define selfprivacy.passthru option (type = types.submodule) Stock NixOS passthru option cannot be defined in multiple places. But we need to pass arbitrary parameters between SP modules. --- selfprivacy-module.nix | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/selfprivacy-module.nix b/selfprivacy-module.nix index 21fd25f..10c6b45 100644 --- a/selfprivacy-module.nix +++ b/selfprivacy-module.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ lib, pkgs, ... }: with lib; { @@ -139,5 +139,20 @@ with lib; default = null; }; }; + ################ + # passthrough # + ################ + passthru = mkOption { + type = types.submodule { + freeformType = (pkgs.formats.json { }).type; + options = { }; + }; + default = { }; + visible = false; + description = '' + This attribute allows to share data between modules. + You can put whatever you want here. + ''; + }; }; } From 89e7145a01deb718935c4ddf31ca324ba49b8467 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 31 Jan 2025 14:26:55 +0400 Subject: [PATCH 032/115] auth: replace useless oauth2-introspection-url with prefix/postfix parts oauth2-introspection-url is useless, because it would contain OAuth client secret right in the URL. OAuth clients contruct URLs on its own. --- sp-modules/auth/config-paths-needed.json | 1 - sp-modules/auth/module.nix | 46 +++++++++++++++--------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/sp-modules/auth/config-paths-needed.json b/sp-modules/auth/config-paths-needed.json index f42cfd1..c59d193 100644 --- a/sp-modules/auth/config-paths-needed.json +++ b/sp-modules/auth/config-paths-needed.json @@ -1,5 +1,4 @@ [ - [ "passthru", "selfprivacy", "auth" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth" ], diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index c4644ca..c6f033c 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -1,8 +1,20 @@ { config, lib, pkgs, ... }: let - passthru = config.passthru.selfprivacy.auth; cfg = config.selfprivacy.modules.auth; domain = config.selfprivacy.domain; + auth-fqdn = cfg.subdomain + "." + domain; + + # e.g. "dc=mydomain,dc=com" + ldap-base-dn = + lib.strings.concatMapStringsSep + "," + (x: "dc=" + x) + (lib.strings.splitString "." domain); + ldap-host = "127.0.0.1"; + ldap-port = 3636; + + admins-group = "sp.admins"; + full-users-group = "sp.full_users"; kanidm-bind-address = "127.0.0.1:3013"; @@ -75,7 +87,7 @@ in # included if it is non-standard (any port except 443). This must match or # be a descendent of the domain name you configure above. If these two # items are not consistent, the server WILL refuse to start! - origin = "https://" + passthru.auth-fqdn; + origin = "https://" + auth-fqdn; # TODO revise this: maybe kanidm must not have access to a public TLS tls_chain = @@ -87,7 +99,7 @@ in bindaddress = kanidm-bind-address; ldapbindaddress = - "${passthru.ldap-host}:${toString passthru.ldap-port}"; + "${ldap-host}:${toString ldap-port}"; # kanidm is behind a proxy trust_x_forward_for = true; @@ -97,12 +109,12 @@ in provision = { enable = true; autoRemove = true; # if false, obsolete oauth2 scopeMaps remain - groups.${passthru.admins-group}.present = true; - groups.${passthru.full-users-group}.present = true; + groups.${admins-group}.present = true; + groups.${full-users-group}.present = true; }; enableClient = true; clientSettings = { - uri = "https://" + passthru.auth-fqdn; + uri = "https://" + auth-fqdn; verify_ca = false; # FIXME verify_hostnames = false; # FIXME }; @@ -119,7 +131,7 @@ in '[Response Body]: $resp_body\n\n'; lua_package_path "${lua_path}"; ''; - virtualHosts.${passthru.auth-fqdn} = { + virtualHosts.${auth-fqdn} = { useACMEHost = domain; forceSSL = true; locations."/" = { @@ -168,10 +180,17 @@ in systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ spApiUserExecStartPostScript ]; - passthru.selfprivacy.auth = rec { - auth-fqdn = cfg.subdomain + "." + domain; - oauth2-introspection-url = client_id: client_secret: - "https://${client_id}:${client_secret}@${auth-fqdn}/oauth2/token/introspect"; + passthru.selfprivacy.auth = { + inherit + admins-group + auth-fqdn + full-users-group + ldap-host + ldap-port + ; + oauth2-introspection-url-prefix = client_id: "https://${client_id}:"; + oauth2-introspection-url-postfix = + "@${auth-fqdn}/oauth2/token/introspect"; oauth2-discovery-url = client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; oauth2-provider-name = "Kanidm"; @@ -183,11 +202,6 @@ in "," (x: "dc=" + x) (lib.strings.splitString "." domain); - ldap-host = "127.0.0.1"; - ldap-port = 3636; - - admins-group = "sp.admins"; - full-users-group = "sp.full_users"; }; }; } From 4c6228d6941633b23d4445dc9704eba190977441 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 31 Jan 2025 14:31:09 +0400 Subject: [PATCH 033/115] roundcube & mailserver: fix oauth: mailserver is an OAuth secret donor Both of them use the same client ID and client secret, but Roundcube depends on mailserver generally, so mailserver is the one to share OAuth client id and secret. --- sp-modules/roundcube/config-paths-needed.json | 4 +- sp-modules/roundcube/module.nix | 45 ++++++++-------- .../simple-nixos-mailserver/auth-dovecot.nix | 51 ++++++++++++++----- .../simple-nixos-mailserver/auth-postfix.nix | 6 +-- sp-modules/simple-nixos-mailserver/common.nix | 3 +- .../config-paths-needed.json | 7 ++- sp-modules/simple-nixos-mailserver/config.nix | 22 ++++---- 7 files changed, 86 insertions(+), 52 deletions(-) diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index aff2a53..a759840 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -7,5 +7,7 @@ [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth" ], - [ "selfprivacy", "modules", "roundcube" ] + [ "selfprivacy", "modules", "roundcube" ], + [ "selfprivacy", "passthru", "mailserver", "oauth-client-id" ], + [ "selfprivacy", "passthru", "mailserver", "oauth-client-secret-fp" ] ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index b5d6ddf..f01831d 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -5,24 +5,21 @@ let is-auth-enabled = config.selfprivacy.modules.auth.enable or false; auth-passthru = config.passthru.selfprivacy.auth; auth-fqdn = auth-passthru.auth-fqdn; - oauth-client-id = "roundcube"; - roundcube-user = "roundcube"; - roundcube-group = "roundcube"; - kanidmExecStartPreScriptRoot = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPre-root-script.sh" - '' - # set-group-ID bit allows for kanidm user to create files, - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} - chown kanidm:${roundcube-group} /run/keys/${oauth-client-id} - ''; + sp-module-name = "roundcube"; + user = "roundcube"; + group = "roundcube"; + oauth-donor = config.selfprivacy.passthru.mailserver; kanidm-oauth-client-secret-fp = - "/run/keys/${oauth-client-id}/kanidm-oauth-client-secret"; - kanidmExecStartPreScript = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' - set -o xtrace - [ -f "${kanidm-oauth-client-secret-fp}" ] || \ - "${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32 - ''; + "/run/keys/${group}/kanidm-oauth-client-secret"; + kanidmExecStartPreScriptRoot = pkgs.writeShellScript + "${sp-module-name}-kanidm-ExecStartPre-root-script.sh" + '' + # set-group-ID bit allows for kanidm user to create files inheriting group + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group} + chown kanidm:${group} /run/keys/${group} + + install -v -m640 -o kanidm -g ${group} ${oauth-donor.oauth-client-secret-fp} ${kanidm-oauth-client-secret-fp} + ''; in { options.selfprivacy.modules.roundcube = { @@ -72,21 +69,23 @@ in }; systemd.slices.roundcube.description = "Roundcube service slice"; + # Roundcube depends on Dovecot and its OAuth2 client secret. + systemd.services.roundcube.after = [ "dovecot2.service" ]; } # the following part is active only when "auth" module is enabled (lib.attrsets.optionalAttrs (options.selfprivacy.modules ? "auth") (lib.mkIf is-auth-enabled { # for phpfpm-roundcube to have access to get through /run/keys directory - users.groups.keys.members = [ roundcube-user ]; + users.groups.keys.members = [ user ]; services.roundcube.extraConfig = lib.mkAfter '' $config['oauth_provider'] = 'generic'; $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; - $config['oauth_client_id'] = '${oauth-client-id}'; + $config['oauth_client_id'] = '${oauth-donor.oauth-client-id}'; $config['oauth_client_secret'] = file_get_contents('${kanidm-oauth-client-secret-fp}'); $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; - $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; + $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo'; $config['oauth_scope'] = 'email profile openid'; # FIXME $config['oauth_auth_parameters'] = []; $config['oauth_identity_fields'] = ['email']; @@ -96,11 +95,9 @@ in # $config['oauth_pkce'] = 'S256'; # FIXME ''; systemd.services.kanidm = { - serviceConfig.ExecStartPre = lib.mkAfter [ + serviceConfig.ExecStartPre = lib.mkBefore [ ("-+" + kanidmExecStartPreScriptRoot) - ("-" + kanidmExecStartPreScript) ]; - requires = [ auth-passthru.oauth2-systemd-service ]; }; services.kanidm.provision = { groups = { @@ -108,7 +105,7 @@ in "sp.roundcube.users".members = [ "sp.roundcube.admins" auth-passthru.full-users-group ]; }; - systems.oauth2.roundcube = { + systems.oauth2.${oauth-donor.oauth-client-id} = { displayName = "Roundcube"; originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; originLanding = "https://${cfg.subdomain}.${domain}/"; diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 10dbb3d..4a2615b 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -1,14 +1,15 @@ { config, lib, pkgs, ... }@nixos-args: let inherit (import ./common.nix nixos-args) - appendLdapBindPwd + appendSetting auth-passthru cfg domain + group is-auth-enabled ; - runtime-directory = "dovecot2"; + runtime-directory = group; ldapConfFile = "/run/${runtime-directory}/dovecot-ldap.conf.ext"; mkLdapSearchScope = scope: ( @@ -37,7 +38,7 @@ let user_filter = ${config.mailserver.ldap.dovecot.userFilter} ''; }; - setPwdInLdapConfFile = appendLdapBindPwd { + setPwdInLdapConfFile = appendSetting { name = "ldap-conf-file"; file = dovecot-ldap-config; prefix = ''dnpass = "''; @@ -45,24 +46,39 @@ let passwordFile = config.mailserver.ldap.bind.passwordFile; destination = ldapConfFile; }; - dovecot-oauth2-conf-file = pkgs.writeTextFile { - name = "dovecot-oauth2.conf.ext"; - text = '' + oauth-client-id = "mailserver"; + oauth-client-secret-fp = + "/run/keys/${group}/kanidm-oauth-client-secret"; + oauth-secret-ExecStartPreScript = pkgs.writeShellScript + "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' + set -o xtrace + [ -f "${oauth-client-secret-fp}" ] || \ + "${lib.getExe pkgs.openssl}" rand -base64 32 | tr -d "\n" > "${oauth-client-secret-fp}" + ''; + dovecot-oauth2-conf-fp = "/run/${runtime-directory}/dovecot-oauth2.conf.ext"; + write-dovecot-oauth2-conf = appendSetting { + name = "oauth2-conf-file"; + file = builtins.toFile "dovecot-oauth2.conf.ext.template" '' introspection_mode = post - introspection_url = ${auth-passthru.oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"} - client_id = roundcube - client_secret = VERYSTRONGSECRETFORROUNDCUBE # FIXME username_attribute = username scope = email profile openid tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt active_attribute = active active_value = true - openid_configuration_url = ${auth-passthru.oauth2-discovery-url "roundcube"} + openid_configuration_url = ${auth-passthru.oauth2-discovery-url oauth-client-id} debug = "no" ''; + prefix = ''introspection_url = "'' + + (auth-passthru.oauth2-introspection-url-prefix oauth-client-id); + suffix = auth-passthru.oauth2-introspection-url-postfix + ''"''; + passwordFile = oauth-client-secret-fp; + destination = dovecot-oauth2-conf-fp; }; in { + # for dovecot2 to have access to get through /run/keys directory + users.groups.keys.members = [ group ]; + mailserver.ldap = { # note: in `ldapsearch` first comes filter, then attributes dovecot.userAttrs = "+"; # all operational attributes @@ -76,7 +92,7 @@ in passdb { driver = oauth2 mechanisms = xoauth2 oauthbearer - args = ${dovecot-oauth2-conf-file} + args = ${dovecot-oauth2-conf-fp} } userdb { @@ -114,13 +130,22 @@ in services.dovecot2.enablePAM = false; systemd.services.dovecot2 = { # TODO does it merge with existing preStart? - preStart = setPwdInLdapConfFile + "\n"; + preStart = setPwdInLdapConfFile + "\n" + write-dovecot-oauth2-conf + "\n"; # FIXME pass dependant services to auth module option instead? wants = [ auth-passthru.oauth2-systemd-service ]; after = [ auth-passthru.oauth2-systemd-service ]; serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ]; }; + systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [ + ("-" + oauth-secret-ExecStartPreScript) + ]; # does it merge with existing restartTriggers? - systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile ]; + systemd.services.postfix.restartTriggers = [ + setPwdInLdapConfFile + write-dovecot-oauth2-conf + ]; + selfprivacy.passthru.mailserver = { + inherit oauth-client-id oauth-client-secret-fp; + }; } diff --git a/sp-modules/simple-nixos-mailserver/auth-postfix.nix b/sp-modules/simple-nixos-mailserver/auth-postfix.nix index 38b141c..1d1029e 100644 --- a/sp-modules/simple-nixos-mailserver/auth-postfix.nix +++ b/sp-modules/simple-nixos-mailserver/auth-postfix.nix @@ -1,7 +1,7 @@ { config, lib, pkgs, ... }@nixos-args: let inherit (import ./common.nix nixos-args) - appendLdapBindPwd + appendSetting auth-passthru is-auth-enabled ; @@ -29,7 +29,7 @@ let query_filter = ${cfg.ldap.postfix.filter} result_attribute = ${cfg.ldap.postfix.mailAttribute} ''; - appendPwdInSenderLoginMap = appendLdapBindPwd { + appendPwdInSenderLoginMap = appendSetting { name = "ldap-sender-login-map"; file = ldapSenderLoginMap; prefix = "bind_pw = "; @@ -43,7 +43,7 @@ let result_attribute = ${cfg.ldap.postfix.uidAttribute} ''; ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf"; - appendPwdInVirtualMailboxMap = appendLdapBindPwd { + appendPwdInVirtualMailboxMap = appendSetting { name = "ldap-virtual-mailbox-map"; file = ldapVirtualMailboxMap; prefix = "bind_pw = "; diff --git a/sp-modules/simple-nixos-mailserver/common.nix b/sp-modules/simple-nixos-mailserver/common.nix index eba175b..17ce303 100644 --- a/sp-modules/simple-nixos-mailserver/common.nix +++ b/sp-modules/simple-nixos-mailserver/common.nix @@ -3,8 +3,9 @@ rec { auth-passthru = config.passthru.selfprivacy.auth; domain = config.selfprivacy.domain; is-auth-enabled = config.selfprivacy.modules.auth.enable or false; + group = "dovecot2"; - appendLdapBindPwd = + appendSetting = { name, file, prefix, suffix ? "", passwordFile, destination }: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' #!${pkgs.stdenv.shell} diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index 3e0ce04..bb0c127 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -1,10 +1,15 @@ [ [ "mailserver" ], + [ "passthru", "selfprivacy", "auth", "admins-group" ], + [ "passthru", "selfprivacy", "auth", "full-users-group" ], [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], [ "passthru", "selfprivacy", "auth", "ldap-port" ], [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], - [ "passthru", "selfprivacy", "auth", "oauth2-introspection-url" ], + [ "passthru", "selfprivacy", "auth", "oauth2-introspection-url-postfix" ], + [ "passthru", "selfprivacy", "auth", "oauth2-introspection-url-prefix" ], [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], + [ "passthru", "selfprivacy", "roundcube", "oauth-client-id" ], + [ "passthru", "selfprivacy", "roundcube", "oauth-client-secret-fp" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "hashedMasterPassword" ], diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 6868666..68cd44c 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -5,20 +5,22 @@ let inherit (import ./common.nix { inherit config pkgs; }) auth-passthru domain + group is-auth-enabled ; mailserver-service-account-name = "sp.mailserver.service-account"; mailserver-service-account-token-name = "mailserver-service-account-token"; mailserver-service-account-token-fp = - "/run/keys/mailserver/kanidm-service-account-token"; # FIXME sync with auth module - kanidmExecStartPostScriptRoot = pkgs.writeShellScript - "mailserver-kanidm-ExecStartPost-root-script.sh" + "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module + kanidmExecStartPreScriptRoot = pkgs.writeShellScript + "mailserver-kanidm-ExecStartPre-root-script.sh" '' - # set-group-ID bit allows for kanidm user to create files, - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/mailserver - chown kanidm:kanidm /run/keys/mailserver + # set-group-ID bit allows for kanidm user to create files inheriting group + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group} + chown kanidm:${group} /run/keys/${group} ''; + # create service account token, needed for LDAP kanidmExecStartPostScript = pkgs.writeShellScript "mailserver-kanidm-ExecStartPost-script.sh" '' @@ -173,7 +175,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ }; }; } - # the following part is active only when "auth" module is enabled + # the following parts are active only when "auth" module is enabled (lib.attrsets.optionalAttrs (options.selfprivacy.modules ? "auth") (lib.mkIf is-auth-enabled { @@ -199,9 +201,11 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ }; }; # FIXME set auth module option instead + systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ + ("-+" + kanidmExecStartPreScriptRoot) + ]; systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ - ("+" + kanidmExecStartPostScriptRoot) - kanidmExecStartPostScript + ("-" + kanidmExecStartPostScript) ]; })) (lib.attrsets.optionalAttrs From 70a946cc6659deb37516037b4591399758e97235 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 31 Jan 2025 14:37:58 +0400 Subject: [PATCH 034/115] auth: add meta to all options --- sp-modules/auth/module.nix | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/sp-modules/auth/module.nix b/sp-modules/auth/module.nix index c6f033c..f102434 100644 --- a/sp-modules/auth/module.nix +++ b/sp-modules/auth/module.nix @@ -53,17 +53,32 @@ let in { options.selfprivacy.modules.auth = { - enable = lib.mkOption { + enable = (lib.mkOption { default = false; type = lib.types.bool; + }) // { + meta = { + type = "enable"; + }; }; - subdomain = lib.mkOption { + subdomain = (lib.mkOption { default = "auth"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + }) // { + meta = { + widget = "subdomain"; + type = "string"; + regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + weight = 0; + }; }; - debug = lib.mkOption { + debug = (lib.mkOption { default = false; type = lib.types.bool; + }) // { + meta = { + type = "enable"; + }; }; }; From 3a8a3dfc953f994a605e7d517976958e3dbb9b9e Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 1 Feb 2025 18:36:01 +0400 Subject: [PATCH 035/115] fix auth meta: add meta to flake.nix and icon.svg --- sp-modules/auth/flake.nix | 17 +++++++++++++++++ sp-modules/auth/icon.svg | 1 + 2 files changed, 18 insertions(+) create mode 100644 sp-modules/auth/icon.svg diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix index fa5f5a7..45d39e2 100644 --- a/sp-modules/auth/flake.nix +++ b/sp-modules/auth/flake.nix @@ -41,5 +41,22 @@ configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + + meta = { lib, ... }: { + spModuleSchemaVersion = 1; + id = "auth"; + name = "Auth"; + description = "Temporary auth module."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = false; + isRequired = false; + backupDescription = "Useless service."; + systemdServices = [ "kanidm.service" ]; + folders = [ ]; + license = [ ]; + homepage = "https://kanidm.com"; + sourcePage = "https://github.com/kanidm"; + supportLevel = "hallucinatory"; + }; }; } diff --git a/sp-modules/auth/icon.svg b/sp-modules/auth/icon.svg new file mode 100644 index 0000000..647bc16 --- /dev/null +++ b/sp-modules/auth/icon.svg @@ -0,0 +1 @@ + From 29d17591860558f502e84e1a2dceafd7ad9b1d69 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 29 Jan 2025 18:17:17 +0400 Subject: [PATCH 036/115] merge auth SP module into main configuration; add `enableSso` option `enableSso` is being added to the following SP modules: * gitea (forgejo) * nextcloud * roundcube * simple-nixos-mailserver --- sp-modules/auth/module.nix => auth/auth.nix | 21 ++++++- {sp-modules/auth => auth}/kanidm.nix | 0 flake.nix | 8 ++- sp-modules/auth/config-paths-needed.json | 9 --- sp-modules/auth/flake.lock | 27 -------- sp-modules/auth/flake.nix | 62 ------------------- sp-modules/auth/icon.svg | 1 - sp-modules/gitea/module.nix | 11 +++- sp-modules/nextcloud/module.nix | 2 +- sp-modules/roundcube/module.nix | 11 +++- sp-modules/simple-nixos-mailserver/common.nix | 3 +- .../simple-nixos-mailserver/options.nix | 9 +++ 12 files changed, 59 insertions(+), 105 deletions(-) rename sp-modules/auth/module.nix => auth/auth.nix (92%) rename {sp-modules/auth => auth}/kanidm.nix (100%) delete mode 100644 sp-modules/auth/config-paths-needed.json delete mode 100644 sp-modules/auth/flake.lock delete mode 100644 sp-modules/auth/flake.nix delete mode 100644 sp-modules/auth/icon.svg diff --git a/sp-modules/auth/module.nix b/auth/auth.nix similarity index 92% rename from sp-modules/auth/module.nix rename to auth/auth.nix index f102434..22923ee 100644 --- a/sp-modules/auth/module.nix +++ b/auth/auth.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +nixpkgs-2411: { config, lib, pkgs, ... }: let cfg = config.selfprivacy.modules.auth; domain = config.selfprivacy.domain; @@ -83,6 +83,25 @@ in }; config = lib.mkIf cfg.enable { + nixpkgs.overlays = [ + ( + _final: prev: { + inherit (nixpkgs-2411.legacyPackages.${prev.system}) kanidm; + kanidm-provision = + nixpkgs-2411.legacyPackages.${prev.system}.kanidm-provision.overrideAttrs (_: { + version = "git"; + src = prev.fetchFromGitHub { + owner = "oddlama"; + repo = "kanidm-provision"; + rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; + hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; + }; + }); + } + ) + ]; + + # kanidm uses TLS in internal connection with nginx too # FIXME revise this: maybe kanidm must not have access to a public TLS users.groups."acmereceivers".members = [ "kanidm" ]; diff --git a/sp-modules/auth/kanidm.nix b/auth/kanidm.nix similarity index 100% rename from sp-modules/auth/kanidm.nix rename to auth/kanidm.nix diff --git a/flake.nix b/flake.nix index 27c0444..5a526a6 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ inputs = { nixpkgs.url = github:nixos/nixpkgs; + nixpkgs-2411.url = github:nixos/nixpkgs/nixos-24.11; selfprivacy-api.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git; @@ -10,7 +11,7 @@ selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = { self, nixpkgs, selfprivacy-api }: { + outputs = { self, nixpkgs, nixpkgs-2411, selfprivacy-api }: { nixosConfigurations-fun = { hardware-configuration , deployment @@ -24,6 +25,11 @@ hardware-configuration deployment ./configuration.nix + (import ./auth/auth.nix nixpkgs-2411) + { + disabledModules = [ "services/security/kanidm.nix" ]; + imports = [ ./auth/kanidm.nix ]; + } selfprivacy-api.nixosModules.default ({ pkgs, lib, ... }: { environment.etc = (lib.attrsets.mapAttrs' diff --git a/sp-modules/auth/config-paths-needed.json b/sp-modules/auth/config-paths-needed.json deleted file mode 100644 index c59d193..0000000 --- a/sp-modules/auth/config-paths-needed.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - [ "security", "acme", "certs" ], - [ "selfprivacy", "domain" ], - [ "selfprivacy", "modules", "auth" ], - [ "services", "kanidm" ], - [ "services", "oauth2-proxy", "enable" ], - [ "services", "oauth2-proxy", "nginx" ], - [ "systemd", "services", "kanidm" ] -] diff --git a/sp-modules/auth/flake.lock b/sp-modules/auth/flake.lock deleted file mode 100644 index d9ce328..0000000 --- a/sp-modules/auth/flake.lock +++ /dev/null @@ -1,27 +0,0 @@ -{ - "nodes": { - "nixpkgs-unstable": { - "locked": { - "lastModified": 1725194671, - "narHash": "sha256-tLGCFEFTB5TaOKkpfw3iYT9dnk4awTP/q4w+ROpMfuw=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "b833ff01a0d694b910daca6e2ff4a3f26dee478c", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "rev": "b833ff01a0d694b910daca6e2ff4a3f26dee478c", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs-unstable": "nixpkgs-unstable" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/sp-modules/auth/flake.nix b/sp-modules/auth/flake.nix deleted file mode 100644 index 45d39e2..0000000 --- a/sp-modules/auth/flake.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - description = "User authentication and authorization module"; - - # TODO remove when Kanidm provisioning without groups assertion lands in NixOS - # inputs.nixos-unstable.url = github:alexoundos/nixpkgs/679fd3fd318ce2d57d0cabfbd7f4b8857d78ae95; - # inputs.nixos-unstable.url = git+file:/data/nixpkgs?ref=kanidm-1.4.0&rev=1bac99358baea6a3268027b4e585c68cd4ef107d; - inputs.nixos-unstable.url = github:nixos/nixpkgs/7ffd9ae656aec493492b44d0ddfb28e79a1ea25d; - - outputs = { self, nixos-unstable }: { - overlays.default = _final: prev: { - inherit (nixos-unstable.legacyPackages.${prev.system}) - kanidm oauth2-proxy; - kanidm-provision = - nixos-unstable.legacyPackages.${prev.system}.kanidm-provision.overrideAttrs (_: { - version = "git"; - src = prev.fetchFromGitHub { - owner = "oddlama"; - repo = "kanidm-provision"; - rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; - hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; - }; - }); - }; - - nixosModules.default = { ... }: { - disabledModules = [ - "services/security/kanidm.nix" - "services/security/oauth2-proxy.nix" - "services/security/oauth2-proxy-nginx.nix" - ]; - imports = [ - ./kanidm.nix - (nixos-unstable.legacyPackages.x86_64-linux.path - + /nixos/modules/services/security/oauth2-proxy.nix) - (nixos-unstable.legacyPackages.x86_64-linux.path - + /nixos/modules/services/security/oauth2-proxy-nginx.nix) - ./module.nix - ]; - nixpkgs.overlays = [ self.overlays.default ]; - }; - - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "auth"; - name = "Auth"; - description = "Temporary auth module."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = false; - isRequired = false; - backupDescription = "Useless service."; - systemdServices = [ "kanidm.service" ]; - folders = [ ]; - license = [ ]; - homepage = "https://kanidm.com"; - sourcePage = "https://github.com/kanidm"; - supportLevel = "hallucinatory"; - }; - }; -} diff --git a/sp-modules/auth/icon.svg b/sp-modules/auth/icon.svg deleted file mode 100644 index 647bc16..0000000 --- a/sp-modules/auth/icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index c992599..062c470 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -14,7 +14,7 @@ let "gitea-light" "gitea-dark" ]; - is-auth-enabled = sp.modules.auth.enable or false; + is-auth-enabled = cfg.enableSso; oauth-client-id = "forgejo"; auth-passthru = config.passthru.selfprivacy.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; @@ -183,6 +183,15 @@ in weight = 6; }; }; + enableSso = (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable SSO for Forgejo"; + }) // { + meta = { + type = "enable"; + }; + }; debug = lib.mkOption { default = false; type = lib.types.bool; diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index f353e17..2478dfa 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -11,8 +11,8 @@ let hostName = "${cfg.subdomain}.${sp.domain}"; auth-passthru = config.passthru.selfprivacy.auth; - is-auth-enabled = sp.modules.auth.enable or false; cfg = sp.modules.nextcloud; + is-auth-enabled = cfg.enableSso; ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index f01831d..dea1b29 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -2,7 +2,7 @@ let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; - is-auth-enabled = config.selfprivacy.modules.auth.enable or false; + is-auth-enabled = cfg.enableSso; auth-passthru = config.passthru.selfprivacy.auth; auth-fqdn = auth-passthru.auth-fqdn; sp-module-name = "roundcube"; @@ -44,6 +44,15 @@ in weight = 0; }; }; + enableSso = (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable SSO for Roundcube"; + }) // { + meta = { + type = "enable"; + }; + }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ diff --git a/sp-modules/simple-nixos-mailserver/common.nix b/sp-modules/simple-nixos-mailserver/common.nix index 17ce303..9ac427d 100644 --- a/sp-modules/simple-nixos-mailserver/common.nix +++ b/sp-modules/simple-nixos-mailserver/common.nix @@ -2,8 +2,9 @@ rec { auth-passthru = config.passthru.selfprivacy.auth; domain = config.selfprivacy.domain; - is-auth-enabled = config.selfprivacy.modules.auth.enable or false; group = "dovecot2"; + is-auth-enabled = + config.selfprivacy.modules.simple-nixos-mailserver.enableSso; appendSetting = { name, file, prefix, suffix ? "", passwordFile, destination }: diff --git a/sp-modules/simple-nixos-mailserver/options.nix b/sp-modules/simple-nixos-mailserver/options.nix index b8a5782..413b279 100644 --- a/sp-modules/simple-nixos-mailserver/options.nix +++ b/sp-modules/simple-nixos-mailserver/options.nix @@ -18,5 +18,14 @@ type = "location"; }; }; + enableSso = (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable SSO for mail server"; + }) // { + meta = { + type = "enable"; + }; + }; }; } From 365e01a4e301d94e63783bb30c03a8aea7fde3d9 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 3 Feb 2025 00:56:12 +0400 Subject: [PATCH 037/115] fix selfprivacy.passthru: allow any types --- selfprivacy-module.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfprivacy-module.nix b/selfprivacy-module.nix index 10c6b45..76897a0 100644 --- a/selfprivacy-module.nix +++ b/selfprivacy-module.nix @@ -1,4 +1,4 @@ -{ lib, pkgs, ... }: +{ lib, ... }: with lib; { @@ -144,7 +144,7 @@ with lib; ################ passthru = mkOption { type = types.submodule { - freeformType = (pkgs.formats.json { }).type; + freeformType = with types; lazyAttrsOf (uniq unspecified); options = { }; }; default = { }; From ee2e404eb81087b70822bdd56d1e2a00d67cda65 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 3 Feb 2025 00:57:08 +0400 Subject: [PATCH 038/115] passthru.selfprivacy -> selfprivacy.passthru --- auth/auth.nix | 8 +------- sp-modules/gitea/config-paths-needed.json | 18 ++++++++--------- sp-modules/gitea/module.nix | 2 +- sp-modules/nextcloud/config-paths-needed.json | 16 +++++++-------- sp-modules/nextcloud/module.nix | 2 +- sp-modules/roundcube/config-paths-needed.json | 10 +++++----- sp-modules/roundcube/module.nix | 2 +- sp-modules/simple-nixos-mailserver/common.nix | 2 +- .../config-paths-needed.json | 20 +++++++++---------- 9 files changed, 37 insertions(+), 43 deletions(-) diff --git a/auth/auth.nix b/auth/auth.nix index 22923ee..cee1a2a 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -4,12 +4,6 @@ let domain = config.selfprivacy.domain; auth-fqdn = cfg.subdomain + "." + domain; - # e.g. "dc=mydomain,dc=com" - ldap-base-dn = - lib.strings.concatMapStringsSep - "," - (x: "dc=" + x) - (lib.strings.splitString "." domain); ldap-host = "127.0.0.1"; ldap-port = 3636; @@ -214,7 +208,7 @@ in systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ spApiUserExecStartPostScript ]; - passthru.selfprivacy.auth = { + selfprivacy.passthru.auth = { inherit admins-group auth-fqdn diff --git a/sp-modules/gitea/config-paths-needed.json b/sp-modules/gitea/config-paths-needed.json index b681d62..b22677e 100644 --- a/sp-modules/gitea/config-paths-needed.json +++ b/sp-modules/gitea/config-paths-needed.json @@ -1,16 +1,16 @@ [ - [ "passthru", "selfprivacy", "auth", "admins-group" ], - [ "passthru", "selfprivacy", "auth", "auth-fqdn" ], - [ "passthru", "selfprivacy", "auth", "full-users-group" ], - [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], - [ "passthru", "selfprivacy", "auth", "ldap-host" ], - [ "passthru", "selfprivacy", "auth", "ldap-port" ], - [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], - [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], - [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth", "enable" ], [ "selfprivacy", "modules", "gitea" ], + [ "selfprivacy", "passthru", "auth", "admins-group" ], + [ "selfprivacy", "passthru", "auth", "auth-fqdn" ], + [ "selfprivacy", "passthru", "auth", "full-users-group" ], + [ "selfprivacy", "passthru", "auth", "ldap-base-dn" ], + [ "selfprivacy", "passthru", "auth", "ldap-host" ], + [ "selfprivacy", "passthru", "auth", "ldap-port" ], + [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], + [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], + [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "useBinds" ], [ "services", "forgejo", "group" ], [ "services", "forgejo", "package" ] diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 062c470..7b32b1f 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -16,7 +16,7 @@ let ]; is-auth-enabled = cfg.enableSso; oauth-client-id = "forgejo"; - auth-passthru = config.passthru.selfprivacy.auth; + auth-passthru = config.selfprivacy.passthru.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; redirect-uri = "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index a263bbd..979bf4e 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -1,16 +1,16 @@ [ - [ "passthru", "selfprivacy", "auth", "admins-group" ], - [ "passthru", "selfprivacy", "auth", "full-users-group" ], - [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], - [ "passthru", "selfprivacy", "auth", "ldap-host" ], - [ "passthru", "selfprivacy", "auth", "ldap-port" ], - [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], - [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], - [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth", "enable" ], [ "selfprivacy", "modules", "nextcloud" ], + [ "selfprivacy", "passthru", "auth", "admins-group" ], + [ "selfprivacy", "passthru", "auth", "full-users-group" ], + [ "selfprivacy", "passthru", "auth", "ldap-base-dn" ], + [ "selfprivacy", "passthru", "auth", "ldap-host" ], + [ "selfprivacy", "passthru", "auth", "ldap-port" ], + [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], + [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], + [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "useBinds" ], [ "services", "nextcloud" ], [ "services", "phpfpm", "pools", "nextcloud", "group" ], diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 2478dfa..7919e07 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -10,7 +10,7 @@ let ; hostName = "${cfg.subdomain}.${sp.domain}"; - auth-passthru = config.passthru.selfprivacy.auth; + auth-passthru = config.selfprivacy.passthru.auth; cfg = sp.modules.nextcloud; is-auth-enabled = cfg.enableSso; ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index a759840..a545298 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -1,13 +1,13 @@ [ [ "mailserver", "fqdn" ], - [ "passthru", "selfprivacy", "auth", "admins-group" ], - [ "passthru", "selfprivacy", "auth", "auth-fqdn" ], - [ "passthru", "selfprivacy", "auth", "full-users-group" ], - [ "passthru", "selfprivacy", "auth", "oauth2-provider-name" ], - [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth" ], [ "selfprivacy", "modules", "roundcube" ], + [ "selfprivacy", "passthru", "auth", "admins-group" ], + [ "selfprivacy", "passthru", "auth", "auth-fqdn" ], + [ "selfprivacy", "passthru", "auth", "full-users-group" ], + [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], + [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "passthru", "mailserver", "oauth-client-id" ], [ "selfprivacy", "passthru", "mailserver", "oauth-client-secret-fp" ] ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index dea1b29..8c201d1 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -3,7 +3,7 @@ let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; is-auth-enabled = cfg.enableSso; - auth-passthru = config.passthru.selfprivacy.auth; + auth-passthru = config.selfprivacy.passthru.auth; auth-fqdn = auth-passthru.auth-fqdn; sp-module-name = "roundcube"; user = "roundcube"; diff --git a/sp-modules/simple-nixos-mailserver/common.nix b/sp-modules/simple-nixos-mailserver/common.nix index 9ac427d..5f21e92 100644 --- a/sp-modules/simple-nixos-mailserver/common.nix +++ b/sp-modules/simple-nixos-mailserver/common.nix @@ -1,6 +1,6 @@ { config, pkgs, ... }: rec { - auth-passthru = config.passthru.selfprivacy.auth; + auth-passthru = config.selfprivacy.passthru.auth; domain = config.selfprivacy.domain; group = "dovecot2"; is-auth-enabled = diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index bb0c127..2229673 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -1,20 +1,20 @@ [ [ "mailserver" ], - [ "passthru", "selfprivacy", "auth", "admins-group" ], - [ "passthru", "selfprivacy", "auth", "full-users-group" ], - [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], - [ "passthru", "selfprivacy", "auth", "ldap-port" ], - [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], - [ "passthru", "selfprivacy", "auth", "oauth2-introspection-url-postfix" ], - [ "passthru", "selfprivacy", "auth", "oauth2-introspection-url-prefix" ], - [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], - [ "passthru", "selfprivacy", "roundcube", "oauth-client-id" ], - [ "passthru", "selfprivacy", "roundcube", "oauth-client-secret-fp" ], [ "security", "acme", "certs" ], [ "selfprivacy", "domain" ], [ "selfprivacy", "hashedMasterPassword" ], [ "selfprivacy", "modules", "auth", "enable" ], [ "selfprivacy", "modules", "simple-nixos-mailserver" ], + [ "selfprivacy", "passthru", "auth", "admins-group" ], + [ "selfprivacy", "passthru", "auth", "full-users-group" ], + [ "selfprivacy", "passthru", "auth", "ldap-base-dn" ], + [ "selfprivacy", "passthru", "auth", "ldap-port" ], + [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], + [ "selfprivacy", "passthru", "auth", "oauth2-introspection-url-postfix" ], + [ "selfprivacy", "passthru", "auth", "oauth2-introspection-url-prefix" ], + [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], + [ "selfprivacy", "passthru", "roundcube", "oauth-client-id" ], + [ "selfprivacy", "passthru", "roundcube", "oauth-client-secret-fp" ], [ "selfprivacy", "useBinds" ], [ "selfprivacy", "username" ], [ "selfprivacy", "users" ], From ea443d2150ed73d7fa54bc78b178ef684ccc57dc Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 3 Feb 2025 01:04:19 +0400 Subject: [PATCH 039/115] gitea,nextcloud,roundcube,mailserver: depend on kanidm systemd service --- sp-modules/gitea/module.nix | 1 + sp-modules/nextcloud/module.nix | 1 + sp-modules/roundcube/module.nix | 4 ++++ sp-modules/simple-nixos-mailserver/auth-dovecot.nix | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 7b32b1f..41381fb 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -399,6 +399,7 @@ in fi ''; # TODO consider passing oauth consumer service to auth module instead + after = [ auth-passthru.oauth2-systemd-service ]; requires = [ auth-passthru.oauth2-systemd-service ]; }; diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 7919e07..70a856b 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -378,6 +378,7 @@ in -vvv ''; # TODO consider passing oauth consumer service to auth module instead + after = [ auth-passthru.oauth2-systemd-service ]; requires = [ auth-passthru.oauth2-systemd-service ]; }; services.kanidm.provision = { diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 8c201d1..92f6df1 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -103,6 +103,10 @@ in $config['oauth_verify_peer'] = false; # FIXME # $config['oauth_pkce'] = 'S256'; # FIXME ''; + systemd.services.roundcube = { + after = [ auth-passthru.oauth2-systemd-service ]; + requires = [ auth-passthru.oauth2-systemd-service ]; + }; systemd.services.kanidm = { serviceConfig.ExecStartPre = lib.mkBefore [ ("-+" + kanidmExecStartPreScriptRoot) diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 4a2615b..a1559e9 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -132,8 +132,8 @@ in # TODO does it merge with existing preStart? preStart = setPwdInLdapConfFile + "\n" + write-dovecot-oauth2-conf + "\n"; # FIXME pass dependant services to auth module option instead? - wants = [ auth-passthru.oauth2-systemd-service ]; after = [ auth-passthru.oauth2-systemd-service ]; + requires = [ auth-passthru.oauth2-systemd-service ]; serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ]; }; From 65548a1e739b37f9041114be5d3bc3c1198ed621 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 3 Feb 2025 02:03:20 +0400 Subject: [PATCH 040/115] SP modules do not depend on selfprivacy.modules.auth --- sp-modules/gitea/module.nix | 267 +++++++++--------- sp-modules/nextcloud/module.nix | 222 ++++++++------- sp-modules/roundcube/module.nix | 105 ++++--- sp-modules/simple-nixos-mailserver/config.nix | 64 ++--- 4 files changed, 322 insertions(+), 336 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 41381fb..3c888df 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -1,4 +1,4 @@ -{ config, lib, options, pkgs, ... }: +{ config, lib, pkgs, ... }: let sp = config.selfprivacy; stateDir = @@ -309,152 +309,149 @@ in }; } # the following part is active only when "auth" module is enabled - (lib.attrsets.optionalAttrs - (options.selfprivacy.modules ? "auth") - (lib.mkIf is-auth-enabled { - services.forgejo.settings = { - auth.DISABLE_LOGIN_FORM = true; - service = { - DISABLE_REGISTRATION = cfg.disableRegistration; - REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; - ALLOW_ONLY_EXTERNAL_REGISTRATION = true; - SHOW_REGISTRATION_BUTTON = false; - ENABLE_BASIC_AUTHENTICATION = false; - }; - - # disallow explore page and access to private repositories, but allow public - "service.explore".REQUIRE_SIGNIN_VIEW = true; - - # TODO control via selfprivacy parameter - # "service.explore".DISABLE_USERS_PAGE = true; - - oauth2_client = { - REDIRECT_URI = redirect-uri; - ACCOUNT_LINKING = "auto"; - ENABLE_AUTO_REGISTRATION = true; - OPENID_CONNECT_SCOPES = "email openid profile"; - }; - # doesn't work if LDAP auth source is not active! - "cron.sync_external_users" = { - ENABLED = true; - RUN_AT_START = true; - NOTICE_ON_SUCCESS = true; - }; + (lib.mkIf is-auth-enabled { + services.forgejo.settings = { + auth.DISABLE_LOGIN_FORM = true; + service = { + DISABLE_REGISTRATION = cfg.disableRegistration; + REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; + ALLOW_ONLY_EXTERNAL_REGISTRATION = true; + SHOW_REGISTRATION_BUTTON = false; + ENABLE_BASIC_AUTHENTICATION = false; }; - systemd.services.forgejo = { - preStart = - let - exe = lib.getExe config.services.forgejo.package; - # FIXME skip-tls-verify, bind-password - ldapConfigArgs = '' - --name LDAP \ - --active \ - --security-protocol LDAPS \ - --skip-tls-verify \ - --host '${auth-passthru.ldap-host}' \ - --port '${toString auth-passthru.ldap-port}' \ - --user-search-base '${auth-passthru.ldap-base-dn}' \ - --user-filter '(&(class=person)(memberof=${users-group})(name=%s))' \ - --admin-filter '(&(class=person)(memberof=${admins-group}))' \ - --username-attribute name \ - --firstname-attribute name \ - --surname-attribute displayname \ - --email-attribute mail \ - --public-ssh-key-attribute sshPublicKey \ - --bind-dn 'dn=token' \ - --bind-password "$(cat ${kanidm-service-account-token-fp})" \ - --synchronize-users - ''; - oauthConfigArgs = '' - --name "${oauth2-provider-name}" \ - --provider openidConnect \ - --key forgejo \ - --secret "$(<${kanidm-oauth-client-secret-fp})" \ - --group-claim-name groups \ - --admin-group admins \ - --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' - ''; - in - lib.mkAfter '' - set -o xtrace - # Check if LDAP is already configured - ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" + # disallow explore page and access to private repositories, but allow public + "service.explore".REQUIRE_SIGNIN_VIEW = true; - if [[ -n "$ldap_line" ]]; then - # update ldap config - id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} - else - # initially configure ldap - ${exe} admin auth add-ldap ${ldapConfigArgs} - fi + # TODO control via selfprivacy parameter + # "service.explore".DISABLE_USERS_PAGE = true; - oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" - if [[ -n "$oauth_line" ]]; then - id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} - else - ${exe} admin auth add-oauth ${oauthConfigArgs} - fi + oauth2_client = { + REDIRECT_URI = redirect-uri; + ACCOUNT_LINKING = "auto"; + ENABLE_AUTO_REGISTRATION = true; + OPENID_CONNECT_SCOPES = "email openid profile"; + }; + # doesn't work if LDAP auth source is not active! + "cron.sync_external_users" = { + ENABLED = true; + RUN_AT_START = true; + NOTICE_ON_SUCCESS = true; + }; + }; + systemd.services.forgejo = { + preStart = + let + exe = lib.getExe config.services.forgejo.package; + # FIXME skip-tls-verify, bind-password + ldapConfigArgs = '' + --name LDAP \ + --active \ + --security-protocol LDAPS \ + --skip-tls-verify \ + --host '${auth-passthru.ldap-host}' \ + --port '${toString auth-passthru.ldap-port}' \ + --user-search-base '${auth-passthru.ldap-base-dn}' \ + --user-filter '(&(class=person)(memberof=${users-group})(name=%s))' \ + --admin-filter '(&(class=person)(memberof=${admins-group})' \ + --username-attribute name \ + --firstname-attribute name \ + --surname-attribute displayname \ + --email-attribute mail \ + --public-ssh-key-attribute sshPublicKey \ + --bind-dn 'dn=token' \ + --bind-password "$(cat ${kanidm-service-account-token-fp})" \ + --synchronize-users ''; - # TODO consider passing oauth consumer service to auth module instead - after = [ auth-passthru.oauth2-systemd-service ]; - requires = [ auth-passthru.oauth2-systemd-service ]; - }; + oauthConfigArgs = '' + --name "${oauth2-provider-name}" \ + --provider openidConnect \ + --key forgejo \ + --secret "$(<${kanidm-oauth-client-secret-fp})" \ + --group-claim-name groups \ + --admin-group admins \ + --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' + ''; + in + lib.mkAfter '' + set -o xtrace - # for ExecStartPost script to have access to /run/keys/* - users.groups.keys.members = [ config.services.forgejo.group ]; + # Check if LDAP is already configured + ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" - systemd.services.kanidm.serviceConfig.ExecStartPre = [ - ("-+" + kanidmExecStartPreScriptRoot) - ("-" + kanidmExecStartPreScript) - ]; - systemd.services.kanidm.serviceConfig.ExecStartPost = - lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]; + if [[ -n "$ldap_line" ]]; then + # update ldap config + id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} + else + # initially configure ldap + ${exe} admin auth add-ldap ${ldapConfigArgs} + fi - services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { - extraConfig = lib.mkAfter '' - rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; - # FIXME is it needed? - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" + if [[ -n "$oauth_line" ]]; then + id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} + else + ${exe} admin auth add-oauth ${oauthConfigArgs} + fi ''; - }; + # TODO consider passing oauth consumer service to auth module instead + after = [ auth-passthru.oauth2-systemd-service ]; + requires = [ auth-passthru.oauth2-systemd-service ]; + }; - services.kanidm.provision = { - groups = { - "${admins-group}".members = [ auth-passthru.admins-group ]; - "${users-group}".members = - [ admins-group auth-passthru.full-users-group ]; + # for ExecStartPost script to have access to /run/keys/* + users.groups.keys.members = [ config.services.forgejo.group ]; + + systemd.services.kanidm.serviceConfig.ExecStartPre = [ + ("-+" + kanidmExecStartPreScriptRoot) + ("-" + kanidmExecStartPreScript) + ]; + systemd.services.kanidm.serviceConfig.ExecStartPost = + lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]; + + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + extraConfig = lib.mkAfter '' + rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; + # FIXME is it needed? + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + ''; + }; + + services.kanidm.provision = { + groups = { + "${admins-group}".members = [ auth-passthru.admins-group ]; + "${users-group}".members = + [ admins-group auth-passthru.full-users-group ]; + }; + systems.oauth2.forgejo = { + displayName = "Forgejo"; + originUrl = redirect-uri; + originLanding = "https://${cfg.subdomain}.${sp.domain}/"; + basicSecretFile = kanidm-oauth-client-secret-fp; + # when true, name is passed to a service instead of name@domain + preferShortUsername = true; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps = { + "${users-group}" = [ + "email" + "openid" + "profile" + ]; }; - systems.oauth2.forgejo = { - displayName = "Forgejo"; - originUrl = redirect-uri; - originLanding = "https://${cfg.subdomain}.${sp.domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = true; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps = { - "${users-group}" = [ - "email" - "openid" - "profile" - ]; - }; - removeOrphanedClaimMaps = true; - # NOTE https://github.com/oddlama/kanidm-provision/issues/15 - # add more scopes when a user is a member of specific group - # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 - # supplementaryScopeMaps."${admins-group}" = - # [ "read:admin" "write:admin" ]; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${admins-group} = [ "admins" ]; - }; + removeOrphanedClaimMaps = true; + # NOTE https://github.com/oddlama/kanidm-provision/issues/15 + # add more scopes when a user is a member of specific group + # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 + # supplementaryScopeMaps."${admins-group}" = + # [ "read:admin" "write:admin" ]; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${admins-group} = [ "admins" ]; }; }; - }) - ) + }; + }) ]); } diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 70a856b..17719f3 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -1,4 +1,4 @@ -{ config, lib, options, pkgs, ... }: +{ config, lib, pkgs, ... }: let inherit (import ./common.nix config) admin-pass-filepath @@ -276,133 +276,131 @@ in }; } # the following part is active only when "auth" module is enabled - (lib.attrsets.optionalAttrs - (options.selfprivacy.modules ? "auth") - (lib.mkIf is-auth-enabled { - systemd.services.nextcloud-setup = { - path = [ pkgs.jq ]; - script = '' - set -o errexit - set -o nounset - ${lib.strings.optionalString cfg.debug "set -o xtrace"} + (lib.mkIf is-auth-enabled { + systemd.services.nextcloud-setup = { + path = [ pkgs.jq ]; + script = '' + set -o errexit + set -o nounset + ${lib.strings.optionalString cfg.debug "set -o xtrace"} - ${occ} app:install user_ldap || : - ${occ} app:enable user_ldap + ${occ} app:install user_ldap || : + ${occ} app:enable user_ldap - # The following code tries to match an existing config or creates a new one. - # The criteria for matching is the ldapHost value. + # The following code tries to match an existing config or creates a new one. + # The criteria for matching is the ldapHost value. - # remove broken link after previous nextcloud (un)installation - [[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \ - rm -v "${override-config-fp}" + # remove broken link after previous nextcloud (un)installation + [[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \ + rm -v "${override-config-fp}" - ALL_CONFIG="$(${occ} ldap:show-config --output=json)" + ALL_CONFIG="$(${occ} ldap:show-config --output=json)" - MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")" - if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then - CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")" - else - CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)" - fi + MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")" + if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then + CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")" + else + CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)" + fi - echo "Using configId $CONFIG_ID" + echo "Using configId $CONFIG_ID" - # The following CLI commands follow - # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way + # The following CLI commands follow + # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way - # StartTLS is not supported in Kanidm due to security risks, whereas - # user_ldap doesn't support SASL. Importing certificate doesn't - # help: - # ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem" - ${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1' + # StartTLS is not supported in Kanidm due to security risks, whereas + # user_ldap doesn't support SASL. Importing certificate doesn't + # help: + # ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem" + ${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${kanidm-service-account-token-fp})" - ${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \ - '(&(class=group)(${wildcard-group}))' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \ - '(&(class=group)(${wildcard-group}))' - # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \ - # 'groupOfUniqueNames' - # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \ - # 'uniqueMember' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \ - '(&(class=person)(memberof=${users-group})(uid=%uid))' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \ - 'uid' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \ - 'displayname' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \ - '(&(class=person)(memberof=${users-group})(name=%s))' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \ - '1' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \ - 'person' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${kanidm-service-account-token-fp})" + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \ + '(&(class=group)(${wildcard-group})' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \ + '(&(class=group)(${wildcard-group}))' + # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \ + # 'groupOfUniqueNames' + # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \ + # 'uniqueMember' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \ + '(&(class=person)(memberof=${users-group})(uid=%uid))' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \ + 'uid' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \ + 'displayname' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \ + '(&(class=person)(memberof=${users-group})(name=%s))' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \ + '1' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \ + 'person' - ${occ} ldap:test-config -- "$CONFIG_ID" + ${occ} ldap:test-config -- "$CONFIG_ID" - # delete all configs except "$CONFIG_ID" - for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do - echo "Deactivating $configid" - ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0' - echo "Deactivated $configid" - echo "Deleting $configid" - ${occ} ldap:delete-config "$configid" - echo "Deleted $configid" - done + # delete all configs except "$CONFIG_ID" + for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do + echo "Deactivating $configid" + ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0' + echo "Deactivated $configid" + echo "Deleting $configid" + ${occ} ldap:delete-config "$configid" + echo "Deleted $configid" + done - ${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1' - ############################################################################ - # OIDC app - ############################################################################ - ${occ} app:install user_oidc || : - ${occ} app:enable user_oidc + ############################################################################ + # OIDC app + ############################################################################ + ${occ} app:install user_oidc || : + ${occ} app:enable user_oidc - ${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \ - --clientid="${oauth-client-id}" \ - --clientsecret="$(<${kanidm-oauth-client-secret-fp})" \ - --discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \ - --unique-uid=0 \ - --scope="email openid profile" \ - --mapping-uid=preferred_username \ - --no-interaction \ - --mapping-groups=groups \ - --group-provisioning=1 \ - -vvv - ''; - # TODO consider passing oauth consumer service to auth module instead - after = [ auth-passthru.oauth2-systemd-service ]; - requires = [ auth-passthru.oauth2-systemd-service ]; + ${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \ + --clientid="${oauth-client-id}" \ + --clientsecret="$(<${kanidm-oauth-client-secret-fp})" \ + --discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \ + --unique-uid=0 \ + --scope="email openid profile" \ + --mapping-uid=preferred_username \ + --no-interaction \ + --mapping-groups=groups \ + --group-provisioning=1 \ + -vvv + ''; + # TODO consider passing oauth consumer service to auth module instead + after = [ auth-passthru.oauth2-systemd-service ]; + requires = [ auth-passthru.oauth2-systemd-service ]; + }; + services.kanidm.provision = { + groups = { + "${admins-group}".members = [ auth-passthru.admins-group ]; + "${users-group}".members = + [ admins-group auth-passthru.full-users-group ]; }; - services.kanidm.provision = { - groups = { - "${admins-group}".members = [ auth-passthru.admins-group ]; - "${users-group}".members = - [ admins-group auth-passthru.full-users-group ]; - }; - systems.oauth2.${oauth-client-id} = { - displayName = "Nextcloud"; - originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = true; - allowInsecureClientDisablePkce = false; - scopeMaps.${users-group} = [ "email" "openid" "profile" ]; - removeOrphanedClaimMaps = true; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${admins-group} = [ "admin" ]; - }; + systems.oauth2.${oauth-client-id} = { + displayName = "Nextcloud"; + originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = kanidm-oauth-client-secret-fp; + # when true, name is passed to a service instead of name@domain + preferShortUsername = true; + allowInsecureClientDisablePkce = false; + scopeMaps.${users-group} = [ "email" "openid" "profile" ]; + removeOrphanedClaimMaps = true; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${admins-group} = [ "admin" ]; }; }; - })) + }; + }) ]); } diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 92f6df1..68147d8 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -1,4 +1,4 @@ -{ config, lib, options, pkgs, ... }: +{ config, lib, pkgs, ... }: let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; @@ -82,61 +82,58 @@ in systemd.services.roundcube.after = [ "dovecot2.service" ]; } # the following part is active only when "auth" module is enabled - (lib.attrsets.optionalAttrs - (options.selfprivacy.modules ? "auth") - (lib.mkIf is-auth-enabled { - # for phpfpm-roundcube to have access to get through /run/keys directory - users.groups.keys.members = [ user ]; - services.roundcube.extraConfig = lib.mkAfter '' - $config['oauth_provider'] = 'generic'; - $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; - $config['oauth_client_id'] = '${oauth-donor.oauth-client-id}'; - $config['oauth_client_secret'] = file_get_contents('${kanidm-oauth-client-secret-fp}'); - $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; - $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; - $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo'; - $config['oauth_scope'] = 'email profile openid'; # FIXME - $config['oauth_auth_parameters'] = []; - $config['oauth_identity_fields'] = ['email']; - $config['oauth_login_redirect'] = true; - $config['auto_create_user'] = true; - $config['oauth_verify_peer'] = false; # FIXME - # $config['oauth_pkce'] = 'S256'; # FIXME - ''; - systemd.services.roundcube = { - after = [ auth-passthru.oauth2-systemd-service ]; - requires = [ auth-passthru.oauth2-systemd-service ]; + (lib.mkIf is-auth-enabled { + # for phpfpm-roundcube to have access to get through /run/keys directory + users.groups.keys.members = [ user ]; + services.roundcube.extraConfig = lib.mkAfter '' + $config['oauth_provider'] = 'generic'; + $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; + $config['oauth_client_id'] = '${oauth-donor.oauth-client-id}'; + $config['oauth_client_secret'] = file_get_contents('${kanidm-oauth-client-secret-fp}'); + $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; + $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; + $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo'; + $config['oauth_scope'] = 'email profile openid'; # FIXME + $config['oauth_auth_parameters'] = []; + $config['oauth_identity_fields'] = ['email']; + $config['oauth_login_redirect'] = true; + $config['auto_create_user'] = true; + $config['oauth_verify_peer'] = false; # FIXME + # $config['oauth_pkce'] = 'S256'; # FIXME + ''; + systemd.services.roundcube = { + after = [ auth-passthru.oauth2-systemd-service ]; + requires = [ auth-passthru.oauth2-systemd-service ]; + }; + systemd.services.kanidm = { + serviceConfig.ExecStartPre = lib.mkBefore [ + ("-+" + kanidmExecStartPreScriptRoot) + ]; + }; + services.kanidm.provision = { + groups = { + "sp.roundcube.admins".members = [ auth-passthru.admins-group ]; + "sp.roundcube.users".members = + [ "sp.roundcube.admins" auth-passthru.full-users-group ]; }; - systemd.services.kanidm = { - serviceConfig.ExecStartPre = lib.mkBefore [ - ("-+" + kanidmExecStartPreScriptRoot) - ]; - }; - services.kanidm.provision = { - groups = { - "sp.roundcube.admins".members = [ auth-passthru.admins-group ]; - "sp.roundcube.users".members = - [ "sp.roundcube.admins" auth-passthru.full-users-group ]; - }; - systems.oauth2.${oauth-donor.oauth-client-id} = { - displayName = "Roundcube"; - originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = false; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps = { - "sp.roundcube.users" = [ - "email" - "openid" - "profile" - ]; - }; - removeOrphanedClaimMaps = true; + systems.oauth2.${oauth-donor.oauth-client-id} = { + displayName = "Roundcube"; + originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + basicSecretFile = kanidm-oauth-client-secret-fp; + # when true, name is passed to a service instead of name@domain + preferShortUsername = false; + allowInsecureClientDisablePkce = true; # FIXME is it needed? + scopeMaps = { + "sp.roundcube.users" = [ + "email" + "openid" + "profile" + ]; }; + removeOrphanedClaimMaps = true; }; - }) - ) + }; + }) ]); } diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 68cd44c..1d864f6 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -1,4 +1,4 @@ -{ config, lib, options, pkgs, ... }@nixos-args: +{ config, lib, pkgs, ... }@nixos-args: let sp = config.selfprivacy; @@ -176,42 +176,36 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ }; } # the following parts are active only when "auth" module is enabled - (lib.attrsets.optionalAttrs - (options.selfprivacy.modules ? "auth") - (lib.mkIf is-auth-enabled { - mailserver = { - extraVirtualAliases = lib.mkForce { }; - loginAccounts = lib.mkForce { }; - # LDAP is needed for Postfix to query Kanidm about email address ownership. - # LDAP is needed for Dovecot also. - ldap = { - # false; otherwise, simple-nixos-mailserver enables auth via LDAP - enable = false; + (lib.mkIf is-auth-enabled { + mailserver = { + extraVirtualAliases = lib.mkForce { }; + loginAccounts = lib.mkForce { }; + # LDAP is needed for Postfix to query Kanidm about email address ownership. + # LDAP is needed for Dovecot also. + ldap = { + # false; otherwise, simple-nixos-mailserver enables auth via LDAP + enable = false; - # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; - bind.dn = "dn=token"; - # TODO change in this file should trigger system restart dovecot - bind.passwordFile = mailserver-service-account-token-fp; + # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; + bind.dn = "dn=token"; + # TODO change in this file should trigger system restart dovecot + bind.passwordFile = mailserver-service-account-token-fp; - # searchBase = "ou=persons," + ldap_base_dn; - searchBase = auth-passthru.ldap-base-dn; # TODO refine this + # searchBase = "ou=persons," + ldap_base_dn; + searchBase = auth-passthru.ldap-base-dn; # TODO refine this - # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) - uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; - }; + # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) + uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; }; - # FIXME set auth module option instead - systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ - ("-+" + kanidmExecStartPreScriptRoot) - ]; - systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ - ("-" + kanidmExecStartPostScript) - ]; - })) - (lib.attrsets.optionalAttrs - (options.selfprivacy.modules ? "auth") - (lib.mkIf is-auth-enabled (import ./auth-dovecot.nix nixos-args))) - (lib.attrsets.optionalAttrs - (options.selfprivacy.modules ? "auth") - (lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args))) + }; + # FIXME set auth module option instead + systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ + ("-+" + kanidmExecStartPreScriptRoot) + ]; + systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ + ("-" + kanidmExecStartPostScript) + ]; + }) + (lib.mkIf is-auth-enabled (import ./auth-dovecot.nix nixos-args)) + (lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args)) ]) From 331fa63b336222ac6e700b4ae0c412ef25082cc8 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 3 Feb 2025 01:35:21 +0400 Subject: [PATCH 041/115] add options: selfprivacy.sso.enable && selfprivacy.sso.debug selfprivacy.sso.enable is true by default. --- auth/auth.nix | 44 +++---------------- selfprivacy-module.nix | 12 +++++ sp-modules/gitea/module.nix | 2 +- sp-modules/nextcloud/module.nix | 2 +- sp-modules/roundcube/module.nix | 2 +- sp-modules/simple-nixos-mailserver/common.nix | 3 +- 6 files changed, 24 insertions(+), 41 deletions(-) diff --git a/auth/auth.nix b/auth/auth.nix index cee1a2a..f889f6d 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -1,8 +1,8 @@ nixpkgs-2411: { config, lib, pkgs, ... }: let - cfg = config.selfprivacy.modules.auth; domain = config.selfprivacy.domain; - auth-fqdn = cfg.subdomain + "." + domain; + subdomain = "auth"; + auth-fqdn = subdomain + "." + domain; ldap-host = "127.0.0.1"; ldap-port = 3636; @@ -46,37 +46,7 @@ let lua_path = "${lua_core_path};${lua_lrucache_path};"; in { - options.selfprivacy.modules.auth = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - }) // { - meta = { - type = "enable"; - }; - }; - subdomain = (lib.mkOption { - default = "auth"; - type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; - }) // { - meta = { - widget = "subdomain"; - type = "string"; - regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; - weight = 0; - }; - }; - debug = (lib.mkOption { - default = false; - type = lib.types.bool; - }) // { - meta = { - type = "enable"; - }; - }; - }; - - config = lib.mkIf cfg.enable { + config = lib.mkIf config.selfprivacy.sso.enable { nixpkgs.overlays = [ ( _final: prev: { @@ -132,7 +102,7 @@ in # kanidm is behind a proxy trust_x_forward_for = true; - log_level = if cfg.debug then "trace" else "info"; + log_level = if config.selfprivacy.sso.debug then "trace" else "info"; }; provision = { enable = true; @@ -151,8 +121,8 @@ in services.nginx = { enable = true; additionalModules = - lib.mkIf cfg.debug [ pkgs.nginxModules.lua ]; - commonHttpConfig = lib.mkIf cfg.debug '' + lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ]; + commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug '' log_format kanidm escape=none '$request $status\n' '[Request body]: $request_body\n' '[Header]: $resp_header\n' @@ -163,7 +133,7 @@ in useACMEHost = domain; forceSSL = true; locations."/" = { - extraConfig = lib.mkIf cfg.debug '' + extraConfig = lib.mkIf config.selfprivacy.sso.debug '' access_log /var/log/nginx/kanidm.log kanidm; lua_need_request_body on; diff --git a/selfprivacy-module.nix b/selfprivacy-module.nix index 76897a0..55842c9 100644 --- a/selfprivacy-module.nix +++ b/selfprivacy-module.nix @@ -34,6 +34,18 @@ with lib; type = types.nullOr types.bool; }; }; + sso = { + enable = mkOption { + description = "Enable SSO."; + default = true; + type = types.nullOr types.bool; + }; + debug = mkOption { + description = "Enable debug for SSO."; + default = false; + type = types.nullOr types.bool; + }; + }; stateVersion = mkOption { description = "State version of the server"; type = types.nullOr types.str; diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 3c888df..97e0f13 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -14,7 +14,7 @@ let "gitea-light" "gitea-dark" ]; - is-auth-enabled = cfg.enableSso; + is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable; oauth-client-id = "forgejo"; auth-passthru = config.selfprivacy.passthru.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 17719f3..9110b9e 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -12,7 +12,7 @@ let hostName = "${cfg.subdomain}.${sp.domain}"; auth-passthru = config.selfprivacy.passthru.auth; cfg = sp.modules.nextcloud; - is-auth-enabled = cfg.enableSso; + is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable; ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 68147d8..cfb5ee0 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -2,7 +2,7 @@ let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; - is-auth-enabled = cfg.enableSso; + is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable; auth-passthru = config.selfprivacy.passthru.auth; auth-fqdn = auth-passthru.auth-fqdn; sp-module-name = "roundcube"; diff --git a/sp-modules/simple-nixos-mailserver/common.nix b/sp-modules/simple-nixos-mailserver/common.nix index 5f21e92..6e60dba 100644 --- a/sp-modules/simple-nixos-mailserver/common.nix +++ b/sp-modules/simple-nixos-mailserver/common.nix @@ -4,7 +4,8 @@ rec { domain = config.selfprivacy.domain; group = "dovecot2"; is-auth-enabled = - config.selfprivacy.modules.simple-nixos-mailserver.enableSso; + config.selfprivacy.modules.simple-nixos-mailserver.enableSso + && config.selfprivacy.sso.enable; appendSetting = { name, file, prefix, suffix ? "", passwordFile, destination }: From 1ff180ad1a0bada53601eb4199536d49986555c2 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 3 Feb 2025 01:51:19 +0400 Subject: [PATCH 042/115] add assertions: selfprivacy.sso.enable -> modules.*.enableSso --- sp-modules/gitea/config-paths-needed.json | 1 + sp-modules/gitea/module.nix | 7 +++++++ sp-modules/nextcloud/config-paths-needed.json | 1 + sp-modules/nextcloud/module.nix | 7 +++++++ sp-modules/roundcube/config-paths-needed.json | 3 ++- sp-modules/roundcube/module.nix | 7 +++++++ .../simple-nixos-mailserver/config-paths-needed.json | 1 + sp-modules/simple-nixos-mailserver/config.nix | 9 +++++++++ 8 files changed, 35 insertions(+), 1 deletion(-) diff --git a/sp-modules/gitea/config-paths-needed.json b/sp-modules/gitea/config-paths-needed.json index b22677e..08ee684 100644 --- a/sp-modules/gitea/config-paths-needed.json +++ b/sp-modules/gitea/config-paths-needed.json @@ -11,6 +11,7 @@ [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], + [ "selfprivacy", "sso", "enable" ], [ "selfprivacy", "useBinds" ], [ "services", "forgejo", "group" ], [ "services", "forgejo", "package" ] diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 97e0f13..b8fd07d 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -200,6 +200,13 @@ in config = lib.mkIf cfg.enable (lib.mkMerge [ { + assertions = [ + { + assertion = cfg.enableSso -> sp.sso.enable; + message = + "SSO cannot be enabled for Forgejo when SSO is disabled globally."; + } + ]; fileSystems = lib.mkIf sp.useBinds { "/var/lib/gitea" = { device = "/volumes/${cfg.location}/gitea"; diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index 979bf4e..cc78e0d 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -11,6 +11,7 @@ [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], + [ "selfprivacy", "sso", "enable" ], [ "selfprivacy", "useBinds" ], [ "services", "nextcloud" ], [ "services", "phpfpm", "pools", "nextcloud", "group" ], diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 9110b9e..e26c3a7 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -152,6 +152,13 @@ in # config = lib.mkIf sp.modules.nextcloud.enable config = lib.mkIf sp.modules.nextcloud.enable (lib.mkMerge [ { + assertions = [ + { + assertion = cfg.enableSso -> sp.sso.enable; + message = + "SSO cannot be enabled for Nextcloud when SSO is disabled globally."; + } + ]; fileSystems = lib.mkIf sp.useBinds { "/var/lib/nextcloud" = { device = "/volumes/${cfg.location}/nextcloud"; diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index a545298..5a893c4 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -9,5 +9,6 @@ [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "passthru", "mailserver", "oauth-client-id" ], - [ "selfprivacy", "passthru", "mailserver", "oauth-client-secret-fp" ] + [ "selfprivacy", "passthru", "mailserver", "oauth-client-secret-fp" ], + [ "selfprivacy", "sso", "enable" ] ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index cfb5ee0..b9653dd 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -57,6 +57,13 @@ in config = lib.mkIf cfg.enable (lib.mkMerge [ { + assertions = [ + { + assertion = cfg.enableSso -> config.selfprivacy.sso.enable; + message = + "SSO cannot be enabled for Roundcube when SSO is disabled globally."; + } + ]; services.roundcube = { enable = true; # this is the url of the vhost, not necessarily the same as the fqdn of diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index 2229673..2833470 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -15,6 +15,7 @@ [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "passthru", "roundcube", "oauth-client-id" ], [ "selfprivacy", "passthru", "roundcube", "oauth-client-secret-fp" ], + [ "selfprivacy", "sso", "enable" ], [ "selfprivacy", "useBinds" ], [ "selfprivacy", "username" ], [ "selfprivacy", "users" ], diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 1d864f6..06678eb 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -71,6 +71,15 @@ let in lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ { + assertions = [ + { + assertion = + config.selfprivacy.modules.simple-nixos-mailserver.enableSso + -> config.selfprivacy.sso.enable; + message = + "SSO cannot be enabled for Roundcube when SSO is disabled globally."; + } + ]; fileSystems = lib.mkIf sp.useBinds { "/var/vmail" = { From c49a93bf9c6a6ebd63224e3084acc916e5cbca0c Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 12 Feb 2025 15:50:15 +0400 Subject: [PATCH 043/115] auth: generate kanidm API token for selfprivacy in /run/keys/... --- auth/auth.nix | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/auth/auth.nix b/auth/auth.nix index f889f6d..3ab2711 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -12,8 +12,22 @@ let kanidm-bind-address = "127.0.0.1:3013"; + selfprivacy-group = config.users.users."selfprivacy-api".group; + selfprivacy-service-account-name = "sp.selfprivacy-api.service-account"; + kanidm-service-account-token-name = + "${selfprivacy-group}-service-account-token"; + kanidm-service-account-token-fp = + "/run/keys/${selfprivacy-group}/kanidm-service-account-token"; + kanidmExecStartPreScriptRoot = pkgs.writeShellScript + "${selfprivacy-group}-kanidm-ExecStartPre-root-script.sh" + '' + # set-group-ID bit allows for kanidm user to create files, + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${selfprivacy-group} + chown kanidm:${selfprivacy-group} /run/keys/${selfprivacy-group} + ''; + spApiUserExecStartPostScript = pkgs.writeShellScript "spApiUserExecStartPostScript" '' export HOME=$RUNTIME_DIRECTORY/client_home @@ -38,6 +52,26 @@ let fi $KANIDM group add-members idm_admins "${selfprivacy-service-account-name}" + + # create a new read-write token for kanidm + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${selfprivacy-service-account-name}" "${kanidm-service-account-token-name}" --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi + + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${kanidm-service-account-token-fp} + then + echo "error: cannot write token to \"${kanidm-service-account-token-fp}\"" + exit 1 + fi ''; # lua stuff for debugging only @@ -175,6 +209,8 @@ in }; }; + systemd.services.kanidm.serviceConfig.ExecStartPre = + [ ("+" + kanidmExecStartPreScriptRoot) ]; systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ spApiUserExecStartPostScript ]; From 403c4b31b1d771066ccc7ff55bf1bcb5c7edaa42 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sun, 16 Mar 2025 19:50:41 +0400 Subject: [PATCH 044/115] refact: auth: variable for generated keys path in auth.nix --- auth/auth.nix | 290 +++++++++++++++++++++++++------------------------- 1 file changed, 145 insertions(+), 145 deletions(-) diff --git a/auth/auth.nix b/auth/auth.nix index 3ab2711..2a44026 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -7,6 +7,8 @@ let ldap-host = "127.0.0.1"; ldap-port = 3636; + keys-path = "/run/keys"; + admins-group = "sp.admins"; full-users-group = "sp.full_users"; @@ -19,7 +21,7 @@ let kanidm-service-account-token-name = "${selfprivacy-group}-service-account-token"; kanidm-service-account-token-fp = - "/run/keys/${selfprivacy-group}/kanidm-service-account-token"; + "${keys-path}/${selfprivacy-group}/kanidm-service-account-token"; kanidmExecStartPreScriptRoot = pkgs.writeShellScript "${selfprivacy-group}-kanidm-ExecStartPre-root-script.sh" '' @@ -74,168 +76,166 @@ let fi ''; - # lua stuff for debugging only + # lua stuff for nginx for debugging only lua_core_path = "${pkgs.luajitPackages.lua-resty-core}/lib/lua/5.1/?.lua"; lua_lrucache_path = "${pkgs.luajitPackages.lua-resty-lrucache}/lib/lua/5.1/?.lua"; lua_path = "${lua_core_path};${lua_lrucache_path};"; in -{ - config = lib.mkIf config.selfprivacy.sso.enable { - nixpkgs.overlays = [ - ( - _final: prev: { - inherit (nixpkgs-2411.legacyPackages.${prev.system}) kanidm; - kanidm-provision = - nixpkgs-2411.legacyPackages.${prev.system}.kanidm-provision.overrideAttrs (_: { - version = "git"; - src = prev.fetchFromGitHub { - owner = "oddlama"; - repo = "kanidm-provision"; - rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; - hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; - }; - }); - } - ) - ]; +lib.mkIf config.selfprivacy.sso.enable { + nixpkgs.overlays = [ + ( + _final: prev: { + inherit (nixpkgs-2411.legacyPackages.${prev.system}) kanidm; + kanidm-provision = + nixpkgs-2411.legacyPackages.${prev.system}.kanidm-provision.overrideAttrs (_: { + version = "git"; + src = prev.fetchFromGitHub { + owner = "oddlama"; + repo = "kanidm-provision"; + rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; + hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; + }; + }); + } + ) + ]; - # kanidm uses TLS in internal connection with nginx too - # FIXME revise this: maybe kanidm must not have access to a public TLS - users.groups."acmereceivers".members = [ "kanidm" ]; + # kanidm uses TLS in internal connection with nginx too + # FIXME revise this: maybe kanidm must not have access to a public TLS + users.groups."acmereceivers".members = [ "kanidm" ]; - # for ExecStartPost scripts to have access to /run/keys/* - users.groups.keys.members = [ "kanidm" ]; + # for ExecStartPost scripts to have access to /run/keys/* + users.groups.keys.members = [ "kanidm" ]; - services.kanidm = { - enableServer = true; + services.kanidm = { + enableServer = true; - # kanidm with Rust code patches for OAuth and admin passwords provisioning - package = pkgs.kanidm.withSecretProvisioning; + # kanidm with Rust code patches for OAuth and admin passwords provisioning + package = pkgs.kanidm.withSecretProvisioning; - serverSettings = { - inherit domain; - # The origin for webauthn. This is the url to the server, with the port - # included if it is non-standard (any port except 443). This must match or - # be a descendent of the domain name you configure above. If these two - # items are not consistent, the server WILL refuse to start! - origin = "https://" + auth-fqdn; + serverSettings = { + inherit domain; + # The origin for webauthn. This is the url to the server, with the port + # included if it is non-standard (any port except 443). This must match or + # be a descendent of the domain name you configure above. If these two + # items are not consistent, the server WILL refuse to start! + origin = "https://" + auth-fqdn; - # TODO revise this: maybe kanidm must not have access to a public TLS - tls_chain = - "${config.security.acme.certs.${domain}.directory}/fullchain.pem"; - tls_key = - "${config.security.acme.certs.${domain}.directory}/key.pem"; + # TODO revise this: maybe kanidm must not have access to a public TLS + tls_chain = + "${config.security.acme.certs.${domain}.directory}/fullchain.pem"; + tls_key = + "${config.security.acme.certs.${domain}.directory}/key.pem"; - # nginx should proxy requests to it - bindaddress = kanidm-bind-address; + # nginx should proxy requests to it + bindaddress = kanidm-bind-address; - ldapbindaddress = - "${ldap-host}:${toString ldap-port}"; + ldapbindaddress = + "${ldap-host}:${toString ldap-port}"; - # kanidm is behind a proxy - trust_x_forward_for = true; + # kanidm is behind a proxy + trust_x_forward_for = true; - log_level = if config.selfprivacy.sso.debug then "trace" else "info"; - }; - provision = { - enable = true; - autoRemove = true; # if false, obsolete oauth2 scopeMaps remain - groups.${admins-group}.present = true; - groups.${full-users-group}.present = true; - }; - enableClient = true; - clientSettings = { - uri = "https://" + auth-fqdn; - verify_ca = false; # FIXME - verify_hostnames = false; # FIXME - }; + log_level = if config.selfprivacy.sso.debug then "trace" else "info"; }; - - services.nginx = { + provision = { enable = true; - additionalModules = - lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ]; - commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug '' - log_format kanidm escape=none '$request $status\n' - '[Request body]: $request_body\n' - '[Header]: $resp_header\n' - '[Response Body]: $resp_body\n\n'; - lua_package_path "${lua_path}"; - ''; - virtualHosts.${auth-fqdn} = { - useACMEHost = domain; - forceSSL = true; - locations."/" = { - extraConfig = lib.mkIf config.selfprivacy.sso.debug '' - access_log /var/log/nginx/kanidm.log kanidm; - - lua_need_request_body on; - - # log header - set $req_header ""; - set $resp_header ""; - header_filter_by_lua ' - local h = ngx.req.get_headers() - for k, v in pairs(h) do - if type(v) == "table" then - ngx.var.req_header = ngx.var.req_header .. k .. "=" .. table.concat(v, ", ") .. " " - else - ngx.var.req_header = ngx.var.req_header .. k .. "=" .. v .. " " - end - end - local rh = ngx.resp.get_headers() - for k, v in pairs(rh) do - if type(v) == "table" then - ngx.var.resp_header = ngx.var.resp_header .. k .. "=" .. table.concat(v, ", ") .. " " - else - ngx.var.resp_header = ngx.var.resp_header .. k .. "=" .. v .. " " - end - end - '; - - # log body - set $resp_body ""; - body_filter_by_lua ' - local resp_body = string.sub(ngx.arg[1], 1, 4000) - ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body - if ngx.arg[2] then - ngx.var.resp_body = ngx.ctx.buffered - end - '; - ''; - proxyPass = "https://${kanidm-bind-address}"; - }; - }; + autoRemove = true; # if false, obsolete oauth2 scopeMaps remain + groups.${admins-group}.present = true; + groups.${full-users-group}.present = true; }; - - systemd.services.kanidm.serviceConfig.ExecStartPre = - [ ("+" + kanidmExecStartPreScriptRoot) ]; - systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter - [ spApiUserExecStartPostScript ]; - - selfprivacy.passthru.auth = { - inherit - admins-group - auth-fqdn - full-users-group - ldap-host - ldap-port - ; - oauth2-introspection-url-prefix = client_id: "https://${client_id}:"; - oauth2-introspection-url-postfix = - "@${auth-fqdn}/oauth2/token/introspect"; - oauth2-discovery-url = client_id: - "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; - oauth2-provider-name = "Kanidm"; - oauth2-systemd-service = "kanidm.service"; - - # e.g. "dc=mydomain,dc=com" - ldap-base-dn = - lib.strings.concatMapStringsSep - "," - (x: "dc=" + x) - (lib.strings.splitString "." domain); + enableClient = true; + clientSettings = { + uri = "https://" + auth-fqdn; + verify_ca = false; # FIXME + verify_hostnames = false; # FIXME }; }; + + services.nginx = { + enable = true; + additionalModules = + lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ]; + commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug '' + log_format kanidm escape=none '$request $status\n' + '[Request body]: $request_body\n' + '[Header]: $resp_header\n' + '[Response Body]: $resp_body\n\n'; + lua_package_path "${lua_path}"; + ''; + virtualHosts.${auth-fqdn} = { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + extraConfig = lib.mkIf config.selfprivacy.sso.debug '' + access_log /var/log/nginx/kanidm.log kanidm; + + lua_need_request_body on; + + # log header + set $req_header ""; + set $resp_header ""; + header_filter_by_lua ' + local h = ngx.req.get_headers() + for k, v in pairs(h) do + if type(v) == "table" then + ngx.var.req_header = ngx.var.req_header .. k .. "=" .. table.concat(v, ", ") .. " " + else + ngx.var.req_header = ngx.var.req_header .. k .. "=" .. v .. " " + end + end + local rh = ngx.resp.get_headers() + for k, v in pairs(rh) do + if type(v) == "table" then + ngx.var.resp_header = ngx.var.resp_header .. k .. "=" .. table.concat(v, ", ") .. " " + else + ngx.var.resp_header = ngx.var.resp_header .. k .. "=" .. v .. " " + end + end + '; + + # log body + set $resp_body ""; + body_filter_by_lua ' + local resp_body = string.sub(ngx.arg[1], 1, 4000) + ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body + if ngx.arg[2] then + ngx.var.resp_body = ngx.ctx.buffered + end + '; + ''; + proxyPass = "https://${kanidm-bind-address}"; + }; + }; + }; + + systemd.services.kanidm.serviceConfig.ExecStartPre = + [ ("+" + kanidmExecStartPreScriptRoot) ]; + systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter + [ spApiUserExecStartPostScript ]; + + selfprivacy.passthru.auth = { + inherit + admins-group + auth-fqdn + full-users-group + ldap-host + ldap-port + ; + oauth2-introspection-url-prefix = client_id: "https://${client_id}:"; + oauth2-introspection-url-postfix = + "@${auth-fqdn}/oauth2/token/introspect"; + oauth2-discovery-url = client_id: + "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; + oauth2-provider-name = "Kanidm"; + oauth2-systemd-service = "kanidm.service"; + + # e.g. "dc=mydomain,dc=com" + ldap-base-dn = + lib.strings.concatMapStringsSep + "," + (x: "dc=" + x) + (lib.strings.splitString "." domain); + }; } From 8013f2e39406e1cd32dfc850f5a40b4e098d4a63 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sun, 16 Mar 2025 19:51:18 +0400 Subject: [PATCH 045/115] auth: module for easier integration of new services with Kanidm - Forgejo is migrated to this module. --- auth/auth-module.nix | 321 ++++++++++++++++++++++ auth/auth.nix | 8 + configuration.nix | 1 + sp-modules/gitea/config-paths-needed.json | 7 +- sp-modules/gitea/module.nix | 156 +++-------- 5 files changed, 363 insertions(+), 130 deletions(-) create mode 100644 auth/auth-module.nix diff --git a/auth/auth-module.nix b/auth/auth-module.nix new file mode 100644 index 0000000..289c4eb --- /dev/null +++ b/auth/auth-module.nix @@ -0,0 +1,321 @@ +{ config, lib, pkgs, ... }: +let + inherit (lib) + mkOption + types + ; + auth-passthru = config.selfprivacy.passthru.auth; + keys-path = auth-passthru.keys-path; + # TODO consider tmpfiles.d for creating a directory in ${keys-path} + mkKanidmExecStartPreScriptRoot = oauthClientID: group: + pkgs.writeShellScript + "${oauthClientID}-kanidm-ExecStartPre-root-script.sh" + '' + # set-group-ID bit allows kanidm user to create files with another group + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx ${keys-path}/${oauthClientID} + chown kanidm:${group} ${keys-path}/${oauthClientID} + ''; + # generate OAuth2 client secret + mkKanidmExecStartPreScript = oauthClientID: + let + secretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID; + in + pkgs.writeShellScript + "${oauthClientID}-kanidm-ExecStartPre-script.sh" '' + [ -f "${secretFP}" ] || \ + "${lib.getExe pkgs.openssl}" rand -base64 -out "${secretFP}" 32 && \ + chmod 640 "${secretFP}" + ''; + mkKanidmExecStartPostScript = oauthClientID: + let + kanidmServiceAccountName = "sp.${oauthClientID}.service-account"; + kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token"; + kanidmServiceAccountTokenFP = + auth-passthru.mkServiceAccountTokenFP oauthClientID; + in + pkgs.writeShellScript + "${oauthClientID}-kanidm-ExecStartPost-script.sh" + '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + + # try to get existing Kanidm service account + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${kanidmServiceAccountName}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${kanidmServiceAccountName}\" is found" + else + echo "kanidm service account \"${kanidmServiceAccountName}\" is not found" + echo "creating new kanidm service account \"${kanidmServiceAccountName}\"" + if $KANIDM service-account create --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountName}" idm_admin + then + echo "kanidm service account \"${kanidmServiceAccountName}\" created" + else + echo "error: cannot create kanidm service account \"${kanidmServiceAccountName}\"" + exit 1 + fi + fi + + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers "${kanidmServiceAccountName}" + + # create a new read-only token for kanidm + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountTokenName}" --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi + + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${kanidmServiceAccountTokenFP} + then + echo "error: cannot write token to \"${kanidmServiceAccountTokenFP}\"" + exit 1 + fi + ''; +in +{ + options.selfprivacy.auth = { + clients = mkOption { + description = + "Configurations for OAuth2 & LDAP servers clients services. Corresponding Kanidm provisioning configuration and systemd scripts are generated."; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + clientID = mkOption { + type = types.nullOr types.str; + description = '' + Name of this client service. Used as OAuth2 client ID and to form Kanidm sp.$\{clientID}.* group names. Defaults to attribute name in virtualHosts; + ''; + default = null; + }; + displayName = mkOption { + type = types.nullOr types.str; + description = "Display name showed in Kanidm Web GUI. Defaults to clientID."; + default = null; + }; + enablePkce = mkOption { + type = lib.types.bool; + description = + "Whether PKCE must be used between client and Kanidm."; + default = false; + }; + adminsGroup = mkOption { + type = + types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.admins"); + description = + "Name of admins group in Kanidm, whose members have admin level access to resources (service) associated with OAuth2 client authorization."; + default = null; + }; + usersGroup = mkOption { + type = + types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.users"); + description = + "Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization."; + default = null; + }; + originUrl = mkOption { + type = types.nullOr lib.types.str; + description = + "The origin URL of the service for OAuth2 redirects."; + }; + subdomain = lib.mkOption { + type = + lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + description = "Subdomain of the service."; + }; + # when true, "name" is passed to a service instead of "name@domain" + useShortPreferredUsername = mkOption { + description = + "Use 'name' instead of 'spn' in the preferred_username claim."; + type = types.bool; + default = true; + }; + linuxUserOfClient = mkOption { + type = types.nullOr lib.types.str; + description = + "Name of a Linux OAuth2 client user, under which it should get access through a folder with keys."; + default = null; + }; + linuxGroupOfClient = mkOption { + type = types.nullOr lib.types.str; + description = + "Name of Linux OAuth2 client group, under which it should read an OAuth2 client secret file."; + default = null; + }; + isTokenNeeded = mkOption { + description = + "Whether a read-only needs to be generated for LDAP access."; + type = types.bool; + default = false; + }; + clientSystemdUnits = mkOption { + description = "A list of systemd services, which depend on OAuth service"; + # taken from nixos/lib/systemd-lib.nix: unitNameType + type = types.listOf + (types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)"); + }; + scopeMaps = mkOption { + description = '' + Maps kanidm groups to returned oauth scopes. + See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. + ''; + type = types.nullOr (types.attrsOf (types.listOf types.str)); + default = null; + }; + claimMaps = mkOption { + description = '' + Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to. + See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. + ''; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + joinType = mkOption { + description = '' + Determines how multiple values are joined to create the claim value. + See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. + ''; + type = types.enum [ + "array" + "csv" + "ssv" + ]; + default = "array"; + }; + + valuesByGroup = mkOption { + description = "Maps kanidm groups to values for the claim."; + default = { }; + type = types.attrsOf (types.listOf types.str); + }; + }; + } + ); + }; + }; + } + ); + }; + }; + # (lib.debug.traceValSeq + config = lib.mkIf config.selfprivacy.sso.enable ( + let + clientsAttrsList = lib.attrsets.mapAttrsToList + (name: attrs: attrs // rec { + clientID = + if attrs.clientID == null + then name + else attrs.clientID; + displayName = + if attrs.displayName == null + then clientID + else attrs.displayName; + adminsGroup = + if attrs.adminsGroup == null + then "sp.${clientID}.admins" + else attrs.adminsGroup; + usersGroup = + if attrs.usersGroup == null + then "sp.${clientID}.users" + else attrs.usersGroup; + basicSecretFile = + "${keys-path}/${clientID}/kanidm-oauth-client-secret"; + linuxUserOfClient = + if attrs.linuxUserOfClient == null + then clientID + else attrs.linuxUserOfClient; + linuxGroupOfClient = + if attrs.linuxGroupOfClient == null + then clientID + else attrs.linuxGroupOfClient; + scopeMaps = + if attrs.scopeMaps == null + then { "${usersGroup}" = [ "email" "openid" "profile" ]; } + else attrs.scopeMaps; + }) + config.selfprivacy.auth.clients; + in + { + # for each OAuth2 client: member of the `keys` group for directory access + users.groups.keys.members = lib.mkMerge (lib.forEach + clientsAttrsList + ({ linuxUserOfClient, ... }: [ linuxUserOfClient ]) + ); + + # for each OAuth2 client: scripts with Kanidm CLI commands + systemd.services.kanidm = { + before = + lib.lists.concatMap + ({ clientSystemdUnits, ... }: clientSystemdUnits) + clientsAttrsList; + serviceConfig = + lib.mkMerge (lib.forEach + clientsAttrsList + ({ clientID, isTokenNeeded, linuxGroupOfClient, ... }: { + ExecStartPre = [ + # "-" prefix means to ignore exit code of prefixed script + # "+" prefix means to run script with superuser priveleges + ("-+" + mkKanidmExecStartPreScriptRoot clientID linuxGroupOfClient) + ("-" + mkKanidmExecStartPreScript clientID) + ]; + ExecStartPost = lib.mkIf isTokenNeeded + (lib.mkAfter [ ("-" + mkKanidmExecStartPostScript clientID) ]); + })); + }; + + # for each OAuth2 client: Kanidm provisioning options + services.kanidm.provision = lib.mkMerge (lib.forEach + clientsAttrsList + ({ adminsGroup + , basicSecretFile + , claimMaps + , clientID + , displayName + , enablePkce + , originUrl + , scopeMaps + , useShortPreferredUsername + , subdomain + , usersGroup + , ... + }: { + groups = { + "${adminsGroup}".members = + [ auth-passthru.admins-group ]; + "${usersGroup}".members = + [ adminsGroup auth-passthru.full-users-group ]; + }; + systems.oauth2.${clientID} = { + inherit + basicSecretFile + claimMaps + displayName + originUrl + scopeMaps + ; + originLanding = + "https://${subdomain}.${config.selfprivacy.domain}/"; + preferShortUsername = useShortPreferredUsername; + allowInsecureClientDisablePkce = ! enablePkce; + removeOrphanedClaimMaps = true; + + # NOTE https://github.com/oddlama/kanidm-provision/issues/15 + # add more scopes when a user is a member of specific group + # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 + # supplementaryScopeMaps."${admins-group}" = + # [ "read:admin" "write:admin" ]; + }; + })); + } + ); +} diff --git a/auth/auth.nix b/auth/auth.nix index 2a44026..246a09d 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -222,6 +222,7 @@ lib.mkIf config.selfprivacy.sso.enable { full-users-group ldap-host ldap-port + keys-path ; oauth2-introspection-url-prefix = client_id: "https://${client_id}:"; oauth2-introspection-url-postfix = @@ -237,5 +238,12 @@ lib.mkIf config.selfprivacy.sso.enable { "," (x: "dc=" + x) (lib.strings.splitString "." domain); + + # TODO consider to pass a value or throw exception if token is not generated + mkServiceAccountTokenFP = oauthClientID: + "${keys-path}/${oauthClientID}/kanidm-service-account-token"; + + mkOAuth2ClientSecretFP = oauthClientID: + "${keys-path}/${oauthClientID}/kanidm-oauth-client-secret"; }; } diff --git a/configuration.nix b/configuration.nix index 6aa93c8..025d3c3 100644 --- a/configuration.nix +++ b/configuration.nix @@ -32,6 +32,7 @@ in { imports = [ ./selfprivacy-module.nix + ./auth/auth-module.nix ./volumes.nix ./users.nix ./letsencrypt/acme.nix diff --git a/sp-modules/gitea/config-paths-needed.json b/sp-modules/gitea/config-paths-needed.json index 08ee684..63ea4fe 100644 --- a/sp-modules/gitea/config-paths-needed.json +++ b/sp-modules/gitea/config-paths-needed.json @@ -2,17 +2,14 @@ [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth", "enable" ], [ "selfprivacy", "modules", "gitea" ], - [ "selfprivacy", "passthru", "auth", "admins-group" ], - [ "selfprivacy", "passthru", "auth", "auth-fqdn" ], - [ "selfprivacy", "passthru", "auth", "full-users-group" ], [ "selfprivacy", "passthru", "auth", "ldap-base-dn" ], [ "selfprivacy", "passthru", "auth", "ldap-host" ], [ "selfprivacy", "passthru", "auth", "ldap-port" ], + [ "selfprivacy", "passthru", "auth", "mkOAuth2ClientSecretFP" ], + [ "selfprivacy", "passthru", "auth", "mkServiceAccountTokenFP" ], [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], - [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "sso", "enable" ], [ "selfprivacy", "useBinds" ], - [ "services", "forgejo", "group" ], [ "services", "forgejo", "package" ] ] diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index b8fd07d..1b8a000 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -15,81 +15,23 @@ let "gitea-dark" ]; is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable; - oauth-client-id = "forgejo"; + oauthClientID = "forgejo"; auth-passthru = config.selfprivacy.passthru.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; redirect-uri = "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; - admins-group = "sp.forgejo.admins"; - users-group = "sp.forgejo.users"; + adminsGroup = "sp.forgejo.admins"; + usersGroup = "sp.forgejo.users"; - kanidm-service-account-name = "sp.${oauth-client-id}.service-account"; - kanidm-service-account-token-name = "${oauth-client-id}-service-account-token"; - kanidm-service-account-token-fp = - "/run/keys/${oauth-client-id}/kanidm-service-account-token"; # FIXME sync with auth module - # TODO rewrite to tmpfiles.d - kanidmExecStartPreScriptRoot = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPre-root-script.sh" - '' - # set-group-ID bit allows kanidm user to create files with another group - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} - chown kanidm:${config.services.forgejo.group} /run/keys/${oauth-client-id} - ''; - kanidm-oauth-client-secret-fp = - "/run/keys/${oauth-client-id}/kanidm-oauth-client-secret"; - kanidmExecStartPreScript = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' - [ -f "${kanidm-oauth-client-secret-fp}" ] || \ - "${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32 - ''; - kanidmExecStartPostScript = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPost-script.sh" - '' - export HOME=$RUNTIME_DIRECTORY/client_home - readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + linuxUserOfService = "gitea"; + linuxGroupOfService = "gitea"; + forgejoPackage = pkgs.forgejo; - # get Kanidm service account for mailserver - KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${kanidm-service-account-name}$")" - echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" - if [ -n "$KANIDM_SERVICE_ACCOUNT" ] - then - echo "kanidm service account \"${kanidm-service-account-name}\" is found" - else - echo "kanidm service account \"${kanidm-service-account-name}\" is not found" - echo "creating new kanidm service account \"${kanidm-service-account-name}\"" - if $KANIDM service-account create --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-name}" idm_admin - then - echo "kanidm service account \"${kanidm-service-account-name}\" created" - else - echo "error: cannot create kanidm service account \"${kanidm-service-account-name}\"" - exit 1 - fi - fi - - # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers "${kanidm-service-account-name}" - - # create a new read-only token for kanidm - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-token-name}" --output json)" - then - echo "error: kanidm CLI returns an error when trying to generate service-account api-token" - exit 1 - fi - if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" - then - echo "error: cannot get service-account API token from JSON" - exit 1 - fi - - if ! install --mode=640 \ - <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ - ${kanidm-service-account-token-fp} - then - echo "error: cannot write token to \"${kanidm-service-account-token-fp}\"" - exit 1 - fi - ''; + serviceAccountTokenFP = + auth-passthru.mkServiceAccountTokenFP oauthClientID; + oauthClientSecretFP = + auth-passthru.mkOAuth2ClientSecretFP oauthClientID; in { options.selfprivacy.modules.gitea = { @@ -216,15 +158,15 @@ in services.gitea.enable = false; services.forgejo = { enable = true; - package = pkgs.forgejo; + package = forgejoPackage; inherit stateDir; - user = "gitea"; - group = "gitea"; + user = linuxUserOfService; + group = linuxGroupOfService; database = { type = "sqlite3"; host = "127.0.0.1"; name = "gitea"; - user = "gitea"; + user = linuxUserOfService; path = "${stateDir}/data/gitea.db"; createDatabase = true; }; @@ -281,10 +223,10 @@ in users.users.gitea = { home = "${stateDir}"; useDefaultShell = true; - group = "gitea"; + group = linuxGroupOfService; isSystemUser = true; }; - users.groups.gitea = { }; + users.groups.${linuxGroupOfService} = { }; services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { useACMEHost = sp.domain; forceSSL = true; @@ -315,7 +257,7 @@ in }; }; } - # the following part is active only when "auth" module is enabled + # the following part is active only when enableSso = true (lib.mkIf is-auth-enabled { services.forgejo.settings = { auth.DISABLE_LOGIN_FORM = true; @@ -359,25 +301,25 @@ in --host '${auth-passthru.ldap-host}' \ --port '${toString auth-passthru.ldap-port}' \ --user-search-base '${auth-passthru.ldap-base-dn}' \ - --user-filter '(&(class=person)(memberof=${users-group})(name=%s))' \ - --admin-filter '(&(class=person)(memberof=${admins-group})' \ + --user-filter '(&(class=person)(memberof=${usersGroup})(name=%s))' \ + --admin-filter '(&(class=person)(memberof=${adminsGroup})' \ --username-attribute name \ --firstname-attribute name \ --surname-attribute displayname \ --email-attribute mail \ --public-ssh-key-attribute sshPublicKey \ --bind-dn 'dn=token' \ - --bind-password "$(cat ${kanidm-service-account-token-fp})" \ + --bind-password "$(< ${serviceAccountTokenFP})" \ --synchronize-users ''; oauthConfigArgs = '' --name "${oauth2-provider-name}" \ --provider openidConnect \ --key forgejo \ - --secret "$(<${kanidm-oauth-client-secret-fp})" \ + --secret "$(< ${oauthClientSecretFP})" \ --group-claim-name groups \ --admin-group admins \ - --auto-discover-url '${auth-passthru.oauth2-discovery-url oauth-client-id}' + --auto-discover-url '${auth-passthru.oauth2-discovery-url oauthClientID}' ''; in lib.mkAfter '' @@ -403,21 +345,8 @@ in ${exe} admin auth add-oauth ${oauthConfigArgs} fi ''; - # TODO consider passing oauth consumer service to auth module instead - after = [ auth-passthru.oauth2-systemd-service ]; - requires = [ auth-passthru.oauth2-systemd-service ]; }; - # for ExecStartPost script to have access to /run/keys/* - users.groups.keys.members = [ config.services.forgejo.group ]; - - systemd.services.kanidm.serviceConfig.ExecStartPre = [ - ("-+" + kanidmExecStartPreScriptRoot) - ("-" + kanidmExecStartPreScript) - ]; - systemd.services.kanidm.serviceConfig.ExecStartPost = - lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]; - services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { extraConfig = lib.mkAfter '' rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; @@ -426,38 +355,15 @@ in ''; }; - services.kanidm.provision = { - groups = { - "${admins-group}".members = [ auth-passthru.admins-group ]; - "${users-group}".members = - [ admins-group auth-passthru.full-users-group ]; - }; - systems.oauth2.forgejo = { - displayName = "Forgejo"; - originUrl = redirect-uri; - originLanding = "https://${cfg.subdomain}.${sp.domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = true; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps = { - "${users-group}" = [ - "email" - "openid" - "profile" - ]; - }; - removeOrphanedClaimMaps = true; - # NOTE https://github.com/oddlama/kanidm-provision/issues/15 - # add more scopes when a user is a member of specific group - # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 - # supplementaryScopeMaps."${admins-group}" = - # [ "read:admin" "write:admin" ]; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${admins-group} = [ "admins" ]; - }; - }; + selfprivacy.auth.clients."${oauthClientID}" = { + inherit adminsGroup usersGroup; + subdomain = cfg.subdomain; + isTokenNeeded = true; + originUrl = redirect-uri; + clientSystemdUnits = [ "forgejo.service" ]; + enablePkce = false; # FIXME maybe Forgejo supports PKCE? + linuxUserOfClient = linuxUserOfService; + linuxGroupOfClient = linuxGroupOfService; }; }) ]); From 838b5dc204da68211a3d23c5760449699991f319 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 26 Mar 2025 14:58:02 +0400 Subject: [PATCH 046/115] auth: add missing nixpkgs-2411 input to flake.lock --- flake.lock | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/flake.lock b/flake.lock index c6dab8e..015149c 100644 --- a/flake.lock +++ b/flake.lock @@ -15,9 +15,26 @@ "type": "github" } }, + "nixpkgs-2411": { + "locked": { + "lastModified": 1738435198, + "narHash": "sha256-5+Hmo4nbqw8FrW85FlNm4IIrRnZ7bn0cmXlScNsNRLo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "f6687779bf4c396250831aa5a32cbfeb85bb07a3", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "nixpkgs": "nixpkgs", + "nixpkgs-2411": "nixpkgs-2411", "selfprivacy-api": "selfprivacy-api" } }, From 3f95b80c3c5fdc6773e69f0fe674d0fd25597ad4 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 26 Mar 2025 15:57:59 +0400 Subject: [PATCH 047/115] auth module: add originLanding option --- auth/auth-module.nix | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index 289c4eb..ffbe5d8 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -122,6 +122,11 @@ in "Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization."; default = null; }; + originLanding = mkOption { + type = types.nullOr lib.types.str; + description = + "The origin landing of the service for OAuth2 redirects."; + }; originUrl = mkOption { type = types.nullOr lib.types.str; description = @@ -238,6 +243,10 @@ in if attrs.linuxGroupOfClient == null then clientID else attrs.linuxGroupOfClient; + originLanding = + if attrs.originLanding == null + then "https://${attrs.subdomain}.${config.selfprivacy.domain}/" + else attrs.originLanding; scopeMaps = if attrs.scopeMaps == null then { "${usersGroup}" = [ "email" "openid" "profile" ]; } @@ -282,6 +291,7 @@ in , clientID , displayName , enablePkce + , originLanding , originUrl , scopeMaps , useShortPreferredUsername @@ -301,10 +311,9 @@ in claimMaps displayName originUrl + originLanding scopeMaps ; - originLanding = - "https://${subdomain}.${config.selfprivacy.domain}/"; preferShortUsername = useShortPreferredUsername; allowInsecureClientDisablePkce = ! enablePkce; removeOrphanedClaimMaps = true; From 2ee27353da1913c2ae39d59f724dcc492cb74296 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 26 Mar 2025 15:59:23 +0400 Subject: [PATCH 048/115] auth,forgejo: fix originLanding --- sp-modules/gitea/module.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 1b8a000..eb446f8 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -359,6 +359,8 @@ in inherit adminsGroup usersGroup; subdomain = cfg.subdomain; isTokenNeeded = true; + originLanding = + "https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f"; originUrl = redirect-uri; clientSystemdUnits = [ "forgejo.service" ]; enablePkce = false; # FIXME maybe Forgejo supports PKCE? From b571449efe1b134bd47f9d1b2927109248552736 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:08:09 +0300 Subject: [PATCH 049/115] refactor: Disable SSH login using password --- configuration.nix | 3 +-- selfprivacy-module.nix | 7 ------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/configuration.nix b/configuration.nix index 025d3c3..0fd3250 100644 --- a/configuration.nix +++ b/configuration.nix @@ -99,9 +99,8 @@ in services.openssh = { enable = config.selfprivacy.ssh.enable; settings = { - PasswordAuthentication = config.selfprivacy.ssh.passwordAuthentication; + PasswordAuthentication = false; PermitRootLogin = "yes"; - LoginGraceTime = 0; }; openFirewall = false; diff --git a/selfprivacy-module.nix b/selfprivacy-module.nix index 55842c9..a90435b 100644 --- a/selfprivacy-module.nix +++ b/selfprivacy-module.nix @@ -108,13 +108,6 @@ with lib; type = types.nullOr (types.listOf types.str); default = [ "" ]; }; - passwordAuthentication = mkOption { - description = '' - Password authentication for SSH - ''; - default = false; - type = types.nullOr types.bool; - }; }; ########### # Users # From e79af804f103a858dde2e5208a5c9ef5d7a0fdd0 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:08:37 +0300 Subject: [PATCH 050/115] feat: Allow services to communicate with Kanidm even when there is no DNS record yet --- auth/auth.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/auth/auth.nix b/auth/auth.nix index 246a09d..75da8f5 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -100,6 +100,12 @@ lib.mkIf config.selfprivacy.sso.enable { ) ]; + networking.hosts = { + # Allow the services to communicate with kanidm even if + # there is no DNS record yet + "127.0.0.1" = [ auth-fqdn ]; + }; + # kanidm uses TLS in internal connection with nginx too # FIXME revise this: maybe kanidm must not have access to a public TLS From d08a5e1ba3bff20c6ccd07e7edb71525d6f2c609 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:09:01 +0300 Subject: [PATCH 051/115] fix: Mark 'idm_all_persons' as a known group for provisioning --- auth/kanidm.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/kanidm.nix b/auth/kanidm.nix index 13360d8..6903b07 100644 --- a/auth/kanidm.nix +++ b/auth/kanidm.nix @@ -683,7 +683,7 @@ in assertGroupsKnown = opt: groups: let - knownGroups = attrNames (filterPresent cfg.provision.groups); + knownGroups = attrNames (filterPresent cfg.provision.groups) ++ [ "idm_all_persons" ]; unknownGroups = subtractLists knownGroups groups; in { From 3144e384a6063c85a8072763109bd45ebcf27f92 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:15:20 +0300 Subject: [PATCH 052/115] fix: Forgejo metadata --- sp-modules/gitea/module.nix | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index eb446f8..9952f9d 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -21,8 +21,9 @@ let redirect-uri = "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; - adminsGroup = "sp.forgejo.admins"; - usersGroup = "sp.forgejo.users"; + # SelfPrivacy uses SP Module ID to identify the group! + adminsGroup = "sp.gitea.admins"; + usersGroup = "sp.gitea.users"; linuxUserOfService = "gitea"; linuxGroupOfService = "gitea"; @@ -128,15 +129,22 @@ in enableSso = (lib.mkOption { default = false; type = lib.types.bool; - description = "Enable SSO for Forgejo"; + description = "Enable Single Sign-On"; }) // { meta = { - type = "enable"; + type = "bool"; + weight = 7; }; }; - debug = lib.mkOption { + debug = (lib.mkOption { default = false; type = lib.types.bool; + description = "Enable debug logging"; + }) // { + meta = { + type = "bool"; + weight = 8; + }; }; }; From c528ea129ff3082f14938dedc0b00879a859066c Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:16:01 +0300 Subject: [PATCH 053/115] feat: Add SSO field to Forgeo SP mdoule metadata --- sp-modules/gitea/flake.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sp-modules/gitea/flake.nix b/sp-modules/gitea/flake.nix index 7ee82ba..21fd7a6 100644 --- a/sp-modules/gitea/flake.nix +++ b/sp-modules/gitea/flake.nix @@ -26,6 +26,10 @@ homepage = "https://forgejo.org"; sourcePage = "https://codeberg.org/forgejo/forgejo"; supportLevel = "normal"; + sso = { + accessGroup = "sp.gitea.users"; + adminGroup = "sp.gitea.admins"; + }; }; }; } From aedc1a4297b33fc158a21c1efc987a588e1b3c63 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:18:16 +0300 Subject: [PATCH 054/115] fix: Nextcloud metadata --- sp-modules/nextcloud/flake.nix | 4 ++++ sp-modules/nextcloud/module.nix | 27 +++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/sp-modules/nextcloud/flake.nix b/sp-modules/nextcloud/flake.nix index 41ee276..84103e3 100644 --- a/sp-modules/nextcloud/flake.nix +++ b/sp-modules/nextcloud/flake.nix @@ -28,6 +28,10 @@ homepage = "https://nextcloud.com/"; sourcePage = "https://github.com/nextcloud"; supportLevel = "normal"; + sso = { + accessGroup = "sp.nextcloud.users"; + adminGroup = "sp.nextcloud.admins"; + }; }; }; } diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index e26c3a7..5d3425a 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -104,15 +104,6 @@ in type = "enable"; }; }; - enableSso = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable SSO for Nextcloud"; - }) // { - meta = { - type = "enable"; - }; - }; location = (lib.mkOption { type = lib.types.str; description = "Nextcloud location"; @@ -143,9 +134,25 @@ in weight = 1; }; }; - debug = lib.mkOption { + enableSso = (lib.mkOption { default = false; type = lib.types.bool; + description = "Enable Single Sign-On"; + }) // { + meta = { + type = "bool"; + weight = 2; + }; + }; + debug = (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable debug logging"; + }) // { + meta = { + type = "bool"; + weight = 3; + }; }; }; From 2b4a9e1f90f3eb571844d43256bdf2cc07d80a06 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:19:36 +0300 Subject: [PATCH 055/115] refactor: Remove redundant subdomain form ocrerv --- sp-modules/ocserv/module.nix | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/sp-modules/ocserv/module.nix b/sp-modules/ocserv/module.nix index a332a17..b45fdd7 100644 --- a/sp-modules/ocserv/module.nix +++ b/sp-modules/ocserv/module.nix @@ -16,18 +16,6 @@ in type = "enable"; }; }; - subdomain = (lib.mkOption { - default = "vpn"; - type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; - description = "Subdomain"; - }) // { - meta = { - widget = "subdomain"; - type = "string"; - regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; - weight = 0; - }; - }; }; config = lib.mkIf cfg.enable { @@ -61,7 +49,7 @@ in idle-timeout=1200 mobile-idle-timeout=2400 - default-domain = ${cfg.subdomain}.${domain} + default-domain = ${domain} device = vpn0 @@ -75,19 +63,6 @@ in route = default ''; }; - services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { - useACMEHost = domain; - forceSSL = true; - extraConfig = '' - add_header Strict-Transport-Security $hsts_header; - #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; - add_header 'Referrer-Policy' 'origin-when-cross-origin'; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - ''; - }; systemd = { services = { ocserv = { From fa9cd82739951c47a5374c92203889a96a8c18e8 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:21:08 +0300 Subject: [PATCH 056/115] fix: roundcube metadata --- sp-modules/roundcube/flake.nix | 4 ++++ sp-modules/roundcube/module.nix | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sp-modules/roundcube/flake.nix b/sp-modules/roundcube/flake.nix index 66cd9a2..3af1bd2 100644 --- a/sp-modules/roundcube/flake.nix +++ b/sp-modules/roundcube/flake.nix @@ -27,6 +27,10 @@ homepage = "https://roundcube.net/"; sourcePage = "https://github.com/roundcube/roundcubemail"; supportLevel = "normal"; + sso = { + accessGroup = "sp.roundcube.users"; + adminGroup = "sp.roundcube.admins"; + }; }; }; } diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index b9653dd..73f6388 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -47,10 +47,11 @@ in enableSso = (lib.mkOption { default = false; type = lib.types.bool; - description = "Enable SSO for Roundcube"; + description = "Enable Single Sign-On"; }) // { meta = { - type = "enable"; + type = "bool"; + weight = 1; }; }; }; @@ -105,8 +106,6 @@ in $config['oauth_identity_fields'] = ['email']; $config['oauth_login_redirect'] = true; $config['auto_create_user'] = true; - $config['oauth_verify_peer'] = false; # FIXME - # $config['oauth_pkce'] = 'S256'; # FIXME ''; systemd.services.roundcube = { after = [ auth-passthru.oauth2-systemd-service ]; From d902a0f3f6a98fd5c740b14305e2faf1fcfec23a Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:23:41 +0300 Subject: [PATCH 057/115] feat: allow plain login to dovecot The password backend is provided by SelfPrivacy API module at the moment --- sp-modules/simple-nixos-mailserver/auth-dovecot.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index a1559e9..0e9235d 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -87,7 +87,7 @@ in }; services.dovecot2.extraConfig = '' - auth_mechanisms = xoauth2 oauthbearer + auth_mechanisms = xoauth2 oauthbearer plain login passdb { driver = oauth2 From cdcc40d2a749a83c9279872c677ef45e496cc6b8 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:23:54 +0300 Subject: [PATCH 058/115] feat: Disallow access to /internal path of API --- webserver/nginx.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webserver/nginx.nix b/webserver/nginx.nix index 5955c1a..81fe3b3 100644 --- a/webserver/nginx.nix +++ b/webserver/nginx.nix @@ -55,6 +55,9 @@ in proxyPass = "http://127.0.0.1:5050"; proxyWebsockets = true; }; + "/internal" = { + return = 403; + }; }; }; }; From a2d184a62c85d5d7ae2a69c2ba36645bf132711e Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:24:45 +0300 Subject: [PATCH 059/115] chore: Use the recent beta build of SelfPrivacy API --- flake.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 015149c..10d5944 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1735059871, - "narHash": "sha256-1UExQNlKFXPv+D7HEAICElIVitl8yU920Ox83qF9hAY=", - "ref": "master", - "rev": "043d280d53fc56a2cd4f60a12f1d7ccf03ec2f78", - "revCount": 1474, + "lastModified": 1743151942, + "narHash": "sha256-bCB4Mbt0YKIib2TqGBHcxsE2DF5y75eOt4/akmLyjJM=", + "ref": "inex/add-oauth", + "rev": "e626367ef256fa1b008c0cd9a063ffaf8ec7caec", + "revCount": 1738, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From c2e1fa41e96950ae5cc518a39dfc3cc3f0a9b3c9 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 17:41:04 +0300 Subject: [PATCH 060/115] refactor: rename accessGroup to userGroup --- flake.lock | 8 ++++---- sp-modules/gitea/flake.nix | 2 +- sp-modules/nextcloud/flake.nix | 2 +- sp-modules/roundcube/flake.nix | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 10d5944..73ed8dc 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1743151942, - "narHash": "sha256-bCB4Mbt0YKIib2TqGBHcxsE2DF5y75eOt4/akmLyjJM=", + "lastModified": 1743172836, + "narHash": "sha256-Ysfmg49O3i0b8vBDYfhFg0d9Ja/RybxXB+qvC+xbr6c=", "ref": "inex/add-oauth", - "rev": "e626367ef256fa1b008c0cd9a063ffaf8ec7caec", - "revCount": 1738, + "rev": "d1db6ba7e4ba6fe7c0b39d1e381720d7b86b4806", + "revCount": 1739, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, diff --git a/sp-modules/gitea/flake.nix b/sp-modules/gitea/flake.nix index 21fd7a6..f18fa21 100644 --- a/sp-modules/gitea/flake.nix +++ b/sp-modules/gitea/flake.nix @@ -27,7 +27,7 @@ sourcePage = "https://codeberg.org/forgejo/forgejo"; supportLevel = "normal"; sso = { - accessGroup = "sp.gitea.users"; + userGroup = "sp.gitea.users"; adminGroup = "sp.gitea.admins"; }; }; diff --git a/sp-modules/nextcloud/flake.nix b/sp-modules/nextcloud/flake.nix index 84103e3..6e171a7 100644 --- a/sp-modules/nextcloud/flake.nix +++ b/sp-modules/nextcloud/flake.nix @@ -29,7 +29,7 @@ sourcePage = "https://github.com/nextcloud"; supportLevel = "normal"; sso = { - accessGroup = "sp.nextcloud.users"; + userGroup = "sp.nextcloud.users"; adminGroup = "sp.nextcloud.admins"; }; }; diff --git a/sp-modules/roundcube/flake.nix b/sp-modules/roundcube/flake.nix index 3af1bd2..d20f919 100644 --- a/sp-modules/roundcube/flake.nix +++ b/sp-modules/roundcube/flake.nix @@ -28,7 +28,7 @@ sourcePage = "https://github.com/roundcube/roundcubemail"; supportLevel = "normal"; sso = { - accessGroup = "sp.roundcube.users"; + userGroup = "sp.roundcube.users"; adminGroup = "sp.roundcube.admins"; }; }; From 0f605401a87ce01850fee897869206904e220402 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Thu, 13 Feb 2025 06:16:05 +0300 Subject: [PATCH 061/115] fix: Ecxlude DeSEC from dns propagation check exceptions --- letsencrypt/acme.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/acme.nix b/letsencrypt/acme.nix index ae206ac..8b85b56 100644 --- a/letsencrypt/acme.nix +++ b/letsencrypt/acme.nix @@ -19,7 +19,7 @@ let dnsCredentialsTemplate = dnsCredentialsTemplates.${cfg.dns.provider}; acme-env-filepath = "/var/lib/selfprivacy/acme-env"; secrets-filepath = "/etc/selfprivacy/secrets.json"; - dnsPropagationCheckExceptions = [ "DIGITALOCEAN" "DESEC" ]; + dnsPropagationCheckExceptions = [ "DIGITALOCEAN" ]; in { users.groups.acmereceivers.members = [ "nginx" ]; From 4dd08c942a9d138d7fe22bd059132f2a7c289d81 Mon Sep 17 00:00:00 2001 From: nhnn Date: Thu, 20 Mar 2025 12:48:41 +0300 Subject: [PATCH 062/115] fix: disable root login using password --- configuration.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration.nix b/configuration.nix index 0fd3250..4fc9baa 100644 --- a/configuration.nix +++ b/configuration.nix @@ -100,7 +100,7 @@ in enable = config.selfprivacy.ssh.enable; settings = { PasswordAuthentication = false; - PermitRootLogin = "yes"; + PermitRootLogin = "prohibit-password"; }; openFirewall = false; From 882e24fba0de865741c0c5332f20aa3a70d6f07e Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 21:53:07 +0300 Subject: [PATCH 063/115] fix: API reported old version of itself --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 73ed8dc..21049fa 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1743172836, - "narHash": "sha256-Ysfmg49O3i0b8vBDYfhFg0d9Ja/RybxXB+qvC+xbr6c=", + "lastModified": 1743187948, + "narHash": "sha256-2lfTnE0zA7qXHG0ilBIei/gulduMqJrZ3/c/QFiaLmU=", "ref": "inex/add-oauth", - "rev": "d1db6ba7e4ba6fe7c0b39d1e381720d7b86b4806", - "revCount": 1739, + "rev": "6653a48b25d449002046b36e43315b0a194cd5d4", + "revCount": 1740, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 71b73b02d4a2c690ccb0edad0a5693e318cc896f Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 22:13:31 +0300 Subject: [PATCH 064/115] chore: Use sso branch during server upgrades --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 21049fa..a7f6762 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1743187948, - "narHash": "sha256-2lfTnE0zA7qXHG0ilBIei/gulduMqJrZ3/c/QFiaLmU=", + "lastModified": 1743189188, + "narHash": "sha256-fpXqy3HA9F59So743X8xLe8MpuBg0sgp4Sj9fU4ixGI=", "ref": "inex/add-oauth", - "rev": "6653a48b25d449002046b36e43315b0a194cd5d4", - "revCount": 1740, + "rev": "c2aa3911fa38c8bd55eaaa6077f1003c50d3536a", + "revCount": 1741, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 11da3e69cebbd6d62d076573ee800fb86d17cdfa Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 22:50:29 +0300 Subject: [PATCH 065/115] fix: API was confused by empty persons list --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index a7f6762..a059846 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1743189188, - "narHash": "sha256-fpXqy3HA9F59So743X8xLe8MpuBg0sgp4Sj9fU4ixGI=", + "lastModified": 1743191390, + "narHash": "sha256-ynZfGpd/Lw3KBgtEAjZj/3dl/JTcaJPtqJOGEl4EPvc=", "ref": "inex/add-oauth", - "rev": "c2aa3911fa38c8bd55eaaa6077f1003c50d3536a", - "revCount": 1741, + "rev": "0ca79a55917b311ceb857d02f809d87ec0f53ad5", + "revCount": 1742, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 537d916ea970901aa2a4d249a69ac4bffb26c877 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 23:17:47 +0300 Subject: [PATCH 066/115] fix: Presumably unused secrets file for Nextcloud --- sp-modules/nextcloud/module.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 5d3425a..edcb06a 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -17,7 +17,6 @@ let occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; - nextcloud-secret-file = "/var/lib/nextcloud/secrets.json"; nextcloud-setup-group = config.systemd.services.nextcloud-setup.serviceConfig.Group; @@ -273,8 +272,6 @@ in adminpassFile = admin-pass-filepath; adminuser = "admin"; }; - - secretFile = lib.mkIf is-auth-enabled nextcloud-secret-file; }; services.nginx.virtualHosts.${hostName} = { useACMEHost = sp.domain; From a10d9cdfb991ed93cc08166b241beab75e69b874 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 29 Mar 2025 00:44:10 +0400 Subject: [PATCH 067/115] forgejo:auth: fix recognition of an admin user --- sp-modules/gitea/module.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 9952f9d..b1e39a1 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -374,6 +374,10 @@ in enablePkce = false; # FIXME maybe Forgejo supports PKCE? linuxUserOfClient = linuxUserOfService; linuxGroupOfClient = linuxGroupOfService; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${adminsGroup} = [ "admins" ]; + }; }; }) ]); From f3593156dc051f798b487efc9c14d3229286f1ed Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 28 Mar 2025 23:47:38 +0300 Subject: [PATCH 068/115] fix: Turn on email SSO by default --- sp-modules/simple-nixos-mailserver/options.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/simple-nixos-mailserver/options.nix b/sp-modules/simple-nixos-mailserver/options.nix index 413b279..03ca031 100644 --- a/sp-modules/simple-nixos-mailserver/options.nix +++ b/sp-modules/simple-nixos-mailserver/options.nix @@ -19,7 +19,7 @@ }; }; enableSso = (lib.mkOption { - default = false; + default = true; type = lib.types.bool; description = "Enable SSO for mail server"; }) // { From c11880215509c00cfb25f20536d682a7cb6b3b9b Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 29 Mar 2025 01:34:26 +0400 Subject: [PATCH 069/115] roundcube:auth: fix OAuth client secret generation and copy order --- sp-modules/roundcube/module.nix | 4 ++-- sp-modules/simple-nixos-mailserver/auth-dovecot.nix | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 73f6388..5ca2351 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -109,10 +109,10 @@ in ''; systemd.services.roundcube = { after = [ auth-passthru.oauth2-systemd-service ]; - requires = [ auth-passthru.oauth2-systemd-service ]; + requires = [ auth-passthru.oauth2-systemd-service "dovecot2.service" ]; }; systemd.services.kanidm = { - serviceConfig.ExecStartPre = lib.mkBefore [ + serviceConfig.ExecStartPre = lib.mkAfter [ ("-+" + kanidmExecStartPreScriptRoot) ]; }; diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 0e9235d..f5fd44d 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -137,7 +137,7 @@ in serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ]; }; - systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [ + systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ ("-" + oauth-secret-ExecStartPreScript) ]; # does it merge with existing restartTriggers? From 339dafb3fda9f9a5de3ddb3d76731d2bceed0810 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Sat, 29 Mar 2025 01:13:00 +0300 Subject: [PATCH 070/115] fix: Password email auth was broken --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index a059846..00fba36 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1743191390, - "narHash": "sha256-ynZfGpd/Lw3KBgtEAjZj/3dl/JTcaJPtqJOGEl4EPvc=", + "lastModified": 1743199948, + "narHash": "sha256-hyNOdGh74/Z36DSHJD8D0LPqOSQFQDiOL4gXDPoKIQY=", "ref": "inex/add-oauth", - "rev": "0ca79a55917b311ceb857d02f809d87ec0f53ad5", - "revCount": 1742, + "rev": "90496478ea114bbb8b8df1404f14fa7bb8bcf32b", + "revCount": 1743, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 74d7f7ef43ef296334854f73400fd112e35fa634 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 29 Mar 2025 03:53:09 +0400 Subject: [PATCH 071/115] dovecot:auth: fix OAuth client secret generation --- .../simple-nixos-mailserver/auth-dovecot.nix | 66 ++++++++++++++++ sp-modules/simple-nixos-mailserver/config.nix | 79 +++---------------- 2 files changed, 76 insertions(+), 69 deletions(-) diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index f5fd44d..46e5122 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -1,3 +1,7 @@ +{ mailserver-service-account-name +, mailserver-service-account-token-name +, mailserver-service-account-token-fp +}: { config, lib, pkgs, ... }@nixos-args: let inherit (import ./common.nix nixos-args) @@ -11,6 +15,62 @@ let runtime-directory = group; + kanidmExecStartPreScriptRoot = pkgs.writeShellScript + "mailserver-kanidm-ExecStartPre-root-script.sh" + '' + # set-group-ID bit allows for kanidm user to create files inheriting group + mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group} + chown kanidm:${group} /run/keys/${group} + ''; + # create service account token, needed for LDAP + kanidmExecStartPostScript = pkgs.writeShellScript + "mailserver-kanidm-ExecStartPost-script.sh" + '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + + # get Kanidm service account for mailserver + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${mailserver-service-account-name}\" is found" + else + echo "kanidm service account \"${mailserver-service-account-name}\" is not found" + echo "creating new kanidm service account \"${mailserver-service-account-name}\"" + if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin + then + "kanidm service account \"${mailserver-service-account-name}\" created" + else + echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\"" + exit 1 + fi + fi + + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers ${mailserver-service-account-name} + + # create a new read-only token for mailserver + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi + + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${mailserver-service-account-token-fp} + then + echo "error: cannot write token to \"${mailserver-service-account-token-fp}\"" + exit 1 + fi + ''; + ldapConfFile = "/run/${runtime-directory}/dovecot-ldap.conf.ext"; mkLdapSearchScope = scope: ( if scope == "sub" then "subtree" @@ -137,9 +197,15 @@ in serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ]; }; + # FIXME set auth module option instead systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ + ("-+" + kanidmExecStartPreScriptRoot) ("-" + oauth-secret-ExecStartPreScript) ]; + systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ + ("-" + kanidmExecStartPostScript) + ]; + # does it merge with existing restartTriggers? systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 06678eb..328c362 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -8,66 +8,12 @@ let group is-auth-enabled ; - - mailserver-service-account-name = "sp.mailserver.service-account"; - mailserver-service-account-token-name = "mailserver-service-account-token"; - mailserver-service-account-token-fp = - "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module - kanidmExecStartPreScriptRoot = pkgs.writeShellScript - "mailserver-kanidm-ExecStartPre-root-script.sh" - '' - # set-group-ID bit allows for kanidm user to create files inheriting group - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group} - chown kanidm:${group} /run/keys/${group} - ''; - # create service account token, needed for LDAP - kanidmExecStartPostScript = pkgs.writeShellScript - "mailserver-kanidm-ExecStartPost-script.sh" - '' - export HOME=$RUNTIME_DIRECTORY/client_home - readonly KANIDM="${pkgs.kanidm}/bin/kanidm" - - # get Kanidm service account for mailserver - KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")" - echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" - if [ -n "$KANIDM_SERVICE_ACCOUNT" ] - then - echo "kanidm service account \"${mailserver-service-account-name}\" is found" - else - echo "kanidm service account \"${mailserver-service-account-name}\" is not found" - echo "creating new kanidm service account \"${mailserver-service-account-name}\"" - if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin - then - "kanidm service account \"${mailserver-service-account-name}\" created" - else - echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\"" - exit 1 - fi - fi - - # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers ${mailserver-service-account-name} - - # create a new read-only token for mailserver - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)" - then - echo "error: kanidm CLI returns an error when trying to generate service-account api-token" - exit 1 - fi - if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" - then - echo "error: cannot get service-account API token from JSON" - exit 1 - fi - - if ! install --mode=640 \ - <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ - ${mailserver-service-account-token-fp} - then - echo "error: cannot write token to \"${mailserver-service-account-token-fp}\"" - exit 1 - fi - ''; + mailserver-service-account = { + mailserver-service-account-name = "sp.mailserver.service-account"; + mailserver-service-account-token-name = "mailserver-service-account-token"; + mailserver-service-account-token-fp = + "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module + }; in lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ { @@ -198,7 +144,8 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; bind.dn = "dn=token"; # TODO change in this file should trigger system restart dovecot - bind.passwordFile = mailserver-service-account-token-fp; + bind.passwordFile = + mailserver-service-account.mailserver-service-account-token-fp; # searchBase = "ou=persons," + ldap_base_dn; searchBase = auth-passthru.ldap-base-dn; # TODO refine this @@ -207,14 +154,8 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; }; }; - # FIXME set auth module option instead - systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ - ("-+" + kanidmExecStartPreScriptRoot) - ]; - systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ - ("-" + kanidmExecStartPostScript) - ]; }) - (lib.mkIf is-auth-enabled (import ./auth-dovecot.nix nixos-args)) + (lib.mkIf is-auth-enabled + (import ./auth-dovecot.nix mailserver-service-account nixos-args)) (lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args)) ]) From f516d2e72241278f6cf67e3dc9da8febaa671d0e Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 31 Mar 2025 19:37:38 +0300 Subject: [PATCH 072/115] chore: Update Nextcloud to version 30 --- sp-modules/nextcloud/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index edcb06a..ca93724 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -228,7 +228,7 @@ in }; services.nextcloud = { enable = true; - package = pkgs.nextcloud29; + package = pkgs.nextcloud30; inherit hostName; # Use HTTPS for links From 010c11ade0a70d307e16004d7b3def875010801b Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 4 Apr 2025 16:38:00 +0400 Subject: [PATCH 073/115] redirect stderr to systemd journal in sp-nixos-* modules --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 00fba36..6b9caca 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1743199948, - "narHash": "sha256-hyNOdGh74/Z36DSHJD8D0LPqOSQFQDiOL4gXDPoKIQY=", + "lastModified": 1743770064, + "narHash": "sha256-r4ri6rY4TL7nk8JTAnEN/ogb55YiOJup+TFumBUlPlw=", "ref": "inex/add-oauth", - "rev": "90496478ea114bbb8b8df1404f14fa7bb8bcf32b", - "revCount": 1743, + "rev": "f584816407634abac2c048521a28b0cd2d0b9396", + "revCount": 1744, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 5e3bb329bd9b45365f72482a215bba2bb91f2bc5 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 7 Apr 2025 01:07:39 +0400 Subject: [PATCH 074/115] autoUpgrade: change hardcoded selfprivacy-nixos-config git ref to "sso" --- configuration.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration.nix b/configuration.nix index 4fc9baa..a01daec 100644 --- a/configuration.nix +++ b/configuration.nix @@ -139,7 +139,7 @@ in pkgs.writeShellScript "flake-update-script" '' set -o xtrace if ${config.nix.package.out}/bin/nix flake update \ - --override-input selfprivacy-nixos-config git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes + --override-input selfprivacy-nixos-config git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=sso then if ${pkgs.diffutils}/bin/diff -u -r /etc/selfprivacy/nixos-config-source/ /etc/nixos/ then From 84461021d7d3adfbe4d7c876b27a329239d41460 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 8 Apr 2025 11:50:26 +0300 Subject: [PATCH 075/115] chore: Update API --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 6b9caca..62082de 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1743770064, - "narHash": "sha256-r4ri6rY4TL7nk8JTAnEN/ogb55YiOJup+TFumBUlPlw=", + "lastModified": 1744102160, + "narHash": "sha256-uuKp7f53N0dtNGiwSOQP6izc58igmP60gtYdmLncyDQ=", "ref": "inex/add-oauth", - "rev": "f584816407634abac2c048521a28b0cd2d0b9396", - "revCount": 1744, + "rev": "233eeb70a1fdb35a3f5fd27f83ecf1db83e9ff08", + "revCount": 1746, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 54bb84ca46636f6a6cf64939376aa385280276c7 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 8 Apr 2025 13:11:29 +0300 Subject: [PATCH 076/115] chore: Fix API not handling unfree licenses --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 62082de..2b32d9f 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1744102160, - "narHash": "sha256-uuKp7f53N0dtNGiwSOQP6izc58igmP60gtYdmLncyDQ=", + "lastModified": 1744107689, + "narHash": "sha256-dC10hcgGqieZHDypWSD489fPJC5lGuz975ZEvV0fEKA=", "ref": "inex/add-oauth", - "rev": "233eeb70a1fdb35a3f5fd27f83ecf1db83e9ff08", - "revCount": 1746, + "rev": "7b4462c902723b539c604c65d3ffcdc8659e8994", + "revCount": 1747, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From a5f497d9cf8f60384d6ef3b64f5afe03967fe95f Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 11 Apr 2025 14:06:06 +0400 Subject: [PATCH 077/115] fix forgejo,auth: wait until OAuth2 discovery URL is online Previously, Forgejo systemd service failed quickly, because kanidm reports a notice to systemd before discovery URL is ready. --- sp-modules/gitea/module.nix | 63 ++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index b1e39a1..15c3038 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -20,6 +20,7 @@ let oauth2-provider-name = auth-passthru.oauth2-provider-name; redirect-uri = "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; + oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; # SelfPrivacy uses SP Module ID to identify the group! adminsGroup = "sp.gitea.admins"; @@ -299,6 +300,23 @@ in systemd.services.forgejo = { preStart = let + waitForURL = url: maxRetries: delaySec: '' + for ((i=1; i<=${toString maxRetries}; i++)) + do + if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null + then + echo "${url} responds to GET HTTP request (attempt #$i)" + exit 0 + else + echo "${url} does not respond to GET HTTP request (attempt #$i)" + echo sleeping for ${toString delaySec} seconds + fi + sleep ${toString delaySec} + done + echo "error, max attempts to access "${url}" have been used unsuccessfully!" + exit 124 + ''; + exe = lib.getExe config.services.forgejo.package; # FIXME skip-tls-verify, bind-password ldapConfigArgs = '' @@ -327,32 +345,35 @@ in --secret "$(< ${oauthClientSecretFP})" \ --group-claim-name groups \ --admin-group admins \ - --auto-discover-url '${auth-passthru.oauth2-discovery-url oauthClientID}' + --auto-discover-url '${oauthDiscoveryURL}' ''; in - lib.mkAfter '' - set -o xtrace + lib.mkMerge [ + (waitForURL oauthDiscoveryURL 10 10) + (lib.mkAfter '' + set -o xtrace - # Check if LDAP is already configured - ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" + # Check if LDAP is already configured + ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" - if [[ -n "$ldap_line" ]]; then - # update ldap config - id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} - else - # initially configure ldap - ${exe} admin auth add-ldap ${ldapConfigArgs} - fi + if [[ -n "$ldap_line" ]]; then + # update ldap config + id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} + else + # initially configure ldap + ${exe} admin auth add-ldap ${ldapConfigArgs} + fi - oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" - if [[ -n "$oauth_line" ]]; then - id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} - else - ${exe} admin auth add-oauth ${oauthConfigArgs} - fi - ''; + oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" + if [[ -n "$oauth_line" ]]; then + id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} + else + ${exe} admin auth add-oauth ${oauthConfigArgs} + fi + '') + ]; }; services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { From b87c37afa2c9a1ce818c0161e5277a883e392b74 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 11 Apr 2025 16:13:59 +0400 Subject: [PATCH 078/115] auth: rewrite /run/keys/* creation to tmpfiles.d --- auth/auth-module.nix | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index ffbe5d8..5ba9211 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -7,14 +7,6 @@ let auth-passthru = config.selfprivacy.passthru.auth; keys-path = auth-passthru.keys-path; # TODO consider tmpfiles.d for creating a directory in ${keys-path} - mkKanidmExecStartPreScriptRoot = oauthClientID: group: - pkgs.writeShellScript - "${oauthClientID}-kanidm-ExecStartPre-root-script.sh" - '' - # set-group-ID bit allows kanidm user to create files with another group - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx ${keys-path}/${oauthClientID} - chown kanidm:${group} ${keys-path}/${oauthClientID} - ''; # generate OAuth2 client secret mkKanidmExecStartPreScript = oauthClientID: let @@ -261,6 +253,17 @@ in ({ linuxUserOfClient, ... }: [ linuxUserOfClient ]) ); + systemd.tmpfiles.settings."kanidm-secrets" = lib.mkMerge (lib.forEach + clientsAttrsList + ({ linuxGroupOfClient, ... }: { + "${keys-path}/${linuxGroupOfClient}".d = { + user = "kanidm"; + group = linuxGroupOfClient; + mode = "2750"; + }; + }) + ); + # for each OAuth2 client: scripts with Kanidm CLI commands systemd.services.kanidm = { before = @@ -270,11 +273,9 @@ in serviceConfig = lib.mkMerge (lib.forEach clientsAttrsList - ({ clientID, isTokenNeeded, linuxGroupOfClient, ... }: { + ({ clientID, isTokenNeeded, ... }: { ExecStartPre = [ # "-" prefix means to ignore exit code of prefixed script - # "+" prefix means to run script with superuser priveleges - ("-+" + mkKanidmExecStartPreScriptRoot clientID linuxGroupOfClient) ("-" + mkKanidmExecStartPreScript clientID) ]; ExecStartPost = lib.mkIf isTokenNeeded From 63ce4d91435eb06a73b432308e1cb0d9c5ad4655 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 11 Apr 2025 16:34:50 +0400 Subject: [PATCH 079/115] fix auth: name of /run/keys/* folder equals to linux group name --- auth/auth-module.nix | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index 5ba9211..fa29470 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -8,9 +8,9 @@ let keys-path = auth-passthru.keys-path; # TODO consider tmpfiles.d for creating a directory in ${keys-path} # generate OAuth2 client secret - mkKanidmExecStartPreScript = oauthClientID: + mkKanidmExecStartPreScript = oauthClientID: linuxGroup: let - secretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID; + secretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroup; in pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPre-script.sh" '' @@ -18,12 +18,12 @@ let "${lib.getExe pkgs.openssl}" rand -base64 -out "${secretFP}" 32 && \ chmod 640 "${secretFP}" ''; - mkKanidmExecStartPostScript = oauthClientID: + mkKanidmExecStartPostScript = oauthClientID: linuxGroup: let kanidmServiceAccountName = "sp.${oauthClientID}.service-account"; kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token"; kanidmServiceAccountTokenFP = - auth-passthru.mkServiceAccountTokenFP oauthClientID; + auth-passthru.mkServiceAccountTokenFP linuxGroup; in pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPost-script.sh" @@ -226,7 +226,7 @@ in then "sp.${clientID}.users" else attrs.usersGroup; basicSecretFile = - "${keys-path}/${clientID}/kanidm-oauth-client-secret"; + "${keys-path}/${linuxGroupOfClient}/kanidm-oauth-client-secret"; linuxUserOfClient = if attrs.linuxUserOfClient == null then clientID @@ -273,13 +273,16 @@ in serviceConfig = lib.mkMerge (lib.forEach clientsAttrsList - ({ clientID, isTokenNeeded, ... }: { + ({ clientID, isTokenNeeded, linuxGroupOfClient, ... }: { ExecStartPre = [ # "-" prefix means to ignore exit code of prefixed script - ("-" + mkKanidmExecStartPreScript clientID) + ("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient) ]; ExecStartPost = lib.mkIf isTokenNeeded - (lib.mkAfter [ ("-" + mkKanidmExecStartPostScript clientID) ]); + (lib.mkAfter [ + ("-" + + mkKanidmExecStartPostScript clientID linuxGroupOfClient) + ]); })); }; From 9d7fa8ec7d82c8d71bf15de11cb7867dbac8cd87 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 11 Apr 2025 20:59:02 +0400 Subject: [PATCH 080/115] clean auth/auth.nix and auth/auth-module.nix --- auth/auth-module.nix | 2 -- auth/auth.nix | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index fa29470..bd89edb 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -204,7 +204,6 @@ in ); }; }; - # (lib.debug.traceValSeq config = lib.mkIf config.selfprivacy.sso.enable ( let clientsAttrsList = lib.attrsets.mapAttrsToList @@ -299,7 +298,6 @@ in , originUrl , scopeMaps , useShortPreferredUsername - , subdomain , usersGroup , ... }: { diff --git a/auth/auth.nix b/auth/auth.nix index 75da8f5..5753bfe 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -246,10 +246,10 @@ lib.mkIf config.selfprivacy.sso.enable { (lib.strings.splitString "." domain); # TODO consider to pass a value or throw exception if token is not generated - mkServiceAccountTokenFP = oauthClientID: - "${keys-path}/${oauthClientID}/kanidm-service-account-token"; + mkServiceAccountTokenFP = linuxGroup: + "${keys-path}/${linuxGroup}/kanidm-service-account-token"; - mkOAuth2ClientSecretFP = oauthClientID: - "${keys-path}/${oauthClientID}/kanidm-oauth-client-secret"; + mkOAuth2ClientSecretFP = linuxGroup: + "${keys-path}/${linuxGroup}/kanidm-oauth-client-secret"; }; } From 0fdcf8a79138d897cd11a14410c512e55eeae947 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Sat, 12 Apr 2025 15:56:54 +0400 Subject: [PATCH 081/115] nextcloud,auth: disable integration with Kanidm when sso is disabled --- sp-modules/nextcloud/module.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index ca93724..8c74267 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -413,5 +413,12 @@ in }; }; }) + (lib.mkIf (! is-auth-enabled) { + systemd.services.nextcloud-setup = { + script = '' + ${occ} app:disable user_oidc + ''; + }; + }) ]); } From b605d07b52187c664623639647af38904327643f Mon Sep 17 00:00:00 2001 From: nhnn Date: Mon, 14 Apr 2025 14:32:42 +0300 Subject: [PATCH 082/115] feat: Vikunja to-do app (#128) Vikunja is fast self-hostable to-do app. Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config/pulls/128 Reviewed-by: Inex Code Co-authored-by: nhnn Co-committed-by: nhnn --- sp-modules/vikunja/config-paths-needed.json | 10 + sp-modules/vikunja/flake.lock | 27 +++ sp-modules/vikunja/flake.nix | 41 ++++ sp-modules/vikunja/icon.svg | 16 ++ .../vikunja/load-client-secret-from-env.patch | 33 ++++ sp-modules/vikunja/module.nix | 186 ++++++++++++++++++ 6 files changed, 313 insertions(+) create mode 100644 sp-modules/vikunja/config-paths-needed.json create mode 100644 sp-modules/vikunja/flake.lock create mode 100644 sp-modules/vikunja/flake.nix create mode 100644 sp-modules/vikunja/icon.svg create mode 100644 sp-modules/vikunja/load-client-secret-from-env.patch create mode 100644 sp-modules/vikunja/module.nix diff --git a/sp-modules/vikunja/config-paths-needed.json b/sp-modules/vikunja/config-paths-needed.json new file mode 100644 index 0000000..0426aa4 --- /dev/null +++ b/sp-modules/vikunja/config-paths-needed.json @@ -0,0 +1,10 @@ +[ + [ "selfprivacy", "domain" ], + [ "selfprivacy", "modules", "auth", "enable" ], + [ "selfprivacy", "modules", "vikunja" ], + [ "selfprivacy", "passthru", "auth", "mkOAuth2ClientSecretFP" ], + [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], + [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], + [ "selfprivacy", "sso", "enable" ], + [ "selfprivacy", "useBinds" ] +] diff --git a/sp-modules/vikunja/flake.lock b/sp-modules/vikunja/flake.lock new file mode 100644 index 0000000..c48dced --- /dev/null +++ b/sp-modules/vikunja/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs-24-11": { + "locked": { + "lastModified": 1744440957, + "narHash": "sha256-FHlSkNqFmPxPJvy+6fNLaNeWnF1lZSgqVCl/eWaJRc4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "26d499fc9f1d567283d5d56fcf367edd815dba1d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs-24-11": "nixpkgs-24-11" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/sp-modules/vikunja/flake.nix b/sp-modules/vikunja/flake.nix new file mode 100644 index 0000000..7558a21 --- /dev/null +++ b/sp-modules/vikunja/flake.nix @@ -0,0 +1,41 @@ +{ + description = "PoC SP module for Vikunja service"; + + inputs = { + nixpkgs-24-11.url = "github:NixOS/nixpkgs/nixos-24.11"; + }; + + outputs = {nixpkgs-24-11, ...}: { + nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux; + configPathsNeeded = + builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = {lib, ...}: { + spModuleSchemaVersion = 1; + id = "vikunja"; + name = "Vikunja"; + description = "Vikunja, the fluffy, open-source, self-hostable to-do app."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + backupDescription = "Tasks and attachments."; + systemdServices = [ + "vikunja.service" + ]; + folders = [ + "/var/lib/vikunja" + ]; + postgreDatabases = [ + "vikunja" + ]; + license = [ + lib.licenses.agpl3Plus + ]; + homepage = "https://vikunja.io"; + sourcePage = "https://github.com/go-vikunja/vikunja"; + supportLevel = "normal"; + sso = { + userGroup = "sp.vikunja.users"; + }; + }; + }; +} diff --git a/sp-modules/vikunja/icon.svg b/sp-modules/vikunja/icon.svg new file mode 100644 index 0000000..b763ee8 --- /dev/null +++ b/sp-modules/vikunja/icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/sp-modules/vikunja/load-client-secret-from-env.patch b/sp-modules/vikunja/load-client-secret-from-env.patch new file mode 100644 index 0000000..c8f4e17 --- /dev/null +++ b/sp-modules/vikunja/load-client-secret-from-env.patch @@ -0,0 +1,33 @@ +diff --git a/pkg/modules/auth/openid/providers.go b/pkg/modules/auth/openid/providers.go +index 5e14c1b31..769dc96e8 100644 +--- a/pkg/modules/auth/openid/providers.go ++++ b/pkg/modules/auth/openid/providers.go +@@ -17,6 +17,7 @@ + package openid + + import ( ++ "os" + "regexp" + "strconv" + "strings" +@@ -129,12 +130,19 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro + if scope == "" { + scope = "openid profile email" + } ++ ++ clientsecret, err := os.ReadFile(os.Getenv("SP_VIKUNJA_CLIENT_SECRET_PATH")) ++ ++ if err != nil { ++ panic(err) ++ } ++ + provider = &Provider{ + Name: pi["name"].(string), + Key: k, + AuthURL: pi["authurl"].(string), + OriginalAuthURL: pi["authurl"].(string), +- ClientSecret: pi["clientsecret"].(string), ++ ClientSecret: strings.TrimSuffix(string(clientsecret), "\n"), + LogoutURL: logoutURL, + Scope: scope, + } diff --git a/sp-modules/vikunja/module.nix b/sp-modules/vikunja/module.nix new file mode 100644 index 0000000..ee22a52 --- /dev/null +++ b/sp-modules/vikunja/module.nix @@ -0,0 +1,186 @@ +latestPkgs: { + config, + lib, + ... +}: let + sp = config.selfprivacy; + cfg = sp.modules.vikunja; + oauthClientID = "vikunja"; + auth-passthru = config.selfprivacy.passthru.auth; + oauth2-provider-name = auth-passthru.oauth2-provider-name; + oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; + + # SelfPrivacy uses SP Module ID to identify the group! + usersGroup = "sp.vikunja.users"; + + oauthClientSecretFP = + auth-passthru.mkOAuth2ClientSecretFP oauthClientID; + + vikunjaPackage = latestPkgs.vikunja.overrideAttrs (old: { + doCheck = false; # Tests are slow. + patches = + (old.patches or []) + ++ [ + ./load-client-secret-from-env.patch + ]; + }); +in { + options.selfprivacy.modules.vikunja = { + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Vikunja"; + }) + // { + meta = { + type = "enable"; + }; + }; + location = + (lib.mkOption { + type = lib.types.str; + description = "Vikunja location"; + }) + // { + meta = { + type = "location"; + }; + }; + subdomain = + (lib.mkOption { + default = "vikunja"; + type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + description = "Subdomain"; + }) + // { + meta = { + widget = "subdomain"; + type = "string"; + regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + weight = 0; + }; + }; + }; + + config = + lib.mkIf cfg.enable + { + assertions = [ + { + assertion = sp.sso.enable; + message = "Vikunja cannot be enabled when SSO is disabled."; + } + ]; + + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/vikunja" = { + device = "/volumes/${cfg.location}/vikunja"; + options = ["bind"]; + }; + }; + + users = { + users.vikunja = { + isSystemUser = true; + group = "vikunja"; + }; + groups.vikunja = {}; + }; + + services.postgresql = { + ensureDatabases = ["vikunja"]; + ensureUsers = [ + { + name = "vikunja"; + ensureDBOwnership = true; + } + ]; + }; + + services.vikunja = { + enable = true; + package = vikunjaPackage; + frontendScheme = "https"; + frontendHostname = "${cfg.subdomain}.${sp.domain}"; + port = 4835; + + database = { + type = "postgres"; + host = "/run/postgresql"; + }; + + settings = { + service = { + enableregistration = false; + enabletotp = false; + enableuserdeletion = true; + }; + + auth = { + local.enabled = false; + openid = { + enabled = true; + providers = [ + { + name = oauth2-provider-name; + authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL; + clientid = oauthClientID; + clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable. + scope = "openid profile email"; + } + ]; + }; + }; + }; + }; + + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + useACMEHost = sp.domain; + forceSSL = true; + extraConfig = '' + add_header Strict-Transport-Security $hsts_header; + #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + ''; + locations = { + "/" = { + proxyPass = "http://127.0.0.1:4835"; + }; + }; + }; + + systemd = { + services.vikunja = { + unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja"; + serviceConfig = { + Slice = "vikunja.slice"; + LoadCredential = "oauth2-secret:${oauthClientSecretFP}"; + DynamicUser = lib.mkForce false; + User = "vikunja"; + Group = "vikunja"; + }; + environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret"; + }; + slices.vikunja = { + description = "Vikunja service slice"; + }; + }; + + selfprivacy.auth.clients.${oauthClientID} = { + inherit usersGroup; + subdomain = cfg.subdomain; + isTokenNeeded = true; + originLanding = "https://${cfg.subdomain}.${sp.domain}/"; + originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}"; + clientSystemdUnits = ["vikunja.service"]; + enablePkce = false; + linuxUserOfClient = "vikunja"; + linuxGroupOfClient = "vikunja"; + }; + }; +} From 6f8477852709d32ddf79a07199635cbdd92388b8 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 14 Apr 2025 14:40:23 +0300 Subject: [PATCH 083/115] chore: Update API --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 2b32d9f..2ee9004 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1744107689, - "narHash": "sha256-dC10hcgGqieZHDypWSD489fPJC5lGuz975ZEvV0fEKA=", + "lastModified": 1744630774, + "narHash": "sha256-kJ8QIsxS9qYWeTQ2TIoovI8ftXOV5yjhcz3yycUhREs=", "ref": "inex/add-oauth", - "rev": "7b4462c902723b539c604c65d3ffcdc8659e8994", - "revCount": 1747, + "rev": "ba94f8ea03704e99ef1a1b59460eb974646e7c09", + "revCount": 1748, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From a38d426c197016390fff2353a2cc01dc80cc9b0b Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 14 Apr 2025 17:14:27 +0300 Subject: [PATCH 084/115] chore: Update API --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 2ee9004..43f5b56 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1744630774, - "narHash": "sha256-kJ8QIsxS9qYWeTQ2TIoovI8ftXOV5yjhcz3yycUhREs=", + "lastModified": 1744640029, + "narHash": "sha256-Rqy+HhW7weEfAc5rPmxusewuo/69sWVXlQOL2a3Y9ZU=", "ref": "inex/add-oauth", - "rev": "ba94f8ea03704e99ef1a1b59460eb974646e7c09", - "revCount": 1748, + "rev": "5393642f896d5fa4b9b7be215d72714554259de6", + "revCount": 1749, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 1f67bb5a854df45074d8b30dc318f09b7047a2bc Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 16 Apr 2025 12:57:26 +0400 Subject: [PATCH 085/115] fix assertion message in sp-modules/simple-nixos-mailserver/config.nix --- sp-modules/simple-nixos-mailserver/config.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 328c362..37d5e8a 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -23,7 +23,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ config.selfprivacy.modules.simple-nixos-mailserver.enableSso -> config.selfprivacy.sso.enable; message = - "SSO cannot be enabled for Roundcube when SSO is disabled globally."; + "SSO cannot be enabled for Mailserver when SSO is disabled globally."; } ]; fileSystems = lib.mkIf sp.useBinds From 56a56b67b4f45afa1ccac15cca9e8d5350911cb3 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 16 Apr 2025 14:55:55 +0400 Subject: [PATCH 086/115] auth: add imageFile option --- auth/auth-module.nix | 11 +++++++- auth/auth.nix | 12 +++------ auth/kanidm-provision.nix | 52 +++++++++++++++++++++++++++++++++++++ auth/kanidm.nix | 10 +++++++ sp-modules/gitea/module.nix | 1 + 5 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 auth/kanidm-provision.nix diff --git a/auth/auth-module.nix b/auth/auth-module.nix index bd89edb..f75334e 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -199,6 +199,13 @@ in } ); }; + imageFile = mkOption { + type = types.nullOr lib.types.path; + description = '' + Filepath of an image which is displayed in Kanidm web GUI for a service. + ''; + default = null; + }; }; } ); @@ -294,6 +301,7 @@ in , clientID , displayName , enablePkce + , imageFile , originLanding , originUrl , scopeMaps @@ -312,8 +320,9 @@ in basicSecretFile claimMaps displayName - originUrl + imageFile originLanding + originUrl scopeMaps ; preferShortUsername = useShortPreferredUsername; diff --git a/auth/auth.nix b/auth/auth.nix index 5753bfe..6e72b48 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -87,15 +87,9 @@ lib.mkIf config.selfprivacy.sso.enable { _final: prev: { inherit (nixpkgs-2411.legacyPackages.${prev.system}) kanidm; kanidm-provision = - nixpkgs-2411.legacyPackages.${prev.system}.kanidm-provision.overrideAttrs (_: { - version = "git"; - src = prev.fetchFromGitHub { - owner = "oddlama"; - repo = "kanidm-provision"; - rev = "d1f55c9247a6b25d30bbe90a74307aaac6306db4"; - hash = "sha256-cZ3QbowmWX7j1eJRiUP52ao28xZzC96OdZukdWDHfFI="; - }; - }); + (nixpkgs-2411.legacyPackages.${prev.system}).callPackage + ./kanidm-provision.nix + { }; } ) ]; diff --git a/auth/kanidm-provision.nix b/auth/kanidm-provision.nix new file mode 100644 index 0000000..fe09d0e --- /dev/null +++ b/auth/kanidm-provision.nix @@ -0,0 +1,52 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, + yq, + versionCheckHook, + nix-update-script, + nixosTests, +}: + +rustPlatform.buildRustPackage rec { + pname = "kanidm-provision"; + version = "1.2.0"; + + src = fetchFromGitHub { + owner = "oddlama"; + repo = "kanidm-provision"; + tag = "v${version}"; + hash = "sha256-+NQJEAJ0DqKEV1cYZN7CLzGoBJNUL3SQAMmxRQG5DMI="; + }; + + postPatch = '' + tomlq -ti '.package.version = "${version}"' Cargo.toml + ''; + + useFetchCargoVendor = true; + cargoHash = "sha256-uo/TGyfNChq/t6Dah0HhXhAwktyQk0V/wewezZuftNk="; + + nativeBuildInputs = [ + yq # for `tomlq` + ]; + + nativeInstallCheckInputs = [ versionCheckHook ]; + versionCheckProgramArg = "--version"; + doInstallCheck = true; + + passthru = { + tests = { inherit (nixosTests) kanidm-provisioning; }; + updateScript = nix-update-script { }; + }; + + meta = { + description = "A small utility to help with kanidm provisioning"; + homepage = "https://github.com/oddlama/kanidm-provision"; + license = with lib.licenses; [ + asl20 + mit + ]; + maintainers = with lib.maintainers; [ oddlama ]; + mainProgram = "kanidm-provision"; + }; +} diff --git a/auth/kanidm.nix b/auth/kanidm.nix index 6903b07..7290374 100644 --- a/auth/kanidm.nix +++ b/auth/kanidm.nix @@ -572,6 +572,16 @@ in default = null; }; + imageFile = mkOption { + description = '' + Application image to display in the WebUI. + Kanidm supports "image/jpeg", "image/png", "image/gif", "image/svg+xml", and "image/webp". + The image will be uploaded each time kanidm-provision is run. + ''; + type = types.nullOr types.path; + default = null; + }; + enableLocalhostRedirects = mkOption { description = "Allow localhost redirects. Only for public clients."; type = types.bool; diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 15c3038..fbf0b0e 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -386,6 +386,7 @@ in selfprivacy.auth.clients."${oauthClientID}" = { inherit adminsGroup usersGroup; + imageFile = "${pkgs.forgejo.data}/public/assets/img/logo.svg"; subdomain = cfg.subdomain; isTokenNeeded = true; originLanding = From 5cc23464d59edb013dabbb49ef83ca463ce91f17 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 16 Apr 2025 21:48:33 +0400 Subject: [PATCH 087/115] fix forgejo,auth: OAuth client secret filepath --- sp-modules/gitea/module.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index fbf0b0e..778a227 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -31,9 +31,9 @@ let forgejoPackage = pkgs.forgejo; serviceAccountTokenFP = - auth-passthru.mkServiceAccountTokenFP oauthClientID; + auth-passthru.mkServiceAccountTokenFP linuxGroupOfService; oauthClientSecretFP = - auth-passthru.mkOAuth2ClientSecretFP oauthClientID; + auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; in { options.selfprivacy.modules.gitea = { From 9dc47e6143fd4ad4a40812386c33eedcf035c913 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 16 Apr 2025 22:17:23 +0400 Subject: [PATCH 088/115] fix forgejo,auth: apply oauth and ldap configurations --- sp-modules/gitea/module.nix | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 778a227..29189e7 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -306,15 +306,18 @@ in if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null then echo "${url} responds to GET HTTP request (attempt #$i)" - exit 0 + break else echo "${url} does not respond to GET HTTP request (attempt #$i)" echo sleeping for ${toString delaySec} seconds fi sleep ${toString delaySec} done - echo "error, max attempts to access "${url}" have been used unsuccessfully!" - exit 124 + if [[ "$i" > "${toString maxRetries}" ]] + then + echo "error, max attempts to access "${url}" have been used unsuccessfully!" + exit 124 + fi ''; exe = lib.getExe config.services.forgejo.package; From e92922d1a17ef62079888f49c34a3ee027b98b73 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 16 Apr 2025 22:23:30 +0400 Subject: [PATCH 089/115] forgejo,auth: enablePkce when forgejo version is at least 8.0 --- sp-modules/gitea/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 29189e7..a415af1 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -396,7 +396,7 @@ in "https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f"; originUrl = redirect-uri; clientSystemdUnits = [ "forgejo.service" ]; - enablePkce = false; # FIXME maybe Forgejo supports PKCE? + enablePkce = lib.versionAtLeast forgejoPackage.version "8.0"; linuxUserOfClient = linuxUserOfService; linuxGroupOfClient = linuxGroupOfService; claimMaps.groups = { From 9a438aab1389eaf89763fabdf86a5e38ab363961 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 16 Apr 2025 22:24:42 +0400 Subject: [PATCH 090/115] forgejo,auth: display name (Forgejo) starts with capital letter --- sp-modules/gitea/module.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index a415af1..ed92429 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -390,6 +390,7 @@ in selfprivacy.auth.clients."${oauthClientID}" = { inherit adminsGroup usersGroup; imageFile = "${pkgs.forgejo.data}/public/assets/img/logo.svg"; + displayName = "Forgejo"; subdomain = cfg.subdomain; isTokenNeeded = true; originLanding = From 791e551b93ad57e123013014a485afb2370e1e9a Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Wed, 16 Apr 2025 22:31:33 +0400 Subject: [PATCH 091/115] forgejo,auth: change icon to sp-module's icon.svg --- sp-modules/gitea/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index ed92429..82479d4 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -389,7 +389,7 @@ in selfprivacy.auth.clients."${oauthClientID}" = { inherit adminsGroup usersGroup; - imageFile = "${pkgs.forgejo.data}/public/assets/img/logo.svg"; + imageFile = ./icon.svg; displayName = "Forgejo"; subdomain = cfg.subdomain; isTokenNeeded = true; From 46971cd2bee27415d476c76040bb5ca06579ca92 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 17 Apr 2025 12:42:46 +0400 Subject: [PATCH 092/115] auth:module: replace special symbols in generated secrets --- auth/auth-module.nix | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index f75334e..4d6c4cd 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -14,9 +14,14 @@ let in pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPre-script.sh" '' - [ -f "${secretFP}" ] || \ - "${lib.getExe pkgs.openssl}" rand -base64 -out "${secretFP}" 32 && \ + set -o pipefail + set -o errexit + if ! [ -f "${secretFP}" ] + then + "${lib.getExe pkgs.openssl}" rand -base64 32 \ + | tr "\n:@/+=" "012345" > "${secretFP}" chmod 640 "${secretFP}" + fi ''; mkKanidmExecStartPostScript = oauthClientID: linuxGroup: let From 5f9be4130e9d6d06733e01a3fee5f0a1ed2491c2 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 17 Apr 2025 12:48:02 +0400 Subject: [PATCH 093/115] roundcube,auth: migrate to auth module --- sp-modules/roundcube/config-paths-needed.json | 4 +- sp-modules/roundcube/module.nix | 82 +++++++++---------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/sp-modules/roundcube/config-paths-needed.json b/sp-modules/roundcube/config-paths-needed.json index 5a893c4..17c45b0 100644 --- a/sp-modules/roundcube/config-paths-needed.json +++ b/sp-modules/roundcube/config-paths-needed.json @@ -3,11 +3,9 @@ [ "selfprivacy", "domain" ], [ "selfprivacy", "modules", "auth" ], [ "selfprivacy", "modules", "roundcube" ], - [ "selfprivacy", "passthru", "auth", "admins-group" ], [ "selfprivacy", "passthru", "auth", "auth-fqdn" ], - [ "selfprivacy", "passthru", "auth", "full-users-group" ], + [ "selfprivacy", "passthru", "auth", "mkOAuth2ClientSecretFP" ], [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], - [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], [ "selfprivacy", "passthru", "mailserver", "oauth-client-id" ], [ "selfprivacy", "passthru", "mailserver", "oauth-client-secret-fp" ], [ "selfprivacy", "sso", "enable" ] diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 5ca2351..099cd3d 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -5,20 +5,24 @@ let is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable; auth-passthru = config.selfprivacy.passthru.auth; auth-fqdn = auth-passthru.auth-fqdn; + + linuxUserOfService = "roundcube"; + linuxGroupOfService = "roundcube"; + sp-module-name = "roundcube"; - user = "roundcube"; - group = "roundcube"; + + # SelfPrivacy uses SP Module ID to identify the group! + adminsGroup = "sp.${sp-module-name}.admins"; + usersGroup = "sp.${sp-module-name}.users"; + oauth-donor = config.selfprivacy.passthru.mailserver; - kanidm-oauth-client-secret-fp = - "/run/keys/${group}/kanidm-oauth-client-secret"; + oauthClientSecretFP = + auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; + # copy client secret from mailserver kanidmExecStartPreScriptRoot = pkgs.writeShellScript "${sp-module-name}-kanidm-ExecStartPre-root-script.sh" '' - # set-group-ID bit allows for kanidm user to create files inheriting group - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group} - chown kanidm:${group} /run/keys/${group} - - install -v -m640 -o kanidm -g ${group} ${oauth-donor.oauth-client-secret-fp} ${kanidm-oauth-client-secret-fp} + install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP} ''; in { @@ -91,53 +95,47 @@ in } # the following part is active only when "auth" module is enabled (lib.mkIf is-auth-enabled { - # for phpfpm-roundcube to have access to get through /run/keys directory - users.groups.keys.members = [ user ]; services.roundcube.extraConfig = lib.mkAfter '' $config['oauth_provider'] = 'generic'; $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; $config['oauth_client_id'] = '${oauth-donor.oauth-client-id}'; - $config['oauth_client_secret'] = file_get_contents('${kanidm-oauth-client-secret-fp}'); + $config['oauth_client_secret'] = file_get_contents('${oauthClientSecretFP}'); $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo'; - $config['oauth_scope'] = 'email profile openid'; # FIXME + $config['oauth_scope'] = 'email profile openid'; $config['oauth_auth_parameters'] = []; $config['oauth_identity_fields'] = ['email']; $config['oauth_login_redirect'] = true; $config['auto_create_user'] = true; ''; systemd.services.roundcube = { - after = [ auth-passthru.oauth2-systemd-service ]; - requires = [ auth-passthru.oauth2-systemd-service "dovecot2.service" ]; + after = [ "dovecot2.service" ]; + requires = [ "dovecot2.service" ]; }; - systemd.services.kanidm = { - serviceConfig.ExecStartPre = lib.mkAfter [ - ("-+" + kanidmExecStartPreScriptRoot) - ]; - }; - services.kanidm.provision = { - groups = { - "sp.roundcube.admins".members = [ auth-passthru.admins-group ]; - "sp.roundcube.users".members = - [ "sp.roundcube.admins" auth-passthru.full-users-group ]; - }; - systems.oauth2.${oauth-donor.oauth-client-id} = { - displayName = "Roundcube"; - originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = false; - allowInsecureClientDisablePkce = true; # FIXME is it needed? - scopeMaps = { - "sp.roundcube.users" = [ - "email" - "openid" - "profile" - ]; - }; - removeOrphanedClaimMaps = true; + systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [ + ("-+" + kanidmExecStartPreScriptRoot) + ]; + + selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = { + inherit adminsGroup usersGroup; + imageFile = ./icon.svg; + displayName = "Roundcube"; + subdomain = cfg.subdomain; + isTokenNeeded = true; + originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + useShortPreferredUsername = false; + clientSystemdUnits = [ "phpfpm-roundcube.service" ]; + enablePkce = false; + linuxUserOfClient = linuxUserOfService; + linuxGroupOfClient = linuxGroupOfService; + scopeMaps = { + "${usersGroup}" = [ + "email" + "openid" + "profile" + ]; }; }; }) From 90758a265282043f79f86526c992356d03d1d5fa Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 17 Apr 2025 12:49:50 +0400 Subject: [PATCH 094/115] fix mailserver,auth: OAuth client secret has only allowed characters --- sp-modules/simple-nixos-mailserver/auth-dovecot.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 46e5122..2819ffd 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -113,7 +113,7 @@ let "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' set -o xtrace [ -f "${oauth-client-secret-fp}" ] || \ - "${lib.getExe pkgs.openssl}" rand -base64 32 | tr -d "\n" > "${oauth-client-secret-fp}" + "${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}" ''; dovecot-oauth2-conf-fp = "/run/${runtime-directory}/dovecot-oauth2.conf.ext"; write-dovecot-oauth2-conf = appendSetting { From eb200cb792ae89c1eddc04d180a89e2e3d9a390a Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 17 Apr 2025 13:12:23 +0400 Subject: [PATCH 095/115] refact dovecot,auth: tmpfiles, minor renames, config-paths-needed.json --- .../simple-nixos-mailserver/auth-dovecot.nix | 30 ++++++++----------- .../config-paths-needed.json | 6 +++- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index 2819ffd..c0d6c20 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -13,15 +13,9 @@ let is-auth-enabled ; - runtime-directory = group; + runtime-folder = group; + keysPath = auth-passthru.keys-path; - kanidmExecStartPreScriptRoot = pkgs.writeShellScript - "mailserver-kanidm-ExecStartPre-root-script.sh" - '' - # set-group-ID bit allows for kanidm user to create files inheriting group - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group} - chown kanidm:${group} /run/keys/${group} - ''; # create service account token, needed for LDAP kanidmExecStartPostScript = pkgs.writeShellScript "mailserver-kanidm-ExecStartPost-script.sh" @@ -71,7 +65,7 @@ let fi ''; - ldapConfFile = "/run/${runtime-directory}/dovecot-ldap.conf.ext"; + ldapConfFile = "/run/${runtime-folder}/dovecot-ldap.conf.ext"; mkLdapSearchScope = scope: ( if scope == "sub" then "subtree" else if scope == "one" then "onelevel" @@ -108,14 +102,14 @@ let }; oauth-client-id = "mailserver"; oauth-client-secret-fp = - "/run/keys/${group}/kanidm-oauth-client-secret"; + "${keysPath}/${group}/kanidm-oauth-client-secret"; oauth-secret-ExecStartPreScript = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' set -o xtrace [ -f "${oauth-client-secret-fp}" ] || \ "${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}" ''; - dovecot-oauth2-conf-fp = "/run/${runtime-directory}/dovecot-oauth2.conf.ext"; + dovecot-oauth2-conf-fp = "/run/${runtime-folder}/dovecot-oauth2.conf.ext"; write-dovecot-oauth2-conf = appendSetting { name = "oauth2-conf-file"; file = builtins.toFile "dovecot-oauth2.conf.ext.template" '' @@ -136,8 +130,13 @@ let }; in { - # for dovecot2 to have access to get through /run/keys directory + # for dovecot2 to have access to get through ${keysPath} directory users.groups.keys.members = [ group ]; + systemd.tmpfiles.settings."kanidm-secrets"."${keysPath}/${group}".d = { + user = "kanidm"; + inherit group; + mode = "2750"; + }; mailserver.ldap = { # note: in `ldapsearch` first comes filter, then attributes @@ -189,24 +188,19 @@ in ''; services.dovecot2.enablePAM = false; systemd.services.dovecot2 = { - # TODO does it merge with existing preStart? preStart = setPwdInLdapConfFile + "\n" + write-dovecot-oauth2-conf + "\n"; - # FIXME pass dependant services to auth module option instead? after = [ auth-passthru.oauth2-systemd-service ]; requires = [ auth-passthru.oauth2-systemd-service ]; - serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ]; + serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-folder ]; }; - # FIXME set auth module option instead systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ - ("-+" + kanidmExecStartPreScriptRoot) ("-" + oauth-secret-ExecStartPreScript) ]; systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]; - # does it merge with existing restartTriggers? systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile write-dovecot-oauth2-conf diff --git a/sp-modules/simple-nixos-mailserver/config-paths-needed.json b/sp-modules/simple-nixos-mailserver/config-paths-needed.json index 2833470..e8ed883 100644 --- a/sp-modules/simple-nixos-mailserver/config-paths-needed.json +++ b/sp-modules/simple-nixos-mailserver/config-paths-needed.json @@ -7,6 +7,7 @@ [ "selfprivacy", "modules", "simple-nixos-mailserver" ], [ "selfprivacy", "passthru", "auth", "admins-group" ], [ "selfprivacy", "passthru", "auth", "full-users-group" ], + [ "selfprivacy", "passthru", "auth", "keys-path" ], [ "selfprivacy", "passthru", "auth", "ldap-base-dn" ], [ "selfprivacy", "passthru", "auth", "ldap-port" ], [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], @@ -20,7 +21,10 @@ [ "selfprivacy", "username" ], [ "selfprivacy", "users" ], [ "services", "dovecot2", "user" ], - [ "services", "opendkim" ], + [ "services", "opendkim", "configFile" ], + [ "services", "opendkim", "group" ], + [ "services", "opendkim", "socket" ], + [ "services", "opendkim", "user" ], [ "services", "postfix", "group" ], [ "services", "postfix", "user" ], [ "services", "redis", "servers", "rspamd", "bind" ], From 952b660aaebdbc50fd2ed2f2e97cd49864f7085e Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 17 Apr 2025 15:06:42 +0400 Subject: [PATCH 096/115] roundcube,auth: disable generation of a kanidm service token --- sp-modules/roundcube/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 099cd3d..ffa6b6e 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -122,7 +122,7 @@ in imageFile = ./icon.svg; displayName = "Roundcube"; subdomain = cfg.subdomain; - isTokenNeeded = true; + isTokenNeeded = false; originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; originLanding = "https://${cfg.subdomain}.${domain}/"; useShortPreferredUsername = false; From 356f9ddb91d8bb198ca79725833576454cf94c83 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 18 Apr 2025 01:26:41 +0400 Subject: [PATCH 097/115] fix forgejo,auth: curl waiting failure condition --- sp-modules/gitea/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 82479d4..969ba47 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -313,7 +313,7 @@ in fi sleep ${toString delaySec} done - if [[ "$i" > "${toString maxRetries}" ]] + if [[ "$i" -gt "${toString maxRetries}" ]] then echo "error, max attempts to access "${url}" have been used unsuccessfully!" exit 124 From 043c192fb799e9e79f63c04f288e28b8abbbd642 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 18 Apr 2025 16:17:38 +0300 Subject: [PATCH 098/115] auth: upgrade kanidm to 1.5 --- auth/auth.nix | 11 ++++----- auth/kanidm-provision.nix | 52 --------------------------------------- flake.lock | 34 ++++++++++++------------- flake.nix | 6 ++--- 4 files changed, 25 insertions(+), 78 deletions(-) delete mode 100644 auth/kanidm-provision.nix diff --git a/auth/auth.nix b/auth/auth.nix index 6e72b48..296d81a 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -1,4 +1,4 @@ -nixpkgs-2411: { config, lib, pkgs, ... }: +nixos-unstable: { config, lib, pkgs, ... }: let domain = config.selfprivacy.domain; subdomain = "auth"; @@ -85,11 +85,10 @@ lib.mkIf config.selfprivacy.sso.enable { nixpkgs.overlays = [ ( _final: prev: { - inherit (nixpkgs-2411.legacyPackages.${prev.system}) kanidm; - kanidm-provision = - (nixpkgs-2411.legacyPackages.${prev.system}).callPackage - ./kanidm-provision.nix - { }; + inherit (nixos-unstable.legacyPackages.${prev.system}) + kanidm + kanidm-provision + ; } ) ]; diff --git a/auth/kanidm-provision.nix b/auth/kanidm-provision.nix deleted file mode 100644 index fe09d0e..0000000 --- a/auth/kanidm-provision.nix +++ /dev/null @@ -1,52 +0,0 @@ -{ - lib, - rustPlatform, - fetchFromGitHub, - yq, - versionCheckHook, - nix-update-script, - nixosTests, -}: - -rustPlatform.buildRustPackage rec { - pname = "kanidm-provision"; - version = "1.2.0"; - - src = fetchFromGitHub { - owner = "oddlama"; - repo = "kanidm-provision"; - tag = "v${version}"; - hash = "sha256-+NQJEAJ0DqKEV1cYZN7CLzGoBJNUL3SQAMmxRQG5DMI="; - }; - - postPatch = '' - tomlq -ti '.package.version = "${version}"' Cargo.toml - ''; - - useFetchCargoVendor = true; - cargoHash = "sha256-uo/TGyfNChq/t6Dah0HhXhAwktyQk0V/wewezZuftNk="; - - nativeBuildInputs = [ - yq # for `tomlq` - ]; - - nativeInstallCheckInputs = [ versionCheckHook ]; - versionCheckProgramArg = "--version"; - doInstallCheck = true; - - passthru = { - tests = { inherit (nixosTests) kanidm-provisioning; }; - updateScript = nix-update-script { }; - }; - - meta = { - description = "A small utility to help with kanidm provisioning"; - homepage = "https://github.com/oddlama/kanidm-provision"; - license = with lib.licenses; [ - asl20 - mit - ]; - maintainers = with lib.maintainers; [ oddlama ]; - mainProgram = "kanidm-provision"; - }; -} diff --git a/flake.lock b/flake.lock index 43f5b56..8650115 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "nixos-unstable": { + "locked": { + "lastModified": 1744463964, + "narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1734835170, @@ -15,26 +31,10 @@ "type": "github" } }, - "nixpkgs-2411": { - "locked": { - "lastModified": 1738435198, - "narHash": "sha256-5+Hmo4nbqw8FrW85FlNm4IIrRnZ7bn0cmXlScNsNRLo=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "f6687779bf4c396250831aa5a32cbfeb85bb07a3", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-24.11", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { + "nixos-unstable": "nixos-unstable", "nixpkgs": "nixpkgs", - "nixpkgs-2411": "nixpkgs-2411", "selfprivacy-api": "selfprivacy-api" } }, diff --git a/flake.nix b/flake.nix index 5a526a6..238d422 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { nixpkgs.url = github:nixos/nixpkgs; - nixpkgs-2411.url = github:nixos/nixpkgs/nixos-24.11; + nixos-unstable.url = github:nixos/nixpkgs/nixos-unstable; selfprivacy-api.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git; @@ -11,7 +11,7 @@ selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = { self, nixpkgs, nixpkgs-2411, selfprivacy-api }: { + outputs = { self, nixpkgs, nixos-unstable, selfprivacy-api }: { nixosConfigurations-fun = { hardware-configuration , deployment @@ -25,7 +25,7 @@ hardware-configuration deployment ./configuration.nix - (import ./auth/auth.nix nixpkgs-2411) + (import ./auth/auth.nix nixos-unstable) { disabledModules = [ "services/security/kanidm.nix" ]; imports = [ ./auth/kanidm.nix ]; From eb5074ba82167f48ed417c5ed096e4e1b0d408bc Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 17 Apr 2025 14:45:46 +0400 Subject: [PATCH 099/115] nextcloud,auth: migrate to auth module --- sp-modules/nextcloud/config-paths-needed.json | 5 +- sp-modules/nextcloud/module.nix | 145 +++++------------- 2 files changed, 39 insertions(+), 111 deletions(-) diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index cc78e0d..ca7356a 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -8,6 +8,8 @@ [ "selfprivacy", "passthru", "auth", "ldap-base-dn" ], [ "selfprivacy", "passthru", "auth", "ldap-host" ], [ "selfprivacy", "passthru", "auth", "ldap-port" ], + [ "selfprivacy", "passthru", "auth", "mkOAuth2ClientSecretFP" ], + [ "selfprivacy", "passthru", "auth", "mkServiceAccountTokenFP" ], [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], @@ -16,6 +18,5 @@ [ "services", "nextcloud" ], [ "services", "phpfpm", "pools", "nextcloud", "group" ], [ "services", "phpfpm", "pools", "nextcloud", "user" ], - [ "systemd", "services", "nextcloud" ], - [ "systemd", "services", "nextcloud-setup" ] + [ "systemd", "services", "nextcloud" ] ] diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 8c74267..20e76de 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -17,80 +17,19 @@ let occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; - nextcloud-setup-group = - config.systemd.services.nextcloud-setup.serviceConfig.Group; + linuxUserOfService = config.services.phpfpm.pools.nextcloud.user; + linuxGroupOfService = config.services.phpfpm.pools.nextcloud.group; - admins-group = "sp.nextcloud.admins"; - users-group = "sp.nextcloud.users"; - wildcard-group = "sp.nextcloud.*"; + oauthClientID = "nextcloud"; - oauth-client-id = "nextcloud"; - kanidm-service-account-name = "sp.${oauth-client-id}.service-account"; - kanidm-service-account-token-name = "${oauth-client-id}-service-account-token"; - kanidm-service-account-token-fp = - "/run/keys/${oauth-client-id}/kanidm-service-account-token"; # FIXME sync with auth module - # TODO rewrite to tmpfiles.d, but make sure the group exists first! - kanidmExecStartPreScriptRoot = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPre-root-script.sh" - '' - # set-group-ID bit allows for kanidm user to create files, - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id} - chown kanidm:${nextcloud-setup-group} /run/keys/${oauth-client-id} - ''; - kanidm-oauth-client-secret-fp = - "/run/keys/${oauth-client-id}/kanidm-oauth-client-secret"; - kanidmExecStartPreScript = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' - [ -f "${kanidm-oauth-client-secret-fp}" ] || \ - "${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32 - ''; - kanidmExecStartPostScript = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPost-script.sh" - '' - export HOME=$RUNTIME_DIRECTORY/client_home - readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + adminsGroup = "sp.${oauthClientID}.admins"; + usersGroup = "sp.${oauthClientID}.users"; + wildcardGroup = "sp.${oauthClientID}.*"; - # get Kanidm service account for mailserver - KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${kanidm-service-account-name}$")" - echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" - if [ -n "$KANIDM_SERVICE_ACCOUNT" ] - then - echo "kanidm service account \"${kanidm-service-account-name}\" is found" - else - echo "kanidm service account \"${kanidm-service-account-name}\" is not found" - echo "creating new kanidm service account \"${kanidm-service-account-name}\"" - if $KANIDM service-account create --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-name}" idm_admin - then - echo "kanidm service account \"${kanidm-service-account-name}\" created" - else - echo "error: cannot create kanidm service account \"${kanidm-service-account-name}\"" - exit 1 - fi - fi - - # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers "${kanidm-service-account-name}" - - # create a new read-only token for kanidm - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidm-service-account-name}" "${kanidm-service-account-token-name}" --output json)" - then - echo "error: kanidm CLI returns an error when trying to generate service-account api-token" - exit 1 - fi - if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" - then - echo "error: cannot get service-account API token from JSON" - exit 1 - fi - - if ! install --mode=640 \ - <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ - ${kanidm-service-account-token-fp} - then - echo "error: cannot write token to \"${kanidm-service-account-token-fp}\"" - exit 1 - fi - ''; + serviceAccountTokenFP = + auth-passthru.mkServiceAccountTokenFP linuxUserOfService; + oauthClientSecretFP = + auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService; in { options.selfprivacy.modules.nextcloud = with lib; { @@ -180,7 +119,7 @@ in # for ExecStartPost script to have access to /run/keys/* users.groups.keys.members = - lib.mkIf is-auth-enabled [ nextcloud-setup-group ]; + lib.mkIf is-auth-enabled [ linuxUserOfService ]; # not needed, due to turnOffCertCheck=1 in used_ldap # users.groups.${config.security.acme.certs.${domain}.group}.members = @@ -193,13 +132,6 @@ in serviceConfig.Slice = "nextcloud.slice"; serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; }; - kanidm.serviceConfig.ExecStartPre = lib.mkIf is-auth-enabled - (lib.mkAfter [ - ("-+" + kanidmExecStartPreScriptRoot) - ("-" + kanidmExecStartPreScript) - ]); - kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled - (lib.mkAfter [ ("-" + kanidmExecStartPostScript) ]); nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; @@ -328,27 +260,27 @@ in ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}' ${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}' ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${kanidm-service-account-token-fp})" + ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${serviceAccountTokenFP})" ${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}' ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}' ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}' ${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail' ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \ - '(&(class=group)(${wildcard-group})' + '(&(class=group)(${wildcardGroup})' ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \ - '(&(class=group)(${wildcard-group}))' + '(&(class=group)(${wildcardGroup}))' # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \ # 'groupOfUniqueNames' # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \ # 'uniqueMember' ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \ - '(&(class=person)(memberof=${users-group})(uid=%uid))' + '(&(class=person)(memberof=${usersGroup})(uid=%uid))' ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \ 'uid' ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \ 'displayname' ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \ - '(&(class=person)(memberof=${users-group})(name=%s))' + '(&(class=person)(memberof=${usersGroup})(name=%s))' ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \ '1' ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \ @@ -375,8 +307,8 @@ in ${occ} app:enable user_oidc ${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \ - --clientid="${oauth-client-id}" \ - --clientsecret="$(<${kanidm-oauth-client-secret-fp})" \ + --clientid="${oauthClientID}" \ + --clientsecret="$(<${oauthClientSecretFP})" \ --discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \ --unique-uid=0 \ --scope="email openid profile" \ @@ -386,30 +318,25 @@ in --group-provisioning=1 \ -vvv ''; - # TODO consider passing oauth consumer service to auth module instead - after = [ auth-passthru.oauth2-systemd-service ]; - requires = [ auth-passthru.oauth2-systemd-service ]; }; - services.kanidm.provision = { - groups = { - "${admins-group}".members = [ auth-passthru.admins-group ]; - "${users-group}".members = - [ admins-group auth-passthru.full-users-group ]; - }; - systems.oauth2.${oauth-client-id} = { - displayName = "Nextcloud"; - originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - basicSecretFile = kanidm-oauth-client-secret-fp; - # when true, name is passed to a service instead of name@domain - preferShortUsername = true; - allowInsecureClientDisablePkce = false; - scopeMaps.${users-group} = [ "email" "openid" "profile" ]; - removeOrphanedClaimMaps = true; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${admins-group} = [ "admin" ]; - }; + selfprivacy.auth.clients."${oauthClientID}" = { + inherit adminsGroup usersGroup; + imageFile = ./icon.svg; + displayName = "Nextcloud"; + subdomain = cfg.subdomain; + isTokenNeeded = true; + originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + useShortPreferredUsername = true; + clientSystemdUnits = + [ "nextcloud-setup.service" "phpfpm-nextcloud.service" ]; + enablePkce = true; + linuxUserOfClient = linuxUserOfService; + linuxGroupOfClient = linuxGroupOfService; + scopeMaps.${usersGroup} = [ "email" "openid" "profile" ]; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${adminsGroup} = [ "admin" ]; }; }; }) From 43c3ea06ab041a3968f51f99ae4e7cdc75af732c Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Thu, 17 Apr 2025 17:18:49 +0400 Subject: [PATCH 100/115] nextcloud,auth: set originLanding to user_oidc/login --- sp-modules/nextcloud/module.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 20e76de..c762efd 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -326,7 +326,8 @@ in subdomain = cfg.subdomain; isTokenNeeded = true; originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; - originLanding = "https://${cfg.subdomain}.${domain}/"; + originLanding = + "https://${cfg.subdomain}.${domain}/apps/user_oidc/login/1"; useShortPreferredUsername = true; clientSystemdUnits = [ "nextcloud-setup.service" "phpfpm-nextcloud.service" ]; From f2e9623d7fbb965c7567b10bac09496c343f1f2e Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Fri, 18 Apr 2025 21:06:18 +0400 Subject: [PATCH 101/115] auth: selfprivacy.sso.useKanidm_1_4 --- auth/auth.nix | 13 +--------- auth/kanidm-provision.nix | 52 +++++++++++++++++++++++++++++++++++++++ flake.lock | 17 +++++++++++++ flake.nix | 32 +++++++++++++++++++++--- selfprivacy-module.nix | 5 ++++ 5 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 auth/kanidm-provision.nix diff --git a/auth/auth.nix b/auth/auth.nix index 296d81a..11f9a96 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -1,4 +1,4 @@ -nixos-unstable: { config, lib, pkgs, ... }: +{ config, lib, pkgs, ... }: let domain = config.selfprivacy.domain; subdomain = "auth"; @@ -82,17 +82,6 @@ let lua_path = "${lua_core_path};${lua_lrucache_path};"; in lib.mkIf config.selfprivacy.sso.enable { - nixpkgs.overlays = [ - ( - _final: prev: { - inherit (nixos-unstable.legacyPackages.${prev.system}) - kanidm - kanidm-provision - ; - } - ) - ]; - networking.hosts = { # Allow the services to communicate with kanidm even if # there is no DNS record yet diff --git a/auth/kanidm-provision.nix b/auth/kanidm-provision.nix new file mode 100644 index 0000000..fe09d0e --- /dev/null +++ b/auth/kanidm-provision.nix @@ -0,0 +1,52 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, + yq, + versionCheckHook, + nix-update-script, + nixosTests, +}: + +rustPlatform.buildRustPackage rec { + pname = "kanidm-provision"; + version = "1.2.0"; + + src = fetchFromGitHub { + owner = "oddlama"; + repo = "kanidm-provision"; + tag = "v${version}"; + hash = "sha256-+NQJEAJ0DqKEV1cYZN7CLzGoBJNUL3SQAMmxRQG5DMI="; + }; + + postPatch = '' + tomlq -ti '.package.version = "${version}"' Cargo.toml + ''; + + useFetchCargoVendor = true; + cargoHash = "sha256-uo/TGyfNChq/t6Dah0HhXhAwktyQk0V/wewezZuftNk="; + + nativeBuildInputs = [ + yq # for `tomlq` + ]; + + nativeInstallCheckInputs = [ versionCheckHook ]; + versionCheckProgramArg = "--version"; + doInstallCheck = true; + + passthru = { + tests = { inherit (nixosTests) kanidm-provisioning; }; + updateScript = nix-update-script { }; + }; + + meta = { + description = "A small utility to help with kanidm provisioning"; + homepage = "https://github.com/oddlama/kanidm-provision"; + license = with lib.licenses; [ + asl20 + mit + ]; + maintainers = with lib.maintainers; [ oddlama ]; + mainProgram = "kanidm-provision"; + }; +} diff --git a/flake.lock b/flake.lock index 8650115..17fba7e 100644 --- a/flake.lock +++ b/flake.lock @@ -31,10 +31,27 @@ "type": "github" } }, + "nixpkgs-2411": { + "locked": { + "lastModified": 1744440957, + "narHash": "sha256-FHlSkNqFmPxPJvy+6fNLaNeWnF1lZSgqVCl/eWaJRc4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "26d499fc9f1d567283d5d56fcf367edd815dba1d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "nixos-unstable": "nixos-unstable", "nixpkgs": "nixpkgs", + "nixpkgs-2411": "nixpkgs-2411", "selfprivacy-api": "selfprivacy-api" } }, diff --git a/flake.nix b/flake.nix index 238d422..ff68281 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ inputs = { nixpkgs.url = github:nixos/nixpkgs; + nixpkgs-2411.url = github:nixos/nixpkgs/f6687779bf4c396250831aa5a32cbfeb85bb07a3; nixos-unstable.url = github:nixos/nixpkgs/nixos-unstable; selfprivacy-api.url = @@ -11,7 +12,7 @@ selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = { self, nixpkgs, nixos-unstable, selfprivacy-api }: { + outputs = { self, nixpkgs, nixpkgs-2411, nixos-unstable, selfprivacy-api }: { nixosConfigurations-fun = { hardware-configuration , deployment @@ -25,11 +26,34 @@ hardware-configuration deployment ./configuration.nix - (import ./auth/auth.nix nixos-unstable) - { + ./auth/auth.nix + ({ config, ... }: { + nixpkgs.overlays = [ + ( + _final: prev: + let + pkgs2411 = + nixpkgs-2411.legacyPackages.${prev.system}; + pkgs-unstable = + nixos-unstable.legacyPackages.${prev.system}; + in + if config.selfprivacy.sso.useKanidm_1_4 or false + then + { + inherit (pkgs2411) kanidm; + kanidm-provision = + pkgs2411.callPackage ./auth/kanidm-provision.nix { }; + } + else + { + inherit (pkgs-unstable) kanidm kanidm-provision; + } + ) + ]; + disabledModules = [ "services/security/kanidm.nix" ]; imports = [ ./auth/kanidm.nix ]; - } + }) selfprivacy-api.nixosModules.default ({ pkgs, lib, ... }: { environment.etc = (lib.attrsets.mapAttrs' diff --git a/selfprivacy-module.nix b/selfprivacy-module.nix index a90435b..f670914 100644 --- a/selfprivacy-module.nix +++ b/selfprivacy-module.nix @@ -45,6 +45,11 @@ with lib; default = false; type = types.nullOr types.bool; }; + useKanidm_1_4 = mkOption { + description = "Whether to use Kanidm v1.4 (instead of upstream)."; + default = false; + type = types.bool; + }; }; stateVersion = mkOption { description = "State version of the server"; From 3f1a2b5baf879e6a28c0858abc7ab2a86a161841 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 21 Apr 2025 20:20:41 +0400 Subject: [PATCH 102/115] fix nixpkgs-2411 in flake.lock --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 17fba7e..daa2e18 100644 --- a/flake.lock +++ b/flake.lock @@ -33,17 +33,17 @@ }, "nixpkgs-2411": { "locked": { - "lastModified": 1744440957, - "narHash": "sha256-FHlSkNqFmPxPJvy+6fNLaNeWnF1lZSgqVCl/eWaJRc4=", + "lastModified": 1738435198, + "narHash": "sha256-5+Hmo4nbqw8FrW85FlNm4IIrRnZ7bn0cmXlScNsNRLo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "26d499fc9f1d567283d5d56fcf367edd815dba1d", + "rev": "f6687779bf4c396250831aa5a32cbfeb85bb07a3", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-24.11", "repo": "nixpkgs", + "rev": "f6687779bf4c396250831aa5a32cbfeb85bb07a3", "type": "github" } }, From a96b6b84447b8dfc29f1ac162d789fdff708f382 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 21 Apr 2025 18:54:49 +0400 Subject: [PATCH 103/115] auth: add only roundcube kanidm service account to idm_mail_servers --- auth/auth-module.nix | 47 +++++++++++++++++++++------------ sp-modules/roundcube/module.nix | 1 + 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index 4d6c4cd..698e43f 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -23,7 +23,7 @@ let chmod 640 "${secretFP}" fi ''; - mkKanidmExecStartPostScript = oauthClientID: linuxGroup: + mkKanidmExecStartPostScript = oauthClientID: linuxGroup: isMailserver: let kanidmServiceAccountName = "sp.${oauthClientID}.service-account"; kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token"; @@ -32,7 +32,7 @@ let in pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPost-script.sh" - '' + ('' export HOME=$RUNTIME_DIRECTORY/client_home readonly KANIDM="${pkgs.kanidm}/bin/kanidm" @@ -54,9 +54,6 @@ let fi fi - # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers "${kanidmServiceAccountName}" - # create a new read-only token for kanidm if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountTokenName}" --output json)" then @@ -76,7 +73,12 @@ let echo "error: cannot write token to \"${kanidmServiceAccountTokenFP}\"" exit 1 fi - ''; + + '' + + lib.strings.optionalString isMailserver '' + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers "${kanidmServiceAccountName}" + ''); in { options.selfprivacy.auth = { @@ -211,6 +213,13 @@ in ''; default = null; }; + isMailserver = mkOption { + type = types.bool; + description = '' + Whether client is a mailserver. + ''; + default = false; + }; }; } ); @@ -284,17 +293,21 @@ in serviceConfig = lib.mkMerge (lib.forEach clientsAttrsList - ({ clientID, isTokenNeeded, linuxGroupOfClient, ... }: { - ExecStartPre = [ - # "-" prefix means to ignore exit code of prefixed script - ("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient) - ]; - ExecStartPost = lib.mkIf isTokenNeeded - (lib.mkAfter [ - ("-" + - mkKanidmExecStartPostScript clientID linuxGroupOfClient) - ]); - })); + ({ clientID, isTokenNeeded, linuxGroupOfClient, isMailserver, ... }: + { + ExecStartPre = [ + # "-" prefix means to ignore exit code of prefixed script + ("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient) + ]; + ExecStartPost = lib.mkIf isTokenNeeded + (lib.mkAfter [ + ("-" + + mkKanidmExecStartPostScript + clientID + linuxGroupOfClient + isMailserver) + ]); + })); }; # for each OAuth2 client: Kanidm provisioning options diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index ffa6b6e..19ba2be 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -123,6 +123,7 @@ in displayName = "Roundcube"; subdomain = cfg.subdomain; isTokenNeeded = false; + isMailserver = true; originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; originLanding = "https://${cfg.subdomain}.${domain}/"; useShortPreferredUsername = false; From 69a5103f8b9958cb51cec66a3b497608f87f71a2 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 21 Apr 2025 19:07:16 +0400 Subject: [PATCH 104/115] refact auth: systemd.tmpfiles for /run/keys/selfprivacy-api; comments --- auth/auth-module.nix | 1 - auth/auth.nix | 17 ++++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index 698e43f..f34194b 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -6,7 +6,6 @@ let ; auth-passthru = config.selfprivacy.passthru.auth; keys-path = auth-passthru.keys-path; - # TODO consider tmpfiles.d for creating a directory in ${keys-path} # generate OAuth2 client secret mkKanidmExecStartPreScript = oauthClientID: linuxGroup: let diff --git a/auth/auth.nix b/auth/auth.nix index 11f9a96..35d333d 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -22,13 +22,6 @@ let "${selfprivacy-group}-service-account-token"; kanidm-service-account-token-fp = "${keys-path}/${selfprivacy-group}/kanidm-service-account-token"; - kanidmExecStartPreScriptRoot = pkgs.writeShellScript - "${selfprivacy-group}-kanidm-ExecStartPre-root-script.sh" - '' - # set-group-ID bit allows for kanidm user to create files, - mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${selfprivacy-group} - chown kanidm:${selfprivacy-group} /run/keys/${selfprivacy-group} - ''; spApiUserExecStartPostScript = pkgs.writeShellScript "spApiUserExecStartPostScript" '' @@ -96,6 +89,14 @@ lib.mkIf config.selfprivacy.sso.enable { # for ExecStartPost scripts to have access to /run/keys/* users.groups.keys.members = [ "kanidm" ]; + systemd.tmpfiles.settings."kanidm-secrets" = { + "${keys-path}/${selfprivacy-group}".d = { + user = "kanidm"; + group = selfprivacy-group; + mode = "2750"; + }; + }; + services.kanidm = { enableServer = true; @@ -198,8 +199,6 @@ lib.mkIf config.selfprivacy.sso.enable { }; }; - systemd.services.kanidm.serviceConfig.ExecStartPre = - [ ("+" + kanidmExecStartPreScriptRoot) ]; systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ spApiUserExecStartPostScript ]; From 217fdce4694d87e77741d0f089aa5f6a758a0462 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Mon, 21 Apr 2025 20:21:55 +0400 Subject: [PATCH 105/115] auth: kanidm.db migration to v1.5.0 for provisioning - ExecStartPre sqlite script for any kanidm version <= 1.5.0. --- auth/auth.nix | 15 ++++++++++ auth/kanidm-db-migration.sql | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 auth/kanidm-db-migration.sql diff --git a/auth/auth.nix b/auth/auth.nix index 35d333d..d1c3c20 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -23,6 +23,16 @@ let kanidm-service-account-token-fp = "${keys-path}/${selfprivacy-group}/kanidm-service-account-token"; + kanidmMigrateDbScript = pkgs.writeShellScript "kanidm-db-migration-script" '' + # handle a case when kanidm database is not yet created (the first startup) + if [ -f ${config.services.kanidm.serverSettings.db_path} ] + then + set -o xtrace + # since it's the last command, it produces an exit code for systemd as well + ${lib.getExe pkgs.sqlite} ${config.services.kanidm.serverSettings.db_path} < ${./kanidm-db-migration.sql} + fi + ''; + spApiUserExecStartPostScript = pkgs.writeShellScript "spApiUserExecStartPostScript" '' export HOME=$RUNTIME_DIRECTORY/client_home @@ -199,6 +209,11 @@ lib.mkIf config.selfprivacy.sso.enable { }; }; + systemd.services.kanidm.serviceConfig.ExecStartPre = + # idempotent script to run on each startup only for kanidm v1.5.0 + lib.mkIf (pkgs.kanidm.version == "1.5.0") + (lib.mkBefore [ kanidmMigrateDbScript ]); + systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ spApiUserExecStartPostScript ]; diff --git a/auth/kanidm-db-migration.sql b/auth/kanidm-db-migration.sql new file mode 100644 index 0000000..0af27ea --- /dev/null +++ b/auth/kanidm-db-migration.sql @@ -0,0 +1,56 @@ +update id2entry +set data = cast( + json_replace( + data, + '$.ent.V3.attrs.acp_create_attr.I8', + json_array( + 'class','description','displayname','image','name', + 'oauth2_allow_insecure_client_disable_pkce', + 'oauth2_allow_localhost_redirect', + 'oauth2_device_flow_enable', + 'oauth2_jwt_legacy_crypto_enable', + 'oauth2_prefer_short_username', + 'oauth2_rs_claim_map', + 'oauth2_rs_basic_secret', + 'oauth2_rs_name', + 'oauth2_rs_origin', + 'oauth2_rs_origin_landing', + 'oauth2_rs_scope_map', + 'oauth2_rs_sup_scope_map', + 'oauth2_strict_redirect_uri' + ) + ) as blob +) +where cast (id as text) = ( + select json_extract(idl, '$.t.s[0]') + from idx_eq_name + where key = 'idm_acp_oauth2_manage' +); + +update id2entry +set data = cast( + json_replace( + data, + '$.ent.V3.attrs.acp_modify_presentattr.I8', + json_array( + 'description','displayname','image','name', + 'oauth2_allow_insecure_client_disable_pkce', + 'oauth2_allow_localhost_redirect', + 'oauth2_device_flow_enable', + 'oauth2_jwt_legacy_crypto_enable', + 'oauth2_prefer_short_username', + 'oauth2_rs_claim_map', + 'oauth2_rs_basic_secret', + 'oauth2_rs_origin', + 'oauth2_rs_origin_landing', + 'oauth2_rs_scope_map', + 'oauth2_rs_sup_scope_map', + 'oauth2_strict_redirect_uri' + ) + ) as blob +) +where cast (id as text) = ( + select json_extract(idl, '$.t.s[0]') + from idx_eq_name + where key = 'idm_acp_oauth2_manage' +); From 849b695aa42cc2e31052b58ea7c7956f5644ffeb Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Tue, 22 Apr 2025 01:21:15 +0400 Subject: [PATCH 106/115] auth: create a proper selfprivacy-api token via auth module - selfprivacy-api NixOS module can use selfprivacy.auth.clients option to configure its own client - when "selfprivacy-api" OAuth ID name is used, read-write token is created and idm_admins membership is set --- auth/auth-module.nix | 11 ++++++--- auth/auth.nix | 56 -------------------------------------------- flake.lock | 10 ++++---- 3 files changed, 13 insertions(+), 64 deletions(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index f34194b..4d8498b 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -28,6 +28,7 @@ let kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token"; kanidmServiceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroup; + isRW = oauthClientID == "selfprivacy-api"; in pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPost-script.sh" @@ -53,8 +54,8 @@ let fi fi - # create a new read-only token for kanidm - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountTokenName}" --output json)" + # create a new token for kanidm + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountTokenName}" ${lib.strings.optionalString isRW "--rw"} --output json)" then echo "error: kanidm CLI returns an error when trying to generate service-account api-token" exit 1 @@ -77,7 +78,11 @@ let + lib.strings.optionalString isMailserver '' # add Kanidm service account to `idm_mail_servers` group $KANIDM group add-members idm_mail_servers "${kanidmServiceAccountName}" - ''); + '' + + lib.strings.optionalString isRW '' + $KANIDM group add-members idm_admins "${kanidmServiceAccountName}" + '' + ); in { options.selfprivacy.auth = { diff --git a/auth/auth.nix b/auth/auth.nix index d1c3c20..ca1e6bb 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -16,13 +16,6 @@ let selfprivacy-group = config.users.users."selfprivacy-api".group; - selfprivacy-service-account-name = "sp.selfprivacy-api.service-account"; - - kanidm-service-account-token-name = - "${selfprivacy-group}-service-account-token"; - kanidm-service-account-token-fp = - "${keys-path}/${selfprivacy-group}/kanidm-service-account-token"; - kanidmMigrateDbScript = pkgs.writeShellScript "kanidm-db-migration-script" '' # handle a case when kanidm database is not yet created (the first startup) if [ -f ${config.services.kanidm.serverSettings.db_path} ] @@ -33,52 +26,6 @@ let fi ''; - spApiUserExecStartPostScript = - pkgs.writeShellScript "spApiUserExecStartPostScript" '' - export HOME=$RUNTIME_DIRECTORY/client_home - readonly KANIDM="${pkgs.kanidm}/bin/kanidm" - - # get Kanidm service account for SelfPrivacyAPI - KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${selfprivacy-service-account-name}$")" - echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" - if [ -n "$KANIDM_SERVICE_ACCOUNT" ] - then - echo "kanidm service account \"${selfprivacy-service-account-name}\" is found" - else - echo "kanidm service account \"${selfprivacy-service-account-name}\" is not found" - echo "creating new kanidm service account \"${selfprivacy-service-account-name}\"" - if $KANIDM service-account create --name idm_admin "${selfprivacy-service-account-name}" "SelfPrivacy API service account" idm_admin - then - echo "kanidm service account \"${selfprivacy-service-account-name}\" created" - else - echo "error: cannot create kanidm service account \"${selfprivacy-service-account-name}\"" - exit 1 - fi - fi - - $KANIDM group add-members idm_admins "${selfprivacy-service-account-name}" - - # create a new read-write token for kanidm - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${selfprivacy-service-account-name}" "${kanidm-service-account-token-name}" --output json)" - then - echo "error: kanidm CLI returns an error when trying to generate service-account api-token" - exit 1 - fi - if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" - then - echo "error: cannot get service-account API token from JSON" - exit 1 - fi - - if ! install --mode=640 \ - <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ - ${kanidm-service-account-token-fp} - then - echo "error: cannot write token to \"${kanidm-service-account-token-fp}\"" - exit 1 - fi - ''; - # lua stuff for nginx for debugging only lua_core_path = "${pkgs.luajitPackages.lua-resty-core}/lib/lua/5.1/?.lua"; lua_lrucache_path = "${pkgs.luajitPackages.lua-resty-lrucache}/lib/lua/5.1/?.lua"; @@ -214,9 +161,6 @@ lib.mkIf config.selfprivacy.sso.enable { lib.mkIf (pkgs.kanidm.version == "1.5.0") (lib.mkBefore [ kanidmMigrateDbScript ]); - systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter - [ spApiUserExecStartPostScript ]; - selfprivacy.passthru.auth = { inherit admins-group diff --git a/flake.lock b/flake.lock index daa2e18..615039c 100644 --- a/flake.lock +++ b/flake.lock @@ -62,11 +62,11 @@ ] }, "locked": { - "lastModified": 1744640029, - "narHash": "sha256-Rqy+HhW7weEfAc5rPmxusewuo/69sWVXlQOL2a3Y9ZU=", - "ref": "inex/add-oauth", - "rev": "5393642f896d5fa4b9b7be215d72714554259de6", - "revCount": 1749, + "lastModified": 1745270427, + "narHash": "sha256-EHbn9AgWTmIuRwAo7Y+sULHNN+/vN0r8h2JbTqmYxZc=", + "ref": "use-auth-nixos-module", + "rev": "582d11c1fbea36d6fdbd0a64f782ca8f8c6a1338", + "revCount": 1750, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 9f5ace525856973e4093d7065666203c7b9cc192 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Tue, 22 Apr 2025 02:07:27 +0400 Subject: [PATCH 107/115] roundcube: specify systemd dependencies with dovecot --- sp-modules/roundcube/module.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 19ba2be..3e9babc 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -91,7 +91,7 @@ in systemd.slices.roundcube.description = "Roundcube service slice"; # Roundcube depends on Dovecot and its OAuth2 client secret. - systemd.services.roundcube.after = [ "dovecot2.service" ]; + systemd.services.phpfpm-roundcube.after = [ "dovecot2.service" ]; } # the following part is active only when "auth" module is enabled (lib.mkIf is-auth-enabled { @@ -127,7 +127,7 @@ in originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; originLanding = "https://${cfg.subdomain}.${domain}/"; useShortPreferredUsername = false; - clientSystemdUnits = [ "phpfpm-roundcube.service" ]; + clientSystemdUnits = [ "dovecot2.service" "phpfpm-roundcube.service" ]; enablePkce = false; linuxUserOfClient = linuxUserOfService; linuxGroupOfClient = linuxGroupOfService; From 8a79551743ebda1ed8d6402cb3c1f98e2cabdc44 Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Tue, 22 Apr 2025 17:34:30 +0400 Subject: [PATCH 108/115] auth: remove possibility to use kanidm 1.4.6 --- auth/kanidm-provision.nix | 52 --------------------------------------- flake.lock | 17 ------------- flake.nix | 31 +++++++---------------- selfprivacy-module.nix | 5 ---- 4 files changed, 9 insertions(+), 96 deletions(-) delete mode 100644 auth/kanidm-provision.nix diff --git a/auth/kanidm-provision.nix b/auth/kanidm-provision.nix deleted file mode 100644 index fe09d0e..0000000 --- a/auth/kanidm-provision.nix +++ /dev/null @@ -1,52 +0,0 @@ -{ - lib, - rustPlatform, - fetchFromGitHub, - yq, - versionCheckHook, - nix-update-script, - nixosTests, -}: - -rustPlatform.buildRustPackage rec { - pname = "kanidm-provision"; - version = "1.2.0"; - - src = fetchFromGitHub { - owner = "oddlama"; - repo = "kanidm-provision"; - tag = "v${version}"; - hash = "sha256-+NQJEAJ0DqKEV1cYZN7CLzGoBJNUL3SQAMmxRQG5DMI="; - }; - - postPatch = '' - tomlq -ti '.package.version = "${version}"' Cargo.toml - ''; - - useFetchCargoVendor = true; - cargoHash = "sha256-uo/TGyfNChq/t6Dah0HhXhAwktyQk0V/wewezZuftNk="; - - nativeBuildInputs = [ - yq # for `tomlq` - ]; - - nativeInstallCheckInputs = [ versionCheckHook ]; - versionCheckProgramArg = "--version"; - doInstallCheck = true; - - passthru = { - tests = { inherit (nixosTests) kanidm-provisioning; }; - updateScript = nix-update-script { }; - }; - - meta = { - description = "A small utility to help with kanidm provisioning"; - homepage = "https://github.com/oddlama/kanidm-provision"; - license = with lib.licenses; [ - asl20 - mit - ]; - maintainers = with lib.maintainers; [ oddlama ]; - mainProgram = "kanidm-provision"; - }; -} diff --git a/flake.lock b/flake.lock index 615039c..15b65d2 100644 --- a/flake.lock +++ b/flake.lock @@ -31,27 +31,10 @@ "type": "github" } }, - "nixpkgs-2411": { - "locked": { - "lastModified": 1738435198, - "narHash": "sha256-5+Hmo4nbqw8FrW85FlNm4IIrRnZ7bn0cmXlScNsNRLo=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "f6687779bf4c396250831aa5a32cbfeb85bb07a3", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "rev": "f6687779bf4c396250831aa5a32cbfeb85bb07a3", - "type": "github" - } - }, "root": { "inputs": { "nixos-unstable": "nixos-unstable", "nixpkgs": "nixpkgs", - "nixpkgs-2411": "nixpkgs-2411", "selfprivacy-api": "selfprivacy-api" } }, diff --git a/flake.nix b/flake.nix index ff68281..d18df25 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,6 @@ inputs = { nixpkgs.url = github:nixos/nixpkgs; - nixpkgs-2411.url = github:nixos/nixpkgs/f6687779bf4c396250831aa5a32cbfeb85bb07a3; nixos-unstable.url = github:nixos/nixpkgs/nixos-unstable; selfprivacy-api.url = @@ -12,7 +11,7 @@ selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = { self, nixpkgs, nixpkgs-2411, nixos-unstable, selfprivacy-api }: { + outputs = { self, nixpkgs, nixos-unstable, selfprivacy-api }: { nixosConfigurations-fun = { hardware-configuration , deployment @@ -27,33 +26,21 @@ deployment ./configuration.nix ./auth/auth.nix - ({ config, ... }: { + { nixpkgs.overlays = [ ( _final: prev: - let - pkgs2411 = - nixpkgs-2411.legacyPackages.${prev.system}; - pkgs-unstable = - nixos-unstable.legacyPackages.${prev.system}; - in - if config.selfprivacy.sso.useKanidm_1_4 or false - then - { - inherit (pkgs2411) kanidm; - kanidm-provision = - pkgs2411.callPackage ./auth/kanidm-provision.nix { }; - } - else - { - inherit (pkgs-unstable) kanidm kanidm-provision; - } + { + inherit (nixos-unstable.legacyPackages.${prev.system}) + kanidm + kanidm-provision + ; + } ) ]; - disabledModules = [ "services/security/kanidm.nix" ]; imports = [ ./auth/kanidm.nix ]; - }) + } selfprivacy-api.nixosModules.default ({ pkgs, lib, ... }: { environment.etc = (lib.attrsets.mapAttrs' diff --git a/selfprivacy-module.nix b/selfprivacy-module.nix index f670914..a90435b 100644 --- a/selfprivacy-module.nix +++ b/selfprivacy-module.nix @@ -45,11 +45,6 @@ with lib; default = false; type = types.nullOr types.bool; }; - useKanidm_1_4 = mkOption { - description = "Whether to use Kanidm v1.4 (instead of upstream)."; - default = false; - type = types.bool; - }; }; stateVersion = mkOption { description = "State version of the server"; From 72472e8edf709a6e9fff69b3f9cd63ef3434b83e Mon Sep 17 00:00:00 2001 From: Alexander Tomokhov Date: Tue, 22 Apr 2025 21:17:59 +0400 Subject: [PATCH 109/115] auth: do not create sp.selfprivacy-api.* groups --- auth/auth-module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/auth-module.nix b/auth/auth-module.nix index 4d8498b..fa831b5 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -331,7 +331,7 @@ in , usersGroup , ... }: { - groups = { + groups = lib.mkIf (clientID != "selfprivacy-api") { "${adminsGroup}".members = [ auth-passthru.admins-group ]; "${usersGroup}".members = From 547eb00544948ef15247ad29c2803547f7e86733 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 25 Apr 2025 14:21:44 +0300 Subject: [PATCH 110/115] feat: Delete nextcloud admin user (#133) Co-authored-by: Alexander Tomokhov Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config/pulls/133 --- selfprivacy-module.nix | 12 ++++++++++++ sp-modules/nextcloud/config-paths-needed.json | 1 + sp-modules/nextcloud/module.nix | 9 ++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/selfprivacy-module.nix b/selfprivacy-module.nix index a90435b..7aeba84 100644 --- a/selfprivacy-module.nix +++ b/selfprivacy-module.nix @@ -159,5 +159,17 @@ with lib; You can put whatever you want here. ''; }; + ################# + # Workarounds # + ################# + workarounds = { + deleteNextcloudAdmin = mkOption { + description = '' + Whether to delete an admin user, which is initially created + ''; + type = types.bool; + default = false; + }; + }; }; } diff --git a/sp-modules/nextcloud/config-paths-needed.json b/sp-modules/nextcloud/config-paths-needed.json index ca7356a..0964f8d 100644 --- a/sp-modules/nextcloud/config-paths-needed.json +++ b/sp-modules/nextcloud/config-paths-needed.json @@ -13,6 +13,7 @@ [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], [ "selfprivacy", "passthru", "auth", "oauth2-systemd-service" ], + [ "selfprivacy", "workarounds", "deleteNextcloudAdmin" ], [ "selfprivacy", "sso", "enable" ], [ "selfprivacy", "useBinds" ], [ "services", "nextcloud" ], diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index c762efd..c58c036 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -11,6 +11,7 @@ let hostName = "${cfg.subdomain}.${sp.domain}"; auth-passthru = config.selfprivacy.passthru.auth; + deleteNextcloudAdmin = config.selfprivacy.workarounds.deleteNextcloudAdmin; cfg = sp.modules.nextcloud; is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable; ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}"; @@ -89,7 +90,7 @@ in }) // { meta = { type = "bool"; - weight = 3; + weight = 4; }; }; }; @@ -317,6 +318,12 @@ in --mapping-groups=groups \ --group-provisioning=1 \ -vvv + + '' + lib.optionalString deleteNextcloudAdmin '' + if [[ ! -f /var/lib/nextcloud/.admin-user-deleted ]]; then + ${occ} user:delete admin + touch /var/lib/nextcloud/.admin-user-deleted + fi ''; }; selfprivacy.auth.clients."${oauthClientID}" = { From 1f1218d89b50675188223d8827d219bf0f6366ea Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 25 Apr 2025 14:28:31 +0300 Subject: [PATCH 111/115] chore: Update API version --- flake.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 15b65d2..cc1169e 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1745270427, - "narHash": "sha256-EHbn9AgWTmIuRwAo7Y+sULHNN+/vN0r8h2JbTqmYxZc=", - "ref": "use-auth-nixos-module", - "rev": "582d11c1fbea36d6fdbd0a64f782ca8f8c6a1338", - "revCount": 1750, + "lastModified": 1745382549, + "narHash": "sha256-EKbLCQg9qtBBoeLW7FzxN4EZfZYGpj/jOawvr9MJQiM=", + "ref": "def/add_users_repositories", + "rev": "347ff0db48d80e503d7a13ed779a5e4574316fe5", + "revCount": 1767, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 24dde4afb5e188b47dd238a795ffab2af4cd7aea Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 25 Apr 2025 14:45:06 +0300 Subject: [PATCH 112/115] chore: Update API version --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index cc1169e..cbfd674 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1745382549, - "narHash": "sha256-EKbLCQg9qtBBoeLW7FzxN4EZfZYGpj/jOawvr9MJQiM=", + "lastModified": 1745581477, + "narHash": "sha256-rrhFUwXu6Vr9RpcNjQZEaXCt0ifqeGbvD6u6xxlxgn8=", "ref": "def/add_users_repositories", - "rev": "347ff0db48d80e503d7a13ed779a5e4574316fe5", - "revCount": 1767, + "rev": "f4bd2246fb688f5f86e2126d9eeb3d928e8c66d4", + "revCount": 1769, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From bd6c0eff023e5ba91e63d59e0718ccbf00ee5e81 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 25 Apr 2025 14:57:04 +0300 Subject: [PATCH 113/115] fix: API tried to read kanidm token form env, not file --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index cbfd674..66cdddc 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1745581477, - "narHash": "sha256-rrhFUwXu6Vr9RpcNjQZEaXCt0ifqeGbvD6u6xxlxgn8=", + "lastModified": 1745582198, + "narHash": "sha256-wsBEYkIKtd+F9Wn0nX+l1gH/eDhMBfoNgvIE9qW8Piw=", "ref": "def/add_users_repositories", - "rev": "f4bd2246fb688f5f86e2126d9eeb3d928e8c66d4", - "revCount": 1769, + "rev": "c168150ecf155b7218dea19e60429f1f1290ebaa", + "revCount": 1770, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From fbcb6be4aa056c0dc39ae16de3a3542610f3cf24 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 25 Apr 2025 15:02:28 +0300 Subject: [PATCH 114/115] fix: API --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 66cdddc..4855618 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1745582198, - "narHash": "sha256-wsBEYkIKtd+F9Wn0nX+l1gH/eDhMBfoNgvIE9qW8Piw=", + "lastModified": 1745582529, + "narHash": "sha256-UxglxWAqX/yQBn7065kbQyd2LGEQrnxkR+9WjJt2fWM=", "ref": "def/add_users_repositories", - "rev": "c168150ecf155b7218dea19e60429f1f1290ebaa", - "revCount": 1770, + "rev": "4d32f026e6727005b18f02963b312db5f8d00177", + "revCount": 1771, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" }, From 604c3caa44ce6c63de987e1806431a291866477f Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 25 Apr 2025 15:08:38 +0300 Subject: [PATCH 115/115] chore: Prepare SSO branch for release --- configuration.nix | 2 +- flake.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/configuration.nix b/configuration.nix index a01daec..4fc9baa 100644 --- a/configuration.nix +++ b/configuration.nix @@ -139,7 +139,7 @@ in pkgs.writeShellScript "flake-update-script" '' set -o xtrace if ${config.nix.package.out}/bin/nix flake update \ - --override-input selfprivacy-nixos-config git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=sso + --override-input selfprivacy-nixos-config git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes then if ${pkgs.diffutils}/bin/diff -u -r /etc/selfprivacy/nixos-config-source/ /etc/nixos/ then diff --git a/flake.lock b/flake.lock index 4855618..f83eba7 100644 --- a/flake.lock +++ b/flake.lock @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1745582529, + "lastModified": 1745582811, "narHash": "sha256-UxglxWAqX/yQBn7065kbQyd2LGEQrnxkR+9WjJt2fWM=", - "ref": "def/add_users_repositories", - "rev": "4d32f026e6727005b18f02963b312db5f8d00177", - "revCount": 1771, + "ref": "master", + "rev": "3bb5e09588c0a60149eea520844fb80eb86f8bdb", + "revCount": 1772, "type": "git", "url": "https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git" },