Network Topology¶
Overview¶
flowchart TB
Internet((Internet))
subgraph Hetzner["Hetzner Cloud FSN1"]
subgraph FW["Hetzner Cloud Firewall"]
direction TB
end
subgraph PubNet["Public Network"]
HubPub["Hub: 91.98.121.97"]
DMZPub["DMZ: 178.104.134.113"]
end
subgraph PrivNet["Private Network 10.0.0.0/16"]
subgraph Subnet["10.0.1.0/24"]
HubPriv["Hub: 10.0.1.1"]
DMZPriv["DMZ: 10.0.1.2"]
BeastPriv["Beast: 10.0.1.3"]
end
end
HubPub --- HubPriv
DMZPub --- DMZPriv
end
Internet -->|"443/tcp"| DMZPub
Internet -->|"2222/tcp (SSH honeypot)"| HubPub
Internet -->|"51820/udp"| HubPub
Home["Home PC"] -.->|"WireGuard 10.0.2.1"| HubPub
style PrivNet fill:#1c2833,stroke:#566573,color:#fff
style FW fill:#4a235a,stroke:#7d3c98,color:#fff
Private Network¶
All three VMs share a Hetzner private network (10.0.0.0/16, subnet 10.0.1.0/24). This network:
- Is free (no additional cost)
- Never traverses the public internet
- Carries all inter-node K3s traffic (etcd sync, pod-to-pod via Cilium VXLAN)
- Is the only path from Hub/DMZ to Beast (Beast has no permanent public IP when hourly-billed)
| Host | Private IP | Public IP |
|---|---|---|
| Hub (cx33) | 10.0.1.1 | 91.98.121.97 |
| DMZ (cx23) | 10.0.1.2 | 178.104.134.113 |
| Beast (cx53) | 10.0.1.3 | Dynamic (hourly) |
WireGuard Tunnel¶
WireGuard runs on the Hub node, providing secure access from the home workstation into the cluster.
| Parameter | Value |
|---|---|
| Listen port | 51820/udp |
| Hub tunnel IP | 10.0.2.1 |
| Home peer IP | 10.0.2.2 |
| Allowed IPs | 10.0.0.0/16, 10.43.0.0/16 (K3s services) |
| Keepalive | 25s |
| Key type | Curve25519 |
Through the WireGuard tunnel, the home workstation can reach:
- All private IPs (10.0.1.x) for SSH and management
- K3s service network (10.43.x.x) for kubectl
- Rancher UI and Grafana dashboards
Firewall Rules¶
Hetzner Cloud Firewall (Perimeter)¶
Hub Firewall
| Direction | Port | Protocol | Source | Purpose |
|---|---|---|---|---|
| Inbound | 2222 | TCP | Any | SSH (real) + endlessh (22) |
| Inbound | 51820 | UDP | Any | WireGuard |
| Inbound | 10250 | TCP | 10.0.1.0/24 | Kubelet API (private only) |
| Inbound | 6443 | TCP | 10.0.0.0/16 | K3s API (private + WG) |
DMZ Firewall
| Direction | Port | Protocol | Source | Purpose |
|---|---|---|---|---|
| Inbound | 443 | TCP | Any | HTTPS (Caddy) |
| Inbound | 80 | TCP | Any | HTTP (redirect to 443) |
| Inbound | 10250 | TCP | 10.0.1.0/24 | Kubelet API (private only) |
Beast Firewall
| Direction | Port | Protocol | Source | Purpose |
|---|---|---|---|---|
| Inbound | 10250 | TCP | 10.0.1.0/24 | Kubelet API (private only) |
| Inbound | 2222 | TCP | 10.0.0.0/16 | SSH (private + WG only) |
Host-Level UFW¶
Each VM runs UFW as a second layer. Egress is restricted to known destinations.
Defense in depth -- two-layer firewall problem
Hetzner Cloud Firewall is stateless and perimeter-only. UFW on each host provides stateful filtering and egress control. Both layers must agree for traffic to pass. This was learned the hard way: Hub was missing ufw allow in 6443/tcp, so the K3s agent on DMZ could not join the cluster even though port 6443 was open in the Hetzner Cloud Firewall. Every port allowed in the Hetzner firewall must also be allowed in UFW on the corresponding host. See firewall-matrix for the complete port mapping across both layers.
SSH Configuration¶
| Parameter | Value |
|---|---|
| SSH port | 2222 (all VMs) |
| Port 22 | endlessh tarpit (Hub/DMZ only) |
| Authentication | Key-only (password disabled) |
| Root login | Disabled |
| Authorized keys | Deploy key + personal key |
# ~/.ssh/config snippet
Host hub
HostName 91.98.121.97
Port 2222
User deploy
IdentityFile ~/.ssh/lron_ed25519
Host hub-wg
HostName 10.0.2.1
Port 2222
User deploy
IdentityFile ~/.ssh/lron_ed25519
Host dmz
HostName 178.104.134.113
Port 2222
User deploy
IdentityFile ~/.ssh/lron_ed25519
Host beast
HostName 10.0.1.3
Port 2222
User deploy
IdentityFile ~/.ssh/lron_ed25519
ProxyJump hub-wg
CrowdSec¶
CrowdSec runs on Hub and DMZ as an IDS/IPS layer:
- Parses SSH, Caddy, and system auth logs
- Shares threat intelligence via CrowdSec Central API
- Blocks malicious IPs via the
cs-firewall-bouncer(iptables integration) - SSH brute-force detection with progressive ban duration
See Hardening for full CrowdSec configuration.
Cilium CNI¶
Cilium replaces the default K3s Flannel CNI for pod networking:
- VXLAN encapsulation over the private network
- NetworkPolicy enforcement at the eBPF level (L3/L4/L7)
- Hubble for network flow observability
- DMZ pods are isolated from management pods via Cilium NetworkPolicy
See Kubernetes for Cilium installation details.
DMZ Isolation Principle¶
The DMZ node runs only ingress-related workloads. Cilium NetworkPolicies enforce:
- DMZ pods can receive traffic from the internet (port 443)
- DMZ pods can forward authenticated requests to Hub-hosted backends
- DMZ pods cannot initiate connections to monitoring, Rancher, or cluster-internal services
- Beast pods cannot receive direct internet traffic
flowchart LR
Inet((Internet)) -->|"443"| DMZ["DMZ Pods<br/>(ingress ns)"]
DMZ -->|"authenticated"| Hub["Hub Pods<br/>(app ns)"]
DMZ -.-x|"blocked"| Mon["Hub Pods<br/>(monitoring ns)"]
Inet -.-x|"blocked"| Beast["Beast Pods<br/>(dev ns)"]
style DMZ fill:#7b241c,stroke:#c0392b,color:#fff
style Hub fill:#1a5276,stroke:#2980b9,color:#fff
style Beast fill:#1e8449,stroke:#27ae60,color:#fff