Skip to content

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:

  1. DMZ pods can receive traffic from the internet (port 443)
  2. DMZ pods can forward authenticated requests to Hub-hosted backends
  3. DMZ pods cannot initiate connections to monitoring, Rancher, or cluster-internal services
  4. 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