Linux Security in 2026: Build a Hardened SSH Bastion with FIDO2 Keys, Fail2ban, and Systemd Sandboxing

If you still protect production SSH with only passwords or static keys, 2026 is the year to fix it. A hardened SSH bastion gives you one controlled entry point, strong identity with hardware-backed FIDO2 keys, and layered defense against brute-force and lateral movement. In this guide, you will build a practical Linux bastion using OpenSSH, FIDO2 resident keys, Fail2ban, and systemd sandboxing so your admin access is safer without becoming painful for your team.

Why a Bastion Host Still Matters in 2026

Even with Zero Trust architecture and private networking, operators still need emergency and maintenance access. A bastion host centralizes that access path. Instead of opening SSH on every server, you expose one hardened endpoint, log every session, and enforce consistent auth policy.

If you are already improving supply chain and CI trust, this is the infrastructure-side companion. For related context, see our posts on trusted Docker CI with provenance, JavaScript supply chain security, and zero-trust internal APIs.

Architecture: Small, Auditable, and Locked Down

  • Public: One Linux bastion VM with a static IP.
  • Private: Internal servers accept SSH only from bastion security group or private subnet.
  • Identity: FIDO2-backed SSH keys (ed25519-sk), no passwords.
  • Defense: Fail2ban + low attack surface + strict sshd policy.
  • Operations: Journald logging and session recording policy.

Step 1: Create Hardware-Backed SSH Keys (FIDO2)

On your admin workstation, generate a security-key SSH credential. The -O resident flag lets you recover key handles from the token when needed, and -O verify-required forces user verification (PIN or touch policy depending on token).

# Requires OpenSSH with security key support
ssh-keygen -t ed25519-sk \
  -O resident \
  -O verify-required \
  -C "ops-bastion-2026" \
  -f ~/.ssh/id_ed25519_sk_bastion

# Copy public key to bastion user account
ssh-copy-id -i ~/.ssh/id_ed25519_sk_bastion.pub ops@BASTION_IP

Use separate keys per environment (staging, production), and rotate on role changes. Avoid sharing one token across teams.

Step 2: Harden sshd_config for Bastion-Only Access

Next, lock SSH policy aggressively. Disable password auth, root login, and weak forwarding defaults. Restrict to a dedicated Unix group like sshadmins.

# /etc/ssh/sshd_config.d/99-bastion-hardening.conf
Protocol 2
Port 22
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
UsePAM yes
AllowGroups sshadmins
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowTcpForwarding local
PermitTunnel no
AllowAgentForwarding no
PermitUserEnvironment no

# Optional: keep bastion non-interactive except for jump usage
# ForceCommand /usr/local/bin/bastion-shell-wrapper

Subsystem sftp internal-sftp

Validate before restart:

sudo sshd -t
sudo systemctl restart ssh

Important Operational Note

Before applying hardening, keep one active root console from your cloud provider or hypervisor panel. This prevents lockout if a config typo blocks login.

Step 3: Add Fail2ban for Fast Abuse Throttling

Fail2ban should not be your only control, but it is still excellent at reducing noisy credential-stuffing traffic and log spam.

# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
backend = systemd
port = 22
logpath = %(sshd_log)s
maxretry = 4
findtime = 10m
bantime = 1h
bantime.increment = true
bantime.factor = 2
ignoreip = 127.0.0.1/8 10.0.0.0/8

Then enable and verify:

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

Step 4: Sandbox SSH Service with systemd Controls

OpenSSH is mature, but defense in depth matters. Use a systemd override to add runtime restrictions where compatible with your distro.

# /etc/systemd/system/ssh.service.d/hardening.conf
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true

Apply changes:

sudo systemctl daemon-reload
sudo systemctl restart ssh
sudo systemctl status ssh --no-pager

Some directives may need tuning depending on your Linux distribution. Test from a second session before ending the first.

Step 5: Enforce Bastion-Only Access to Internal Hosts

The best bastion hardening fails if private servers still expose SSH publicly. On internal nodes:

  • Accept port 22 only from bastion subnet or bastion security group.
  • Disable direct public SSH listeners.
  • Use ProxyJump for operator ergonomics.
# ~/.ssh/config on operator machine
Host bastion
  HostName BASTION_IP
  User ops
  IdentityFile ~/.ssh/id_ed25519_sk_bastion

Host app-prod-*
  User ubuntu
  ProxyJump bastion

Monitoring and Audit Checklist

  • Track failed auth spikes and ban events daily.
  • Alert on sshd config drift using Git-managed config.
  • Keep OpenSSH and kernel security patches current.
  • Review bastion users and authorized keys monthly.
  • Correlate access logs with incident timelines.

If you are standardizing secure operations workflows, also check our Linux systemd automation guide on rootless containers with Podman Quadlet and our GitHub troubleshooting workflow with deterministic tests.

Conclusion

A secure bastion is not old-school, it is foundational. With FIDO2-backed SSH, strict daemon policy, adaptive banning, and service sandboxing, you reduce both accidental exposure and active attack success rates. Start with one environment this week, automate your config, and then roll the same pattern to every production boundary.

FAQ

1) Is Fail2ban still useful if password login is disabled?

Yes. It still reduces automated probing noise, protects logs from flood conditions, and gives you quick signal about internet scanning patterns.

2) Should I use FIDO2 SSH keys for every admin user?

Yes, ideally one hardware-backed key per person, per trust boundary. Shared keys remove accountability and make incident response harder.

3) Can I skip bastion and use direct VPN + SSH?

You can, but bastion centralizes policy, logging, and audit controls. For many teams, that significantly simplifies operational security.

4) What breaks most often during hardening?

Misconfigured sshd_config, missing group membership, and over-restrictive systemd directives. Always test in a second active session before closing your current terminal.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials