Skip to content

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.