Skip to content

Migration v1 to v2

Eran Kampf edited this page Aug 12, 2026 · 15 revisions

v2 makes four breaking changes, notably to the TwingateResource CRD, to better support multiple Identity Firewall protocols and integration with the Gateway. Start with Does this affect you? to see which of them apply to your install.

Throughout this page, a TwingateResource is the Kubernetes object and a Twingate Resource is what it registers with Twingate, identified by spec.id. The distinction is what decides whether access is interrupted: an object that keeps its spec.id has its Twingate Resource updated in place, while one that is replaced has its Twingate Resource deprovisioned and a new one registered.

<release> in the object names below is your Helm release name, twop in the command examples. If your release name already contains gateway, or you set the subchart's gateway.fullnameOverride, the subchart drops the -gateway suffix and the object names differ; confirm them with kubectl get twingateresource.

<release> in the object names below is your Helm release name, twop in the command examples. They assume the subchart's fullname is <release>-gateway, which differs if your release name already contains gateway or you set gateway.nameOverride or gateway.fullnameOverride; confirm the names with kubectl get twingateresource.

Does this affect you?

If your install has What you have to do Access impact
Any install Apply the new CRDs, then run helm upgrade None
The bundled gateway subchart (gateway.enabled: true) Upgrade through v1.3 first, then restructure the subchart values None
The bundled gateway subchart, and you cannot reach v1.3 Disable the subchart, delete the in-cluster Kubernetes resource, then re-provision it Kubernetes API access is down until it is re-provisioned
A hand-authored TwingateResource of type: Kubernetes Bind it to a TwingateGateway by patching spec.gatewayRef onto it None
A TwingateResourceAccess outside the default namespace whose resourceRef omits namespace Delete it and recreate it with an explicit resourceRef.namespace, while still on v1 (step 1) That grant is down between the delete and the recreate
Legacy twingate.com/resource* annotations on a Service Rename them to the resource.twingate.com* form, while still on v1 (step 2) Network resources are interrupted until the replacement reconciles

What changed

Bundled gateway subchart values restructured

The bundled gateway subchart no longer configures in-cluster Kubernetes access by annotating the Gateway Service. It renders the TwingateGateway, its TwingateCertificateAuthority, and the TwingateResource directly, so the twingate.resource block and its extraAnnotations are replaced by fields under twingateOperator.gateway.* and twingateOperator.kubernetesResource.*. See Gateway subchart values for the before and after.

TwingateResource.spec.proxy replaced by gatewayRef

The proxy serving a Kubernetes resource is now a separate TwingateGateway object, referenced with spec.gatewayRef, in place of the inline spec.proxy block (see Identity Firewall).

  • spec.proxy is removed from the CRD, along with its address, certificateAuthorityCert, and certificateAuthorityCertSecretRef fields.
  • spec.gatewayRef is required on Kubernetes and WebApp resources, and rejected on Network ones.

Applying the new CRDs does not rewrite stored objects, but from that point a Kubernetes resource without gatewayRef is frozen: it can be read, and the only write the API server accepts is one that adds gatewayRef (which also drops spec.proxy). Keep that window short; see If you hand-authored Kubernetes resources.

Object references resolve in their own namespace

A reference that omits namespace now resolves in the declaring object's namespace instead of the default namespace, following Kubernetes convention. On a TwingateResourceAccess:

  • resourceRef has to be fixed. It carried no default in the v1 CRD, so nothing is stored on existing objects and resolution moves the moment the v2 operator runs, silently granting or withdrawing access. Pin it while still on v1, in Before you upgrade, step 1.
  • groupRef needs no action. The v1 CRD stored namespace: default on every object, so it keeps resolving to the default namespace no matter which namespace the TwingateResourceAccess is in.

See Namespace resolution for the detail.

Legacy Service annotations removed

The old twingate.com/resource* annotations no longer work; use the resource.twingate.com* form. The mapping is:

v1 (removed) v2
twingate.com/resource resource.twingate.com
twingate.com/resource-name resource.twingate.com/name
twingate.com/resource-alias resource.twingate.com/alias
twingate.com/resource-isVisible resource.twingate.com/isVisible
twingate.com/resource-isBrowserShortcutEnabled resource.twingate.com/isBrowserShortcutEnabled
twingate.com/resource-securityPolicyId resource.twingate.com/securityPolicyId
twingate.com/resource-syncLabels resource.twingate.com/syncLabels

