Tech Blog by vClusterPress and Media Resources

Syncing Custom Resources into Tenant Clusters - Part 1

Aug 18, 2026
|
12
min Read
Syncing Custom Resources into Tenant Clusters - Part 1

Diagram: custom resource sync copying a tenant cluster's AuthorizationPolicy CRD across the sync boundary so Istio on the control plane cluster can recognize and enforce it

Make AuthorizationPolicy work in tenant clusters with custom resource sync

vCluster makes it easy to give each team a “real” Kubernetes API inside a fully isolated tenant cluster. Each team gets an isolated tenant cluster with its own Kubernetes API server, which is cheaper and faster to spin up than a separate full-blown cluster, with strong isolation.

The first time a team tries to lock down traffic with Istio AuthorizationPolicy, though, it often fails immediately:

no matches for kind "AuthorizationPolicy" in version "security.istio.io/v1"

This post shows why that happens and how to fix it using custom resource sync. You’ll reproduce the failure on a vCluster Standalone control plane, enable sync, and verify that policies created in the tenant cluster appear on the control plane cluster where Istio can enforce them.

This is Part 1 of a three-part series:

  • Part 1 (this post): Make the policy sync successfully.
  • Part 2: Turn on enforcement, observe a 000 connection reset, then fix it with sync patches.
  • Part 3: Automate the setup for every tenant cluster.

The mental model (two clusters)

There are always two clusters involved:

  • Control plane cluster: the real Kubernetes cluster where workloads run and where Istio is installed. In this post it's a vCluster Standalone cluster, which runs vCluster's own Kubernetes control plane directly on a node with no underlying host cluster.
  • Tenant cluster: the cluster your team interacts with (its own API server), backed by the control plane cluster.

vCluster’s job is to synchronize resources across that boundary.

Why AuthorizationPolicy fails in a tenant cluster

Two quick Kubernetes concepts explain the error:

  1. CRDs (CustomResourceDefinitions) teach Kubernetes about new kinds of objects. Without a CRD, the API server rejects the kind with “no matches for kind …”.
  2. API groups are how Kubernetes organizes those kinds. For Istio, the key groups are networking.istio.io for traffic management (Gateway, VirtualService, DestinationRule) and security.istio.io for security (AuthorizationPolicy, PeerAuthentication, RequestAuthentication).

vCluster’s Istio integration is useful, but it only syncs DestinationRules, Gateways, and VirtualServices from the networking.istio.io/v1 group, and it does not sync the security.istio.io kinds.

So even if Istio is installed on the control plane cluster (and the AuthorizationPolicy CRD exists there), the tenant cluster still doesn’t know that kind exists. When a developer tries to apply an AuthorizationPolicy in the tenant cluster, the tenant API server rejects it because it has never seen the CRD.

The fix: custom resource sync

With custom resource sync, you tell vCluster which CRDs you want available in the tenant cluster. vCluster then:

  • copies the CRD definition down into the tenant cluster (so the kind is recognized), and
  • syncs the objects up to the control plane cluster (so Istio can enforce them).

Two things happen automatically that are worth knowing about. vCluster adds the cluster- and namespace-level RBAC it needs to read the CRD and sync the objects, and if a synced object creates other resources on the control plane cluster (for example, a Secret), vCluster detects them and syncs them back into the tenant cluster.

Prerequisites

You’ll need:

  • A Linux node or VM with root access to host the vCluster Standalone control plane. Ubuntu 22.04/24.04 or RHEL 8/9/10 (systemd required); on AWS or GCP roughly 4 vCPU, 16 GB RAM, and a 40 GB disk is comfortable for the control plane plus Istio plus one tenant cluster.
  • kubectl
  • vcluster CLI
  • istioctl
  • helm (required if you self-host the Platform with vcluster platform start in Step 3)
  • Access to vCluster Platform or vCluster Cloud (free tier, covered in Step 3)

Check your tooling:

kubectl version --client
vcluster version
istioctl version --remote=false

Why vCluster Platform is required here

Custom resource sync (and the patching you’ll use in Part 2) requires the tenant cluster to be connected to vCluster Platform, either self-hosted or the hosted vCluster Cloud offering. The free tier is sufficient, there’s no license key to manage.

If you skip this, the syncer typically crash-loops with an error like:

you are trying to use a vCluster pro feature 'Generic Sync' ... not available in the open source vcluster

💡 Note for Part 2: Part 1 runs entirely on the free tier. Part 2 turns on vCluster's Istio integration, which is a Pro feature and needs a vCluster Platform licensed for it. Customers normally have this enabled by the vCluster team through the backend, so there's usually nothing to install yourself; just confirm your Platform is licensed for the Istio integration before starting Part 2.

Step 1: Deploy the vCluster Standalone control plane

vCluster Standalone runs vCluster's own Kubernetes control plane directly on a Linux node, with no underlying host cluster. SSH into your node, become root, and create the config directory:

sudo su -
mkdir -p /etc/vcluster

