Every SSH port you expose to the internet is a door someone else is also trying to open. If your engineers connect directly to production instances from their laptops, you're carrying that risk on every host, all the time. A bastion host collapses that exposure down to a single, hardened, heavily monitored entry point — and this guide walks you through how to set up bastion host SSH access correctly, from network placement to key management to the client-side config that makes it invisible to your team's daily workflow. By the end, you'll have a working jump box, a ProxyJump configuration your engineers can drop into their laptops, and a verification checklist to confirm nothing was left open. We'll use AWS for the concrete examples, but the underlying pattern applies to any cloud or on-prem VPC design.
Step 1: Set Up Bastion Host SSH Access with a Minimal AMI and Security Group
Start with the smallest possible attack surface. Provision a dedicated instance in a public subnet running a minimal, current OS image — Amazon Linux 2023 or Ubuntu's minimal AMI both work well. Do not install anything on this box beyond SSH and your monitoring agent. No web servers, no databases, no application code. Its entire job is to broker connections.
In this bastion host AWS tutorial step, the security group is where most of the actual security lives:
# Bastion security group: inbound
aws ec2 authorize-security-group-ingress \
--group-id sg-0bastion123456 \
--protocol tcp --port 22 \
--cidr 203.0.113.0/24 # your office/VPN CIDR only, never 0.0.0.0/0
Never open port 22 to 0.0.0.0/0. Restrict it to your office IP range, corporate VPN egress, or a small allowlist of static IPs. If your team is fully remote with rotating IPs, put the bastion behind a VPN or use AWS Systems Manager Session Manager as a supplementary access path instead of widening the CIDR.
Step 2: Lock Down Private Instance Security Groups
Your internal hosts should never accept SSH from the internet or from arbitrary VPC ranges — only from the bastion itself.
aws ec2 authorize-security-group-ingress \
--group-id sg-0private789 \
--protocol tcp --port 22 \
--source-group sg-0bastion123456
Referencing the bastion's security group as the source (rather than a CIDR block) means the rule stays correct even if the bastion's IP changes on a future replacement or Auto Scaling event.
Step 3: Generate and Distribute SSH Keys Properly
Each engineer should use their own key pair — never a shared bastion key. Generate a modern Ed25519 key:
ssh-keygen -t ed25519 -C "engineer@safeguard.sh" -f ~/.ssh/id_ed25519_bastion
Push public keys to the bastion (and only the bastion) through your configuration management tool or an IAM-integrated mechanism like EC2 Instance Connect, rather than manually editing authorized_keys. If you're managing this by hand at small scale, keep a single source-controlled file of authorized public keys and deploy it via your provisioning pipeline so key removal on offboarding is a one-line diff, not a manual SSH session to hunt down.
Step 4: Configure SSH ProxyJump for Transparent Access
This is the step that turns a bastion from an annoyance into something your team forgets is even there. Rather than SSHing into the bastion and then SSHing again into the target host, use ProxyJump so a single command handles both hops. This ssh proxyjump setup lives entirely in ~/.ssh/config on each engineer's machine:
Host bastion
HostName 203.0.113.50
User ec2-user
IdentityFile ~/.ssh/id_ed25519_bastion
IdentitiesOnly yes
Host prod-app-*
User ec2-user
IdentityFile ~/.ssh/id_ed25519_prod
ProxyJump bastion
IdentitiesOnly yes
With this in place, ssh prod-app-01 transparently routes through the bastion using an encrypted, nested tunnel — no manual double-hop, no copying keys onto the bastion itself (which you should avoid entirely; the bastion should never hold keys to the hosts behind it). This same pattern is the backbone of any solid jump box configuration guide, whether you're managing five instances or five hundred, and it scales cleanly as you add hosts by pattern-matching Host blocks.
Step 5: Enable Session Logging and Auditing
A bastion's second job, after access control, is producing a clean audit trail. Enable detailed logging on the bastion itself:
# /etc/ssh/sshd_config
LogLevel VERBOSE
LogLevel VERBOSE records the key fingerprint used for each login, which is essential for tracing which engineer's credential was used in an incident. Pair this with session recording — script, auditd, or a commercial session-capture tool — so that command-level activity on the bastion is retained, not just connection metadata. Ship these logs off-box immediately to a SIEM or log aggregator; a compromised bastion with only local logs gives an attacker the means to erase their own trail.
Step 6: Harden the Bastion Configuration
A few sshd_config changes meaningfully shrink the remaining attack surface:
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers ec2-user engineer1 engineer2
Disabling password authentication entirely removes the most common brute-force vector. Combine this with fail2ban or AWS Network Firewall rate-limiting on port 22, automatic patching (dnf-automatic or unattended-upgrades), and a policy of rotating or replacing the bastion instance periodically rather than letting it accumulate configuration drift over months of uptime.
Step 7: Automate Bastion Lifecycle with Infrastructure as Code
Hand-built bastions become undocumented snowflakes fast. Define the instance, security groups, and key policy in Terraform or CloudFormation so the entire setup is reproducible, reviewable in pull requests, and destroyable/recreatable on demand — which is itself a useful security control, since a freshly rebuilt bastion carries no accumulated cruft or forgotten test keys. Tag the resource clearly and include it in your regular infrastructure security review rather than treating it as a one-time setup task you configure and forget.
Troubleshooting and Verification
Once the pieces are in place, verify each layer independently before declaring the bastion production-ready:
- Connection refused from outside your CIDR: Confirm the security group ingress rule and check whether a network ACL or on-prem firewall is also blocking the range — security groups aren't the only gate.
Permission denied (publickey)through ProxyJump: Test the bastion hop alone first (ssh -v bastion) before testing the full chain, and check thatIdentitiesOnly yesis set so SSH doesn't offer the wrong key from your agent.- ProxyJump hangs or times out: Verify the private instance's security group actually references the bastion's security group ID (Step 2) and that the bastion has a route to the private subnet.
- Can't tell who accessed what after an incident: This almost always traces back to skipping Step 5 — confirm
LogLevel VERBOSEis active and that logs are being shipped off-instance, not just sitting in local/var/log/secure. - Bastion still reachable from
0.0.0.0/0: Re-runaws ec2 describe-security-groupsagainst the bastion's group ID and diff the output against your intended state — this is the single most common misconfiguration we see in bastion reviews, often introduced by a well-meaning "just for testing" rule that never got removed.
Run a full end-to-end test from a machine outside your allowlisted range and confirm the connection is rejected, then test again from inside the range to confirm it succeeds. Both outcomes matter equally.
How Safeguard Helps
A bastion host is only as trustworthy as the software supply chain that built it — the base AMI, the SSH daemon package, the configuration management tooling, and every dependency your provisioning pipeline pulls in along the way. Safeguard continuously verifies the provenance and integrity of the artifacts and packages flowing into infrastructure like this, so a tampered base image or a compromised upstream dependency doesn't quietly become the foundation of your most privileged access point. We also help teams catch configuration drift and exposed secrets in infrastructure-as-code before it ships, flagging things like overly permissive security group rules or hardcoded keys in the same pull request review where a human reviewer might miss them. For a component as security-critical as a bastion host, that extra layer of automated scrutiny is the difference between a control that looks secure on paper and one that actually holds up under attack.