← Back to blog

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

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.