Onboard A Target Through A Reverse-Tunnel Bastion
on this page
Use this guide once you have a hardened bastion — see Set up a reverse-tunnel bastion — and want to add one more machine behind it: a museum PC, kiosk, or exhibit PC on a private network with no inbound access, reachable only via VNC or another local console.
Each target gets its own keypair, scoped on the bastion to exactly one loopback port, so one PC’s key can never be used to reach another PC’s tunnel or open an arbitrary listener.
Prerequisites
- A hardened bastion with an
adminuser and atunneluser, per Set up a reverse-tunnel bastion - Local console access to the target (VNC or similar) — the tunnel is started manually, never as a scheduled task or auto-start service
- A registry, even a simple spreadsheet, of PC name → assigned port → site. You will need to guarantee every PC gets a unique port.
1. Generate A Keypair On The Target
On the museum PC, in an elevated PowerShell session:
New-Item -ItemType Directory -Path C:\ProgramData\preflight-tunnel -Force | Out-Null
ssh-keygen -t ed25519 -f C:\ProgramData\preflight-tunnel\id_ed25519 -N '' -C "museum-pc-01"The private key never leaves the PC. Print the public key so you can copy it to the bastion:
Get-Content C:\ProgramData\preflight-tunnel\id_ed25519.pub2. Register The Key On The Bastion, Scoped To One Port
Pick the next unused port from your registry (for example 2201) and
record museum-pc-01 → 2201 before you do anything else — a duplicate port
assignment will silently break either the new PC or an existing one.
On the bastion, append a restricted entry to the tunnel user’s
authorized_keys:
echo 'restrict,port-forwarding,permitlisten="localhost:2201",command="/usr/sbin/nologin" ssh-ed25519 AAAA...museum-pc-01-key... museum-pc-01' \
| sudo tee -a /home/tunnel/.ssh/authorized_keys
sudo chown tunnel:tunnel /home/tunnel/.ssh/authorized_keys
sudo chmod 600 /home/tunnel/.ssh/authorized_keysWhat each restriction does:
| Clause | Effect |
|---|---|
restrict | Denies everything by default (PTY, agent forwarding, X11, generic port forwarding) — every capability below has to be explicitly re-enabled |
port-forwarding | Re-enables port forwarding, which restrict had turned off |
permitlisten="localhost:2201" | Narrows that forwarding to only a reverse listener on 127.0.0.1:2201 — this key cannot bind any other port |
command="/usr/sbin/nologin" | Even if someone used this key to open an interactive session, they’d get no shell |
This is what makes per-PC keys mutually isolated: museum-pc-02’s key
physically cannot open a listener on port 2201, so it can never observe or
hijack museum-pc-01’s tunnel.
flowchart TB
subgraph Tunnel["tunnel user's authorized_keys on the bastion"]
K1["key: museum-pc-01<br/>permitlisten=localhost:2201"]
K2["key: museum-pc-02<br/>permitlisten=localhost:2202"]
K3["key: museum-pc-03<br/>permitlisten=localhost:2203"]
end
P1["127.0.0.1:2201"]
P2["127.0.0.1:2202"]
P3["127.0.0.1:2203"]
K1 --> P1
K2 --> P2
K3 --> P33. Add A Manual-Trigger Script On The Target
Save this as a .ps1 on the museum PC’s desktop (or wherever the on-site
operator will find it via VNC). It is deliberately not a scheduled task
or a service — the tunnel only exists while this window is open and someone
chose to run it.
ssh -N -R 2201:localhost:22 tunnel@bastion.example.org `
-i C:\ProgramData\preflight-tunnel\id_ed25519 `
-o ServerAliveInterval=30 `
-o ExitOnForwardFailure=yes-N means “no remote command, just forward” and blocks for as long as the
tunnel is open; closing the window ends the session. ExitOnForwardFailure
makes a port conflict or a rejected key fail loudly instead of silently
running with no working forward.
Double-clicking a .ps1 file often just flashes a window and closes it,
depending on the default file association and execution policy. If that
happens, right-click → Run with PowerShell, or wrap the command in a
.cmd file that calls powershell.exe -File open-tunnel.ps1 so the
window behaves predictably for a non-technical on-site operator.
4. Confirm Windows’ administrators_authorized_keys Quirk (If Applicable)
This tunnel forwards to the museum PC’s own sshd on port 22 — so whatever
account Preflight or the operator authenticates as through the tunnel is
subject to normal Windows OpenSSH rules. If that account is in the local
Administrators group, Windows OpenSSH Server ignores the per-user
authorized_keys file entirely and requires
C:\ProgramData\ssh\administrators_authorized_keys instead, with ACLs
restricted to Administrators and SYSTEM only:
icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r
icacls C:\ProgramData\ssh\administrators_authorized_keys /grant "Administrators:F" "SYSTEM:F"Non-admin accounts use the normal per-user authorized_keys file and don’t
need this. If a key that looks correctly installed still gets rejected,
check whether the target account is a local admin first.
5. Configure The Controller For Ad Hoc Interactive Access
For a person opening an interactive session (troubleshooting, a quick console check), add a dedicated include file so museum-specific hosts stay out of your main SSH config:
# ~/.ssh/config
Include ~/.ssh/museum-tunnels.conf# ~/.ssh/museum-tunnels.conf
Host bastion
HostName bastion.example.org
User admin
IdentityFile ~/.ssh/bastion_admin_key
IdentitiesOnly yes
Host museum-pc-01
HostName localhost
Port 2201
User exhibit
ProxyJump bastion
IdentityFile ~/.ssh/museum-pc-01_key
IdentitiesOnly yesIdentitiesOnly yes on both blocks matters: without it, SSH offers
every key it knows about at each hop, and enough wrong offers trips the
server’s MaxAuthTries — you’ll see Too many authentication failures
even though the right key is in your config. Pinning one identity per
Host block avoids that entirely.
ssh museum-pc-01 now does both hops in one command: jump to bastion,
then connect to localhost:2201 from the bastion’s point of view, which
lands on the museum PC’s forwarded sshd.
6. Point Preflight’s Inventory At The Same Path
Preflight connects with its own SSH client and doesn’t read ~/.ssh/config
at all — the host aliases and ProxyJump from step 5 only apply to
interactive ssh. Give Preflight the same two-hop path natively with a
jump block. (Key auth still works the same way: Preflight uses your SSH
agent and default ~/.ssh keys automatically, or an explicit
private_key.) address here is resolved from
the bastion’s perspective, which is why it’s 127.0.0.1, not the
museum PC’s real (unreachable) LAN address:
inventory:
hosts:
- name: museum-pc-01
address: 127.0.0.1
port: 2201
transport: ssh
username: exhibit
private_key: secret:museum-pc-01-os-key
jump:
address: bastion.example.org
username: admin
private_key: secret:bastion-admin-keyprivate_key on the host entry is the museum PC’s own OS-level key for
the exhibit account — a different key from the tunnel-scoped key used
to open the tunnel itself. See
Bastion / Jump Hosts
for how Preflight’s native jump support behaves.
7. Open A Session And Confirm It Works
On-site: VNC in and run the trigger script from step 3. From the controller, once the tunnel is up:
ssh museum-pc-01 # ad hoc interactive check
preflight facts museum-pc-01 --output json # proves Preflight can authenticate through both hopsA successful facts response confirms the jump host, the forwarded port,
and the target’s own credentials are all correct.
sequenceDiagram
actor VNC as On-site operator (VNC)
participant PC as Museum PC
participant Bastion
actor Ctrl as Controller (ssh / Preflight)
VNC->>PC: Double-click open-tunnel.ps1
PC->>Bastion: ssh -N -R 2201:localhost:22 (tunnel user, restricted key)
Note over Bastion: GatewayPorts no: binds 127.0.0.1:2201, not reachable externally
Ctrl->>Bastion: ssh admin@bastion (jump hop)
Bastion->>PC: proxied to 127.0.0.1:2201 -> PC:22
PC-->>Ctrl: SSH session over the forwarded port
Note over VNC,PC: Closing the VNC window ends the tunnel - access never outlives the sessionTroubleshooting
| Symptom | Likely cause |
|---|---|
Too many authentication failures | Missing IdentitiesOnly yes on one of the Host blocks — see step 5 |
| Tunnel script exits immediately with a forwarding error | The port in -R doesn’t match what’s registered in the bastion’s authorized_keys permitlisten, or another PC is already holding that port |
| Key accepted but no shell / commands silently no-op | Expected for the tunnel user’s key — it carries command="/usr/sbin/nologin" by design; only use it for -N forwarding, never interactive login |
Connection to the target’s own sshd rejected despite a correct key | Check whether the target account is a local Administrator — see step 4 |
| Session dies when the on-site operator closes the VNC window | Expected — the tunnel is tied to that interactive process by design, so access doesn’t outlive the person who opened it. Reopen it from the desktop script for the next session. |