tailscale/tailscale · error
failed to remove finalizer: %w
Error message
failed to remove finalizer: %w
What it means
After cleanup reported done, the reconciler strips the tailscale finalizer from the Ingress and calls a.Update. If that update fails — most commonly a 409 optimistic-lock conflict because the Ingress was modified concurrently (by a controller or kubectl apply), or an RBAC update denial — the finalizer stays and the Ingress cannot be garbage-collected.
Source
Thrown at cmd/k8s-operator/ingress.go:115
if ix < 0 {
logger.Debugf("no finalizer, nothing to do")
a.mu.Lock()
defer a.mu.Unlock()
a.managedIngresses.Remove(ing.UID)
gaugeIngressResources.Set(int64(a.managedIngresses.Len()))
return nil
}
if done, err := a.ssr.Cleanup(ctx, operatorTailnet, logger, childResourceLabels(ing.Name, ing.Namespace, "ingress"), proxyTypeIngressResource); err != nil {
return fmt.Errorf("failed to cleanup: %w", err)
} else if !done {
logger.Debugf("cleanup not done yet, waiting for next reconcile")
return nil
}
ing.Finalizers = append(ing.Finalizers[:ix], ing.Finalizers[ix+1:]...)
if err := a.Update(ctx, ing); err != nil {
return fmt.Errorf("failed to remove finalizer: %w", err)
}
// Unlike most log entries in the reconcile loop, this will get printed
// exactly once at the very end of cleanup, because the final step of
// cleanup removes the tailscale finalizer, which will make all future
// reconciles exit early.
logger.Infof("unexposed ingress from tailnet")
a.mu.Lock()
defer a.mu.Unlock()
a.managedIngresses.Remove(ing.UID)
gaugeIngressResources.Set(int64(a.managedIngresses.Len()))
return nil
}
// maybeProvision ensures that ing is exposed over tailscale, taking any actions
// necessary to reach that state.
//
// This function adds a finalizer to ing, ensuring that we can handle orderlyView on GitHub (pinned to cfe32b8be6)
Solutions
- If conflict: no action — the reconciler logs 'optimistic lock error, retrying' and the requeue retries the finalizer removal
- Verify update permission on ingresses.networking.k8s.io for the operator service account
- Reduce churn from other controllers writing the Ingress metadata during deletion
- If stuck, manually strip the finalizer: kubectl patch ingress <name> -n <ns> --type=json -p='[{"op":"remove","path":"/metadata/finalizers"}]' after confirming proxy resources are gone
Example fix
// before
if err := a.Update(ctx, ing); err != nil {
return fmt.Errorf("failed to remove finalizer: %w", err)
}
// after (retry once on conflict before surfacing)
if err := a.Update(ctx, ing); apierrors.IsConflict(err) {
// requeue immediately; someone else wrote the Ingress
return reconcile.Result{Requeue: true}, nil
} else if err != nil {
return fmt.Errorf("failed to remove finalizer: %w", err)
} Defensive patterns
Strategy: retry
Try / catch
if err := a.Update(ctx, ing); err != nil {
if apierrors.IsConflict(err) {
// Ingress changed concurrently; requeue rebuilds from a fresh Get
return reconcile.Result{Requeue: true}, nil
}
return fmt.Errorf("failed to remove finalizer: %w", err)
} Prevention
- Limit concurrent writers to Ingress metadata (one controller owns finalizers)
- Keep ingresses update permission in the operator RBAC
- Verify finalizer removal completed (kubectl get ingress -o jsonpath='{.metadata.finalizers}') before assuming cleanup finished
When it happens
Trigger: a.Update(ctx, ing) right after ing.Finalizers is sliced. Fires on concurrent Ingress writes between the Get at reconcile start and this Update (conflict), missing update permission on ingresses, or apiserver errors. The parent Reconcile already special-cases optimistic-lock errors and retries them.
Common situations: Another controller (cert-manager, Istio, Argo) writing the Ingress at the same moment; kubectl apply racing the deletion; RBAC without ingresses/update.
Related errors
- failed to remove finalizer %q: %w
- failed to cleanup Connector resources: %w
- failed to update Ingress status: %w
- error updating ProxyGroup config Secret: %w
- failed to get ing: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/138422cee21dc8d5.
Report an issue: GitHub.