Firewall configuration
Which ports HeroCtl uses, which need to stay open, and which should never be exposed to the internet.
A healthy HeroCtl cluster has a small, fixed set of ports. This document shows which they are, what each one does, and how to configure the operating system firewall to open exactly what's needed, no more and no less.
Ports in use
| Port | Protocol | Function | Who needs access |
|---|---|---|---|
| 80 | TCP | Ingress router (HTTP) | The whole internet |
| 443 | TCP | Ingress router (HTTPS) | The whole internet |
| 8080 | TCP | Control plane API | Operators and CLI, via VPN or allowlist |
| 8443 | TCP | TLS web panel | Operators, via VPN or allowlist |
| 4646 | TCP | Internal coordination between nodes (consensus) | Only other cluster nodes |
| 4647 | TCP | RPC between coordinator and workers | Only other cluster nodes |
| 4648 | TCP+UDP | Gossip between nodes | Only other cluster nodes |
The general rule is simple:
- 80 and 443 open to the world. They are the entry point for your applications.
- 8080 and 8443 should never be open to the public internet.
- 4646, 4647, and 4648 stay restricted to the cluster's internal IPs.
Warning: Exposing 8080 without an allowlist is the most common misconfiguration in new clusters. Anyone with the admin token can submit jobs. Treat this port like SSH.
Recommended topology
internet
│
┌─────┴─────┐
│ 80, 443 │ ← any origin
└─────┬─────┘
│
┌───────┴───────┐
│ cluster │
│ nodes │
└───────┬───────┘
│
┌─────┴─────┐
│ 4646-4648 │ ← internal IPs only
└───────────┘
│
┌─────┴─────┐
│ 8080,8443 │ ← VPN or allowlist only
└───────────┘
Ubuntu and Debian (ufw)
Minimum configuration for a server node:
# regras default
sudo ufw default deny incoming
sudo ufw default allow outgoing
# acesso administrativo (ajuste o IP de origem)
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp comment 'SSH operador'
sudo ufw allow from 203.0.113.10 to any port 8080 proto tcp comment 'API'
sudo ufw allow from 203.0.113.10 to any port 8443 proto tcp comment 'Painel'
# tráfego público
sudo ufw allow 80/tcp comment 'Ingress HTTP'
sudo ufw allow 443/tcp comment 'Ingress HTTPS'
# comunicação interna entre nós (substitua pelos IPs reais)
for ip in 10.0.0.1 10.0.0.2 10.0.0.3 10.0.0.4; do
sudo ufw allow from $ip to any port 4646 proto tcp
sudo ufw allow from $ip to any port 4647 proto tcp
sudo ufw allow from $ip to any port 4648
done
sudo ufw enable
sudo ufw status numbered
On worker-only nodes (no exposed panel), skip the 8080 and 8443 lines.
RHEL, Fedora, AlmaLinux (firewalld)
# zonas separadas: pública para 80/443, interna para portas de cluster
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
# zona internal recebe os IPs do cluster
sudo firewall-cmd --permanent --zone=internal --add-source=10.0.0.0/24
sudo firewall-cmd --permanent --zone=internal --add-port=4646/tcp
sudo firewall-cmd --permanent --zone=internal --add-port=4647/tcp
sudo firewall-cmd --permanent --zone=internal --add-port=4648/tcp
sudo firewall-cmd --permanent --zone=internal --add-port=4648/udp
# acesso admin: zone trusted com IP do operador
sudo firewall-cmd --permanent --zone=trusted --add-source=203.0.113.10
sudo firewall-cmd --permanent --zone=trusted --add-port=8080/tcp
sudo firewall-cmd --permanent --zone=trusted --add-port=8443/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-all-zones
iptables directly
If you prefer explicit rules, or use a system without ufw/firewalld:
# tráfego estabelecido
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
# ingress público
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# admin restrito
iptables -A INPUT -p tcp -s 203.0.113.10 --dport 22 -j ACCEPT
iptables -A INPUT -p tcp -s 203.0.113.10 --dport 8080 -j ACCEPT
iptables -A INPUT -p tcp -s 203.0.113.10 --dport 8443 -j ACCEPT
# cluster interno (repita para cada IP)
iptables -A INPUT -p tcp -s 10.0.0.0/24 --dport 4646 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/24 --dport 4647 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/24 --dport 4648 -j ACCEPT
iptables -A INPUT -p udp -s 10.0.0.0/24 --dport 4648 -j ACCEPT
# default deny
iptables -P INPUT DROP
# persistir
sudo netfilter-persistent save
Cloud firewall as an upper layer
Even with ufw correctly set on each node, it's worth enabling the cloud provider firewall as a second layer. If ufw goes down due to a bad change, the cloud firewall keeps holding.
DigitalOcean Cloud Firewall
Create a firewall and assign it to all cluster droplets:
| Direction | Type | Source | Port |
|---|---|---|---|
| Inbound | TCP | Anywhere | 80, 443 |
| Inbound | TCP | Operator IPs | 22, 8080, 8443 |
| Inbound | TCP | Tag heroctl-cluster | 4646, 4647, 4648 |
| Inbound | UDP | Tag heroctl-cluster | 4648 |
| Outbound | All | Anywhere | All |
Use tags instead of IPs for 4646-4648 — when you add a new node with the tag, it joins the rule automatically.
AWS Security Groups
Create two security groups:
heroctl-public: 80 and 443 from0.0.0.0/0. Assign to all nodes.heroctl-cluster: 4646-4648 with source set to the security group itself (self-reference). For 8080 and 8443, source the bastion's security group.
Hetzner Cloud Firewall
Hetzner has no self-reference. Use the project's private network and open by CIDR:
allow tcp 80,443 from 0.0.0.0/0
allow tcp 4646,4647,4648 from 10.0.0.0/16
allow udp 4648 from 10.0.0.0/16
allow tcp 8080,8443 from <ip-do-operador>/32
Cloudflare in front
For protection against volumetric attacks, putting Cloudflare in proxy mode in front of the cluster works well. Watch out for:
- For certificate issuance, use DNS-01 (not HTTP-01). Proxy mode breaks HTTP-01.
- Restrict ports 80 and 443 on the nodes to accept only Cloudflare IP ranges. The official list lives at
https://www.cloudflare.com/ips-v4. Update via monthly cron. - Enable "Full (strict)" on Cloudflare. Don't use "Flexible" — that makes Cloudflare speak HTTP with your cluster even when the user has HTTPS.
Blocking administrative access
The most important rule in this document. Port 8080 cannot be reachable from the public internet. Three viable paths:
- VPN. WireGuard or Tailscale between operator machines and the cluster. Block 8080 from any source outside the VPN network. Recommended for teams.
- Fixed IP allowlist. Works for solo operators with stable residential IPs or a bastion VPS.
- SSH tunnel.
ssh -L 8080:localhost:8080 serverevery time you use the CLI. Works, but adds friction.
The combination most often seen in production is VPN + cloud firewall. The operator joins the VPN, the cloud firewall only allows 8080 from the VPN range, and the node's ufw does the same on the inside.
Validation
After applying the rules, validate from outside:
# from your machine, no VPN, against a cluster IP
nmap -p 80,443,8080,8443,4646,4647,4648 <ip-do-no>
Correct result: 80 and 443 open, all others closed (closed or filtered).
Next steps
- Configure ingress and TLS to start exposing applications.
- Review secret management.