Before you upgrade

Apply both while still running v1. Neither needs the v2 operator. Step 1 has to land before the v2 operator starts, because that is when resourceRef resolution moves. Doing it before you apply the new CRDs is less work: the v1 CRDs still inject namespace: default into groupRef on write, and once they are replaced you have to set that namespace yourself.

  1. Pin resourceRef references. If a TwingateResourceAccess outside the default namespace references a TwingateResource without naming its namespace, delete it and recreate it with an explicit resourceRef.namespace. resourceRef is immutable, so it cannot be patched in place, and the principal loses access to the resource until the object is back.

    Recreate it while still on v1 so groupRef keeps its stored namespace: default. If you instead recreate it after applying the v2 CRDs, set groupRef.namespace explicitly whenever the TwingateGroup is not in the TwingateResourceAccess's own namespace.

  2. Rename legacy annotations. Rename any legacy annotations on your Service objects to the resource.twingate.com* form using the mapping in Legacy Service annotations removed.

    On Network resources the rename replaces the generated TwingateResource, so its Twingate Resource is deprovisioned and a new one is registered under a new spec.id. Access through it is interrupted until the replacement reconciles. TwingateResourceAccess objects reference the generated object by its name, <service>-resource, which does not change, so they rebind with no edits.

Upgrade to v2

Complete Before you upgrade on v1, then follow the steps below.

If you also hand-authored any TwingateResource of type: Kubernetes, each has to be bound to a TwingateGateway; see If you hand-authored Kubernetes resources.

  1. Upgrade to v1.3 (bundled gateway subchart only). Skip this step if gateway.enabled is false. v1.3 prepares the in-cluster Kubernetes resource so the v2 subchart can take it over instead of re-creating it; see Why the upgrade goes through v1.3.

    Keep your existing v1 values here; restructuring them early drops the resource.twingate.com annotations from the Service, which removes the in-cluster Kubernetes resource.

    helm upgrade twop oci://ghcr.io/twingate/helmcharts/twingate-operator --version 1.3.0 --install --wait -f ./values.yaml

    v1.3 stamps the ownership metadata when it starts, and the pod reporting ready does not mean it has finished. To check that it landed, the command below prints your release name once the stamp is in place and nothing before that:

    kubectl get twingateresource <release>-gateway-resource \
      -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-name}{"\n"}'
  2. Apply the new CRDs. Helm does not upgrade CRDs, and they are not published on their own; they ship inside the chart, so you must apply them yourself or the new fields and validations will be missing. Pull the same chart you are about to install and apply the CRDs from its crds/ directory so they match the chart version:

    helm pull oci://ghcr.io/twingate/helmcharts/twingate-operator --version 2.0.0 --untar
    kubectl apply -f twingate-operator/crds/
  3. Upgrade the operator. If you use the bundled gateway subchart, restructure its values first: the subchart now renders the Gateway custom resources directly instead of annotating the Gateway Service, so the old block has to be replaced (see Gateway subchart values). gateway.enabled stays true throughout.

    This upgrade also registers a TwingateCertificateAuthority, which the v1 subchart never created. Its name has to be unique across your Twingate network and is immutable once set, so this is where the name is fixed. The subchart derives one by hashing the cluster and the release namespace, which needs a live cluster read: rendering without one, as with Argo CD, helm template, or --dry-run=client, fails until you set gateway.twingateOperator.gateway.certificateAuthority.name. The failure message gives that path as twingateOperator.gateway.certificateAuthority.name, which is relative to the subchart. See Bundled subchart.

    helm upgrade twop oci://ghcr.io/twingate/helmcharts/twingate-operator --version 2.0.0 --install --wait -f ./values.yaml

If you cannot upgrade through v1.3

Disable the subchart first so in-cluster access can be recreated cleanly under the new model. Unlike the v1.3 path, nothing here preserves the existing object: it is recreated rather than adopted, so it keeps the Kubernetes object name <release>-gateway-resource but spec.id names a new Twingate Resource.

