UDN private subnet¶
You want two virtual machines on their own isolated network - unreachable from anywhere else
in your cluster by default - that can still talk to each other and reach the internet in a
controlled way. On AWS you'd reach for a VPC private subnet with a Security Group. On
OpenShift, the equivalent is a User Defined Network (UDN) paired with a matching set of
NetworkPolicy objects.
This example builds exactly that: two Fedora VMs in their own Layer2 subnet, isolated by default, able to reach each other, and gated to HTTPS + DNS egress only - the same shape as a Security Group rule.
Prerequisites¶
- An OpenShift/OKD cluster with OVN-Kubernetes as the network type
- The
userdefinednetworks.k8s.ovn.orgCRD installed (ships with OCP/OKD 4.17+) - OpenShift Virtualization installed, with a
fedoraboot-sourceDataSourceavailable ocandvirtctlon your workstation
Ready to run it? Jump to the step-by-step guide. Keep reading for how it works.
The AWS analogy¶
flowchart LR
subgraph AWS["AWS"]
direction TB
SG["Security Group<br/>self-ref: allow all<br/>egress 443/dns only"]
EC2A["EC2 A<br/>10.0.1.11"]
EC2B["EC2 B<br/>10.0.1.12"]
EC2A <-->|allowed| EC2B
end
subgraph OCP["OpenShift"]
direction TB
NP["NetworkPolicy<br/>self-ref: allow all<br/>egress 443/dns only"]
VMA["vm-a<br/>10.200.0.9"]
VMB["vm-b<br/>10.200.0.10"]
VMA <-->|allowed| VMB
end
Outside1["outside the VPC"] -.->|blocked| EC2A
Outside2["outside the namespace"] -.->|blocked| VMA
Same mental model, different platform: a private subnet plus a self-referencing egress rule.
Why a Primary UDN, not a Secondary one¶
A UserDefinedNetwork with role: Primary replaces your namespace's default pod network
entirely, instead of adding an extra interface alongside it (role: Secondary). That's what
makes the isolation structural rather than something you configure after the fact - and because
it's the pod's primary interface, plain Kubernetes NetworkPolicy applies to it directly, no
CNI-specific policy object required.
apiVersion: k8s.ovn.org/v1
kind: UserDefinedNetwork
metadata:
name: private-subnet
namespace: udn-demo
spec:
topology: Layer2
layer2:
role: Primary
subnets:
- 10.200.0.0/24
ipam:
lifecycle: Persistent
topology: Layer2 gives you one flat subnet shared by every node - the closest match to an AWS
subnet, which is also just one CIDR block. ipam.lifecycle: Persistent keeps a VM's IP stable
across restarts, the same way an AWS ENI keeps its private IP when you stop and start an
instance.
Two things that aren't obvious the first time¶
The namespace needs a label before it exists, not after
A Primary UDN needs the k8s.ovn.org/primary-user-defined-network label on its namespace.
On most clusters, a ValidatingAdmissionPolicy blocks adding that label to a namespace that
already exists - so it has to ship in the namespace manifest from the start. It already is,
in namespace-udn-demo.yaml.
Egress NetworkPolicy has no \"same namespace is free\" rule
If you write a default-deny-egress policy with podSelector: {}, it blocks traffic
between your own VMs too - ICMP, SSH, everything - not just traffic leaving the namespace.
The fix is the same pattern you'd already reach for in an AWS Security Group: a
self-referencing rule that lets members of the group talk to each other.
Use bridge interface binding, not masquerade
masquerade binding layers KubeVirt's own internal NAT on top of the network attachment -
built for the default pod network's single-IP constraint. On a Primary UDN, your VM's pod
already gets a real routable address directly, so masquerade just adds a confusing extra
translation layer, where the guest's own reported IP ends up different from its actual
address. bridge binding attaches the guest straight to the UDN - no translation, no
mismatch.