diff --git a/machines/compute01/_configuration.nix b/machines/compute01/_configuration.nix index aa82b23..9b19f69 100644 --- a/machines/compute01/_configuration.nix +++ b/machines/compute01/_configuration.nix @@ -18,6 +18,7 @@ let "mastodon" "nextcloud" "outline" + "satosa" ]; in diff --git a/machines/compute01/satosa/default.nix b/machines/compute01/satosa/default.nix index 16dce47..3757e67 100644 --- a/machines/compute01/satosa/default.nix +++ b/machines/compute01/satosa/default.nix @@ -1,9 +1,154 @@ -{ lib, pkgs, ... }: +{ config, lib, dgn-lib, ... }: let - package = import ./package { inherit lib pkgs; }; + inherit (dgn-lib) setDefault; - host = "saml.dgnum.eu"; + host = "saml-idp.dgnum.eu"; in { + imports = [ ./module.nix ]; + + services.satosa = { + enable = true; + + inherit host; + port = 8090; + + envFile = config.age.secrets."satosa-env_file".path; + + frontendModules = { + saml2IDP = { + module = "satosa.frontends.saml2.SAMLFrontend"; + name = "Saml2IDP"; + config = { + endpoints.single_sign_on_service = { + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" = "sso/post"; + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" = + "sso/redirect"; + }; + entityid_endpoint = true; + enable_metadata_reload = false; + idp_config = { + organization = { + display_name = "Délégation Générale Numérique"; + name = "DGNum"; + url = "https://dgnum.eu"; + }; + + contact_person = [{ + contact_type = "technical"; + email_address = "mailto:tom.hubrecht@dgnum.eu"; + given_name = "Tom Hubrecht"; + }]; + + key_file = "/var/lib/satosa/ssl/key.pem"; + cert_file = "/var/lib/satosa/ssl/cert.pem"; + + metadata.local = [ ]; + + entityid = "https://${host}/Saml2IDP"; + accepted_time_diff = 60; + service = { + idp = { + endpoints.single_sign_on_service = [ ]; + name = "DGNum proxy IdP"; + ui_info = { + display_name = [{ + lang = "fr"; + text = "Service de connexion DGNum"; + }]; + }; + name_id_format = [ + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ]; + policy = { + default = { + attribute_restrictions = null; + fail_on_missing_requested = false; + lifetime = { minutes = 15; }; + name_form = + "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"; + encrypt_assertion = false; + encrypted_advice_attributes = false; + }; + }; + }; + }; + }; + }; + }; + }; + + backendModules = { + # module: satosa.backends.openid_connect.OpenIDConnectBackend + # name: openid_connect + # config: + # provider_metadata: + # issuer: https://op.example.com + # client: + # verify_ssl: yes + # auth_req_params: + # response_type: code + # scope: [openid, profile, email, address, phone] + # client_metadata: + # application_name: SATOSA + # application_type: web + # contacts: [ops@example.com] + # redirect_uris: [/] + # subject_type: public + # entity_info: + # contact_person: + # - contact_type: "technical" + # email_address: ["technical_test@example.com", "support_test@example.com"] + # given_name: "Test" + # sur_name: "OP" + # - contact_type: "support" + # email_address: ["support_test@example.com"] + # given_name: "Support_test" + # organization: + # display_name: + # - ["OP Identities", "en"] + # name: + # - ["En test-OP", "se"] + # - ["A test OP", "en"] + # url: + # - ["http://www.example.com", "en"] + # - ["http://www.example.se", "se"] + # ui_info: + # description: + # - ["This is a test OP", "en"] + # display_name: + # - ["OP - TEST", "en"] + kanidm = { + module = "satosa.backends.openid_connect.OpenIDConnectBackend"; + name = "kanidm"; + config = { + provider_metadata.issuer = + "https://sso.dgnum.eu/oauth2/openid/satosa_dgn/"; + client = { + auth_req_params = { + response_type = "code"; + scope = [ "openid" "profile" "email" ]; + }; + client_metadata = { + client_id = "satosa_dgn"; + client_secret = "ENV! SATOSA_FRONTEND_KANIDM_CLIENT_SECRET"; + redirect_uris = [ "https://${host}/kanidm" ]; + }; + }; + }; + }; + }; + }; + + services.nginx.virtualHosts.${host} = { + enableACME = true; + forceSSL = true; + }; + + dgn-secrets.options = [ + (setDefault { owner = "satosa"; } + (builtins.filter (lib.hasPrefix "satosa-") config.dgn-secrets.names)) + ]; } diff --git a/machines/compute01/satosa/module.nix b/machines/compute01/satosa/module.nix new file mode 100644 index 0000000..65e590e --- /dev/null +++ b/machines/compute01/satosa/module.nix @@ -0,0 +1,198 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) mkDefault mkEnableOption mkIf mkOption types; + + yamlFormat = pkgs.formats.yaml { }; + + configFile = yamlFormat.generate "proxy_conf.yaml" cfg.proxyConf; + + cfg = config.services.satosa; + + mkYamlFiles = files: + builtins.attrValues + (builtins.mapAttrs (name: yamlFormat.generate "${name}.yaml") files); + + pyEnv = cfg.package.python.withPackages (ps: [ cfg.package ps.gunicorn ]); +in { + options.services.satosa = { + enable = mkEnableOption "SATOSA, a SAML and OIDC proxy."; + + package = mkOption { + type = types.package; + default = let pkgs = import { }; + in import ./package { + inherit pkgs; + inherit (pkgs) lib; + }; + }; + + port = mkOption { + type = types.port; + default = 8080; + }; + + host = mkOption { type = types.str; }; + + workers = mkOption { + type = types.int; + default = 1; + }; + + configureNginx = mkOption { + type = types.bool; + default = true; + }; + + proxyConf = mkOption { + inherit (yamlFormat) type; + default = { }; + }; + + envFile = mkOption { + type = with types; nullOr path; + default = null; + }; + + internalAttributes = mkOption { + inherit (yamlFormat) type; + default = { }; + }; + + frontendModules = mkOption { + type = types.attrsOf yamlFormat.type; + default = { }; + }; + + backendModules = mkOption { + type = types.attrsOf yamlFormat.type; + default = { }; + }; + + microServices = mkOption { + type = types.attrsOf yamlFormat.type; + default = { }; + }; + }; + + config = mkIf cfg.enable { + services.satosa.proxyConf = builtins.mapAttrs (_: mkDefault) { + BASE = "https://${cfg.host}"; + COOKIE_STATE_NAME = "satosa_state"; + COOKIE_SECURE = true; + COOKIE_HTTPONLY = true; + COOKIE_SAMESITE = "None"; + COOKIE_MAX_AGE = "1200"; + CONTEXT_STATE_DELETE = true; + INTERNAL_ATTRIBUTES = yamlFormat.generate "internal_attributes.yaml" { + attributes = cfg.internalAttributes; + }; + BACKEND_MODULES = mkYamlFiles cfg.backendModules; + FRONTEND_MODULES = mkYamlFiles cfg.frontendModules; + MICRO_SERVICES = mkYamlFiles cfg.microServices; + LOGGING = { + version = 1; + formatters.simple.format = + "[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s] %(message)s"; + handlers.stdout = { + class = "logging.StreamHandler"; + stream = "ext://sys.stdout"; + level = "DEBUG"; + formatter = "simple"; + }; + loggers = { + satosa.level = "DEBUG"; + saml2.level = "DEBUG"; + oidcendpoint.level = "DEBUG"; + pyop.level = "DEBUG"; + oic.level = "DEBUG"; + root = { + level = "DEBUG"; + handlers = [ "stdout" ]; + }; + }; + }; + }; + + systemd.services = { + satosa-metadata = { + script = '' + umask 077 + + # Generate a secret key/certificate if none are present + mkdir -p ssl + if [ ! -f "ssl/.created" ]; then + ${pkgs.openssl}/bin/openssl req -x509 \ + -newkey rsa:2048 \ + -keyout ssl/key.pem \ + -out ssl/cert.pem \ + -sha256 \ + -days 3650 \ + -nodes \ + -subj "/C=FR/ST=Île de France/L=Paris/O=DGNum/OU=./CN=saml-idp.dgnum.eu" \ + && touch ssl/.created + fi + + mkdir -p metadata + + ${cfg.package}/bin/satosa-saml-metadata \ + --dir metadata \ + --sign ${configFile} ssl/key.pem ssl/cert.pem + ''; + + serviceConfig = { + Type = "oneshot"; + User = "satosa"; + Group = "satosa"; + DynamicUser = true; + StateDirectory = "satosa"; + WorkingDirectory = "/var/lib/satosa"; + EnvironmentFile = lib.optional (cfg.envFile != null) cfg.envFile; + }; + }; + + satosa = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + wants = [ "satosa-metadata.service" ]; + serviceConfig = { + User = "satosa"; + Group = "satosa"; + DynamicUser = true; + Type = "notify"; + RuntimeDirectory = "satosa"; + StateDirectory = "satosa"; + WorkingDirectory = cfg.package; + ExecStart = '' + ${pyEnv}/bin/gunicorn \ + -w ${builtins.toString cfg.workers} \ + -b 127.0.0.1:${builtins.toString cfg.port} \ + --pythonpath ${pyEnv}/${pkgs.python3.sitePackages} \ + satosa.wsgi:app + ''; + ExecReload = "${pkgs.util-linux}/bin/kill -s HUP $MAINPID"; + KillMode = "mixed"; + TimeoutStopSec = "5"; + EnvironmentFile = lib.optional (cfg.envFile != null) cfg.envFile; + }; + environment = { SATOSA_CONFIG = configFile; }; + }; + }; + + services.nginx = mkIf cfg.configureNginx { + enable = true; + + virtualHosts.${cfg.host} = { + locations."/".proxyPass = + "http://127.0.0.1:${builtins.toString cfg.port}"; + }; + }; + + users.users.satosa = { + isSystemUser = true; + group = "satosa"; + home = "/var/lib/satosa"; + }; + users.groups.satosa = { }; + }; +} diff --git a/machines/compute01/satosa/package/default.nix b/machines/compute01/satosa/package/default.nix index cf1fc94..481d056 100644 --- a/machines/compute01/satosa/package/default.nix +++ b/machines/compute01/satosa/package/default.nix @@ -4,6 +4,8 @@ let callPackage = lib.callPackageWith (pkgs // self); self = { + satosa = callPackage ./satosa.nix { }; + cookies-samesite-compat = callPackage ./cookies-samesite-compat.nix { }; pyop = callPackage ./pyop.nix { }; oic = callPackage ./oic.nix { }; @@ -12,4 +14,4 @@ let pydantic-core = callPackage ./pydantic-core.nix { }; }; -in callPackage ./satosa.nix { } +in self.satosa diff --git a/machines/compute01/satosa/package/satosa.nix b/machines/compute01/satosa/package/satosa.nix index 24bfc64..b7ecca4 100644 --- a/machines/compute01/satosa/package/satosa.nix +++ b/machines/compute01/satosa/package/satosa.nix @@ -47,6 +47,8 @@ python3.pkgs.buildPythonPackage rec { ]; }; + passthru.python = python3; + pythonImportsCheck = [ "satosa" ]; meta = with lib; { diff --git a/machines/compute01/secrets/satosa-env_file b/machines/compute01/secrets/satosa-env_file new file mode 100644 index 0000000..4cbf502 Binary files /dev/null and b/machines/compute01/secrets/satosa-env_file differ diff --git a/machines/compute01/secrets/secrets.nix b/machines/compute01/secrets/secrets.nix index bd0c5ab..6a1da07 100644 --- a/machines/compute01/secrets/secrets.nix +++ b/machines/compute01/secrets/secrets.nix @@ -11,4 +11,5 @@ lib.setDefault { inherit publicKeys; } [ "outline-oidc_client_secret_file" "outline-smtp_password_file" "outline-storage_secret_key_file" + "satosa-env_file" ]