Standalone needs a higher inotify watch limit than the default:

sysctl -w fs.inotify.max_user_watches=524288

Optionally pin a Kubernetes version:

cat <<EOF > /etc/vcluster/vcluster.yaml
controlPlane:
distro:
k8s:
version: v1.34.0
EOF

Install the control plane:

curl -sfL https://github.com/loft-sh/vcluster/releases/download/v0.35.1/install-standalone.sh | sh -s -- --vcluster-name standalone

vCluster writes a kubeconfig to /var/lib/vcluster/kubeconfig.yaml and installs a CLI at /var/lib/vcluster/bin/vcluster-cli. Point kubectl at the control plane and confirm the node is Ready:

export KUBECONFIG=/var/lib/vcluster/kubeconfig.yaml
kubectl get nodes

To drive the cluster from your laptop instead, copy /var/lib/vcluster/kubeconfig.yaml locally and change its server field to the node's public IP.

Step 2: Install Istio on the control plane cluster

For Part 1, the minimal profile is enough. The important side effect is that installing Istio registers all Istio CRDs on the control plane cluster (including the security.istio.io CRDs):

istioctl install --set profile=minimal -y

With KUBECONFIG still pointed at the Standalone control plane, verify the security CRDs exist:

kubectl get crd | grep security.istio.io

Expected output:

authorizationpolicies.security.istio.io 2026-06-01T...
peerauthentications.security.istio.io 2026-06-01T...
requestauthentications.security.istio.io 2026-06-01T...

These exist on the control plane cluster. The tenant cluster won’t have them unless you sync them.

Step 3: Connect to vCluster Platform or vCluster Cloud

Custom resource sync needs a Platform connection. You have two options.

Option A: vCluster Cloud (hosted). vCluster Labs runs the Platform for you while your control plane and tenant clusters stay in your own infrastructure. Sign up for the free tier at vcluster.cloud, then log your CLI in with the URL your organization is given:

vcluster platform login https://your-org.vcluster.cloud

Option B: self-hosted Platform. Start Platform on the Standalone control plane instead:

vcluster platform start

This prints a *.loft.host URL and logs your CLI in. Whichever option you choose, the Platform endpoint must be reachable from the control plane cluster, because the tenant syncer runs inside it (a localhost port-forward on your laptop won’t work for the syncer).

Confirm your CLI is logged in:

vcluster platform get current-user

If you later see:

platform access key not found. Please login again

just re-run the login command.

Step 4: Create a tenant cluster

Create a tenant cluster named tenant in the team-x namespace:

vcluster create tenant --namespace team-x

The CLI will connect you to the tenant cluster and switch your kubectl context to it. Since you're connecting through a port-forward, this command keeps running in the foreground; leave it open and run the following commands in a second terminal. (If you're scripting this instead, add --connect=false and run tenant commands as one-shots with vcluster connect tenant -n team-x -- kubectl ....)

Verify the tenant cluster has no Istio security CRDs:

kubectl get crd | grep security.istio.io

Expected output:

No resources found

Step 5: Reproduce the failure

Create authz-policy.yaml:

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: httpbin-allow
namespace: default
spec:
selector:
matchLabels:
app: httpbin
action: ALLOW
rules:
- from:
- source:
namespaces: ["default"]

Apply it inside the tenant cluster:

kubectl apply -f authz-policy.yaml

Expected output:

error: resource mapping not found for name: "httpbin-allow" namespace: "default"
from "authz-policy.yaml": no matches for kind "AuthorizationPolicy" in version "security.istio.io/v1"
ensure CRDs are installed first

That’s the core problem: the tenant API server doesn’t know the AuthorizationPolicy kind yet.

Step 6: Enable custom resource sync for AuthorizationPolicy

Create vcluster.yaml:

sync:
toHost:
customResources:
authorizationpolicies.security.istio.io/v1:
enabled: true
scope: Namespaced

This:

  • copies the AuthorizationPolicy CRD into the tenant cluster on startup, and
  • syncs AuthorizationPolicy objects created in the tenant up to the control plane cluster.

Why the /v1 suffix? Istio registers this CRD with two versions, and its storage version is v1beta1. Without a pinned version, vCluster copies only the storage version into the tenant cluster, so applying a security.istio.io/v1 policy would still fail with "no matches for kind". Pinning /v1 makes the tenant cluster serve the version the policy uses.

A quick note on scope: custom resource sync currently supports namespace-scoped resources only. If you omit scope, vCluster defaults to Namespaced.

Important: run the upgrade from the control plane cluster context

After vcluster create, your context is the tenant cluster. If you try to upgrade from there, you may get prompted about creating a tenant cluster inside another tenant cluster.

Switch back to the Standalone control plane context first, then upgrade:

vcluster disconnect
vcluster create tenant --namespace team-x --values vcluster.yaml --upgrade

If you see an error about not being logged in (despite having Platform running), don’t work around it, re-run the Platform login from Step 3 so the cluster registers properly.

