A Kubernetes operator that automatically manages Hetzner Cloud Firewall rules based on the current nodes in your cluster. Designed for RKE2 + Cilium clusters running on Hetzner Cloud.
When nodes join or leave the cluster, the operator updates the Hetzner Cloud Firewall to allow only the required ports between the current set of node IPs. It supports both IPv4 and IPv6.
Optionally, the Helm chart can also deploy Cilium Host Firewall policies (CiliumClusterwideNetworkPolicy) that enforce the same rules at the kernel level using eBPF, providing defense in depth.
- The operator watches Kubernetes
Noderesources using controller-runtime. - It extracts each node's Hetzner Cloud server ID from
spec.providerID(hcloud://SERVER_ID). - It reads each node's IPv4 and IPv6 addresses from
status.addresses. - It classifies nodes as control-plane or worker based on standard labels (
node-role.kubernetes.io/control-plane,node-role.kubernetes.io/master,node-role.kubernetes.io/etcd). - It computes the desired firewall rules and applies them via the Hetzner Cloud API.
- It attaches the firewall to any new servers that don't have it yet.
Reconciliation is triggered by node events (create, delete, address change) and runs periodically as a safety net.
The following ports are managed for an RKE2 + Cilium cluster:
| Port | Protocol | Source | Description |
|---|---|---|---|
| 9345 | TCP | Cluster nodes | RKE2 supervisor API (node registration) |
| 6443 | TCP | Cluster nodes | Kubernetes API server |
| 8472 | UDP | Cluster nodes | Cilium VXLAN overlay |
| 10250 | TCP | Cluster nodes | kubelet API |
| 4240 | TCP | Cluster nodes | Cilium health checks |
| 4245 | TCP | Cluster nodes | Hubble Relay |
| ICMP | - | Cluster nodes | Ping, Path MTU Discovery |
| 2379 | TCP | Control-plane nodes | etcd client requests |
| 2380 | TCP | Control-plane nodes | etcd peer communication |
| 80 | TCP | Public | HTTP ingress |
| 443 | TCP | Public | HTTPS ingress |
| 30000-32767 | TCP+UDP | Configurable | NodePort services |
| 22 | TCP | Configurable | SSH access |
- A Kubernetes cluster running on Hetzner Cloud (nodes must have
hcloud://provider IDs) - A Hetzner Cloud API token with read/write permissions
- Helm 3
- Cilium CNI (required if using the Cilium Host Firewall policies)
helm install hetzner-fw ./charts/hetzner-firewall-operator \
-n hetzner-firewall-operator --create-namespace \
--set hcloudToken=YOUR_HCLOUD_TOKEN \
--set operator.firewallName=my-cluster \
--set 'operator.allowSSHFrom={YOUR_IP/32}'If you manage secrets externally (e.g. via Sealed Secrets or External Secrets), create a secret with a HCLOUD_TOKEN key and reference it:
kubectl create secret generic hcloud-credentials \
-n hetzner-firewall-operator \
--from-literal=HCLOUD_TOKEN=YOUR_TOKEN
helm install hetzner-fw ./charts/hetzner-firewall-operator \
-n hetzner-firewall-operator --create-namespace \
--set existingSecret=hcloud-credentialsTo deploy both Hetzner Cloud Firewall management and Cilium Host Firewall policies:
helm install hetzner-fw ./charts/hetzner-firewall-operator \
-n hetzner-firewall-operator --create-namespace \
--set hcloudToken=YOUR_HCLOUD_TOKEN \
--set 'operator.allowSSHFrom={YOUR_IP/32}' \
--set ciliumPolicies.enabled=true \
--set ciliumPolicies.defaultDeny=trueWarning: Enabling
ciliumPolicies.defaultDenywill block all host ingress traffic not explicitly allowed by the other policies. Make sure your SSH CIDR and all required ports are configured before enabling this, or you will lose access to your nodes.
docker build -t ghcr.io/atroo/hetzner-firewall-operator:latest .
docker push ghcr.io/atroo/hetzner-firewall-operator:latestexport HCLOUD_TOKEN=your-token
go run ./cmd/operator \
--firewall-name=k8s-cluster \
--allow-ssh-from=YOUR_IP/32 \
--reconcile-interval=1mThis uses your local ~/.kube/config to connect to the cluster.
| Key | Type | Default | Description |
|---|---|---|---|
image.repository |
string | ghcr.io/atroo/hetzner-firewall-operator |
Container image repository |
image.tag |
string | latest |
Container image tag |
image.pullPolicy |
string | IfNotPresent |
Image pull policy |
imagePullSecrets |
list | [] |
Image pull secrets for private registries |
| Key | Type | Default | Description |
|---|---|---|---|
hcloudToken |
string | "" |
Hetzner Cloud API token. Creates a Secret automatically. |
existingSecret |
string | "" |
Name of an existing Secret with a HCLOUD_TOKEN key. If set, hcloudToken is ignored. |
| Key | Type | Default | Description |
|---|---|---|---|
operator.firewallName |
string | k8s-cluster |
Name of the Hetzner Cloud Firewall to create and manage |
operator.reconcileInterval |
string | 5m |
Interval between full reconciliation runs (Go duration) |
operator.nodePortPublic |
bool | false |
If true, NodePort range (30000-32767) is open to 0.0.0.0/0. If false, only cluster node IPs are allowed. |
operator.allowSSHFrom |
list | [] |
CIDRs allowed SSH access. Empty = no SSH rule. Example: ["1.2.3.4/32", "10.0.0.0/8"] |
operator.labelSelector |
string | "" |
Kubernetes label selector to filter which nodes the operator considers |
operator.metricsAddr |
string | :8080 |
Bind address for the Prometheus metrics endpoint |
operator.healthAddr |
string | :8081 |
Bind address for health/readiness probes |
| Key | Type | Default | Description |
|---|---|---|---|
replicaCount |
int | 2 |
Number of replicas. >1 enables HA via leader election. |
resources.requests.cpu |
string | 10m |
CPU request |
resources.requests.memory |
string | 32Mi |
Memory request |
resources.limits.cpu |
string | 100m |
CPU limit |
resources.limits.memory |
string | 64Mi |
Memory limit |
nodeSelector |
object | {} |
Node selector for the operator pod |
tolerations |
list | [] |
Tolerations for the operator pod |
affinity |
object | {} |
Affinity rules for the operator pod |
podAnnotations |
object | {} |
Additional pod annotations |
podLabels |
object | {} |
Additional pod labels |
| Key | Type | Default | Description |
|---|---|---|---|
serviceAccount.create |
bool | true |
Create a ServiceAccount |
serviceAccount.name |
string | "" |
Override ServiceAccount name (defaults to release fullname) |
serviceAccount.annotations |
object | {} |
ServiceAccount annotations |
rbac.create |
bool | true |
Create ClusterRole and ClusterRoleBinding |
| Key | Type | Default | Description |
|---|---|---|---|
serviceMonitor.enabled |
bool | false |
Create a Prometheus ServiceMonitor resource |
serviceMonitor.interval |
string | 60s |
Scrape interval |
serviceMonitor.labels |
object | {} |
Additional labels on the ServiceMonitor (e.g. for Prometheus selector matching) |
These settings control the optional CiliumClusterwideNetworkPolicy resources that enforce the same port rules at the eBPF/kernel level.
| Key | Type | Default | Description |
|---|---|---|---|
ciliumPolicies.enabled |
bool | false |
Deploy Cilium Host Firewall policies |
ciliumPolicies.defaultDeny |
bool | false |
Deploy a default-deny host ingress policy. Warning: will block all traffic not explicitly allowed. |
ciliumPolicies.apiServerPublic |
bool | false |
Allow external access to the Kubernetes API server (port 6443) |
ciliumPolicies.allNodesSelector |
object | {} |
Override nodeSelector for all-node policies. Defaults to kubernetes.io/os: linux. |
ciliumPolicies.controlPlaneSelector |
object | {} |
Override nodeSelector for the etcd policy. Defaults to node-role.kubernetes.io/control-plane: "true". |
+-----------------------+
| Hetzner Cloud API |
| (Firewall Rules) |
+-----------^-----------+
|
+-----------+-----------+
| hetzner-firewall- |
| operator |
| (controller-runtime) |
+-----------^-----------+
|
watches Node resources
|
+----------+----------+----------+
| | | |
+--+--+ +--+--+ +--+--+ +--+--+
|Node1| |Node2| |Node3| |Node4|
|CP | |CP | |Wkr | |Wkr |
+-----+ +-----+ +-----+ +-----+
Hetzner Firewall (edge): IP-based rules per node
Cilium Host FW (kernel): eBPF policies on each node
The two firewall layers are complementary:
- Hetzner Cloud Firewall filters traffic at the network edge before it reaches the server. It uses IP/CIDR + port rules and is managed dynamically by the operator.
- Cilium Host Firewall filters traffic at the kernel level using eBPF on each node. It uses Kubernetes-native identity (
remote-node,world,host) and is deployed as static policies via the Helm chart.
MIT