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 {