Hardened Jump Hosts on OCI, AWS, and DigitalOcean
Hardened Jump Hosts on OCI, AWS, and DigitalOcean
BLUF: If you need a "jump point" for legitimate administration, make it a hardened bastion host with narrowly scoped ingress, strong identity controls, centralized logging, and no direct trust beyond what is required.
This note covers defensive bastion/jump-host architecture for approved administrative access. It does not cover offensive redirectors, covert proxying, or C2 infrastructure.
Authorized use only: Use these notes only in owned, explicitly authorized, or isolated lab environments.
Detection awareness: Assume commands, binaries, network calls, identity changes, and cloud or directory actions may be logged by endpoint tooling, audit frameworks, SIEM pipelines, proxy logs, DNS logs, auth logs, and platform telemetry.
Blue-team view: Treat every technique as a defender validation exercise too: note what artifacts it creates, what alerts or hunts could surface it, and what monitoring or hardening would prevent or contain it.
CTF/lab boundary: If a sandbox or CTF includes bypass-oriented exercises, keep them confined to that environment and translate the lesson into detection, prevention, and cleanup notes rather than real-world evasion guidance.
1. Safe Architecture
graph TD
Admin[Admin laptop
VPN or allowlisted IP] --> Bastion[Bastion / Jump Host]
Bastion --> Private1[Private workload subnet]
Bastion --> Private2[Internal admin endpoint]
Bastion --> Logs[Central logs / SIEM]
Bastion --> Patch[Patch + config management]
FW[Cloud firewall / SG / NSG] --> BastionCore Pattern
- Put the bastion in its own subnet or isolate it with strict firewall policy.
- Allow inbound access only from your admin IP range or VPN.
- Use SSH keys only or a managed access service.
- Send auth, audit, and network logs to a central location.
- Treat the bastion as ephemeral infrastructure, not a snowflake box.
2. Best Option by Cloud
| Cloud | Preferred approach | Good fallback |
|---|---|---|
| OCI | OCI Bastion service for private SSH sessions | Small hardened compute instance with NSG-restricted SSH |
| AWS | SSM Session Manager with no inbound SSH if possible | EC2 bastion with SG allowlisting and SSH keys only |
| DigitalOcean | Hardened Droplet with Cloud Firewall and private networking | Same, plus WireGuard/Tailscale if you want no public SSH from the internet |
Rule: If the cloud gives you a managed access path, prefer that over a permanently exposed VM.
3. Shared Hardening Checklist
- Disable password authentication.
- Disable direct root login.
- Restrict inbound access to a fixed source IP or VPN only.
- Use the smallest VM size that satisfies the admin task.
- Enable automatic security updates.
- Forward logs off-host.
- Use a separate admin user and
sudo; do not work as root. - Remove anything not needed: compilers, unused services, sample packages.
- Rotate SSH keys and review authorized keys regularly.
- Rebuild from code or image when possible instead of hand-tuning forever.
Minimal sshd_config Baseline
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
Re-enable
AllowTcpForwardingonly if you explicitly need it for legitimate admin workflows.
4. GitHub Resources
These are useful as reference material for bastions, reverse proxies, and hardening patterns:
| Resource | Use |
|---|---|
| BastionBox | Simple bastion host setup ideas for cloud labs |
| docker-bastion | Minimal OpenSSH bastion container reference |
| jumpbox | Multi-user SSH jumpbox reference |
| secured-bastion-host-terraform | AWS sample for secured bastion infrastructure |
| reproxy | Lightweight reverse-proxy reference |
| ansible-cis-nginx-hardening | NGINX hardening examples |
5. Step-by-Step Walkthrough
Step 1 — Decide whether you need a VM at all
Use this decision rule:
- OCI: use OCI Bastion if the target is private and you only need admin SSH access.
- AWS: use SSM Session Manager if you can avoid inbound SSH completely.
- DigitalOcean: if no managed equivalent is available, use a hardened Droplet plus Cloud Firewall.
If you only need a short-lived admin path, avoid creating a permanent public bastion.
Step 2 — Build isolated network boundaries
Before you launch anything:
- Create or identify the target VPC/VCN/project network.
- Put the bastion in a separate subnet or isolate it with dedicated firewall rules.
- Allow inbound admin access only from:
- your static office/home IP
- a corporate VPN
- a private overlay like WireGuard or Tailscale
- Allow outbound traffic only to:
- package mirrors
- logging destinations
- the specific internal systems you administer
Do not allow 0.0.0.0/0 to SSH unless this is a temporary lab and you understand the risk.
Step 3 — Launch the smallest possible host
Preferred baseline:
- OS: Ubuntu LTS or another minimal supported image
- Size: smallest size that still handles your admin workload
- Storage: encrypted boot volume
- Identity: SSH key injection at launch, no password auth
- Metadata: do not store secrets in user-data unless you must
After first boot:
sudo apt update && sudo apt -y upgrade
sudo apt -y install fail2ban unattended-upgrades auditd
Create a dedicated admin user:
sudo adduser adminops
sudo usermod -aG sudo adminops
sudo mkdir -p /home/adminops/.ssh
sudo cp /home/ubuntu/.ssh/authorized_keys /home/adminops/.ssh/authorized_keys
sudo chown -R adminops:adminops /home/adminops/.ssh
sudo chmod 700 /home/adminops/.ssh
sudo chmod 600 /home/adminops/.ssh/authorized_keys
Step 4 — Harden SSH immediately
Edit /etc/ssh/sshd_config and apply the baseline above.
Then restart SSH:
sudo systemctl restart ssh
sudo systemctl status ssh --no-pager
Enable Fail2ban:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status
Keep one working session open while testing SSH changes so you do not lock yourself out.
Step 5 — Enable logging and updates
At minimum:
- auth logs
- sudo logs
- Fail2ban logs
- cloud flow logs / VPC flow logs / NSG logs
- auditd for privileged activity
Also enable automatic updates:
sudo dpkg-reconfigure --priority=low unattended-upgrades
If you have a SIEM or log collector, ship logs off-host so compromise of the bastion does not erase the trail.
Step 6 — Test the path like an operator
Validate all of the following:
- You can log in from the approved admin source.
- You cannot log in from an unapproved source.
- Password auth is rejected.
- Root login is rejected.
- Logs arrive centrally.
- The bastion can reach only the internal systems it is supposed to manage.
Useful checks:
ssh -o PreferredAuthentications=password adminops@bastion.example
ssh adminops@bastion.example
sudo tail -n 50 /var/log/auth.log
sudo ufw status verbose
6. OCI Walkthrough
Preferred: OCI Bastion Service
- Create or identify a private subnet for the target host.
- In OCI, create a Bastion in the same VCN.
- Create a session scoped to the target private instance.
- Restrict session lifetime and allowed CIDR/source where possible.
- Use the generated SSH command to connect through the managed bastion path.
Why this is best: no permanently exposed public SSH service is required.
If you must use a bastion VM
- Launch a small compute instance in a subnet with a public IP.
- Create an NSG or Security List rule allowing TCP/22 only from your admin IP.
- Block all other inbound ports unless there is a documented need.
- Permit outbound traffic only to required private subnets and update/logging endpoints.
- Apply the shared hardening checklist.
- Install Tailscale and lock SSH to
tailscale0only — see OCI-specific iptables note below.
OCI quick checks
- NSG ingress: only TCP/22 from your IP or VPN
- Instance access: SSH key only
- Logging: VCN flow logs + instance logs if available
- Session preference: OCI Bastion over direct exposure
OCI-specific: UFW does not work as expected
OCI Ubuntu images ship with /etc/iptables/rules.v4 managed by iptables-persistent. This file is loaded at boot before UFW initialises, and its chain structure (ACCEPT → REJECT-all fallthrough) makes UFW's chains unreachable dead code. Do not rely on UFW alone on OCI.
Verify with:
sudo iptables -L INPUT -n -v --line-numbers
If you see a bare ACCEPT tcp dpt:22 rule before the ufw-before-input chain, UFW is not enforcing it.
Fix: edit /etc/iptables/rules.v4 directly and scope the port 22 rule to the interface you want:
# Replace open rule:
# -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
# With Tailscale-only rule:
sudo sed -i 's|-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT|-A INPUT -i tailscale0 -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT|' /etc/iptables/rules.v4
# Apply live without reboot:
sudo iptables -D INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
sudo iptables -I INPUT 5 -i tailscale0 -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
Also scope SSH daemon to only listen on the Tailscale interface:
# Add to /etc/ssh/sshd_config.d/99-hardening.conf:
ListenAddress <tailscale_ip>
ListenAddress 127.0.0.1
OCI-specific: disable unnecessary default services
OCI Ubuntu images enable several services that a jump box does not need. Disable them:
sudo systemctl disable --now rpcbind rpcbind.socket iscsid multipathd packagekit
Verify nothing unexpected is listening after:
ss -tlnp
OCI-specific: oracle-cloud-agent may reset iptables
The oracle-cloud-agent snap service runs as root and can modify host configuration including iptables. After locking down /etc/iptables/rules.v4, verify your rules survive a reboot and monitor for unexpected changes. If the agent rewrites your rules, you may need to add a post-boot systemd unit to re-apply your restrictions.
7. AWS Walkthrough
Preferred: SSM Session Manager
- Launch EC2 in a private subnet if possible.
- Attach an IAM role with the minimum SSM permissions needed.
- Ensure the SSM Agent is installed and online.
- Create VPC endpoints or controlled outbound internet access so the instance can reach SSM.
- Connect with Session Manager instead of exposing inbound SSH.
Why this is best: no inbound port 22 required.
If you must use an EC2 bastion
- Place the EC2 bastion in a public subnet.
- Assign a Security Group that allows:
- inbound TCP/22 from your admin IP only
- no other inbound rules unless documented
- Use private Security Groups on internal workloads so only the bastion can reach them.
- Enable CloudTrail, VPC Flow Logs, and instance logs.
- Apply the shared SSH hardening baseline.
AWS quick checks
- Security Group:
22/tcpfrom your IP only - Prefer SSM over public SSH
- CloudTrail enabled for account and region
- Session logging enabled if using Session Manager
8. DigitalOcean Walkthrough
DigitalOcean usually means you will run a real bastion host, so be stricter with boundaries.
- Create a Droplet with a minimal Ubuntu image.
- Inject your SSH public key at creation time.
- Put the Droplet behind a Cloud Firewall:
- inbound TCP/22 from your admin IP or VPN only
- deny all other inbound by default
- Use VPC networking for private traffic to internal systems.
- Apply SSH hardening, Fail2ban, and automatic updates.
- If possible, place admin access behind WireGuard or Tailscale and remove broad public exposure.
DigitalOcean quick checks
- Cloud Firewall: strict source allowlist
- VPC networking enabled for east-west traffic
- Backups/snapshots configured if the bastion is important
- Monitoring and alerts enabled
9. Reverse Proxy Guidance
Only use a reverse proxy if you need controlled application-layer access, not just SSH administration.
Safe baseline:
- TLS enabled
- only required hostnames/routes exposed
- rate limits where possible
- access logs shipped centrally
- upstreams restricted to explicit internal services
If you do not need HTTP(S) application access, do not add a reverse proxy just because it seems flexible.
10. Operator Notes
- Bastions are for access control and auditability, not convenience.
- The best bastion is often the one you can replace quickly.
- Managed access services usually beat self-managed exposed SSH.
- Keep the jump host boring, small, logged, and easy to rebuild.
11. Tailscale-Only Access Pattern
This is the recommended pattern when you want zero public SSH exposure and stable access regardless of your source IP changing.
How it works: the jump box joins your Tailscale tailnet and gets a stable 100.x.x.x address. SSH is locked to the tailscale0 interface only. The public IP serves no SSH surface.
# 1. Install Tailscale on the jump box
curl -fsSL https://tailscale.com/install.sh | sudo sh
# 2. Bring it up (outputs an auth URL — open it in your browser)
sudo tailscale up
# 3. Verify the box appears in your tailnet and note its Tailscale IP
sudo tailscale ip -4
# 4. Verify SSH works via Tailscale BEFORE locking down the firewall
ssh -i <key> ubuntu@<tailscale_ip>
# 5. Lock down firewall and SSH daemon (see OCI section for platform specifics)
Tailscale does not affect the public interface — it adds a tailscale0 virtual interface alongside ens3/eth0. Outbound internet, other services, and the public IP all continue working. Only SSH is restricted.
Connect after setup:
ssh -i ~/.ssh/<key> ubuntu@<tailscale_ip>