External Traffic Flow & Redirects¶
Single Entry Point Design¶
All external traffic enters through a single WAN IP on BPI-R4 and is demultiplexed by protocol on the jump host. This approach means only one port is exposed externally for all TCP traffic (HTTPS, SSH, OpenVPN), reducing the attack surface.
flowchart TD
client["External Client"]
subgraph bpir4["BPI-R4 — OpenWrt 25.12"]
dnat_tcp["TCP :443 DNAT\n→ mbptillo:4443"]
dnat_udp["UDP :443 DNAT\n→ mbptillo:4443"]
end
subgraph mbptillo["mbptillo — 192.168.1.246"]
sslh["sslh :4443\nprotocol demux"]
socat["socat UDP4/UDP6-LISTEN:4443\n→ localhost:443"]
mosh_srv["mosh-server :443\nauthbind"]
openvpn["OpenVPN :9443\ntun10 10.8.10.0/24"]
sshd["sshd :22"]
end
subgraph k8s["mdapi-prod"]
ingress["ingress-nginx\n192.168.1.191:443"]
end
client -->|"TCP :443"| dnat_tcp --> sslh
client -->|"UDP :443"| dnat_udp --> socat --> mosh_srv
sslh -->|"TLS"| ingress
sslh -->|"SSH"| sshd
sslh -->|"OpenVPN"| openvpn
ingress --> svc["*.mdapi.ch services"]
Protocol Demultiplexing with sslh¶
sslh inspects the first bytes of each TCP connection to identify the protocol, then forwards to the appropriate backend — all on the same port. This allows SSH, HTTPS, and OpenVPN to coexist on port 443, which is reliably unblocked on most networks (hotels, corporate firewalls).
| Protocol detected | Forwarded to |
|---|---|
| TLS / HTTPS | ingress-nginx at 192.168.1.191:443 |
| SSH | local sshd at :22 |
| OpenVPN | OpenVPN server at 127.0.0.1:9443 |
mosh Relay¶
mosh uses UDP for its transport layer, which requires separate handling from the TCP sslh demux. Two socat instances forward UDP from the external port to the local mosh-server.
flowchart LR
mc["mosh client\nvpn.home.tillo.ch:443"]
bpir4["BPI-R4\nDNAT UDP :443 → :4443"]
sc["socat UDP-LISTEN:4443,fork\n→ UDP:localhost:443"]
ms["mosh-server :443\nauthbind"]
mc --> bpir4 --> sc --> ms
socat LISTEN vs RECVFROM
socat UDP-LISTEN:4443,fork forks once per client session, preserving the source IP:port for the lifetime of the connection. Using UDP-RECVFROM:4443,fork instead forks per packet, creating a new ephemeral source port each time — mosh-server interprets each packet as a new client and the session breaks immediately.
TLS Certificate Automation¶
All certificates are issued by Let's Encrypt via cert-manager using DNS-01 challenges. DNS-01 is used instead of HTTP-01 because it supports wildcard certificates and works for services that are not publicly reachable.
flowchart LR
ing["Ingress\ncert-manager.io/issuer:\nletsencrypt-prod"]
cm["cert-manager"]
bind9["BIND9\nauthoritative nameserver\n192.168.1.53"]
le["Let's Encrypt\nACME v2"]
secret["TLS Secret\nin namespace"]
ing --> cm
cm -->|"nsupdate RFC 2136\nTSIG signed"| bind9
bind9 -->|"_acme-challenge TXT"| le
le -->|"certificate"| cm --> secret --> ing
The BIND9 instance is the authoritative nameserver for mdapi.ch, tillo.ch, and all hosted zones. cert-manager uses TSIG to sign DNS update requests (nsupdate RFC 2136), so no DNS provider API token is needed — the key stays on-premise.
Ingress Routing¶
ingress-nginx terminates TLS and routes by hostname. ModSecurity WAF runs inline on every request.
flowchart LR
nginx["ingress-nginx\n192.168.1.191\nModSecurity WAF"]
nginx --> ha["home.mdapi.ch\nHome Assistant"]
nginx --> joplin["notes.mdapi.ch\nJoplin"]
nginx --> windmill["windmill.mdapi.ch\nWindmill"]
nginx --> nextcloud["cloud.envuassu.ch\nNextcloud"]
nginx --> keycloak["login.envuassu.ch\nKeycloak"]
nginx --> more["...30+ more services"]
GitLab uses its own bundled nginx-ingress-controller (at 192.168.1.197) to handle its own domains (gitlab.mdapi.ch, registry.mdapi.ch, kas.mdapi.ch).