Hetzner Private Network + NAT
Private network with subnets, routes, and a NAT gateway server for egress-only fleets.
Verification
Static-verifiedPassed: validated and lint-clean (provider-schema-validated for AWS/Azure/GCP; Terraform-language lint elsewhere).
Conformance
- Static validation (fmt · validate · tflint)
- No applicable security policies for this provider
- Plan tests (mocked: validation rules · outputs)
Provenance
- SHA-256 checksum
- Signature (pending)
Functional
- Live test pending (no cloud run yet)
Last verified 2026-06-28 · how we verify
Documentation
hetzner-private-network
Status: static-validated, live-test pending. Ships under live-test quarantine — validated with
tofu fmt,tofu validate, andtflint. Real apply/verify/destroy testing is pending a Hetzner Cloud sandbox account.
Private network for Hetzner Cloud with cloud subnets, egress routes, and a
self-managed NAT gateway so egress-only fleets can reach the internet
without holding a public IPv4. One module call gives you an RFC1918 network,
one or more cloud subnets, a hardened NAT gateway server, and the
0.0.0.0/0 route that funnels private servers' outbound traffic through it.
Works with Terraform and OpenTofu (>= 1.6), hcloud provider
>= 1.0, < 2.0.
Design & secure defaults
- Private by default. The network and its subnets carry no public exposure. The only resource with an internet-facing address is the (optional) NAT gateway, and it gets exactly one public IPv4.
- Deny-by-default gateway firewall. No inbound port is open on the NAT
gateway — not even SSH — until you allowlist
nat_gateway_ssh_source_ips. Only ICMP is allowed. Outbound stays open so MASQUERADE can work. - NAT is a pattern, not a managed service. Hetzner has no managed NAT
gateway. This module provisions a small server with a fixed private IP,
enables IPv4 forwarding, and installs a persistent
iptables MASQUERADErule via cloud-init. You operate and patch this server — keep SSH key access on it (nat_gateway_ssh_key_ids) and apply OS updates. It is a single instance, so it is also a single point of failure for egress; for HA you would front two gateways yourself. - Stable route target. The gateway's private IP is computed deterministically
(
cidrhost(subnet, nat_gateway_host_index)) and reused as the route gateway, so the egress route always points at the right host. A precondition rejects the network's reserved first IP. - One-flag delete protection across the network and the gateway server.
Egress-only servers you create elsewhere simply attach to one of these subnets
with ipv4_enabled = false; the network route does the rest.
Usage
module "private_network" {
source = "./hetzner-private-network"
name = "core"
ip_range = "10.0.0.0/16"
network_zone = "eu-central"
subnets = {
app = { ip_range = "10.0.1.0/24" }
data = { ip_range = "10.0.2.0/24" }
}
nat_gateway_enabled = true
nat_gateway_location = "fsn1"
nat_gateway_ssh_key_ids = ["ops-key"]
nat_gateway_ssh_source_ips = ["203.0.113.0/24"]
labels = { env = "prod" }
}
# An egress-only server reachable only inside the network:
resource "hcloud_server" "worker" {
name = "worker-1"
server_type = "cx22"
image = "ubuntu-24.04"
location = "fsn1"
public_net {
ipv4_enabled = false
ipv6_enabled = false
}
network {
network_id = module.private_network.network_id
# IP auto-assigned from the subnet; routes egress via the NAT gateway.
}
}
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
name | string | — | Base name for network and derived resources (required) |
ip_range | string | 10.0.0.0/16 | RFC1918 range for the whole network |
network_zone | string | eu-central | Zone for cloud subnets (eu-central, us-east, us-west, ap-southeast) |
subnets | map(object) | { private = { ip_range = "10.0.1.0/24" } } | Cloud subnets keyed by logical name |
expose_routes_to_vswitch | bool | false | Expose routes to a connected Hetzner vSwitch |
delete_protection | bool | false | Delete/rebuild protection on network + gateway |
nat_gateway_enabled | bool | true | Provision the NAT gateway and egress route |
nat_gateway_subnet_key | string | null | Subnet hosting the gateway (null = first by sorted key) |
nat_gateway_host_index | number | 6 | Host index for the gateway's fixed private IP (2–250) |
nat_gateway_server_type | string | cx22 | Server type for the gateway |
nat_gateway_image | string | ubuntu-24.04 | OS image (Debian/Ubuntu — cloud-init assumes apt/iptables) |
nat_gateway_location | string | fsn1 | Location for the gateway (within network_zone) |
nat_gateway_ssh_key_ids | list(string) | [] | SSH keys granted root on the gateway (recommended) |
egress_destinations | list(string) | ["0.0.0.0/0"] | Destination CIDRs routed via the gateway |
nat_gateway_ssh_source_ips | list(string) | [] | CIDRs allowed to SSH the gateway from the internet; empty = closed |
labels | map(string) | {} | Labels applied to all resources |
Outputs
network_id, network_ip_range, subnet_ids (map), subnet_ip_ranges (map),
nat_gateway_id, nat_gateway_private_ip, nat_gateway_public_ipv4,
nat_gateway_firewall_id, egress_route_destinations.
Notes
nat_gateway_locationmust sit insidenetwork_zone(e.g.fsn1/nbg1/hel1foreu-central,ashforus-east). Hetzner does not let you mix a location with the wrong zone.- The default
nat_gateway_host_index = 6keeps the gateway clear of the low reserved addresses. If you change the NAT subnet to anything smaller than a/24, make sure the index still fits. - The NAT gateway is stateful, single-instance infrastructure: its iptables
rule is restored from
iptables-persistenton reboot, but the box itself is yours to patch and monitor. Treat it like any internet-facing jump host. - Routes only affect destinations not already inside the network, so subnet-to- subnet traffic is never sent through the gateway.
Requirements
- Terraform or OpenTofu
>= 1.6 hetznercloud/hcloudprovider>= 1.0, < 2.0(private networks, subnets, routes andhcloud_server_network— latest recommended)
Verification
Static-validated (tofu fmt, tofu validate, tflint). Live apply/destroy
testing pending a Hetzner Cloud sandbox account — see catalog status.
License
Commercial — LicenseRef-IaCBazaar-Commercial. See the IaC Bazaar terms.