Remote Access¶
Remote access to the homelab is built around three seats — three independent places to work from, so losing any one of them never means losing access. The OpenVPN endpoint described on the rest of this page runs on the first seat.
Three Seats¶
| Seat | Where | Role |
|---|---|---|
| Jump pod | In-cluster, jump namespace, LoadBalancer 192.168.1.62 |
Primary seat — ssh -p443 plus mosh + byobu for day-to-day and on-the-go sessions, and the host for the OpenVPN endpoint below. Automation state lives on its own PVC, so it survives a pod roll. |
| Mac workstation | Laptop | Build/fix seat — full local toolchain for development and repair work |
| bpi-r4 jumpfail | Edge router | Emergency seat — a self-contained failover work environment baked into the router firmware, restorable from the NAS; see BPI-R4 → Jumpfail Seat |
The seats degrade gracefully: everyday sessions land on the jump pod, heavier local work happens on the workstation, and if both the workstation and the cluster are down, the router still provides a working seat.
Because the jump seat lives inside the cluster, its state is held in etcd and on a replicated volume rather than on any one machine — which is what lets the primary seat survive the loss of any single host.
Separate from these three operator seats, roaming personal devices (phones, laptops) reach the LAN through their own door — a dedicated WireGuard tunnel on the router; see BPI-R4 → WireGuard.
OpenVPN¶
The OpenVPN server runs in the cluster, in the jump namespace, and shares port 443 with SSH and mosh via sslh — so no extra firewall rules are needed and no dedicated VPN port has to be open.
One pod, two containers¶
The server and its admin UI run as two containers in a single pod. That is the design decision worth noting: the admin UI needs OpenVPN's management socket on 127.0.0.1, and co-locating them in one pod is what makes that reachable over loopback. The previous arrangement had to combine host networking, a node selector and a host path to achieve the same thing — three properties that between them pinned the service to one specific machine. Sharing a pod network namespace dissolves all three at once, which is what made the service movable into the cluster at all.
Traffic Flow¶
flowchart TD
client["OpenVPN client\n:443"]
subgraph bpir4["BPI-R4"]
dnat[":443 DNAT\n→ jump pod"]
end
subgraph jumpns["mdapi-prod — jump namespace"]
subgraph bastion["jump-bastion pod"]
sslh["sslh :4443\nprotocol demux"]
sshd["sshd :22"]
end
subgraph ovpnpod["openvpn pod — two containers"]
openvpn["OpenVPN :1194\nmanagement socket\non loopback"]
admin["ovpn-admin UI\nbasic auth"]
end
pki["EasyRSA PKI\non its own\nbacked-up PVC"]
end
services["K8s services\n192.168.1.x"]
client --> dnat --> sslh
sslh -->|"OpenVPN detected"| openvpn
sslh -->|"SSH"| sshd
sslh -->|"TLS"| ingress["ingress-nginx"]
admin -.->|"loopback"| openvpn
openvpn --> pki
openvpn --> services
sslh reaches OpenVPN through its Service name, not an IP — so the demux keeps working across pod restarts and reschedules on either side.
Components¶
| Component | Location | Notes |
|---|---|---|
| sslh | jump-bastion pod :4443 |
Protocol demux — routes OpenVPN to the openvpn Service, SSH to the local shell, and anything TLS on to ingress-nginx |
| OpenVPN server | openvpn pod :1194 |
Reached by sslh via its Service name |
| ovpn-admin | openvpn pod, second container |
Admin UI on its own host behind basic auth — deliberately not behind the SSO gateway, so it stays reachable when identity itself is the thing that is broken |
| mosh | jump-bastion pod, UDP :443 |
mosh-server bound directly to the Service's UDP port; not multiplexed through sslh, which is TCP-only |
| EasyRSA PKI | dedicated PVC | EC/prime256v1 CA; ta.key for tls-crypt. On its own backed-up volume rather than a host path. |
Authentication¶
Authentication is client-certificate only. Each client presents a certificate issued by the EasyRSA CA, with tls-crypt (ta.key) wrapping the control channel.
The earlier design chained PAM → sssd → OpenLDAP so that VPN accounts came from the same directory as mail. That coupling was dropped when the service moved into the cluster: it made the VPN depend on the directory being up, which is the wrong dependency for the door you use to reach a broken estate. Certificates are self-contained, so the VPN now stays usable when LDAP does not.
Revoking a certificate is a delayed-action change
The admin UI lists the server's own certificate alongside client certificates. Revoking that entry does not break anything immediately — the running process keeps its already-loaded key — so the damage stays invisible until the next restart, which may be hours or days later. Check what a certificate belongs to before revoking it.
Why sslh instead of a dedicated port?¶
Exposing port 443 externally is reliably unblocked on all networks — hotels, corporate firewalls, mobile carriers. A dedicated VPN port (1194, 1194/UDP, etc.) is frequently blocked. sslh lets OpenVPN, SSH, and mosh coexist on 443, so the endpoint is reachable regardless of network restrictions — ssh -p443 against 192.168.1.62 on the LAN, and the same port from outside. An alternate port is published as well, for the rarer networks that interfere with 443 itself.
Note that the demux carries both TCP and UDP on that port: mosh is a UDP protocol, so a forward that covers only TCP silently gives you working SSH and a mosh client that hangs.