From 01be6580d38ccc3755bbe8408bf7b38bf075d746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gutyina=20Gerg=C5=91?= Date: Thu, 3 Apr 2025 06:49:33 +0200 Subject: [PATCH] nixos/pocket-id: init Co-authored-by: ymstnt <21342713+YMSTNT@users.noreply.github.com> --- .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/security/pocket-id.nix | 278 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/pocket-id.nix | 47 +++ pkgs/by-name/po/pocket-id/package.nix | 4 + 6 files changed, 333 insertions(+) create mode 100644 nixos/modules/services/security/pocket-id.nix create mode 100644 nixos/tests/pocket-id.nix diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index f531ff61af86..9d05c32c697d 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -166,6 +166,8 @@ - [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable). +- [pocket-id](https://pocket-id.org/), an OIDC provider with passkeys support. Available as [services.pocket-id](#opt-services.pocket-id.enable). + - [nvidia-gpu](https://github.com/utkuozdemir/nvidia_gpu_exporter), a Prometheus exporter that scrapes `nvidia-smi` for GPU metrics. Available as [services.prometheus.exporters.nvidia-gpu](#opt-services.prometheus.exporters.nvidia-gpu.enable). - [OpenGamepadUI](https://github.com/ShadowBlip/OpenGamepadUI/), an open source gamepad-native game launcher and overlay for Linux. Available as [programs.opengamepadui](#opt-programs.opengamepadui.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0a23a0d0d653..5aa0af4c442c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1419,6 +1419,7 @@ ./services/security/paretosecurity.nix ./services/security/pass-secret-service.nix ./services/security/physlock.nix + ./services/security/pocket-id.nix ./services/security/shibboleth-sp.nix ./services/security/sks.nix ./services/security/sshguard.nix diff --git a/nixos/modules/services/security/pocket-id.nix b/nixos/modules/services/security/pocket-id.nix new file mode 100644 index 000000000000..87c13b638dcb --- /dev/null +++ b/nixos/modules/services/security/pocket-id.nix @@ -0,0 +1,278 @@ +{ + lib, + pkgs, + config, + ... +}: + +let + inherit (lib) + mkEnableOption + mkIf + mkOption + optionalAttrs + optional + mkPackageOption + ; + inherit (lib.types) + bool + path + str + submodule + ; + + cfg = config.services.pocket-id; + + format = pkgs.formats.keyValue { }; + settingsFile = format.generate "pocket-id-env-vars" cfg.settings; +in +{ + meta.maintainers = with lib.maintainers; [ + gepbird + ymstnt + ]; + + options.services.pocket-id = { + enable = mkEnableOption "Pocket ID server"; + + package = mkPackageOption pkgs "pocket-id" { }; + + environmentFile = mkOption { + type = path; + description = '' + Path to an environment file loaded for the Pocket ID service. + + This can be used to securely store tokens and secrets outside of the world-readable Nix store. + + Example contents of the file: + MAXMIND_LICENSE_KEY=your-license-key + ''; + default = "/dev/null"; + example = "/var/lib/secrets/pocket-id"; + }; + + settings = mkOption { + type = submodule { + freeformType = format.type; + + options = { + PUBLIC_APP_URL = mkOption { + type = str; + description = '' + The URL where you will access the app. + ''; + default = "http://localhost"; + }; + + TRUST_PROXY = mkOption { + type = bool; + description = '' + Whether the app is behind a reverse proxy. + ''; + default = false; + }; + }; + }; + + default = { }; + + description = '' + Environment variables that will be passed to Pocket ID, see + [configuration options](https://pocket-id.org/docs/configuration/environment-variables) + for supported values. + ''; + }; + + dataDir = mkOption { + type = path; + default = "/var/lib/pocket-id"; + description = '' + The directory where Pocket ID will store its data, such as the database. + ''; + }; + + user = mkOption { + type = str; + default = "pocket-id"; + description = "User account under which Pocket ID runs."; + }; + + group = mkOption { + type = str; + default = "pocket-id"; + description = "Group account under which Pocket ID runs."; + }; + }; + + config = mkIf cfg.enable { + warnings = ( + optional (cfg.settings ? MAXMIND_LICENSE_KEY) + "config.services.pocket-id.settings.MAXMIND_LICENSE_KEY will be stored as plaintext in the Nix store. Use config.services.pocket-id.environmentFile instead." + ); + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group}" + ]; + + systemd.services = { + pocket-id-backend = { + description = "Pocket ID backend"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ + cfg.package + cfg.environmentFile + settingsFile + ]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.dataDir; + ExecStart = "${cfg.package}/bin/pocket-id-backend"; + Restart = "always"; + EnvironmentFile = [ + cfg.environmentFile + settingsFile + ]; + + # Hardening + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + DeviceAllow = ""; + DevicePolicy = "closed"; + #IPAddressDeny = "any"; # communicates with the frontend + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateNetwork = false; # communicates with the frontend + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "full"; # needs to write in cfg.dataDir + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = lib.concatStringsSep " " [ + "~" + "@clock" + "@cpu-emulation" + "@debug" + "@module" + "@mount" + "@obsolete" + "@privileged" + "@raw-io" + "@reboot" + #"@resources" # vm test segfaults + "@swap" + ]; + UMask = "0077"; + }; + }; + + pocket-id-frontend = { + description = "Pocket ID frontend"; + after = [ + "network.target" + "pocket-id-backend.service" + ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ + cfg.package + cfg.environmentFile + settingsFile + ]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + ExecStart = "${cfg.package}/bin/pocket-id-frontend"; + Restart = "always"; + EnvironmentFile = [ + cfg.environmentFile + settingsFile + ]; + + # Hardening + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + DeviceAllow = ""; + DevicePolicy = "closed"; + #IPAddressDeny = "any"; # communicates with the backend and client + LockPersonality = true; + MemoryDenyWriteExecute = false; # V8_Fatal segfault + NoNewPrivileges = true; + PrivateDevices = true; + PrivateNetwork = false; # communicates with the backend and client + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = lib.concatStringsSep " " [ + "~" + "@clock" + "@cpu-emulation" + "@debug" + "@module" + "@mount" + "@obsolete" + "@privileged" + "@raw-io" + "@reboot" + "@resources" + "@swap" + ]; + UMask = "0077"; + }; + }; + }; + + users.users = optionalAttrs (cfg.user == "pocket-id") { + pocket-id = { + isSystemUser = true; + group = cfg.group; + description = "Pocket ID backend user"; + home = cfg.dataDir; + }; + }; + + users.groups = optionalAttrs (cfg.group == "pocket-id") { + pocket-id = { }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index cc8eecddf778..186e078c8451 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1039,6 +1039,7 @@ in pleroma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./pleroma.nix { }; plikd = handleTest ./plikd.nix { }; plotinus = handleTest ./plotinus.nix { }; + pocket-id = handleTest ./pocket-id.nix { }; podgrab = handleTest ./podgrab.nix { }; podman = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./podman/default.nix { }; podman-tls-ghostunnel = handleTestOn [ diff --git a/nixos/tests/pocket-id.nix b/nixos/tests/pocket-id.nix new file mode 100644 index 000000000000..753fa251473f --- /dev/null +++ b/nixos/tests/pocket-id.nix @@ -0,0 +1,47 @@ +import ./make-test-python.nix ( + { lib, ... }: + + { + name = "pocket-id"; + meta.maintainers = with lib.maintainers; [ + gepbird + ymstnt + ]; + + nodes = { + machine = + { ... }: + { + services.pocket-id = { + enable = true; + settings = { + PORT = 10001; + INTERNAL_BACKEND_URL = "http://localhost:10002"; + BACKEND_PORT = 10002; + }; + }; + }; + }; + + testScript = + { nodes, ... }: + let + inherit (nodes.machine.services.pocket-id) settings; + inherit (builtins) toString; + in + '' + machine.wait_for_unit("pocket-id-backend.service") + machine.wait_for_open_port(${toString settings.BACKEND_PORT}) + machine.wait_for_unit("pocket-id-frontend.service") + machine.wait_for_open_port(${toString settings.PORT}) + + backend_status = machine.succeed("curl -L -o /tmp/backend-output -w '%{http_code}' http://localhost:${toString settings.BACKEND_PORT}/api/users/me") + assert backend_status == "401" + machine.succeed("grep 'You are not signed in' /tmp/backend-output") + + frontend_status = machine.succeed("curl -L -o /tmp/frontend-output -w '%{http_code}' http://localhost:${toString settings.PORT}") + assert frontend_status == "200" + machine.succeed("grep 'Sign in to Pocket ID' /tmp/frontend-output") + ''; + } +) diff --git a/pkgs/by-name/po/pocket-id/package.nix b/pkgs/by-name/po/pocket-id/package.nix index f29410a8e3c5..0fd97ab64c86 100644 --- a/pkgs/by-name/po/pocket-id/package.nix +++ b/pkgs/by-name/po/pocket-id/package.nix @@ -7,6 +7,7 @@ makeWrapper, nodejs, stdenvNoCC, + nixosTests, nix-update-script, }: @@ -91,6 +92,9 @@ stdenvNoCC.mkDerivation rec { ''; passthru = { + tests = { + inherit (nixosTests) pocket-id; + }; updateScript = nix-update-script { extraArgs = [ "--subpackage"