SSH Bastion Host in an LXC
Run one small, hardened host as the only SSH entry point to your lab, so servers do not each face your network or VPN. This recipe builds it in a minimal Proxmox LXC, restricts what it can do, and connects to targets through it with ProxyJump.
Documentation: ssh_config · sshd_config · Proxmox pct
Verification: the OpenSSH options below were checked against the OpenBSD manual pages. The
pct createline was composed from the option list, with no worked example, and the firewall and logging advice is general practice. Nothing here was run end to end (unverified).
Goal #
You reach every internal host with ssh <TARGET>, through one bastion. The bastion allows only key logins (with optional second factor), forwards only to approved hosts, keeps no agent keys and logs every session.
Prerequisites #
- A Proxmox host with a container template downloaded, see Proxmox VE Overview and Install.
- A way to reach the bastion from outside, such as WireGuard, so SSH is not exposed to the internet directly.
- Console access to the container in case you lock yourself out.
Steps #
Create a minimal unprivileged container.
pct create <VMID> <STORAGE>:vztmpl/<TEMPLATE> --hostname bastion --unprivileged 1 \ --net0 name=eth0,bridge=vmbr0,ip=dhcp,firewall=1--unprivilegeddefaults to 1 on create. Do not enable nesting (--features nesting=1), and give it little CPU and memory. Put it on its own management VLAN if you have one (VLANs).Harden sshd. Follow SSH Hardening, then add to
/etc/ssh/sshd_config:PasswordAuthentication no AllowAgentForwarding no AllowTcpForwarding yes PermitOpen 192.0.2.10:22 192.0.2.11:22 PermitTTY no ClientAliveInterval 60 LogLevel VERBOSEPermitOpenlimits forwarding to the listedhost:portpairs, so the bastion cannot be used to reach anything else. Useanyornonefor the extremes.PermitTTY noblocks an interactive shell on the bastion itself. Jump connections still work, because they only forward a TCP connection.LogLevel VERBOSE(defaultINFO) records key fingerprints and forwarding.- Disabling agent forwarding adds little on its own if people can run a shell, so combine it with
PermitTTY noorForceCommand.
Restrict each key. In
~/.ssh/authorized_keyson the bastion:restrict,port-forwarding,permitopen="192.0.2.10:22" ssh-ed25519 AAAA... user@laptoprestrictturns off port, agent and X11 forwarding and the PTY, andport-forwardingturns forwarding back on for that key.permitopenlimits it to a target. Options such ascommand="..."andno-agent-forwardingalso exist.Optional: require a second factor.
AuthenticationMethods publickey,keyboard-interactivedemands both, and each method must also be enabled on its own. The keyboard-interactive side needs a PAM module for your second factor, which is not covered here.Optional: use SSH certificates. Short-lived certificates replace key distribution:
ssh-keygen -s <CA_KEY> -I <KEY_ID> -n <USER> -V +52w1d <USER_KEY>.pubSet
TrustedUserCAKeys /etc/ssh/ca.pubon the bastion. A certificate with no principals is rejected. The-Vvalue above is the documented format, so a short validity such as+1hfollows the same pattern but was not shown verbatim.Connect through it. On your client in
~/.ssh/config:Host bastion HostName bastion.example.com User <USER> Host 192.0.2.10 192.0.2.11 ProxyJump bastionssh -J bastion <USER>@192.0.2.10does the same on one line. Jump hosts do not inherit the destination’s settings, so keep options in~/.ssh/config.ProxyCommandis the older form, andssh -W host:portforwards standard input and output.Close the internal hosts. On each target, allow SSH only from the bastion’s address using UFW or the VLAN firewall rules.
Add brute-force protection. Install fail2ban or CrowdSec on the bastion.
Keep records. Ship the bastion’s auth log to your logging stack (Graylog, Elastic Stack or Loki and Alloy) and into Wazuh. A bastion that nobody reads logs from is only a delay.
Verify #
ssh -J bastion <TARGET>works, andssh <TARGET>directly from your client does not.- An attempt to forward to a host that is not in
PermitOpenis refused. - A password login to the bastion is refused.
- The login appears in the bastion’s log with the key fingerprint.
Roll Back #
Keep the Proxmox console open while you edit sshd_config, run sshd -t before systemctl reload ssh, and take a container snapshot first. Remove ProxyJump from your client config to go back to direct access.
Alternatives #
A bastion is one way to centralise SSH. These replace it with identity-based access:
| OPTION | HOW IT WORKS | TRADE-OFF |
|---|---|---|
| Tailscale SSH | tailscaled serves SSH on each node’s Tailscale IP, and tailnet policy decides who gets in | No bastion to run. Needs Tailscale on every target and a tailnet policy. Linux and open-source macOS only as servers |
| Teleport | Nodes tunnel out to a Proxy, so port 22 can stay closed. Short-lived certificates, SSO and session recording | Most features, but you run a cluster. Community Edition from v16 has a commercial licence |
| HashiCorp Boundary | Same goal | Not researched here |
The bastion in this recipe needs only OpenSSH and no extra service, which is its main advantage.
Not Verified #
The pct create line, the firewall and logging advice, and the PAM setup for a second factor. The Tailscale SSH and Teleport entries come from their official docs and were not run here.