Warning

Twingate access to the cluster's Kubernetes API is down from the moment the subchart is disabled in step 1 until step 3 re-provisions it. Plan a maintenance window.

  1. Set gateway.enabled: false and upgrade, still on v1.

  2. Delete the in-cluster Kubernetes resource:

    kubectl delete twingateresource <release>-gateway-resource --ignore-not-found
  3. Run Upgrade to v2 from step 2, with gateway.enabled: true restored.

Step 1 deletes the Gateway Service, and the generated TwingateResource is garbage-collected behind it through its owner reference, so step 2 is usually a no-op. It is there because helm upgrade does not wait for that collection: if the object is still present when step 3 renders one under the same name, the upgrade aborts with an ownership error, since the v1 operator created it and it carries no Helm metadata.

If the delete does not return, the operator could not deregister the resource from Twingate; check its log. Clearing metadata.finalizers forces the object out, but then the Twingate Resource is left behind and has to be deleted in the admin console.

If you hand-authored Kubernetes resources

Only applies if you hand-authored a TwingateResource of type: Kubernetes. These are backed by the removed spec.proxy field, so each one has to be bound to a TwingateGateway instead.

Stand up that TwingateGateway before you patch. It carries the certificate authority that spec.proxy.certificateAuthorityCert used to hold, so it is a prerequisite, not a follow-up (see Identity Firewall). If the bundled subchart is providing it, that is step 3 of Upgrade to v2, so these objects stay frozen from step 2 until you patch; the sooner you have the Gateway ready, the shorter that window. Then patch spec.gatewayRef onto the existing object rather than replacing it:

kubectl patch twingateresource my-cluster --type=merge \
  -p '{"spec":{"gatewayRef":{"name":"my-gateway"}}}'

spec.proxy is dropped on the same write, so it does not need removing. The object keeps its spec.id, so the operator updates the Twingate Resource it already registered instead of registering a new one, and the access granted to it is preserved. Do not delete and re-create the object: that deprovisions its Twingate Resource and takes access down until the replacement reconciles.

Set gatewayRef.namespace as well when the Gateway is in another namespace.

Reference

Gateway subchart values: v1 to v2

Before (v1 subchart, annotation-based):

# values.yaml
twingateOperator:
  network: "<network slug>"
  apiKey: "<api key>"
  remoteNetworkId: "<remote network id>"

gateway:
  enabled: true
  twingate:
    network: "<network slug>"
    # v1: Kubernetes resource configured via annotations here; removed in v2
    resource:
      enabled: true
      extraAnnotations:
        resource.twingate.com/name: "My Kubernetes Cluster"
        resource.twingate.com/alias: "my-k8s-cluster.int"

After (v2 subchart, resource fields):

# values.yaml
twingateOperator:
  network: "<network slug>"
  apiKey: "<api key>"
  remoteNetworkId: "<remote network id>"

gateway:
  enabled: true
  twingate:
    network: "<network slug>"
  # v2: replaces the v1 twingate.resource block
  twingateOperator:
    gateway:
      enabled: true
      # Required only when rendering without cluster access, as with Argo CD:
      # certificateAuthority:
      #   name: "<a name unique within your Twingate network>"
    kubernetesResource:
      enabled: true
      name: "My Kubernetes Cluster"
      alias: "my-k8s-cluster.int"

