Skip to content

Commit 621114c

Browse files
authored
Merge pull request #4 from sundaycarwash/cp/oauth-client
Add support for authenticating against Tailscale using OAuth client secrets
2 parents ae3af9a + f111f22 commit 621114c

9 files changed

+198
-30
lines changed

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"WebFetch(domain:tailscale.com)"
5+
],
6+
"deny": [],
7+
"ask": []
8+
}
9+
}

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
## Unreleased
22

3-
- Update README.
3+
- **Add OAuth client support**: The buildpack now supports Tailscale OAuth clients as an authentication method. OAuth client secrets never expire (unlike traditional auth keys which expire every 90 days), making them the recommended approach for Heroku deployments. When using OAuth clients:
4+
- Set `TAILSCALE_AUTH_KEY` to your OAuth client secret (starts with `tskey-client-`)
5+
- Set `TAILSCALE_ADVERTISE_TAGS` to match the tags configured in your OAuth client (required)
6+
- The buildpack automatically configures `ephemeral=true` and `preauthorized=true` for Heroku's dyno lifecycle
7+
- Traditional auth keys remain fully supported for backward compatibility
8+
- **Fix**: `TAILSCALE_ADVERTISE_TAGS` now properly passed to `tailscale up` command
9+
- Update README with OAuth client setup instructions
410
- Upgrade Tailscale (1.76.6)
511

612
## 1.1.2

