Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
ENTRYPOINT ["/run.sh"]
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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

<details>
Expand All @@ -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 .
```

</details>
Expand Down Expand Up @@ -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 <path>` | Directory path to save tsnet and tsidp state. Recommend to be set. | `""` |
| `-hostname <hostname>` | hostname on tailnet. Will become `<hostname>.your-tailnet.ts.net` | `idp` |
| `-port <port>` | Port to listen on | `443` |
Expand All @@ -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 <path>`.

- `TS_AUTHKEY=<key>`: 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>`: 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=<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
Expand All @@ -162,13 +193,15 @@ The Docker image exposes the CLI flags through environment variables. If omitted
| `TSIDP_LOG=<level>` | `-log <level>` |
| `TSIDP_DEBUG_TSNET=1` | `-debug-tsnet` |
| `TSIDP_DEBUG_ALL_REQUESTS=1` | `-debug-all-requests` |
| `TS_AUTHKEY=<key>` | _(env var only)_ |
| `TS_ADVERTISE_TAGS=<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)

Expand Down
8 changes: 4 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
2 changes: 1 addition & 1 deletion scripts/docker/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ if [ -n "$TSIDP_DEBUG_TSNET" ]; then
fi

# Execute tsidp-server with the built arguments
exec /tsidp-server $ARGS "$@"
exec /tsidp-server $ARGS "$@"
14 changes: 14 additions & 0 deletions tsidp-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -131,23 +132,36 @@ 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
slog.Debug(fmt.Sprintf(format, args...))
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 {
Expand Down