
September 12, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
SSH bastion host patterns solve a problem every production team hits once private servers stop accepting direct internet SSH. You expose one hardened jump box on a public subnet. Every other machine stays on private addresses. Operators and CI/CD pipelines reach them through that single gate. On real client projects I maintain on shared EC2 infrastructure, this pattern is how we deploy Laravel apps without opening port 22 on application servers. The sections below walk through the patterns that actually work in 2026, with copy-paste config you can drop into Ubuntu SSH server setup today.
What Are SSH Bastion Host Patterns and When Do You Need One?
A bastion host—also called a jump host or jump box—is a minimal Linux server whose only job is to relay SSH into a private network. It sits in a DMZ or public subnet. Your web servers, databases, and queue workers live behind it with no inbound SSH from the world.
You need this pattern when any of these apply:
- Servers run on cloud VPCs with private-only application tiers, as in a typical Laravel on AWS EC2 layout.
- Compliance or insurance asks for a single auditable SSH entry point.
- Your team rotates keys and wants one place to enforce SSH key-only authentication.
- Direct port 22 on every box creates too many attack surfaces for a small ops team.
You do not need a bastion for a single VPS where you are the only operator. One box, one firewall rule, one key pair—that is enough. The pattern pays off once you have three or more private hosts or shared deploy access.
How Do You Configure SSH ProxyJump for a Bastion Host?
Modern OpenSSH supports ProxyJump (the -J flag). One command reaches a private host through the bastion. No manual two-step login. No shell session left open on the jump box unless you want one.
Client-side ~/.ssh/config
Put this on each developer laptop. Replace hostnames and keys with your values.
Host bastion
HostName bastion.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Host app-*
User deploy
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion
StrictHostKeyChecking accept-new
Host app-web1
HostName 10.0.2.15
Host app-web2
HostName 10.0.2.16 Connect with ssh app-web1. OpenSSH opens a channel to the bastion first, then forwards to the private IP. The official OpenSSH manual documents ProxyJump and ProxyCommand at man.openbsd.org/ssh_config.5.
One-liner without config file
ssh -J deploy@bastion.example.com deploy@10.0.2.15 For scripts and CI pipelines, prefer the config file. Hard-coded jumps break when IPs change. Use DNS private records or inventory tags instead.
scp and rsync through the bastion
scp -J deploy@bastion.example.com ./release.tar.gz deploy@10.0.2.15:/var/www/
rsync -avz -e "ssh -J deploy@bastion.example.com" ./public/ deploy@10.0.2.15:/var/www/current/public/ Deployer 7 and GitLab CI jobs I run on sister legal-tech sites use this exact rsync-over-ProxyJump flow. The CI runner holds the deploy key. It never needs a VPN client installed.
Which SSH Bastion Host Patterns Fit Different Network Layouts?
Not every team needs the same topology. Pick the pattern that matches your subnet design and compliance tier.
Pattern 1: Single bastion (most common)
One jump host in a public subnet. All private servers accept SSH only from the bastion security group. This covers most Laravel, WordPress, and WooCommerce stacks on a single VPC. I use it on the shared EC2 pipeline that deploys sites like Notary Kathmandu and related legal-tech portals.
Pattern 2: Bastion per environment
Separate jump boxes for production, staging, and development. Production bastion gets stricter IP allowlists and MFA. Staging may allow the whole office CIDR. Blast radius stays smaller when a staging key leaks.
Pattern 3: Multi-tier bastion chain
Large or regulated networks sometimes require two jumps: internet to DMZ bastion, then DMZ bastion to internal bastion, then app servers. Chain ProxyJump entries in ~/.ssh/config:
Host internal-bastion
HostName 10.0.1.5
User jump
ProxyJump dmz-bastion
Host prod-db
HostName 10.0.3.20
User admin
ProxyJump internal-bastion Use this only when policy demands it. Each extra hop adds latency and debugging pain. Most Nepal SMB hosting setups never need it.
Pattern 4: Bastion with Session Manager overlay
On AWS, some teams pair a traditional bastion with SSM Session Manager for break-glass access when SSH keys fail. The bastion remains the primary path for Deployer and rsync. SSM covers emergency console access without opening port 22 wider. AWS documents bastion host guidance in their Linux bastion Quick Start.
| Pattern | Best for | Ops overhead | Security posture |
|---|---|---|---|
| Single bastion | 1–20 private servers, single VPC | Low | Good with hardening |
| Per-environment bastion | Prod/staging isolation | Medium | Better blast-radius control |
| Multi-tier chain | Regulated enterprise, multi-DMZ | High | Strong segmentation |
| Bastion + SSM break-glass | AWS-heavy teams | Medium | Good resilience |
| No bastion (VPN only) | Full-tunnel remote workforce | Medium–high | Depends on VPN hygiene |
How Do You Harden a Bastion Host for Production SSH?
The bastion is your highest-value SSH target. Compromise it and an attacker can reach every private box that trusts it. Harden it harder than any other server.
Baseline hardening checklist
- Disable password authentication and root login in
/etc/ssh/sshd_config. - Allow only Ed25519 or ECDSA keys; retire weak RSA below 4096 bits.
- Restrict
AllowUsersorAllowGroupsto named deploy accounts. - Bind SSH to a non-default port only if your firewall logs help you; security through obscurity alone is worthless.
- Install
fail2banwith sshd jails, as covered in SSH hardening with fail2ban. - Enable automatic security updates on Ubuntu 22.04 or 24.04.
- Ship auth logs to a central syslog or CloudWatch—never rely on local rotation alone.
Example sshd fragment for the bastion:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers deploy
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowTcpForwarding yes
X11Forwarding no
AllowAgentForwarding no Set AllowAgentForwarding no on the bastion unless you have a documented reason. Agent forwarding from a laptop through a bastion to a production box is a common lateral-movement path. See SSH agent forwarding risks for safer alternatives like short-lived certificates.
Security group and firewall rules
On the bastion: allow inbound TCP 22 (or your chosen port) only from known office IPs, VPN egress, or CI runner ranges. On private app servers: allow inbound 22 only from the bastion private IP or its security group ID. Deny everything else. Outbound from the bastion to private subnets on 22 should be explicit—not a wide 0.0.0.0/0 egress rule.
Keep the bastion minimal
Do not run nginx, MySQL, or Redis on the jump box. No customer data. No cron jobs beyond log shipping and patching. A smaller package list means fewer CVEs. Patch monthly at minimum; weekly if you expose SSH to broad IP ranges.
How Should CI/CD and Deploy Pipelines Use a Bastion Host?
Automated deploys need the same jump path as humans. Treat the CI runner like a machine user with its own key pair—never reuse a developer laptop key.
GitLab CI example
deploy_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client rsync
- eval "$(ssh-agent -s)"
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_CONFIG" > ~/.ssh/config && chmod 600 ~/.ssh/config
script:
- dep deploy production
only:
- main Store the private key and a minimal SSH_CONFIG with ProxyJump in GitLab CI variables. Rotate the deploy key when staff leave. The same pattern applies to GitHub Actions with webfactory/ssh-agent or native ssh-agent setup.
Deployer 7 host definition
host('production')
->setHostname('10.0.2.15')
->setRemoteUser('deploy')
->set('ssh_arguments', ['-o ProxyJump=deploy@bastion.example.com'])
->setDeployPath('/var/www/app'); After symlink swap, reload PHP-FPM over the same jump path. I have seen deployments succeed but cron still pointing at an old release path because someone ran crontab edits outside the bastion workflow. Keep all server access consistent.
For tunnel-based database migrations during deploy, use local port forwarding through the bastion instead of opening MySQL to the internet. That pairs well with guidance in SSH tunneling and port forwarding:
ssh -J deploy@bastion.example.com -L 3307:10.0.3.20:3306 deploy@10.0.2.15 -N Connect your migration tool to 127.0.0.1:3307. The tunnel closes when the session ends.
What Are Common SSH Bastion Host Mistakes to Avoid?
These failures show up repeatedly on production Laravel and WordPress hosts I troubleshoot.
- Shared personal keys on the bastion. Every operator should use their own key. Shared keys break audit trails. Generate strong keys with a password generator tool for passphrases, not for SSH key material itself—use
ssh-keygen -t ed25519. - Agent forwarding enabled globally. An attacker who lands on the bastion can pivot through forwarded agents. Disable it server-side.
- No session logging. Install
auditdor use structured auth logging. You need to know who reached which private IP and when. - Bastion used as an application server. Disk fills up, Apache gets installed, and suddenly the jump box runs PHP. Do not do this.
- Forgetting outbound rules. Inbound hardening on private hosts is useless if the bastion can SSH to the entire RFC1918 space uncontrolled.
- Skipping key rotation. Rotate deploy keys quarterly or on any staff change. For larger teams, move toward SSH certificate authorities with short TTLs.
Another subtle issue: stale known_hosts entries after rebuilds. Private IPs get reused. Use HashKnownHosts yes on clients and update entries when you reprovision.
If you outgrow a single bastion—high concurrency deploys, many simultaneous operator sessions—consider a second bastion behind a load balancer for availability. That moves into high-availability architecture territory. Document which bastion IP your security groups reference.
Key Takeaways
- Place one hardened bastion in a public subnet; private servers accept SSH only from that host.
- Use OpenSSH
ProxyJumpin~/.ssh/configso developers and CI share the same path. - Disable password auth, agent forwarding, and root login on the bastion; log every session.
- Give CI/CD its own deploy key—never mount a human laptop key into GitLab variables.
- Pick single-bastion for most VPCs; add tiers only when policy requires segmentation.
- Pair bastion access with SSH hardening on Linux across all tiers, not just the jump box.
People Also Ask
What is the difference between a bastion host and a jump server?
They are the same thing. "Bastion" emphasises the military DMZ idea—a fortified entry point. "Jump server" or "jump box" describes the SSH relay behaviour. Documentation and cloud providers mix the terms freely.
Can you use a bastion host without opening port 22 to the internet?
Yes. Restrict inbound SSH on the bastion to your office CIDR, a VPN egress IP, or a Zero Trust edge. Some teams replace public SSH entirely with WireGuard plus an internal jump. The bastion pattern still applies; only the reachability of port 22 changes.
Is a bastion host better than a VPN for server access?
They solve different problems. A bastion gives you a narrow SSH gate ideal for deploy scripts and occasional admin. A VPN puts the client inside the private network for many protocols. Most web teams need a bastion first. Add VPN later if staff need direct access to internal HTTP tools or databases beyond SSH.
How much does a bastion host cost to run?
On AWS, a t3.micro or t4g.nano bastion costs roughly Rs 800–1,500/month (~USD 6–11) plus a small EBS volume. The real cost is operational: patching, monitoring, and key rotation. For Nepal SMB budgets, that is usually cheaper than a managed VPN appliance.
Build a Bastion Layer Your Deploy Pipeline Can Trust
SSH bastion host patterns are not exotic infrastructure. They are the minimum sane gate between the internet and your private Laravel, WordPress, or eCommerce servers. Start with one jump box, ProxyJump in team SSH config, security groups that trust only the bastion, and a dedicated CI deploy key. Harden the bastion first; private tiers second. If you want help designing this for a production stack—or wiring Deployer and GitLab CI through a jump host—Linux system administration support and ongoing server maintenance are where I usually start with clients. See deployed examples on the Adventure Third Pole Trek portfolio entry, or contact us to review your current SSH exposure.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

