From 7e5920bcf3b7ddbaa998fa88720ac0a8f5260f7f Mon Sep 17 00:00:00 2001 From: akotro Date: Thu, 25 Sep 2025 13:23:42 +0100 Subject: [PATCH] nixos/tsidp: init module --- .../manual/release-notes/rl-2511.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/security/tsidp.nix | 236 ++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 nixos/modules/services/security/tsidp.nix diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 8240c63ab2d8..f69aeae62847 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -78,6 +78,8 @@ - [crowdsec](https://www.crowdsec.net/), a free, open-source and collaborative IPS. Available as [services.crowdsec](#opt-services.crowdsec.enable). +- [tsidp](https://github.com/tailscale/tsidp), a simple OIDC / OAuth Identity Provider (IdP) server for your tailnet. Available as [services.tsidp](#opt-services.tsidp.enable). + - [Newt](https://github.com/fosrl/newt), a fully user space WireGuard tunnel client and TCP/UDP proxy, designed to securely expose private resources controlled by Pangolin. Available as [services.newt](options.html#opt-services.newt.enable). - [IfState](https://ifstate.net), manage host interface settings in a declarative manner. Available as [networking.ifstate](options.html#opt-networking.ifstate.enable) and [boot.initrd.network.ifstate](options.html#opt-boot.initrd.network.ifstate.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 876e463d7a86..cc338fc65b21 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1485,6 +1485,7 @@ ./services/security/tor.nix ./services/security/torify.nix ./services/security/torsocks.nix + ./services/security/tsidp.nix ./services/security/usbguard.nix ./services/security/vault-agent.nix ./services/security/vault.nix diff --git a/nixos/modules/services/security/tsidp.nix b/nixos/modules/services/security/tsidp.nix new file mode 100644 index 000000000000..f79c4eceaf3e --- /dev/null +++ b/nixos/modules/services/security/tsidp.nix @@ -0,0 +1,236 @@ +{ + lib, + pkgs, + config, + ... +}: +let + inherit (lib) + getExe + maintainers + mkEnableOption + mkIf + mkOption + mkPackageOption + optional + ; + inherit (lib.types) + path + str + port + bool + enum + nullOr + ; + + cfg = config.services.tsidp; +in +{ + options.services.tsidp = { + enable = mkEnableOption "tsidp server"; + + package = mkPackageOption pkgs "tsidp" { }; + + environmentFile = mkOption { + type = nullOr path; + description = '' + Path to an environment file loaded for the tsidp service. + + This can be used to securely store tokens and secrets outside of the world-readable Nix store. + + Example contents of the file: + ``` + TS_AUTH_KEY=YOUR_TAILSCALE_AUTHKEY + ``` + ''; + default = null; + example = "/run/secrets/tsidp"; + }; + + settings = { + hostName = mkOption { + type = str; + default = "idp"; + description = '' + The hostname to use for the tsnet node. + ''; + }; + + port = mkOption { + type = port; + default = 443; + description = '' + Port to listen on (default: 443). + ''; + }; + + localPort = mkOption { + type = nullOr port; + default = null; + description = "Listen on localhost:."; + }; + + useLocalTailscaled = mkOption { + type = bool; + description = '' + Use local tailscaled instead of tsnet. + ''; + default = false; + }; + + enableFunnel = mkOption { + type = bool; + default = false; + description = '' + Use Tailscale Funnel to make tsidp available on the public internet so it works with SaaS products. + ''; + }; + + enableSts = mkOption { + type = bool; + default = true; + description = '' + Enable OAuth token exchange using RFC 8693. + ''; + }; + + logLevel = mkOption { + type = enum [ + "debug" + "info" + "warn" + "error" + ]; + description = '' + Set logging level: debug, info, warn, error. + ''; + default = "info"; + }; + + debugAllRequests = mkOption { + type = bool; + description = '' + For development. Prints all requests and responses. + ''; + default = false; + }; + + debugTsnet = mkOption { + type = bool; + description = '' + For development. Enables debug level logging with tsnet connection. + ''; + default = false; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.settings.useLocalTailscaled -> config.services.tailscale.enable == true; + message = "Tailscale service must be enabled if services.tsidp.settings.useLocalTailscaled is used."; + } + ]; + + systemd.services.tsidp = + let + deps = [ + "network.target" + ] + ++ optional (cfg.settings.useLocalTailscaled) "tailscaled.service"; + in + { + description = "tsidp"; + after = deps; + wants = deps; + wantedBy = [ + "multi-user.target" + "network-online.target" + ]; + restartTriggers = [ + cfg.package + cfg.environmentFile + ]; + + environment = { + HOME = "/var/lib/tsidp"; + TAILSCALE_USE_WIP_CODE = "1"; # Needed while tsidp is in development (< v1.0.0). + }; + + serviceConfig = { + Type = "simple"; + ExecStart = + let + args = lib.cli.toGNUCommandLineShell { mkOptionName = k: "-${k}"; } { + hostname = cfg.settings.hostName; + port = cfg.settings.port; + local-port = cfg.settings.localPort; + use-local-tailscaled = cfg.settings.useLocalTailscaled; + funnel = cfg.settings.enableFunnel; + enable-sts = cfg.settings.enableSts; + log = cfg.settings.logLevel; + debug-all-requests = cfg.settings.debugAllRequests; + debug-tsnet = cfg.settings.debugTsnet; + }; + in + "${getExe cfg.package} ${args}"; + Restart = "always"; + RestartSec = "15"; + + DynamicUser = true; + StateDirectory = "tsidp"; + WorkingDirectory = "/var/lib/tsidp"; + ReadWritePaths = mkIf (cfg.settings.useLocalTailscaled) [ + "/var/run/tailscale" # needed due to `ProtectSystem = "strict";` + "/var/lib/tailscale" + ]; + BindPaths = mkIf (cfg.settings.useLocalTailscaled) [ + "/var/run/tailscale:/var/run/tailscale" + ]; + + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + # Hardening + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + DeviceAllow = ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateNetwork = false; # provides the service through network + PrivateTmp = true; + PrivateUsers = true; + PrivateDevices = true; + ProtectHome = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + ProtectHostname = true; + ProtectProc = "invisible"; + ProcSubset = "all"; # tsidp needs access to /proc/net/route + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + "AF_NETLINK" + ]; + RestrictRealtime = true; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" ]; + }; + }; + }; + + meta.maintainers = with maintainers; [ + akotro + mikeodr + yethal + ]; +}