What moved:

  • gateway.twingate.resource.enabled splits into two toggles: gateway.twingateOperator.gateway.enabled renders the Gateway, and gateway.twingateOperator.kubernetesResource.enabled renders the in-cluster Kubernetes resource. The operator chart already defaults both to true, so they only need setting to turn something off. Enabling the resource without the Gateway fails the render rather than skipping it.
  • The resource.twingate.com/* annotations under gateway.twingate.resource.extraAnnotations become fields under gateway.twingateOperator.kubernetesResource (name, alias, securityPolicyId, and so on). Any other TwingateResource spec field passes through as well, apart from type, address, and gatewayRef, which the subchart sets itself, and enabled, which is the subchart's own toggle. name passes through but defaults to <release>-gateway Kubernetes cluster rather than being left unset.
  • remoteNetworkId is the one exception to that pass-through: set it as gateway.twingateOperator.remoteNetworkId, which the subchart applies to the Gateway and the resource together. Setting it under kubernetesResource fails the render with twingateOperator.kubernetesResource.remoteNetworkId is not supported, set twingateOperator.remoteNetworkId instead.

Remove the old twingate.resource block; the v2 subchart's schema rejects it.

Why the upgrade goes through v1.3

  • v1.3 stamps Helm's ownership metadata onto the TwingateResource the operator generated from the Gateway Service. Without it, Helm refuses to take over an object it did not create. With it, the v2 subchart adopts the existing object instead of creating a second one, so it keeps its spec.id, and the Twingate Resource and the access granted to it are preserved.
  • v1.3 also stops deleting that object when the subchart drops the resource.twingate.com annotations. In v1.2 that removal deprovisions the Twingate Resource in the middle of the upgrade.
  • The v2 subchart declares gatewayRef on the resource, but Helm 3 adopts an existing object without patching its fields, so on an adopted resource the field starts out unset. The v2 operator binds the resource to the TwingateGateway rendered from the same Service and drops the now-stale Service owner reference, retrying until that Gateway exists, so no manual step is required. Clients that use server-side apply, such as Helm 4 and Argo CD, set the field themselves.

Namespace resolution

Two fields matter for objects created under v1, both on TwingateResourceAccess:

  • spec.resourceRef.namespace needs fixing. It had no default in the v1 CRD, so nothing is stored on existing objects. The fallback to the default namespace lived in the operator, which is why this is the one field that changes with no edit: resolution moves to the declaring namespace as soon as the v2 operator is running.
  • spec.groupRef.namespace needs no action. It carried default: default, so the API server stamped namespace: default onto it on write. Existing objects still carry that value and keep resolving to the default namespace whichever operator version runs; objects written after the new CRDs are applied carry no stamped value, so their groupRef resolves in the declaring namespace once the v2 operator is running.

Both fields are immutable as a whole, so neither can be edited in place. That is why fixing resourceRef means deleting and recreating the TwingateResourceAccess, and why an existing groupRef cannot later be pointed at a TwingateGroup outside the default namespace without doing the same.

Troubleshooting

Rendering fails on the certificate authority name

Set twingateOperator.gateway.certificateAuthority.name to a name unique within your
Twingate network. The chart derives one from the kube-system namespace UID, which is
unavailable when rendering without cluster access such as helm template, --dry-run=client
or Argo CD.

The subchart cannot derive the certificate authority name without a live cluster read. Set gateway.twingateOperator.gateway.certificateAuthority.name in the operator chart's values.yaml; the path in the message is relative to the subchart. See Bundled subchart for the values.yaml shape.

The upgrade fails with an ownership error, or in-cluster access disappeared

Error: UPGRADE FAILED: Unable to continue with update: TwingateResource
"twop-gateway-resource" in namespace "default" exists and cannot be imported into the
current release: invalid ownership metadata; annotation validation error: missing key
"meta.helm.sh/release-name": must be set to "twop"; annotation validation error: missing
key "meta.helm.sh/release-namespace": must be set to "default"

Helm found a TwingateResource it does not own: the ownership metadata is stamped by v1.3, and v1.3 was skipped.

Recover by running If you cannot upgrade through v1.3 from step 1, still on your v1 chart. Do not just fix the metadata and retry: the v2 subchart stops annotating the Gateway Service, and your pre-1.3 operator deletes the generated TwingateResource as soon as those annotations come off. That operator is still running when Helm updates the Service, so it would delete the object the v2 chart has just taken over. Step 1 of that section deletes the Service while you are still on v1, which leaves the old operator nothing to react to.

The in-cluster Kubernetes resource was renamed in the admin console

If you never set a name for the in-cluster Kubernetes resource, the name it shows in the Twingate Admin Console changes: v1 fell back to the Kubernetes object name <release>-gateway-resource, and the v2 subchart names it <release>-gateway Kubernetes cluster instead. This is a display change only, as the resource itself is the same one and keeps its access. Set gateway.twingateOperator.kubernetesResource.name to keep the old name or choose your own.

Clone this wiki locally