Browser (HTTPS :443)
│
▼
OVH VPS — 51.89.139.223
nginx reverse proxy
TLS termination (Let's Encrypt)
Security headers (HSTS, CSP, X-Frame-Options)
CSF firewall
│
▼ proxy_pass http://10.99.101.xxx
WireGuard tunnel (wg0)
Encrypted — OVH → homelab LAN
│
▼
MetalLB — Layer 2 load balancer
Pool: 10.99.101.200–230
ARP announcement to LAN
│
├── 10.99.101.205 → wordpress6/wordpress (port 80)
├── 10.99.101.206 → cluster-dashboard/dashboard-nginx (port 80)
└── 10.99.101.207 → mfa2fa-docs/docs-nginx (port 80)
│
▼
Kubernetes pod (nginx / WordPress)
Serves the response
│
▼ (reverse)
Back through WireGuard → OVH nginx → Browser
The nginx vhost on the OVH box handles TLS termination and routing. Each path gets its own location block with a rewrite to strip the path prefix before proxying to the cluster:
server {
listen 80;
server_name mfa2fa.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name mfa2fa.com;
ssl_certificate /etc/letsencrypt/live/mfa2fa.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mfa2fa.com/privkey.pem;
# Portal landing page — served locally from OVH box
root /var/www/mfa2fa-portal;
location = / {
try_files /index.html =404;
}
# WordPress — strip /wp prefix
location /wp {
rewrite ^/wp(/.*)$ $1 break;
proxy_pass http://10.99.101.205;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Cluster dashboard — status.json needs its own rewrite
location /cluster/status.json {
rewrite ^/cluster/status.json$ /status.json break;
proxy_pass http://10.99.101.206;
proxy_set_header Host $host;
}
location /cluster {
rewrite ^/cluster(/.*)$ $1 break;
rewrite ^/cluster$ / break;
proxy_pass http://10.99.101.206;
proxy_set_header Host $host;
}
# Docs
location /docs {
rewrite ^/docs(/.*)$ $1 break;
rewrite ^/docs$ / break;
proxy_pass http://10.99.101.207;
proxy_set_header Host $host;
}
}
/wp/wp-content/... URLs which it doesn't recognise — everything breaks. Same applies to /cluster/ and /docs/.The OVH VPS connects to the homelab over WireGuard. All MetalLB IPs are routed through wg0.
# /etc/wireguard/wg0.conf on OVH VPS [Interface] Address = 10.99.0.1/24 PrivateKey = <private-key> [Peer] PublicKey = <homelab-public-key> AllowedIPs = 10.99.0.0/24 Endpoint = <homelab-public-ip>:51820 PersistentKeepalive = 25
Routes to MetalLB IPs are added via an external bash script called from rc.local:
#!/bin/bash # /usr/local/bin/add-routes.sh ip route add 10.99.101.205 dev wg0 ip route add 10.99.101.206 dev wg0 ip route add 10.99.101.207 dev wg0 exit 0
ip route add ... scope link does NOT work when called from a script at boot — only works when run manually from the CLI. Drop scope link entirely; the kernel infers scope from the interface.
MetalLB runs in Layer 2 mode — it uses ARP to announce which node owns each LoadBalancer IP. The speaker DaemonSet runs on all nodes and handles announcements.
| IP | Service | Namespace | Port |
|---|---|---|---|
| 10.99.101.200 | demo-nginx | default | 80 |
| 10.99.101.201 | tomcat-webapp-base | default | 8080 |
| 10.99.101.202 | tomcat-webapp-dev | dev | 8081 |
| 10.99.101.203 | tomcat-webapp-prod | prod | 8086 |
| 10.99.101.204 | my-redis-master-external | redis | 6379 |
| 10.99.101.205 | wordpress | wordpress6 | 80 |
| 10.99.101.206 | dashboard-nginx | cluster-dashboard | 80 |
| 10.99.101.207 | docs-nginx | mfa2fa-docs | 80 |
metallb.io/loadBalancerIPs not metallb.universe.tf/loadBalancerIPs. The old annotation still works in v0.14 but logs a deprecation warning.Local causes MetalLB healthcheck failures on this cluster — the IP gets assigned but never bound to a node interface, making it unreachable. Always use Cluster.The OVH box runs CSF (ConfigServer Firewall). MetalLB IPs in the 10.99.101.x range are private/bogon addresses — CSF's BOGON blocklist blocks them by default.
# /etc/csf/csf.conf — skip BOGON checks on wg0 LF_BOGON_SKIP = "wg0" # /etc/csf/csf.ignore — whitelist the MetalLB subnet 10.99.101.0/24 # Apply changes csf -r
wg0 means any future MetalLB IP in the pool just works — no individual IP whitelisting needed.To debug CSF blocking:
# Check if an IP is being blocked and by which rule csf -g 10.99.101.206 # Temporarily allow an IP for testing csf -a 10.99.101.206 # Check BOGON match in ipset ipset list bl_BOGON | grep "10.99.101"