tailscale/tailscale · error

failed to remove finalizer from Tailnet %q: %w

Error message

failed to remove finalizer from Tailnet %q: %w

What it means

During Tailnet deletion, reconciler.ClearFinalizer patches the object to drop the operator's finalizer so Kubernetes can complete the delete. If that PATCH fails, this wrapped error is returned and the Tailnet object stays in Terminating because the finalizer was never removed.

Source

Thrown at k8s-operator/reconciler/tailnet/tailnet.go:141

	var tailnet tsapi.Tailnet
	err := r.Get(ctx, req.NamespacedName, &tailnet)
	switch {
	case apierrors.IsNotFound(err):
		return reconcile.Result{}, nil
	case err != nil:
		return reconcile.Result{}, fmt.Errorf("failed to get Tailnet %q: %w", req.NamespacedName, err)
	}

	if !tailnet.DeletionTimestamp.IsZero() {
		return r.delete(ctx, &tailnet)
	}

	return r.createOrUpdate(ctx, &tailnet)
}

func (r *Reconciler) delete(ctx context.Context, tailnet *tsapi.Tailnet) (reconcile.Result, error) {
	if err := reconciler.ClearFinalizer(ctx, r.Client, tailnet, reconciler.Finalizer); err != nil {
		return reconcile.Result{}, fmt.Errorf("failed to remove finalizer from Tailnet %q: %w", tailnet.Name, err)
	}

	r.tracker.Remove(tailnet.UID)
	r.registry.Remove(tailnet.Name)

	return reconcile.Result{}, nil
}

// Constants for condition reasons.
const (
	ReasonInvalidOAuth  = "InvalidOAuth"
	ReasonInvalidSecret = "InvalidSecret"
	ReasonValid         = "TailnetValid"
)

func (r *Reconciler) createOrUpdate(ctx context.Context, tailnet *tsapi.Tailnet) (reconcile.Result, error) {
	r.tracker.Add(tailnet.UID)

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Verify RBAC on the finalizers subresource: kubectl auth can-i update tailnets/finalizers --as=system:serviceaccount:<ns>:<operator-sa>
  2. Check kubectl describe tailnet <name> for events and webhook denials
  3. Wait for the automatic requeue; conflicts usually clear on the next attempt
  4. If the operator was uninstalled and the object is stuck, strip the finalizer manually: kubectl patch tailnet <name> --type=merge -p '{"metadata":{"finalizers":[]}}'

Example fix

# before: object stuck in Terminating after operator removal
kubectl get tailnet <name>   # shows DELETING forever
# after: manual finalizer removal
kubectl patch tailnet <name> --type=merge -p '{"metadata":{"finalizers":[]}}'
Defensive patterns

Strategy: retry

Try / catch

if err := reconciler.ClearFinalizer(ctx, r.Client, tailnet, reconciler.Finalizer); err != nil {
    if apierrors.IsConflict(err) {
        return reconcile.Result{Requeue: true}, nil // next pass uses a fresh object
    }
    return reconcile.Result{}, err
}

Prevention

When it happens

Trigger: The PATCH on the Tailnet failing: operator RBAC missing update/patch on tailnets/finalizers; apiserver errors or optimistic conflicts when other controllers patch the same object concurrently; admission webhooks (OPA/Kyverno) rejecting finalizer removal.

Common situations: Operator permissions narrowed after install; policy engines blocking finalizer patches; operator restarted mid-delete with a stale cached object.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/9ade041c018c15ae. Report an issue: GitHub.