VM with a direct public IP¶
By the end of this guide a virtual machine holds a routable address on its own interface, reaches the internet as itself, and serves a page to the outside world.
For the same thing in the console, see the console walkthrough. For why the path looks like this, see the overview.
1. Create the public subnet¶
Create a project carrying the range. The network, the advertisement and your access to the project are built from it:
cat <<EOF | oc apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: <subnet>
annotations:
hoolia.eu/subnet-cidr: <cidr>
hoolia.eu/subnet-type: Public
EOF
The range comes from the public pool with a prefix of /28 or /29.
The platform takes the first two usable addresses
The first is the gateway your VM routes through and the second is the node management
port. OVN reserves both itself, so a /29 gives four usable addresses rather than six.
A /30 would give none, which is why it is rejected.
See the address table.
Confirm the network came up before going further - a network that failed to start still looks present:
oc get clusteruserdefinednetworks.k8s.ovn.org <subnet>
oc get routeadvertisements.k8s.ovn.org <subnet>
The advertisement must report Accepted.
2. Deploy a VM into it¶
Any VM created in that project attaches to the subnet automatically - there is no network field to set. This one installs a web server on first boot so there is something to reach:
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: pubvm
namespace: <subnet>
spec:
runStrategy: Always
template:
spec:
domain:
cpu:
cores: 2
memory:
guest: 2Gi
devices:
disks:
- name: rootdisk
disk:
bus: virtio
- name: cloudinit
disk:
bus: virtio
interfaces:
- name: default
binding:
name: l2bridge
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
dataVolume:
name: pubvm-root
- name: cloudinit
cloudInitNoCloud:
userData: |
#cloud-config
user: cloud-user
ssh_authorized_keys:
- ssh-ed25519 AAAA... you@example
packages:
- httpd
- firewalld
runcmd:
- systemctl enable --now firewalld
- firewall-cmd --permanent --add-service=http
- firewall-cmd --permanent --add-service=https
- firewall-cmd --reload
- echo "ingress OK" > /var/www/html/index.html
- restorecon -Rv /var/www/html
- systemctl enable --now httpd
dataVolumeTemplates:
- metadata:
name: pubvm-root
spec:
sourceRef:
kind: DataSource
name: centos-stream9
namespace: openshift-virtualization-os-images
storage:
resources:
requests:
storage: 30Gi
The guest opens its own ports. Nothing in the platform does it for you, and SELinux stays
enforcing - restorecon is what makes the page readable by the web server.
3. Find the address¶
That address is on the VM's own interface, not on a Service.
4. Check egress¶
Log in over SSH, on the address itself:
The address and the default route are visible from inside the guest:
$ ip -4 addr show | grep global
inet 146.19.177.11/29 brd 146.19.177.15 scope global dynamic noprefixroute eth0
$ ip route | grep default
default via 146.19.177.9 dev eth0 proto dhcp src 146.19.177.11 metric 100
The gateway is the first usable address of the range. Outbound traffic works, and arrives as the VM's own address rather than a translated one:
$ curl -sI https://www.google.com | head -1
HTTP/2 200
$ curl -s https://ifconfig.me
146.19.177.11
$ ping -c2 1.1.1.1 | tail -2
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 7.428/7.919/8.411/0.491 ms
ifconfig.me reporting the VM's own address is the check that matters - it confirms there is
no NAT in the path.
5. Check ingress¶
From outside the cluster:
If that hangs, the guest firewall is the first thing to look at, not the platform:
The platform only filters if the project has a firewall that selects the VM. The project of a public subnet is created without any, so this list is empty unless you added one:
Restrict what reaches the VM¶
A NetworkPolicy selects the pod the VM runs in, so it filters a VM like any other
workload. The platform labels that pod with vm.kubevirt.io/name, so the VM's name is all
the selector needs. This one admits SSH and HTTP from anywhere and nothing else:
cat <<EOF | oc apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: pubvm-from-internet
namespace: <subnet>
spec:
podSelector:
matchLabels:
vm.kubevirt.io/name: pubvm
policyTypes:
- Ingress
ingress:
- from:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 22
- protocol: TCP
port: 80
EOF
It applies within seconds. Ports 22 and 80 keep answering from outside; everything else,
ping included, is dropped from then on, whatever the guest firewall allows. A rule with no
ports at all admits every protocol, ICMP included. Outbound traffic is not affected, and
deleting the firewall opens the VM again. See
Firewalls for narrower sources than the whole
internet.
List¶
Change¶
A VM's address comes from the subnet and is not something you set, so there is no address to
edit. To publish different ports, change the guest firewall. If you added a firewall that
selects the VM, its ports list has to change as well.
The subnet's range cannot change either. Making it private means deleting the advertisement; see Change.
Delete¶
Delete the VM first, then the subnet's three objects in order:
Delete the network, not the project
The project and the advertisement belong to the network and are removed with it. Deleting the project on its own never finishes: it holds a network attachment whose finalizer is released only when the network goes.
Do not reuse a subnet name or range straight away
Deleting a network does not remove the per-node state behind it. Recreating a subnet under a name or range that has been used before can bind to that leftover state, and the new subnet then passes no traffic even though every object reports healthy. Pick a fresh name and a fresh range.