Broker SSH and RDP with a bastion host and no public ports
This is for developers who need SSH and RDP access to private machines without leaving ports 22 or 3389 open to the internet. You will set up a small broker host, force all access through it, and verify that only the broker is reachable while target hosts stay private.
TL;DR — Put a single broker host on the public internet, lock it down to HTTPS and your identity layer, and tunnel SSH/RDP through it to private hosts over the VPC/LAN. The key fix if you are still exposing ports is: remove public IPs or inbound rules for 22/3389 from target machines, and only allow those ports from the broker's private IP/security group. Reading time: ~5 min
Goal
When you finish, developers will reach Linux and Windows machines through one broker host over a single audited path, while the target machines have no public SSH or RDP exposure and reject direct internet connections on ports 22 and 3389.
Prerequisites
- A public Linux VM to act as the broker/bastion, with a public IP and
sudoaccess; Ubuntu 22.04 or 24.04 works. Check with:
lsb_release -ds
- One or more private target machines reachable from the broker over private network: Linux targets for SSH on
22, Windows targets for RDP on3389. - Firewall or security-group control for the broker and targets.
- OpenSSH client/server
>= 8.4. Check with:
ssh -V
sshd -V
iptablesornftablesavailable on the broker. This article usesiptables-nftsyntax. Check with:
iptables --version
nginxavailable on the broker if you want a browser entrypoint over HTTPS. Check with:
nginx -v
- A DNS name for the broker, for example
access.example.com, and your DNS provider login. - An IdP or SSO-aware reverse proxy if your org requires browser auth before reaching the broker UI; if not, you can still use SSH certificates or SSH keys directly.
- The private IPs of your targets, for example
10.0.2.10for Linux and10.0.2.20for Windows.
Steps
Step 1: Remove direct internet exposure from target hosts
For each target machine, delete any public IP and remove inbound rules allowing 22/tcp or 3389/tcp from 0.0.0.0/0. Then add rules allowing only the broker's private IP, shown here as 10.0.1.5.
Linux target firewall example:
sudo ufw delete allow 22/tcp
sudo ufw allow from 10.0.1.5 to any port 22 proto tcp
sudo ufw status numbered
Windows target PowerShell example:
Remove-NetFirewallRule -DisplayName "OpenSSH-Server-In-TCP"
New-NetFirewallRule -DisplayName "RDP from broker only" -Direction Inbound -Protocol TCP -LocalPort 3389 -RemoteAddress 10.0.1.5 -Action Allow
Get-NetFirewallRule -DisplayName "RDP from broker only" | Get-NetFirewallAddressFilter
What you should see: the target still accepts SSH/RDP from the broker, but direct internet scans to :22 and :3389 time out or are filtered.
Step 2: Harden the broker and allow only HTTPS plus SSH from admins
On the broker, allow inbound 443/tcp for the browser-facing entrypoint and optionally 22/tcp only from your office/VPN CIDR. Deny forwarding except to approved private targets.
⚠️ If you apply firewall rules over SSH without allowing your current source IP, you can lock yourself out. Keep a second console open from your cloud provider before running these commands.
sudo apt-get update
sudo apt-get install -y openssh-server nginx iptables-persistent
sudo iptables -F
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -A INPUT -p tcp -s 203.0.113.0/24 --dport 22 -j ACCEPT
sudo netfilter-persistent save
What you should see: netfilter-persistent saves rules without error, and new inbound connections work only on 443 and 22 from your admin CIDR.
Step 3: Restrict SSH on the broker to tunneling only
Create a dedicated Unix account that cannot get a shell and can only open approved TCP forwards.
sudo adduser --disabled-password --gecos "" broker
sudo mkdir -p /home/broker/.ssh
sudo chmod 700 /home/broker/.ssh
sudo nano /home/broker/.ssh/authorized_keys
Put one public key per line in /home/broker/.ssh/authorized_keys, with forced restrictions like this:
restrict,port-forwarding,permitopen="10.0.2.10:22",permitopen="10.0.2.20:3389" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... dev@example
Then lock down sshd:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo bash -c 'cat >> /etc/ssh/sshd_config <<"EOF"
Match User broker
AllowTcpForwarding yes
X11Forwarding no
PermitTTY no
ForceCommand /bin/false
PasswordAuthentication no
EOF'
sudo sshd -t && sudo systemctl restart ssh
What you should see: sshd -t exits 0, and systemctl restart ssh completes with no output.
Step 4: Point DNS and HTTPS at the broker
Create an A record for access.example.com to the broker public IP in your DNS provider dashboard, for example DNS → Records → Add record.
Then install TLS on the broker:
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d access.example.com --redirect -m ops@example.com --agree-tos --non-interactive
What you should see: Certbot prints Congratulations! and https://access.example.com returns HTTP/2 200 or your login redirect.
Step 5: Add a minimal HTTPS landing page with connection instructions
This gives users one audited URL instead of sharing raw hostnames and ports.
sudo tee /var/www/html/index.html > /dev/null <<'EOF'
<!doctype html>
<html><body>
<h1>Private Access Broker</h1>
<p>SSH Linux: <code>ssh -N -L 2222:10.0.2.10:22 broker@access.example.com</code></p>
<p>Then connect: <code>ssh -p 2222 dev@localhost</code></p>
<p>RDP Windows: <code>ssh -N -L 13389:10.0.2.20:3389 broker@access.example.com</code></p>
<p>Then open your RDP client to <code>localhost:13389</code>.</p>
</body></html>
EOF
sudo systemctl reload nginx
What you should see: browsing to https://access.example.com shows the three command lines above.
Step 6: Connect through the broker
For SSH to a private Linux host:
ssh -N -L 2222:10.0.2.10:22 broker@access.example.com
In a second terminal:
ssh -p 2222 dev@localhost
For RDP to a private Windows host:
ssh -N -L 13389:10.0.2.20:3389 broker@access.example.com
Then connect your RDP client to localhost:13389.
What you should see: the first SSH command stays open with no shell; the second SSH command prompts for the target host key or logs in; the RDP client opens the Windows login screen.
View as text
[Developer] ---- 1. HTTPS/SSH ----> [Broker] [Broker] ---- 2. SSH 22 ----------> [Linux Target] [Broker] ---- 3. RDP 3389 --------> [Windows Target] [Internet] ----X 22/3389 ---------> [Targets]
Verify it works
Run these checks from your laptop and from a host outside your private network.
Broker HTTPS:
curl -I https://access.example.com
Expected shape:
HTTP/2 200
server: nginx
date: Tue, 30 Oct 2026 12:00:00 GMT
content-type: text/html
If you used an auth gateway in front, a redirect is also fine:
HTTP/2 302
location: https://idp.example.com/login?redirect=https%3A%2F%2Faccess.example.com%2F
Direct target exposure should fail from the internet:
nc -vz target-public-name-or-ip 22
nc -vz target-public-name-or-ip 3389
Expected shape:
nc: connect to 198.51.100.20 port 22 (tcp) failed: Operation timed out
nc: connect to 198.51.100.20 port 3389 (tcp) failed: Operation timed out
Brokered SSH should work:
ssh -N -L 2222:10.0.2.10:22 broker@access.example.com
ssh -p 2222 dev@localhost 'hostname && whoami'
Expected shape:
app-01
dev
Common pitfalls
Port 22 or 3389 is still open on the target
Mistake: you left a public IP or a security-group rule allowing 0.0.0.0/0 to the target.
Symptom: nc -vz target-ip 22 or 3389 succeeds directly from the internet.
One-line fix: remove the public IP or delete the inbound rule, then add an allow rule only from the broker private IP or broker security group.
SSH tunnel opens, but the second hop says "channel 2: open failed: administratively prohibited"
Mistake: permitopen in authorized_keys does not include the target IP and port, or AllowTcpForwarding is off.
Symptom:
channel 2: open failed: administratively prohibited: open failed
stdio forwarding failed
One-line fix: add the exact permitopen="10.0.2.10:22" or permitopen="10.0.2.20:3389" entry and restart sshd after sshd -t passes.
Certbot fails because DNS is not pointing at the broker yet
Mistake: you ran certificate issuance before the A record resolved publicly.
Symptom:
Could not resolve host: access.example.com
or HTTP challenge failures.
One-line fix: wait for DNS to resolve with dig +short access.example.com, then rerun the same certbot --nginx -d access.example.com ... command.
Browser shows a redirect loop on the broker URL
Mistake: you put another proxy in front that also forces HTTPS or rewrites X-Forwarded-Proto incorrectly.
Symptom:
curl -I https://access.example.com
HTTP/2 301
location: https://access.example.com/
repeats on every request.
One-line fix: keep HTTPS redirect in one layer only, and if using a reverse proxy, pass X-Forwarded-Proto https consistently.
RDP tunnel connects but the Windows login never appears
Mistake: Windows firewall allows 3389 from the wrong source, often your laptop IP instead of the broker private IP.
Symptom: the SSH tunnel stays open, but the RDP client hangs on Configuring remote session or times out.
One-line fix: replace the firewall rule with -RemoteAddress 10.0.1.5 for the broker private IP and verify with Test-NetConnection -ComputerName 10.0.2.20 -Port 3389 from the broker.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI