On a NAT-backed overlay, a workload can listen on a private address without exposing anything on the physical host. Until you attach a security group that opens (and optionally DNATs) traffic, curling the host’s LAN IP does not hit nginx.
The commands below use realistic RFC1918 addresses from a lab: host 192.168.40.105, overlay guest
10.250.0.7, node id kvm-node-192-168-40-105, network private.
Substitute your own names and IPs.
Why a VM in the manifest: Security group DNAT resolves targetVm to a
VM workload’s stored private IP in the controller today. Running nginx in an OCI container on the same NAT
hits the same forwarding behaviour; use the VM-shaped flow below for a copy-paste match with kctl describe vm
and targetVm.
Prerequisites
- Controller reachable from your operator machine;
kctlconfigured. - A NAT-style network named
private(or change the manifest) attached to your node. - A Debian or similar image already registered for VM creation (same as other quickstarts).
1) Run nginx on the overlay
Create a small VM workload that will run nginx on port 80 inside the guest. The controller assigns an overlay address such as
10.250.0.7 on network private.
kctl workload create --kind vm nginx-demo \
--network private \
--target-node kvm-node-192-168-40-105 \
--image <your-debian-or-nginx-image>
Install and start nginx inside the guest (for example via cloud-init or SSH) so it listens on 0.0.0.0:80.
Confirm the private IP:
kctl describe vm nginx-demo
Expect a line or table entry showing 10.250.0.7 (or another address in your overlay range).
2) From the host, curl fails without a security group
On the hypervisor host (192.168.40.105), direct access to the overlay address is not wired through ingress/DNAT yet:
curl -v --connect-timeout 3 http://10.250.0.7/
curl -v --connect-timeout 3 http://192.168.40.105:8080/
The first request may time out or be refused; the second hits the host address but nothing is published on port 8080 yet.
3) Declare a security group (DNAT host 8080 → guest 80)
Save the following as expose-nginx.yaml. It attaches to the network on the node and sends TCP traffic destined for
the host’s external IP on port 8080 to the guest’s private IP on port 80.
kind: SecurityGroup
metadata:
name: expose-nginx-demo
spec:
description: DNAT TCP/8080 on the host to nginx on the overlay
rules:
- id: http-dnat
protocol: tcp
hostPort: 8080
targetPort: 80
sourceCidr: 0.0.0.0/0
targetVm: nginx-demo
enableDnat: true
attachments:
- kind: network
target: private
node: kvm-node-192-168-40-105
4) Apply and reconcile
kctl security-group apply -f expose-nginx.yaml
Wait for the node to reconcile Nix and nftables (usually within one controller/node sync cycle). If you use a different
external IP than 192.168.40.105 for the NAT network, the DNAT rule still keys off that network’s
externalIP as rendered on the node.
5) Curl succeeds via the host
From any machine that can reach the node on the LAN:
curl -sS -D- http://192.168.40.105:8080/ -o /dev/null | head -n 5
You should see HTTP 200 and nginx response headers. Traffic is DNATted to 10.250.0.7:80 inside the overlay.
What about an nginx container?
Create one with kctl workload create --kind container nginx-demo ... --network private. The isolation story is the same:
the service starts on a private address, not on the host’s Ethernet port, until policy opens a path.
Until DNAT can bind directly to container endpoints in the controller, pair the container with a VM front or use the VM flow
above for rules that reference targetVm. See
security-groups.md
in the product repo for the rule model.
For more examples, see expose-nginx-host.yaml in the kcore tree.