Skip to content

Step-by-step: UDN private subnet

Everything here is real, captured output from an actual run - not invented numbers.

1. Deploy

git clone https://github.com/hoolia/examples.openshift.eu.git
cd examples.openshift.eu
oc apply -k examples/udn-private-subnet/
oc wait userdefinednetwork/private-subnet -n udn-demo --for=condition=NetworkCreated --timeout=60s
oc wait vm/vm-a vm/vm-b -n udn-demo --for=condition=Ready --timeout=900s

You'll see nine resources come up: two namespaces, the UDN itself, two VMs, three NetworkPolicy objects, and a probe pod. No manual route tables, no subnet CIDR wizard, no separate console flow for a Security Group.

2. Confirm isolation, from outside the namespace

Find your VMs' addresses:

oc get vmi vm-a vm-b -n udn-demo -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.interfaces[0].ipAddress}{"\n"}{end}'

From the probe pod - outside the udn-demo namespace, on the ordinary default pod network:

oc exec -n udn-demo-probe netshoot-probe -- ping -c2 -W2 <VM_A_IP>

What you'll see:

PING 10.200.0.9 (10.200.0.9) 56(84) bytes of data.

--- 10.200.0.9 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1009ms

Notice the NetworkPolicy objects you just applied are all Egress-only - none of them restrict inbound traffic. The isolation you're about to see comes from the UDN itself, not from a firewall rule. The subnet is unreachable from outside because the UDN is the namespace's primary network.

3. Confirm your VMs can still reach each other

Open a console session on vm-a:

virtctl console vm-a -n udn-demo

Log in as fedora / udndemo123, then:

ping -c2 <VM_B_IP>

What you'll see:

PING 10.200.0.10 (10.200.0.10) 56(84) bytes of data.
64 bytes from 10.200.0.10: icmp_seq=1 ttl=64 time=1.87 ms
64 bytes from 10.200.0.10: icmp_seq=2 ttl=64 time=0.764 ms

--- 10.200.0.10 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms

Zero config beyond the manifests you already applied - the allow-intra-namespace-egress policy is what makes this possible.

4. Confirm egress is gated like a Security Group rule

From the same vm-a console session:

curl -sS -o /dev/null -w 'http_code=%{http_code}\n' https://example.com

What you'll see: http_code=200 - allowed.

curl -sS --max-time 5 -o /dev/null -w 'http_code=%{http_code}\n' http://example.com
ping -c2 -W2 8.8.8.8

What you'll see:

curl: (28) Connection timed out after 5001 milliseconds
http_code=000

PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.

--- 8.8.8.8 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1007ms

Plain HTTP times out and ICMP is blocked entirely. The NetworkPolicy pair is doing the job of an AWS Security Group egress rule: HTTPS + DNS out, everything else denied.

To exit the console session, press Ctrl+].

5. Tear it down

oc delete -k examples/udn-private-subnet/

The manifests

namespace-udn-demo.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: udn-demo
  labels:
    k8s.ovn.org/primary-user-defined-network: ""
namespace-udn-demo-probe.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: udn-demo-probe
userdefinednetwork-private-subnet.yaml
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
virtualmachine-vm-a.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: vm-a
  namespace: udn-demo
spec:
  dataVolumeTemplates:
  - metadata:
      name: vm-a-rootdisk
    spec:
      sourceRef:
        kind: DataSource
        name: fedora
        namespace: openshift-virtualization-os-images
      storage:
        accessModes:
        - ReadWriteOnce
        volumeMode: Filesystem
        resources:
          requests:
            storage: 30Gi
  instancetype:
    kind: virtualmachineclusterinstancetype
    name: u1.small
  preference:
    kind: virtualmachineclusterpreference
    name: fedora
  runStrategy: Always
  template:
    metadata:
      labels:
        vm.kubevirt.io/name: vm-a
    spec:
      domain:
        devices:
          interfaces:
          - name: default
            bridge: {}
      networks:
      - name: default
        pod: {}
      volumes:
      - dataVolume:
          name: vm-a-rootdisk
        name: rootdisk
      - cloudInitNoCloud:
          userData: |
            #cloud-config
            chpasswd:
              expire: false
            password: udndemo123
            user: fedora
            ssh_pwauth: true
            runcmd:
            - systemctl enable --now sshd
        name: cloudinitdisk
virtualmachine-vm-b.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: vm-b
  namespace: udn-demo
spec:
  dataVolumeTemplates:
  - metadata:
      name: vm-b-rootdisk
    spec:
      sourceRef:
        kind: DataSource
        name: fedora
        namespace: openshift-virtualization-os-images
      storage:
        accessModes:
        - ReadWriteOnce
        volumeMode: Filesystem
        resources:
          requests:
            storage: 30Gi
  instancetype:
    kind: virtualmachineclusterinstancetype
    name: u1.small
  preference:
    kind: virtualmachineclusterpreference
    name: fedora
  runStrategy: Always
  template:
    metadata:
      labels:
        vm.kubevirt.io/name: vm-b
    spec:
      domain:
        devices:
          interfaces:
          - name: default
            bridge: {}
      networks:
      - name: default
        pod: {}
      volumes:
      - dataVolume:
          name: vm-b-rootdisk
        name: rootdisk
      - cloudInitNoCloud:
          userData: |
            #cloud-config
            chpasswd:
              expire: false
            password: udndemo123
            user: fedora
            ssh_pwauth: true
            runcmd:
            - systemctl enable --now sshd
        name: cloudinitdisk
networkpolicy-default-deny-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: udn-demo
spec:
  podSelector: {}
  policyTypes:
  - Egress
networkpolicy-allow-https-dns-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-https-dns-egress
  namespace: udn-demo
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
    ports:
    - protocol: TCP
      port: 443
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
networkpolicy-allow-intra-namespace-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-intra-namespace-egress
  namespace: udn-demo
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector: {}
pod-netshoot-probe.yaml
apiVersion: v1
kind: Pod
metadata:
  name: netshoot-probe
  namespace: udn-demo-probe
  labels:
    app: netshoot-probe
spec:
  containers:
  - name: netshoot
    image: nicolaka/netshoot:latest
    command: ["sleep", "infinity"]
kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- namespace-udn-demo.yaml
- namespace-udn-demo-probe.yaml
- userdefinednetwork-private-subnet.yaml
- networkpolicy-default-deny-egress.yaml
- networkpolicy-allow-https-dns-egress.yaml
- networkpolicy-allow-intra-namespace-egress.yaml
- pod-netshoot-probe.yaml
- virtualmachine-vm-a.yaml
- virtualmachine-vm-b.yaml