Step 7: Apply the policy again (and verify it synced)

The CLI reconnects you to the tenant cluster when the upgrade finishes. The CRD takes a few seconds to be copied down and become ready, so wait for it explicitly:

kubectl wait --for condition=established --timeout=60s \
crd/authorizationpolicies.security.istio.io

Expected output:

customresourcedefinition.apiextensions.k8s.io/authorizationpolicies.security.istio.io condition met

If the kubectl wait reports the CRD doesn't exist yet, give it a few more seconds and re-run it. Even after the CRD is established, the API server's discovery cache can lag by a couple of seconds; if the next apply still says "no matches for kind", just retry it.

Now apply the same policy again:

kubectl apply -f authz-policy.yaml

Expected output:

authorizationpolicy.security.istio.io/httpbin-allow created

Confirm it exists in the tenant cluster:

kubectl get authorizationpolicy -n default

Expected output:

NAME AGE
httpbin-allow 10s

Now switch back to the control plane cluster and find the synced object in the tenant’s namespace:

vcluster disconnect
kubectl describe authorizationpolicy -n team-x

Expected output (trimmed):

Name: v2ck5gn05koj6c
Namespace: team-x
Annotations: vcluster.loft.sh/object-name: httpbin-allow
vcluster.loft.sh/object-namespace: default
Spec:
Action: ALLOW
Rules:
From:
Source:
Namespaces:
default
Selector:
Match Labels:
App: httpbin

Two details matter here:

  1. The name changed on the control plane cluster. vCluster renames synced objects (to a v-prefixed hash) to avoid collisions across tenants. The original name/namespace are preserved in annotations.
  2. The policy content synced “as-is”. Notice it still references Namespaces: default.

That second point is the cliffhanger for Part 2.

Beyond Istio: syncing any CRD

Nothing about this setup is Istio-specific. You can sync any namespace-scoped CRD the same way:

sync:
toHost:
customResources:
<plural>.<group>: # the CRD name
enabled: true
scope: Namespaced # only Namespaced is currently supported

To find the correct key, look at the control plane cluster’s CRDs (from the control plane context):

kubectl get crd | grep <your-tool>

Whatever it prints (e.g. certificates.cert-manager.io) is exactly what you use under customResources.

If a CRD defines multiple versions, vCluster uses the storage version by default. You can pin a specific one by appending it to the key as <plural>.<group>/<version> — exactly why the AuthorizationPolicy key in Step 6 pins /v1.

A few examples:

ToolcustomResources keyScope
Istio securityauthorizationpolicies.security.istio.io/v1Namespaced
cert-managercertificates.cert-manager.ioNamespaced
Prometheus Operatorprometheuses.monitoring.coreos.comNamespaced
Your own operatorwidgets.example.comNamespaced

Two rules always apply:

  • The CRD must already exist on the control plane cluster. vCluster copies the CRD definition down from the control plane cluster. If the control plane cluster doesn’t have it, there’s nothing to copy.
  • Synced objects get a translated name on the control plane cluster (with the original recorded in annotations).

If your resource references other objects by name (a Secret, a Service, a namespace), those references cross the boundary untranslated by default. vCluster supports reference patches and expression patches to rewrite them during sync, exactly the tool you’ll reach for in Part 2.

Common mistakes to avoid

  1. Assuming the Istio integration includes security policies. It syncs networking.istio.io/v1 (DestinationRules, Gateways, VirtualServices), not security.istio.io.
  2. Forgetting Istio must be installed on the control plane cluster first. The CRD is copied from the control plane cluster into the tenant cluster, so if it isn’t on the control plane cluster there’s nothing to copy.
  3. Upgrading from inside the tenant context. Run vcluster disconnect to return to the Standalone control plane context before running the --upgrade.
  4. Forgetting to pin the CRD version. Without a /<version> suffix, vCluster copies only the CRD's storage version into the tenant cluster. Istio stores v1beta1, so an unpinned key leaves the tenant unable to serve security.istio.io/v1 and the policy still fails with "no matches for kind" even though sync is enabled.

Key takeaway

If you want Istio AuthorizationPolicy (and other security.istio.io resources) to work inside tenant clusters, enable custom resource sync for the relevant CRDs. That makes the kinds available in the tenant cluster and syncs the resulting objects to the control plane cluster where Istio runs.

What’s next (Part 2)

Your policy synced, but it still says “allow from namespace default”. On the control plane cluster, your tenant workloads actually live in team-x. In Part 2 you’ll turn on enforcement and see how this mismatch produces a surprising deny, then fix it with a small sync patch (no hand-editing translated names).

Clean up

vcluster delete tenant --namespace team-x

Then tear down the Standalone control plane by deleting the node or VM you provisioned for it (for example, terraform destroy if you created it with Terraform).

References

Share:
Get started with the #1 tenant isolation platform.

Give your tenants the hyperscaler experience, ready in seconds.

Ready to take vCluster for a spin?

Deploy your first virtual cluster today.