README.md

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,37 @@ Run [Tailscale](https://tailscale.com/) on a Heroku dyno.
44

55
## Usage
66

7-
To set up your Heroku application, add the buildpack and `TAILSCALE_AUTH_KEY`
8-
environment variable:
7+
### OAuth Clients (Recommended)
98

10-
$ heroku buildpacks:add https://github.com/sundaycarwash/heroku-buildpack-tailscale
11-
$ heroku config:set TAILSCALE_AUTH_KEY="..."
9+
OAuth clients are the recommended authentication method because they **never expire**, unlike traditional auth keys which expire every 90 days.
10+
11+
**1. Create an OAuth client in Tailscale:**
12+
- Go to [Tailscale Admin Console → Settings → OAuth clients](https://login.tailscale.com/admin/settings/oauth)
13+
- Click "Generate OAuth client"
14+
- Select the `auth_keys` scope (under "Read" section)
15+
- Select one or more tags (e.g., `tag:heroku`, `tag:production`)
16+
- Save the **Client Secret** (starts with `tskey-client-`)
17+
18+
**2. Configure your Heroku app:**
19+
20+
```bash
21+
$ heroku buildpacks:add https://github.com/sundaycarwash/heroku-buildpack-tailscale
22+
$ heroku config:set TAILSCALE_AUTH_KEY="tskey-client-..."
23+
$ heroku config:set TAILSCALE_ADVERTISE_TAGS="tag:heroku" # Must match tags from OAuth client
24+
```
25+
26+
**Note:** The `TAILSCALE_ADVERTISE_TAGS` must match one of the tags you selected when creating the OAuth client.
27+
28+
### Traditional Auth Keys (Legacy)
29+
30+
You can also use traditional auth keys, but note they expire every 90 days:
31+
32+
```bash
33+
$ heroku buildpacks:add https://github.com/sundaycarwash/heroku-buildpack-tailscale
34+
$ heroku config:set TAILSCALE_AUTH_KEY="tskey-auth-..."
35+
```
36+
37+
### Using the SOCKS5 Proxy
1238

1339
To have your processes connect through the Tailscale proxy, you need to use
1440
the `socks5` proxy provided by `tailscaled`.
@@ -62,9 +88,14 @@ The following settings are available for configuration via environment variables
6288
- `TAILSCALE_ADVERTISE_EXIT_NODES` - Offer to be an exit node for outbound internet traffic
6389
from the Tailscale network. Defaults to not advertising.
6490
- `TAILSCALE_ADVERTISE_TAGS` - Give tagged permissions to this device. You must be listed in
65-
\"TagOwners\" to be able to apply tags. Defaults to none.
66-
- `TAILSCALE_AUTH_KEY` - Provide an auth key[^1] to automatically authenticate the node as your
67-
user account. **This must be set.**
91+
\"TagOwners\" to be able to apply tags. **Required when using OAuth clients.** Defaults to none
92+
for traditional auth keys.
93+
- `TAILSCALE_AUTH_KEY` - Provide authentication credentials to automatically authenticate the node.
94+
**This must be set.** Can be either:
95+
- **OAuth client secret** (recommended, starts with `tskey-client-`): Never expires. Requires
96+
`TAILSCALE_ADVERTISE_TAGS` to be set. Automatically configured with `ephemeral=true` and
97+
`preauthorized=true` for Heroku's dyno lifecycle.
98+
- **Traditional auth key** (starts with `tskey-auth-`): Expires every 90 days[^1].
6899
- `TAILSCALE_HOSTNAME` - Provide a hostname to use for the device instead of the one provided
69100
by the OS. Note that this will change the machine name used in MagicDNS. Defaults to the
70101
hostname of the application (a guid). If you have [Heroku Labs runtime-dyno-metadata](https://devcenter.heroku.com/articles/dyno-metadata)

bin/heroku-tailscale-start.sh

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,27 @@ if [ -z "$TAILSCALE_AUTH_KEY" ]; then
1616
else
1717
log "Starting Tailscale"
1818

19+
# Detect if using OAuth client secret (starts with tskey-client-)
20+
if [[ "$TAILSCALE_AUTH_KEY" == tskey-client-* ]]; then
21+
log "Detected OAuth client secret"
22+
23+
# OAuth clients require tags to be set
24+
if [ -z "$TAILSCALE_ADVERTISE_TAGS" ]; then
25+
log "ERROR: TAILSCALE_ADVERTISE_TAGS must be set when using OAuth client authentication"
26+
log "OAuth clients require tags. Set TAILSCALE_ADVERTISE_TAGS to match the tags configured in your OAuth client."
27+
exit 1
28+
fi
29+
30+
# Append OAuth parameters for Heroku dyno environment
31+
# ephemeral=true: Auto-remove nodes when dynos stop (keeps node list clean)
32+
# preauthorized=true: Skip manual approval (required for automation)
33+
auth_key="${TAILSCALE_AUTH_KEY}?ephemeral=true&preauthorized=true"
34+
log "Using OAuth client with ephemeral=true and preauthorized=true"
35+
else
36+
log "Using traditional auth key"
37+
auth_key="${TAILSCALE_AUTH_KEY}"
38+
fi
39+
1940
# Only use the first 8 characters of the commit sha.
2041
# Swap the . and _ in the dyno with a - since tailscale doesn't
2142
# allow for periods.
@@ -33,16 +54,61 @@ else
3354
fi
3455
log "Using Tailscale hostname=$tailscale_hostname"
3556

57+
# Build the advertise-tags parameter if set
58+
if [ -n "$TAILSCALE_ADVERTISE_TAGS" ]; then
59+
advertise_tags="--advertise-tags=${TAILSCALE_ADVERTISE_TAGS}"
60+
else
61+
advertise_tags=""
62+
fi
63+
3664
tailscaled -verbose ${TAILSCALED_VERBOSE:-0} --tun=userspace-networking --socks5-server=localhost:1055 &
37-
until tailscale up \
38-
--authkey=${TAILSCALE_AUTH_KEY} \
39-
--hostname="$tailscale_hostname" \
40-
--accept-dns=${TAILSCALE_ACCEPT_DNS:-true} \
41-
--accept-routes=${TAILSCALE_ACCEPT_ROUTES:-true} \
42-
--advertise-exit-node=${TAILSCALE_ADVERTISE_EXIT_NODE:-false} \
43-
--shields-up=${TAILSCALE_SHIELDS_UP:-false}
44-
do
45-
log "Waiting for 5s for Tailscale to start"
65+
66+
# Retry configuration
67+
max_retries=6 # 6 attempts * 5 seconds = 30 seconds max wait time
68+
retry_count=0
69+
70+
while true; do
71+
# Try to start Tailscale
72+
# On success, tailscale up returns 0 and we break out of the loop
73+
# On failure, we capture the output for error detection
74+
set +e # Temporarily disable exit on error
75+
tailscale up \
76+
--authkey=${auth_key} \
77+
--hostname="$tailscale_hostname" \
78+
--accept-dns=${TAILSCALE_ACCEPT_DNS:-true} \
79+
--accept-routes=${TAILSCALE_ACCEPT_ROUTES:-true} \
80+
--advertise-exit-node=${TAILSCALE_ADVERTISE_EXIT_NODE:-false} \
81+
--shields-up=${TAILSCALE_SHIELDS_UP:-false} \
82+
${advertise_tags} 2>&1 | tee /tmp/tailscale-up-output.log
83+
exit_code=$?
84+
set -e # Re-enable exit on error
85+
86+
if [ $exit_code -eq 0 ]; then
87+
# Success!
88+
break
89+
fi
90+
91+
# Failed - check for permanent authentication errors
92+
if grep -qi "invalid key" /tmp/tailscale-up-output.log 2>/dev/null; then
93+
log "ERROR: Authentication failed - invalid or expired auth key"
94+
log "The auth key may have expired (traditional keys expire after 90 days) or been revoked."
95+
log "Please generate a new auth key or OAuth client secret and update TAILSCALE_AUTH_KEY."
96+
exit 1
97+
fi
98+
99+
# Increment retry counter
100+
retry_count=$((retry_count + 1))
101+
102+
# Check if we've exceeded max retries
103+
if [ $retry_count -ge $max_retries ]; then
104+
log "ERROR: Tailscale failed to start after $max_retries attempts ($(($max_retries * 5)) seconds)"
105+
log "Last error output:"
106+
cat /tmp/tailscale-up-output.log 2>/dev/null | indent || true
107+
exit 1
108+
fi
109+
110+
# Wait before retrying
111+
log "Waiting for 5s for Tailscale to start (attempt $retry_count/$max_retries)"
46112
sleep 5
47113
done
48114

test/fixtures/heroku-tailscale-start.sh-envs.stdout.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
-----> Starting Tailscale
2+
-----> Using traditional auth key
23
-----> Using Tailscale hostname=test-host--
34
>>> mocked tailscaled -verbose 1 call <<<
45
>>> mocked tailscale call
5-
--authkey=ts-auth-test
6-
--hostname=test-host
6+
--authkey=tskey-auth-test
7+
--hostname=test-host--
78
--accept-dns=false
89
--accept-routes=false
910
--advertise-exit-node=true

test/fixtures/heroku-tailscale-start.sh-hostname.stdout.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
-----> Starting Tailscale
2+
-----> Using traditional auth key
23
-----> Using Tailscale hostname=hunter20-another-web-1-heroku-app
34
>>> mocked tailscaled -verbose 1 call <<<
45
>>> mocked tailscale call
5-
--authkey=ts-auth-test
6-
--hostname=test
6+
--authkey=tskey-auth-test
7+
--hostname=hunter20-another-web-1-heroku-app
78
--accept-dns=false
89
--accept-routes=false
910
--advertise-exit-node=true

test/fixtures/heroku-tailscale-start.sh-oauth-with-tags.stderr.txt

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-----> Starting Tailscale
2+
-----> Detected OAuth client secret
3+
-----> Using OAuth client with ephemeral=true and preauthorized=true
4+
-----> Using Tailscale hostname=oauth-test--
5+
>>> mocked tailscaled -verbose 1 call <<<
6+
>>> mocked tailscale call
7+
--authkey=tskey-client-oauth-test?ephemeral=true&preauthorized=true
8+
--hostname=oauth-test--
9+
--accept-dns=false
10+
--accept-routes=false
11+
--advertise-exit-node=false
12+
--shields-up=false
13+
--advertise-tags=tag:heroku
14+
<<<
15+
-----> Tailscale started

test/test-heroku-tailscale-start.sh

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,75 @@ function tailscale() {
1313
# Sleep to allow tailscaled to finish processing in the
1414
# background and avoid flapping tests.
1515
sleep 0.01
16+
17+
# Extract parameters from the command line
18+
local auth_key=""
19+
local hostname=""
20+
local accept_dns=""
21+
local accept_routes=""
22+
local advertise_exit_node=""
23+
local shields_up=""
24+
local advertise_tags=""
25+
26+
while [[ "$#" -gt 0 ]]; do
27+
case $1 in
28+
--authkey=*) auth_key="${1#*=}" ;;
29+
--hostname=*) hostname="${1#*=}" ;;
30+
--accept-dns=*) accept_dns="${1#*=}" ;;
31+
--accept-routes=*) accept_routes="${1#*=}" ;;
32+
--advertise-exit-node=*) advertise_exit_node="${1#*=}" ;;
33+
--shields-up=*) shields_up="${1#*=}" ;;
34+
--advertise-tags=*) advertise_tags="${1#*=}" ;;
35+
esac
36+
shift
37+
done
38+
1639
echo ">>> mocked tailscale call
17-
--authkey=${TAILSCALE_AUTH_KEY}
18-
--hostname=${TAILSCALE_HOSTNAME:-test}
19-
--accept-dns=${TAILSCALE_ACCEPT_DNS:-true}
20-
--accept-routes=${TAILSCALE_ACCEPT_ROUTES:-true}
21-
--advertise-exit-node=${TAILSCALE_ADVERTISE_EXIT_NODE:-false}
22-
--shields-up=${TAILSCALE_SHIELDS_UP:-false}
40+
--authkey=${auth_key}
41+
--hostname=${hostname}
42+
--accept-dns=${accept_dns}
43+
--accept-routes=${accept_routes}
44+
--advertise-exit-node=${advertise_exit_node}
45+
--shields-up=${shields_up}${advertise_tags:+
46+
--advertise-tags=${advertise_tags}}
2347
<<<"
2448
}
2549

