diff --git a/Dockerfile b/Dockerfile index 678c251c0..1bb65f433 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ # docker buildx build --platform linux/amd64 -t tsidp-server:amd64 --load . # Build stage -FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder WORKDIR /app # BuildKit will set these automatically when using buildx @@ -37,4 +37,4 @@ COPY scripts/docker/run.sh /run.sh RUN chmod +x /run.sh # Run the binary through the entrypoint script -ENTRYPOINT ["/run.sh"] \ No newline at end of file +ENTRYPOINT ["/run.sh"] diff --git a/README.md b/README.md index 071051e56..da5040d0e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ docker run -d \ -e TAILSCALE_USE_WIP_CODE=1 \ -e TS_STATE_DIR=/data \ -e TS_HOSTNAME=idp \ + -e TS_AUTHKEY=YOUR_TAILSCALE_AUTHKEY \ -e TSIDP_ENABLE_STS=1 \ ghcr.io/tailscale/tsidp:latest ``` @@ -53,6 +54,32 @@ Visit `https://idp.yourtailnet.ts.net` to confirm the service is running. > [!NOTE] > If you're running tsidp for the first time it may take a few minutes for the TLS certificate to generate. You may not be able to access the service until the certificate is ready. +#### Using OAuth Client Secrets + +As an alternative to traditional auth keys, you can use OAuth client secrets for authentication by passing them through `TS_AUTHKEY`: + +```bash +# Run tsidp with OAuth client secret +docker run -d \ + --name tsidp \ + -p 443:443 \ + -v tsidp-data:/data \ + -e TAILSCALE_USE_WIP_CODE=1 \ + -e TS_STATE_DIR=/data \ + -e TS_HOSTNAME=idp \ + -e TSIDP_ENABLE_STS=1 \ + -e TS_AUTHKEY=tskey-client-xxxxxxxxxxxx \ + -e TS_ADVERTISE_TAGS=tag:tsidp,tag:server \ + ghcr.io/tailscale/tsidp:latest +``` + +> [!IMPORTANT] +> When using OAuth client secrets: +> - Pass the OAuth client secret through `TS_AUTHKEY` (same as regular auth keys) +> - Specify advertise tags using `TS_ADVERTISE_TAGS` +> - The OAuth client secret must start with `tskey-client-` +> - The tags must be properly configured in your Tailscale ACL policy + ### Other Ways to Build and Run
@@ -75,7 +102,7 @@ $ git clone https://github.com/tailscale/tsidp.git $ cd tsidp # run with default values for flags -$ TAILSCALE_USE_WIP_CODE=1 TS_AUTHKEY={YOUR_TAILSCALE_AUTHKEY} TSNET_FORCE_LOGIN=1 go run . +$ TAILSCALE_USE_WIP_CODE=1 TS_AUTHKEY=YOUR_TAILSCALE_AUTHKEY TSNET_FORCE_LOGIN=1 go run . ```
@@ -118,7 +145,7 @@ This is a permissive grant that is suitable for testing purposes: The `tsidp-server` is configured by several command-line flags: | Flag | Description | Default | -| ----------------------- | -------------------------------------------------------------------------------------------------- | -------- | +| ------------------------| -------------------------------------------------------------------------------------------------- | -------- | | `-dir ` | Directory path to save tsnet and tsidp state. Recommend to be set. | `""` | | `-hostname ` | hostname on tailnet. Will become `.your-tailnet.ts.net` | `idp` | | `-port ` | Port to listen on | `443` | @@ -142,7 +169,11 @@ The `tsidp-server` binary is configured through the CLI flags above. However, th These environment variables are used when tsidp does not have any state information set in `-dir `. -- `TS_AUTHKEY=`: Key for registering a tsidp as a new node on your tailnet. If omitted a link will be printed to manually register. +> [!WARNING] +> **Serverless/Stateless Deployment**: tsidp requires persistent state storage to function properly in production. Without a persistent `-dir`, the service will re-register with Tailscale on every restart, lose dynamic OIDC client registrations, and invalidate user sessions. Serverless environments without persistent storage are not recommended for production use. + +- `TS_AUTHKEY=`: Key for registering a tsidp as a new node on your tailnet. Can be a traditional auth key or OAuth client secret (tskey-client-xxx). If omitted, a link will be printed to manually register. +- `TS_ADVERTISE_TAGS=`: Comma-separated advertise tags (e.g., "tag:tsidp,tag:server"). Optional, but recommended when using OAuth client secrets. - `TSNET_FORCE_LOGIN=1`: Force re-login of the node. Useful during development. ### Docker Environment Variables @@ -162,13 +193,15 @@ The Docker image exposes the CLI flags through environment variables. If omitted | `TSIDP_LOG=` | `-log ` | | `TSIDP_DEBUG_TSNET=1` | `-debug-tsnet` | | `TSIDP_DEBUG_ALL_REQUESTS=1` | `-debug-all-requests` | +| `TS_AUTHKEY=` | _(env var only)_ | +| `TS_ADVERTISE_TAGS=` | _(env var only)_ | ## Application Configuration Guides (WIP) tsidp can be used as IdP server for any application that supports custom OIDC providers. > [!IMPORTANT] -> Note: If you'd like to use tsidp to login to a SaaS application outside of your tailnet rather than a self-hosted app inside of your tailnet, you'll need to run tsidp with `--funnel` enabled. +> Note: If you'd like to use tsidp to login to a SaaS application outside of your tailnet rather than a self-hosted app inside of your tailnet, you'll need to run tsidp with `-funnel` enabled. - [Proxmox](docs/proxmox/README.md) diff --git a/flake.nix b/flake.nix index e9c99ccf7..aafa170aa 100644 --- a/flake.nix +++ b/flake.nix @@ -11,15 +11,15 @@ nixpkgs, systems, }: let - go125Version = "1.24.7"; - goHash = "sha256-Ko9Q2w+IgDYHxQ1+qINNy3vUg8a0KKkeNg/fhiS0ZGQ="; + go125Version = "1.25.1"; + goHash = "sha256-0BDBCc7pTYDv5oHqtGvepJGskGv0ZYPDLp8NuwvRpZQ="; eachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f (import nixpkgs { system = system; overlays = [ (final: prev: { - go_1_24 = prev.go_1_24.overrideAttrs { + go_1_25 = prev.go_1_25.overrideAttrs { version = go125Version; src = prev.fetchurl { url = "https://go.dev/dl/go${go125Version}.src.tar.gz"; @@ -33,7 +33,7 @@ formatter = eachSystem (pkgs: pkgs.nixpkgs-fmt); packages = eachSystem (pkgs: { - default = pkgs.buildGo124Module { + default = pkgs.buildGo125Module { pname = "tsidp"; version = if (self ? shortRev) diff --git a/go.mod b/go.mod index bb413824a..8bbe18d78 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/tailscale/tsidp -go 1.24.7 +go 1.25.1 require ( gopkg.in/square/go-jose.v2 v2.6.0 - tailscale.com v1.86.5 + tailscale.com v1.88.3 ) require ( @@ -31,7 +31,7 @@ require ( github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gaissmai/bart v0.18.0 // indirect - github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -60,7 +60,7 @@ require ( github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect - github.com/vishvananda/netns v0.0.4 // indirect + github.com/vishvananda/netns v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect diff --git a/go.sum b/go.sum index f1c14bd1f..9560400d0 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= @@ -176,8 +176,8 @@ github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1 github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= -github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= @@ -218,8 +218,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -236,5 +236,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.86.5 h1:yBtWFjuLYDmxVnfnvPbZNZcKADCYgNfMd0rUAOA9XCs= -tailscale.com v1.86.5/go.mod h1:Lm8dnzU2i/Emw15r6sl3FRNp/liSQ/nYw6ZSQvIdZ1M= +tailscale.com v1.88.3 h1:OiE6iVqzykhbITxmIKjH8d00cw0LsJFO3TuFd4jQVXU= +tailscale.com v1.88.3/go.mod h1:LHaTiwRgzebPDLgZ6RQQVzX+1SR5fbNl51fzm7UtMaw= diff --git a/scripts/docker/run.sh b/scripts/docker/run.sh index bb861eedf..22bf57873 100755 --- a/scripts/docker/run.sh +++ b/scripts/docker/run.sh @@ -50,4 +50,4 @@ if [ -n "$TSIDP_DEBUG_TSNET" ]; then fi # Execute tsidp-server with the built arguments -exec /tsidp-server $ARGS "$@" \ No newline at end of file +exec /tsidp-server $ARGS "$@" diff --git a/tsidp-server.go b/tsidp-server.go index 91bc8bc97..9f9cc0926 100644 --- a/tsidp-server.go +++ b/tsidp-server.go @@ -58,6 +58,7 @@ var ( func main() { flag.Parse() ctx := context.Background() + if !envknob.UseWIPCode() { slog.Error("cmd/tsidp is a work in progress and has not been security reviewed;\nits use requires TAILSCALE_USE_WIP_CODE=1 be set in the environment for now.") os.Exit(1) @@ -131,6 +132,16 @@ func main() { Hostname: *flagHostname, Dir: *flagDir, } + + if advertiseTags := os.Getenv("TS_ADVERTISE_TAGS"); advertiseTags != "" { + tags := strings.Split(advertiseTags, ",") + for i, tag := range tags { + tags[i] = strings.TrimSpace(tag) + } + ts.AdvertiseTags = tags + slog.Info("Using advertise tags", slog.String("tags", strings.Join(tags, ","))) + } + if *flagDebugTSNet { ts.Logf = func(format string, args ...any) { cur := slog.SetLogLoggerLevel(slog.LevelDebug) // force debug if this option is on @@ -138,16 +149,19 @@ func main() { slog.SetLogLoggerLevel(cur) } } + st, err = ts.Up(ctx) if err != nil { slog.Error("failed to start tsnet server", slog.Any("error", err)) os.Exit(1) } + lc, err = ts.LocalClient() if err != nil { slog.Error("failed to get local client", slog.Any("error", err)) os.Exit(1) } + var ln net.Listener if *flagFunnel { if err := ipn.CheckFunnelAccess(uint16(*flagPort), st.Self); err != nil {