Support Tailscale OAuth clients, not just API keys - #9
Open
jmeekhof wants to merge 1 commit into
Open
Conversation
Tailscale API keys expire 90 days after creation. When one expires,
dnsscale does not fail in any visible way: it keeps running, keeps
polling on its interval, and every request returns 401. Reconciliation
stops, the process stays healthy, and the zone quietly goes stale until
someone happens to read the logs.
I hit this on a deployment where dnsscale had been running for over
seven months since its last successful DNS write - the key expired 90
days in, and the only symptom was a 401 every 30 seconds.
OAuth client credentials do not expire, and the token is refreshed
automatically, so this failure mode goes away.
- tailscale.oauth_client_id / tailscale.oauth_client_secret, also
settable as TAILSCALE_OAUTH_CLIENT_ID / TAILSCALE_OAUTH_CLIENT_SECRET
and via flags.
- The default scope is devices:core:read and nothing else. dnsscale
issues exactly one Tailscale call, GET /api/v2/tailnet/{tailnet}
/devices, and never writes, so there is no reason for a wider grant.
tailscale.oauth_scopes can override it.
- Validate() requires exactly one of the two auth methods. Accepting
both would leave which one actually authenticates up to the reader.
- The API-key path now warns at startup that the key will expire, and a
401 on that path says so explicitly rather than reporting the bare
status text. That is what made this failure mode so quiet.
golang.org/x/oauth2 was already in the module graph as an indirect
dependency; it is pinned at the version already present (v0.27.0) so
this does not move the go directive or the toolchain.
Tests cover the auth-selection rules and run the client-credentials
exchange against a stub server, asserting the grant type, the requested
scope, and that the access token reaches the devices call.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Tailscale API keys expire 90 days after creation. When one expires, dnsscale doesn't fail in any way an operator would notice — it keeps running, keeps polling on its interval, and every request comes back 401. Reconciliation stops, the process stays "healthy", and the zone quietly goes stale.
I found this on a deployment where dnsscale had gone over seven months since its last successful DNS write. The key expired 90 days in and the only symptom was a 401 every 30 seconds in a log nobody was reading. Records that should have been managed had been created by hand instead, because as far as anyone could tell dnsscale just wasn't doing anything.
OAuth client credentials don't expire, and the token refreshes automatically, so the failure mode goes away entirely.
What changed
tailscale.oauth_client_id/tailscale.oauth_client_secret, also settable viaTAILSCALE_OAUTH_CLIENT_ID/TAILSCALE_OAUTH_CLIENT_SECRETand via flags.Default scope is
devices:core:readand nothing else. dnsscale makes exactly one Tailscale call —GET /api/v2/tailnet/{tailnet}/devices— and never writes, so there's no reason to ask for a wider grant.tailscale.oauth_scopescan override it if you disagree.Validate()requires exactly one of the two auth methods. Accepting both would leave which one actually authenticates up to the reader.The API-key path now warns at startup that the key will expire, and a 401 on that path says so explicitly instead of reporting the bare status text:
That message is arguably the more valuable half of this PR — it's what turns a silent stall into something diagnosable.
Compatibility
Existing API-key configurations are unaffected apart from the new startup warning. Nothing is deprecated or removed.
golang.org/x/oauth2was already in the module graph as an indirect dependency, and I pinned it at the version already present (v0.27.0) rather than lettinggo getupgrade it — so this does not move thegodirective or the toolchain version.Tests
Cover the auth-selection rules, and run the client-credentials exchange against a stub server asserting the grant type, the requested scope, and that the access token actually reaches the devices call (with a regression guard that the empty API key doesn't clobber the OAuth header). Plus a case pinning the improved 401 message.
go build ./...,go vet ./...andgo test ./...all pass.This is independent of #6, #7 and #8 and can be taken in any order.