2650
export -f tailscale
2751

2852

53+
# Test 1: Basic sanity test with traditional auth key
2954
run_test sanity heroku-tailscale-start.sh
55+
56+
# Test 2: Traditional auth key with all env vars
3057
TAILSCALED_VERBOSE=1 \
31-
TAILSCALE_AUTH_KEY="ts-auth-test" \
58+
TAILSCALE_AUTH_KEY="tskey-auth-test" \
3259
TAILSCALE_HOSTNAME="test-host" \
3360
TAILSCALE_ACCEPT_DNS="false" \
3461
TAILSCALE_ACCEPT_ROUTES="false" \
3562
TAILSCALE_ADVERTISE_EXIT_NODE="true" \
3663
TAILSCALE_SHIELDS_UP="true" \
3764
run_test envs heroku-tailscale-start.sh
3865

66+
# Test 3: Traditional auth key with hostname generation
3967
TAILSCALED_VERBOSE=1 \
40-
TAILSCALE_AUTH_KEY="ts-auth-test" \
68+
TAILSCALE_AUTH_KEY="tskey-auth-test" \
4169
HEROKU_APP_NAME="heroku-app" \
4270
DYNO="another_web.1" \
4371
HEROKU_SLUG_COMMIT="hunter20123456789"\
4472
TAILSCALE_ACCEPT_DNS="false" \
4573
TAILSCALE_ACCEPT_ROUTES="false" \
4674
TAILSCALE_ADVERTISE_EXIT_NODE="true" \
4775
TAILSCALE_SHIELDS_UP="true" \
48-
run_test hostname heroku-tailscale-start.sh
76+
run_test hostname heroku-tailscale-start.sh
77+
78+
# Test 4: OAuth client with tags (should succeed)
79+
TAILSCALED_VERBOSE=1 \
80+
TAILSCALE_AUTH_KEY="tskey-client-oauth-test" \
81+
TAILSCALE_ADVERTISE_TAGS="tag:heroku" \
82+
TAILSCALE_HOSTNAME="oauth-test" \
83+
TAILSCALE_ACCEPT_DNS="false" \
84+
TAILSCALE_ACCEPT_ROUTES="false" \
85+
TAILSCALE_ADVERTISE_EXIT_NODE="false" \
86+
TAILSCALE_SHIELDS_UP="false" \
87+
run_test oauth-with-tags heroku-tailscale-start.sh

0 commit comments

Comments
 (0)