Plain VMs default to inbound TCP/22 (SSH) only. Every other port is closed at the Hetzner edge until you add a rule.
From the dashboard
- Open the VM detail page:
/dashboard/plain-vms/{id}. - Scroll to the Firewall section.
- Click Add rule.
- Fill the form:
- Protocol —
tcporudp. ICMP is not supported at v1 — the API rejects anything else with a 400. - Port — a single port number like
80, or a port range like8000-8100. Range bounds must satisfy1 ≤ low ≤ high ≤ 65535. - Source IPs — array of CIDR strings. Bare IPs are auto-coerced (
/32for IPv4,/128for IPv6).0.0.0.0/0is anywhere; restrict to your own IP for security-sensitive ports. - Description — free-form label that shows in the rule list.
- Protocol —
- Click Save.
The dashboard issues PUT /api/plain-vms/[id]/firewall with the complete desired rule set (full-replace semantics — partial updates are not supported). Propagation is under 5 seconds.
The underlying endpoint
The dashboard editor calls PUT /api/plain-vms/[id]/firewall with the complete desired rule set:
{
"rules": [
{ "protocol": "tcp", "port": "22", "sourceIps": ["0.0.0.0/0"], "description": "SSH" },
{ "protocol": "tcp", "port": "80", "sourceIps": ["0.0.0.0/0"], "description": "HTTP" }
]
}
This endpoint uses your dashboard (Clerk) session — there is no on-VM HMAC path for firewall edits, so manage rules from the dashboard. The direction field is server-set to "in" on every rule (Hetzner Cloud Firewall only supports inbound at v1) and must not be passed. The response is the canonical post-mutation ruleset, re-fetched from Hetzner — never an optimistic copy of the request.
Limits
- Maximum 50 rules per VM.
- Full-replace semantics — partial updates require sending the complete rule set every time.