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
@@ -1,11 +1,11 @@
ARG GO_VERSION=1.24
ARG GO_VERSION=1.25
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine as build

WORKDIR /work

# Install git so that go build populates the VCS details in build info, which
# is then reported to Tailscale in the node version string.
RUN apk --no-cache add git=2.47.2-r0
RUN apk --no-cache add git

COPY go.mod go.sum ./
RUN go mod download
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ go build ./cmd/caddy

### Running examples


Multiple example configurations are provided in the [examples directory].
These examples expect an [auth key] to be set in the `TS_AUTHKEY` environment variable.
All nodes registered while running these examples will be ephemeral and removed after disconnect.
Expand All @@ -56,6 +57,29 @@ Run them with:
TS_AUTHKEY=<tskey-auth-XXXXX> ./caddy run -c examples/<file>
```

#### Using OAuth for Tailscale Auth

You can use OAuth credentials to register Tailscale nodes instead of a standard auth key.
Set the following environment variables before running Caddy:

```sh
export TS_AUTHKEY=$TS_CLIENT_SECRET
export TS_API_CLIENT_ID=$TS_CLIENT_ID
```

This will use your OAuth client credentials to generate a Tailscale auth key at runtime.

See the [multi-node example](./examples/multi-node.caddyfile) for a sample configuration and Docker run command:

```sh
docker run -it \
-e TS_API_CLIENT_ID=$TS_CLIENT_ID \
-e TS_AUTHKEY=$TS_CLIENT_SECRET \
-e TS_DOMAIN=$TS_DOMAIN \
-v ./examples/multi-node.caddyfile:/etc/caddy/Caddyfile \
caddy-tailscale
```

[examples directory]: ./examples/

### Docker
Expand Down
26 changes: 26 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type App struct {
// WebUI specifies whether Tailscale nodes should run the Web UI for remote management.
WebUI bool `json:"webui,omitempty" caddy:"namespace=tailscale.webui"`

// Tags specifies the default tags to apply to Tailscale nodes.
Tags []string `json:"tags,omitempty" caddy:"namespace=tailscale.tags"`

// Nodes is a map of per-node configuration which overrides global options.
Nodes map[string]Node `json:"nodes,omitempty" caddy:"namespace=tailscale"`

Expand Down Expand Up @@ -71,6 +74,9 @@ type Node struct {
// StateDir specifies the state directory for the node.
StateDir string `json:"state_dir,omitempty" caddy:"namespace=tailscale.state_dir"`

// Tags specifies the tags to apply to the node.
Tags []string `json:"tags,omitempty" caddy:"namespace=tailscale.tags"`

name string
}

Expand All @@ -91,6 +97,22 @@ func (t *App) Start() error {
}

func (t *App) Stop() error {
// On shutdown, destruct all nodes to ensure ephemeral nodes are removed.
var errList []error
nodes.Range(func(key, value any) bool {
if n, ok := value.(interface{ Destruct() error }); ok && n != nil {
if err := n.Destruct(); err != nil {
if t.logger != nil {
t.logger.Warn("Failed to destruct node", zap.Any("node", key), zap.Error(err))
}
errList = append(errList, err)
}
}
return true
})
if len(errList) > 0 {
return errList[0] // Return first error (could be improved)
}
return nil
}

Expand Down Expand Up @@ -142,6 +164,8 @@ func parseAppConfig(d *caddyfile.Dispenser, _ any) (any, error) {
} else {
app.WebUI = true
}
case "tags":
app.Tags = d.RemainingArgs()
default:
node, err := parseNodeConfig(d)
if app.Nodes == nil {
Expand Down Expand Up @@ -223,6 +247,8 @@ func parseNodeConfig(d *caddyfile.Dispenser) (Node, error) {
} else {
node.WebUI = opt.NewBool(true)
}
case "tags":
node.Tags = segment.RemainingArgs()
default:
return node, segment.Errf("unrecognized subdirective: %s", segment.Val())
}
Expand Down
37 changes: 37 additions & 0 deletions examples/multi-node.caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Multi-node Tailscale configuration example
# This creates two separate Tailscale nodes under one Caddy instance
# Each node responds with a simple message identifying itself


# Run it with:
# docker run -it \
# -e TS_API_CLIENT_ID=$TS_API_CLIENT_ID \
# -e TS_AUTHKEY=$TS_AUTHKEY \
# -e TS_DOMAIN=$TS_DOMAIN \
# -v ./examples/multi-node.caddyfile:/etc/caddy/Caddyfile \
# caddy-tailscale

{
tailscale {
ephemeral true
tags tag:test

test-server-1 {
hostname test-server-1
}

test-server-2 {
hostname test-server-2
}
}
}

https://test-server-1.{$TS_DOMAIN} {
bind tailscale/test-server-1
respond "Hello from test-server-1! 🎯"
}

https://test-server-2.{$TS_DOMAIN} {
bind tailscale/test-server-2
respond "Hello from test-server-2! 🎯"
}
2 changes: 2 additions & 0 deletions examples/simple.caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
{
tailscale {
ephemeral
# Using tags to allow OAuth tokens usage
tags tag:test
}
}

Expand Down
87 changes: 39 additions & 48 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
module github.com/tailscale/caddy-tailscale

go 1.23.1

toolchain go1.23.5
go 1.25.1

require (
github.com/caddyserver/caddy/v2 v2.9.1
github.com/caddyserver/certmagic v0.21.6
github.com/google/go-cmp v0.6.0
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
go.uber.org/zap v1.27.0
tailscale.com v1.80.1
golang.org/x/oauth2 v0.31.0
tailscale.com v1.88.2
)

require (
Expand All @@ -27,30 +26,29 @@ require (
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
Expand All @@ -62,10 +60,10 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gaissmai/bart v0.11.1 // indirect
github.com/gaissmai/bart v0.18.0 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
Expand All @@ -82,19 +80,16 @@ require (
github.com/google/btree v1.1.2 // indirect
github.com/google/cel-go v0.21.0 // indirect
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/go-tpm v0.9.4 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/illarion/gonotify/v2 v2.0.3 // indirect
github.com/illarion/gonotify/v3 v3.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
Expand All @@ -107,7 +102,6 @@ require (
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
Expand All @@ -124,11 +118,10 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
Expand All @@ -148,26 +141,24 @@ require (
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
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-20250107165329-0b8b35511f19 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
github.com/urfave/cli v1.22.14 // 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
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.3.9 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 // indirect
Expand All @@ -191,25 +182,25 @@ require (
go.uber.org/zap/exp v0.3.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
google.golang.org/protobuf v1.36.3 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect
howett.net/plist v1.0.0 // indirect
)
Loading