diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml index 0f768fbe7a89..c832d28779b8 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml @@ -99,7 +99,15 @@ clipcat, an X11 clipboard manager written in Rust. Available at - [services.clipcat](options.html#o pt-services.clipcat.enable). + services.clipcat. + + + + + dex, + an OpenID Connect (OIDC) identity and OAuth 2.0 provider. + Available at + services.dex. diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md index e169c0a5b8d2..015653f1b2f6 100644 --- a/nixos/doc/manual/release-notes/rl-2111.section.md +++ b/nixos/doc/manual/release-notes/rl-2111.section.md @@ -32,8 +32,9 @@ In addition to numerous new and upgraded packages, this release has the followin - [btrbk](https://digint.ch/btrbk/index.html), a backup tool for btrfs subvolumes, taking advantage of btrfs specific capabilities to create atomic snapshots and transfer them incrementally to your backup locations. Available as [services.btrbk](options.html#opt-services.brtbk.instances). -- [clipcat](https://github.com/xrelkd/clipcat/), an X11 clipboard manager written in Rust. Available at [services.clipcat](options.html#o - pt-services.clipcat.enable). +- [clipcat](https://github.com/xrelkd/clipcat/), an X11 clipboard manager written in Rust. Available at [services.clipcat](options.html#opt-services.clipcat.enable). + +- [dex](https://github.com/dexidp/dex), an OpenID Connect (OIDC) identity and OAuth 2.0 provider. Available at [services.dex](options.html#opt-services.dex.enable). - [geoipupdate](https://github.com/maxmind/geoipupdate), a GeoIP database updater from MaxMind. Available as [services.geoipupdate](options.html#opt-services.geoipupdate.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index a7decf889877..9f4664d3295f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -962,6 +962,7 @@ ./services/web-apps/calibre-web.nix ./services/web-apps/convos.nix ./services/web-apps/cryptpad.nix + ./services/web-apps/dex.nix ./services/web-apps/discourse.nix ./services/web-apps/documize.nix ./services/web-apps/dokuwiki.nix diff --git a/nixos/modules/services/web-apps/dex.nix b/nixos/modules/services/web-apps/dex.nix new file mode 100644 index 000000000000..2b5999706d72 --- /dev/null +++ b/nixos/modules/services/web-apps/dex.nix @@ -0,0 +1,115 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.dex; + fixClient = client: if client ? secretFile then ((builtins.removeAttrs client [ "secretFile" ]) // { secret = client.secretFile; }) else client; + filteredSettings = mapAttrs (n: v: if n == "staticClients" then (builtins.map fixClient v) else v) cfg.settings; + secretFiles = flatten (builtins.map (c: if c ? secretFile then [ c.secretFile ] else []) (cfg.settings.staticClients or [])); + + settingsFormat = pkgs.formats.yaml {}; + configFile = settingsFormat.generate "config.yaml" filteredSettings; + + startPreScript = pkgs.writeShellScript "dex-start-pre" ('' + '' + (concatStringsSep "\n" (builtins.map (file: '' + ${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' /run/dex/config.yaml + '') secretFiles))); +in +{ + options.services.dex = { + enable = mkEnableOption "the OpenID Connect and OAuth2 identity provider"; + + settings = mkOption { + type = settingsFormat.type; + default = {}; + example = literalExample '' + { + # External url + issuer = "http://127.0.0.1:5556/dex"; + storage = { + type = "postgres"; + config.host = "/var/run/postgres"; + }; + web = { + http = "127.0.0.1:5556"; + }; + enablePasswordDB = true; + staticClients = [ + { + id = "oidcclient"; + name = "Client"; + redirectURIs = [ "https://example.com/callback" ]; + secretFile = "/etc/dex/oidcclient"; # The content of `secretFile` will be written into to the config as `secret`. + } + ]; + } + ''; + description = '' + The available options can be found in + the example configuration. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.dex = { + description = "dex identity provider"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ] ++ (optional (cfg.settings.storage.type == "postgres") "postgresql.service"); + + serviceConfig = { + ExecStart = "${pkgs.dex-oidc}/bin/dex serve /run/dex/config.yaml"; + ExecStartPre = [ + "${pkgs.coreutils}/bin/install -m 600 ${configFile} /run/dex/config.yaml" + "+${startPreScript}" + ]; + RuntimeDirectory = "dex"; + + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + BindReadOnlyPaths = [ + "/nix/store" + "-/etc/resolv.conf" + "-/etc/nsswitch.conf" + "-/etc/hosts" + "-/etc/localtime" + "-/etc/dex" + ]; + BindPaths = optional (cfg.settings.storage.type == "postgres") "/var/run/postgresql"; + CapabilityBoundingSet = "CAP_NET_BIND_SERVICE"; + # ProtectClock= adds DeviceAllow=char-rtc r + DeviceAllow = ""; + DynamicUser = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + # Port needs to be exposed to the host network + #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 = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ]; + TemporaryFileSystem = "/:ro"; + # Does not work well with the temporary root + #UMask = "0066"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 66b0f4f258d3..ccdeb33d2e27 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -97,6 +97,7 @@ in cryptpad = handleTest ./cryptpad.nix {}; deluge = handleTest ./deluge.nix {}; dendrite = handleTest ./dendrite.nix {}; + dex-oidc = handleTest ./dex-oidc.nix {}; dhparams = handleTest ./dhparams.nix {}; disable-installer-tools = handleTest ./disable-installer-tools.nix {}; discourse = handleTest ./discourse.nix {}; diff --git a/nixos/tests/dex-oidc.nix b/nixos/tests/dex-oidc.nix new file mode 100644 index 000000000000..37275a97ef0f --- /dev/null +++ b/nixos/tests/dex-oidc.nix @@ -0,0 +1,78 @@ +import ./make-test-python.nix ({ lib, ... }: { + name = "dex-oidc"; + meta.maintainers = with lib.maintainers; [ Flakebi ]; + + nodes.machine = { pkgs, ... }: { + environment.systemPackages = with pkgs; [ jq ]; + services.dex = { + enable = true; + settings = { + issuer = "http://127.0.0.1:8080/dex"; + storage = { + type = "postgres"; + config.host = "/var/run/postgresql"; + }; + web.http = "127.0.0.1:8080"; + oauth2.skipApprovalScreen = true; + staticClients = [ + { + id = "oidcclient"; + name = "Client"; + redirectURIs = [ "https://example.com/callback" ]; + secretFile = "/etc/dex/oidcclient"; + } + ]; + connectors = [ + { + type = "mockPassword"; + id = "mock"; + name = "Example"; + config = { + username = "admin"; + password = "password"; + }; + } + ]; + }; + }; + + # This should not be set from nix but through other means to not leak the secret. + environment.etc."dex/oidcclient" = { + mode = "0400"; + user = "dex"; + text = "oidcclientsecret"; + }; + + services.postgresql = { + enable = true; + ensureDatabases =[ "dex" ]; + ensureUsers = [ + { + name = "dex"; + ensurePermissions = { "DATABASE dex" = "ALL PRIVILEGES"; }; + } + ]; + }; + }; + + testScript = '' + with subtest("Web server gets ready"): + machine.wait_for_unit("dex.service") + # Wait until server accepts connections + machine.wait_until_succeeds("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid'") + + with subtest("Login"): + state = machine.succeed("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid' | sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip() + print(f"Got state {state}") + machine.succeed(f"curl -fs 'localhost:8080/dex/auth/mock/login?back=&state={state}' -d 'login=admin&password=password'") + code = machine.succeed(f"curl -fs localhost:8080/dex/approval?req={state} | sed -n 's/.*code=\\(.*\\)&.*/\\1/p'").strip() + print(f"Got approval code {code}") + bearer = machine.succeed(f"curl -fs localhost:8080/dex/token -u oidcclient:oidcclientsecret -d 'grant_type=authorization_code&redirect_uri=https://example.com/callback&code={code}' | jq .access_token -r").strip() + print(f"Got access token {bearer}") + + with subtest("Get userinfo"): + assert '"sub"' in machine.succeed( + f"curl -fs localhost:8080/dex/userinfo --oauth2-bearer {bearer}" + ) + ''; +}) diff --git a/pkgs/servers/dex/default.nix b/pkgs/servers/dex/default.nix index a27870453c82..16dc5f8bafd6 100644 --- a/pkgs/servers/dex/default.nix +++ b/pkgs/servers/dex/default.nix @@ -1,4 +1,4 @@ -{ lib, buildGoModule, fetchFromGitHub }: +{ lib, buildGoModule, fetchFromGitHub, nixosTests }: buildGoModule rec { pname = "dex"; @@ -26,6 +26,8 @@ buildGoModule rec { cp -r $src/web $out/share/web ''; + passthru.tests = { inherit (nixosTests) dex-oidc; }; + meta = with lib; { description = "OpenID Connect and OAuth2 identity provider with pluggable connectors"; homepage = "https://github.com/dexidp/dex";