Upgrade and roll back
Upgrade
Validate existing CRs against the new schema before installing new CRDs — admission rules tighten between releases and the check is offline (Validation ):
live=$(mktemp); kubectl get tailnet,tailnetegress,tailnetingress -A -o json > "$live"
LIVE_CR_FILE="$live" task test-validation; rm -f -- "$live"- Apply the new CRDs. Helm installs CRDs from
crds/but never upgrades them: runtask crd-upgrade KUBE_CONTEXT=<context>for a manual installation, or setinstall.crds: CreateReplaceandupgrade.crds: CreateReplaceon a FluxHelmRelease. helm upgrade(or apply the newdeploy/). The operator re-rolls every agent whose image changed; agent Deployments useRecreate, so each affected CR’s data path is interrupted for the restart.kubectl get tailnet—COMPAT=Compatible,POLICY=InSyncorApplied,DNS=CannotVerify.kubectl get tailnetegress,tailnetingress -A— every rowREADY=True; test one egress and one ingress connection.
A pure operator restart with unchanged desired state does not restart agents; a Ready=False during
an agent image change is a real interruption, not a status flap. Breaking changes per version are
Verify after the upgrade
- Apply the new CRDs, then the Deployment (image +
OPERATOR_IMAGE). - The operator re-rolls agent Deployments whose image changed.
- Verify:
kubectl -n tailnet-system rollout status deploy/tailnet-operator
kubectl get tailnetegress,tailnetingress -A
# every row READY=True
kubectl get tailnet corp # COMPAT=Compatible; POLICY=Applied/InSync; DNS=CannotVerify (manual)Rollback
| Situation | Action |
|---|---|
A render is wrong but not yet applied (DryRun) | fix the base or the CR; nothing reached Headscale |
Headscale rejected the render (PolicyApplied=False/PolicyRejected) | the previous policy is still active; read the message, fix — the write is retried with backoff (15 s doubling to 1 min; transport failures to 5 min), a base/CR change retries at once |
| The operator must stop writing | policy.mode: DryRun — the operator only renders from now on |
| Restore the Git policy on the server | kubectl -n tailnet-system exec deploy/headscale -- headscale policy set -f /acl/policy (mode DryRun first, or it is overwritten within 5 min) |
| Leave database mode | HEADSCALE_POLICY_MODE=file, restart; the mounted file is authoritative again |
| Downgrade the operator | restore both the previous container image and OPERATOR_IMAGE (agents also roll back); the newer CRDs and their admission rules remain installed |
| Relax the v0.3.0 admission rules | restore the previous deploy/crds/ manifests through the same deployment mechanism; never delete the CRDs, which also deletes their CRs |
Neither rollback restores a previously applied Headscale policy — switch to DryRun first if writes
must stop. Keep the desired CR manifests in Git: an older schema prunes newer optional fields on write.
Source: docs/how-to/upgrade-rollback.md at v0.18.0 — found a mistake? Tell us.