Tailscale operator OAuth credentials are rejected, blocking device cleanup and namespace termination #765
Labels
No labels
burndown-2026-06
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/infrastructure#765
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The Tailscale operator on kai-server cannot authenticate to the Tailscale API. Every device deletion fails with
401 Unauthorized, so the operator never releasestailscale.com/finalizeron the Services it manages. This blocks namespace termination and leaves orphaned tailnet devices behind.This is not caused by any single workload. It affects the operator globally, including a live service.
Evidence
The operator's service-reconciler logs the same failure on a loop:
1430 occurrences in the last 2000 log lines. Observed continuously from at least 16:19 UTC on 2026-08-06 through 20:12 UTC, retrying roughly every 17 minutes per affected Service.
Impact
Three namespaces retired by
ward exec retire-zombie-namespaces -- applyare stuck inTerminating. Their workloads, PVCs, and ExternalSecrets are gone. Only the namespace objects remain, each held by a Service carrying the Tailscale finalizer:coilysiren-backendcoilysiren-eco-mcp-appcoilysiren-eco-spec-trackerThe operator also fails to reconcile the live
forgejoService in theforgejonamespace. That failure predates and is independent of the retirement work, which means the operator cannot currently manage tailnet devices for anything, including services in active use.Each failed cleanup also leaves its device registered in the tailnet.
Root cause candidates
The operator reads its credential from the
operator-oauthSecret in thetailscalenamespace, mounted at/oauth/client_idand/oauth/client_secretviaCLIENT_ID_FILEandCLIENT_SECRET_FILE.That Secret was created on 2025-04-24 and is not backed by an ExternalSecret. No
ExternalSecretexists in thetailscalenamespace at all, so the value is static, hand-created, over a year old, and has no rotation path. An expired or revoked OAuth client is the leading explanation.Proposed work
operator-oauthis expired, revoked, or missing the scopes needed to delete devices.tag:k8sownership.operator-oauthto an ExternalSecret so it converges like every other credential and stops being a hand-created one-off. This is the durable fix, since the static Secret is the reason there is no rotation path today.Verification
kubectl -n tailscale logs deploy/operatorreports noAPI token invaliderrors.forgejoService reconciles cleanly.Notes
Do not force-remove
tailscale.com/finalizeras a shortcut. It unblocks the namespace but orphans the backing tailnet device and hides the credential failure that also affects the liveforgejoService.Found while retiring zombie namespaces on kai-server. Runbook is
scripts/k8s/retire-zombie-namespaces.sh, which now names this finalizer explicitly when a namespace survives.Still failing, 22 days later. Raising P3 to P2.
Re-verified against the live operator on kai-server 2026-08-28. The failure this issue describes has not stopped for a moment:
Six occurrences in the last 25 log lines alone, at 20:00Z, 20:17Z, 20:20Z, 20:34Z, 20:50Z and 21:07Z. Same
401, sameAPI token invalid, sameforgejoService. The operator pod itself is1/1 Runningwith0restarts and 28 days uptime, which is exactly why this stays invisible: nothing crashes, nothing alerts, and a health check on the pod passes while the controller it runs is completely unable to do its job.Why the priority is wrong
Filed as P3 on 2026-08-06 and untouched since. Three things make that the wrong tier:
coilysiren-backend,coilysiren-eco-mcp-appandcoilysiren-eco-spec-trackerremain stuck inTerminatingwith their workloads already goneRaising to P2. Not P1: nothing user-facing is down and the live
forgejoService still works, it just cannot have its tailnet device managed.The diagnosis in the body still holds
I checked nothing that contradicts it. The static
operator-oauthSecret from 2025-04-24, with no rotation path and no ExternalSecret in thetailscalenamespace, remains the leading explanation. Step 3 of the proposed work, converting it to an ExternalSecret, is the part that stops this recurring rather than just clearing it once.Also still true
The body's warning not to force-remove
tailscale.com/finalizeras a shortcut. That unblocks the namespaces and hides both the device leak and the credential failure affecting the live Service. Nothing here changes that.Half the impact is gone, and the way it went is the shortcut this issue warned against
Verified 2026-08-28. All three namespaces this issue names as stuck in
Terminatingare now absent:36 namespaces on kai-server, none in a non-
Activephase.Why that is not good news
The credential is still broken. The operator was logging
401 Unauthorized/API token invalidas recently as 21:07Z today, so it still cannot calltailscale.DeleteDevice.Those two facts together mean the namespaces did not finish terminating through the normal path, because the normal path requires the operator to release
tailscale.com/finalizerand it demonstrably still cannot. The most likely explanation is that the finalizer was force-removed, which is precisely what the Notes section of this issue says not to do:I did not do this and I do not know who or what did. Stated as the leading explanation given the evidence, not as established fact. An alternative is that something cleared them by another route, but any route that worked would have needed a working API token, and the token is not working.
What this changes
forgejoService failure is untouched and is the remaining live symptomRevised remaining work
Steps 1 through 3 of the original plan are unchanged and still the fix: confirm the OAuth client state, mint a replacement with device-write scope, and convert
operator-oauthto an ExternalSecret so it stops being a hand-created one-off with no rotation path.Step 4 is now moot, since the namespaces are gone.
Step 5 grew. The tailnet device audit was already needed for the ongoing failures. It now also needs to cover the three devices behind those namespaces, which nothing will clean up automatically.
Still failing, now measured rather than sampled. Most recent failure 2026-08-29T05:11:01Z, minutes before this comment.
#981raised this P3 to P2 on the strength of one log line at 21:07Z. Here is the shape of it.The error, verbatim and unchanging
API token invalid. Not expired, not rate-limited, not a network failure. The OAuth client credential the operator holds is rejected outright by the Tailscale API.The cadence is exact
Consecutive failures from the last seven hours:
Every 1000 seconds, to the second. That is the controller-runtime backoff cap, so it has been retrying at its slowest permitted rate rather than degrading. At 86.4 failures a day and the 22 days
#981established, that is roughly 1,900 consecutive failures.Every one is the same reconcile: same controller, same
forgejo/forgejoService, same single device id. The operator is wedged trying to delete one orphaned tailnet device and cannot authenticate to do it.The pod is still lying about it
Zero restarts in 28 days.
#981called this out as the reason nobody noticed and it is worth restating with the number attached: a component that has failed 1,900 times in a row presents as the healthiest pod on the cluster. Nothing inkubectl get poddistinguishes it from one doing its job.Third instance today of the
deploy#637class, afterdeploy#818at 851 restarts anddeploy#436at sixteen OOMKills. This one is the worst of the three for detection, because the other two at least move a restart counter.What is actually blocked, stated as inference
The observed failure is a delete that cannot authenticate. Marked as inference rather than measurement, because I did not test it: an invalid OAuth credential should fail device creation the same way, which would mean no new tailnet-exposed Service can be provisioned on kai-server at all, not merely that one stale device cannot be cleaned up.
If that holds it reframes this issue from a cleanup failure into a provisioning outage, and it touches
deploy#803anddeploy#456, both of which reason aboutingress-tailscalebehaviour. The cheap test is to apply a throwaway Service with the tailnet annotation and see whether a device appears. That wants an attended window, not a backlog pass.The fix is not reachable from this seat
A new Tailscale OAuth client is minted in the Tailscale admin console. No agent can do that. Same shape as
deploy#818, and it is the second credential today whose rotation only Kai can perform.Sequence once the credential exists: update the SSM-backed projection the operator reads, restart the operator, and confirm the 1000-second error cadence stops. The cadence itself is the verification signal and it is unusually clean, since silence for over 1000 seconds is proof rather than encouragement.
No change made. The credential is out of reach and restarting the operator without a valid one would only reset a counter that is already at zero.