linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Simulator
  • Knowledge base
  • Interview prep
Index
Categories
All entries
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
home/linux/kb/Security/ssh-hardening

kb/security ── Security ── intermediate

SSH hardening: locking down the server

SSH hardening: keys only (PasswordAuthentication no), disable root login, AllowUsers/AllowGroups, MaxAuthTries, a fail2ban jail on sshd. Optionally a custom port plus Match blocks for guests.

view as markdownaka: ssh-secure, sshd-hardening, openssh-hardening

Why it matters

SSH is the main attack vector on Linux servers. Botnets brute-force it around the clock: on any publicly routable IP with port 22 open, within a day you will see thousands of attempts in auth.log. The default OpenSSH configuration is too open for prod:

  • Root login over a password is allowed
  • Passwords are allowed at all
  • All users can connect
  • There is no rate limit on attempts
  • X11 forwarding is on

This document is a set of "disable by default" recipes.

Minimal hardening: what to change in sshd_config

/etc/ssh/sshd_config (or the drop-in /etc/ssh/sshd_config.d/00-hardening.conf):

ini
# ===== Authentication =====
PermitRootLogin no                         # never root directly
PasswordAuthentication no                   # keys only
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey            # key only; for 2FA: publickey,keyboard-interactive
# ===== Who is allowed in =====
AllowUsers alice bob carol                 # whitelist
# OR by group
AllowGroups ssh-users
# ===== Limiting attempts =====
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30                          # disconnect if not authenticated within 30s
# ===== Server identity =====
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# ===== Cipher / KEX =====
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com
# ===== Forwarding =====
X11Forwarding no
AllowAgentForwarding no                    # enable only where needed
AllowTcpForwarding no                      # yes for bastion hosts
PermitTunnel no
GatewayPorts no
# ===== Other =====
PermitEmptyPasswords no
ClientAliveInterval 300
ClientAliveCountMax 2                      # 10 minutes idle = disconnect
Banner /etc/issue.net                       # legal warning
PrintLastLog yes
UseDNS no                                   # speeds up login (no reverse DNS)

Apply:

bash
sshd -t                                    # syntax check
systemctl reload sshd

Do not close your active session right away! Open a second SSH terminal and confirm that new connections go through.

Keys only

Generate a strong key:

bash
ssh-keygen -t ed25519 -a 100 -C "alice@laptop"
# ed25519 is modern, short, and fast
# -a 100 is 100 KDF rounds to protect the passphrase

Copy it to the server:

bash
ssh-copy-id alice@server                   # automatically
# or by hand:
cat ~/.ssh/id_ed25519.pub | ssh alice@server 'cat >> ~/.ssh/authorized_keys'

Permissions are mandatory:

bash
ssh -o LogLevel=DEBUG alice@server         # shows if authorized_keys mode is 644 + group/other = drop
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

SSH Certificate Authority: scaling keys

Handing out keys to 100 servers for every user does not scale. An SSH CA signs short-lived certificates:

bash
# On the CA machine (isolated)
ssh-keygen -t ed25519 -f ssh_ca_key
# Distribute the CA public key to all servers in /etc/ssh/ca.pub
# In sshd_config:
TrustedUserCAKeys /etc/ssh/ca.pub
# Sign the user's key
ssh-keygen -s ssh_ca_key -I "alice-2026-05-01" -n alice -V +1d ~/.ssh/id_ed25519.pub

▸creates id_ed25519-cert.pub

Servers accept anyone whose key is signed by the CA. The validity period is built into the certificate. Revocation goes through RevokedKeys.

It is used at Facebook, Netflix, and Uber. For 1 or 2 servers it is overkill; for 100 or more it is essential.

Match blocks

Conditional rules:

ini
# SFTP-only users
Match Group sftponly
    ChrootDirectory /var/sftp/%u
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
# Bastion: allow TCP forwarding only from a specific network
Match Address 10.0.0.0/24
    AllowTcpForwarding yes
    PermitTunnel yes
# Deny login from the deploy user outside the corp network
Match User deploy Address !10.0.0.0/8,!172.16.0.0/12
    DenyUsers *

Custom port: security through obscurity?

Changing Port 22 → Port 2222 does not make SSH more secure. Bots scan all 65535 ports. But it reduces noise in the logs:

ini
Port 2222

Do not forget to:

  • Open 2222 in the firewall
  • Close 22
  • Update SELinux on RHEL: semanage port -a -t ssh_port_t -p tcp 2222
  • Update the fail2ban jail
  • Tell clients (~/.ssh/config or Port 2222 on the command line)

I would keep 22 plus fail2ban, and use a custom port for bastion hosts that carry a lot of automation.

A fail2ban jail for sshd

fail2ban reads auth.log and, after N failed attempts, bans the IP in iptables/nftables.

/etc/fail2ban/jail.local:

ini
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
banaction = nftables
backend = systemd
[sshd]
enabled = true
port    = ssh
filter  = sshd
logpath = %(sshd_log)s
maxretry = 3
bash
systemctl restart fail2ban
fail2ban-client status sshd
fail2ban-client unban 1.2.3.4              # unban by hand

2FA through PAM

Make sshd require Google Authenticator or a Yubikey on top of the key. Note: the baseline config above disables KbdInteractiveAuthentication and ChallengeResponseAuthentication. For 2FA you have to remove these lines (or set them to yes), otherwise the keyboard-interactive step will not fire:

ini
# sshd_config
AuthenticationMethods publickey,keyboard-interactive
KbdInteractiveAuthentication yes          # overrides the baseline ban
ChallengeResponseAuthentication yes
UsePAM yes

In /etc/pam.d/sshd:

auth required pam_google_authenticator.so nullok

And each user runs google-authenticator to generate a QR code.

Auditing an existing config

Tools:

bash
ssh-audit example.com                      # external check of cipher/kex/host-key
sshd -T                                    # effective config (after Match)
sshd -T | grep -iE 'permitroot|password|kex|cipher|mac|maxauth'

When things go wrong

  • Permission denied (publickey): the key is not in authorized_keys, or the mode on ~/.ssh (must be 700) and authorized_keys (600) is wrong.
  • Locked yourself out of SSH while changing the config: open a second active SSH session BEFORE the reload. If it is already too late, use the console/IPMI/ILO/cloud console. Clouds offer a serial console through the CLI.
  • no matching host key type found: you disabled RSA, but the client has only RSA. Add HostKey /etc/ssh/ssh_host_rsa_key back, or ask the client to update openssh.
  • no matching cipher found: client offers: an old client, the cipher is not in your whitelist. Check sshd -T | grep cipher.
  • fail2ban bans itself: your IP landed in a jail. Whitelist it with ignoreip = 127.0.0.1/8 10.0.0.0/24 my.public.ip/32.
  • GSSAPI/Kerberos handshake slows down login: turn it off with GSSAPIAuthentication no if you do not use kerberos. Also UseDNS no.
  • A Match block does not apply: Match is parsed in order, and the first match wins. A Match block ends at the next Match or at EOF.

Checklist

  • PermitRootLogin no
  • PasswordAuthentication no
  • AllowUsers/Groups whitelist
  • MaxAuthTries ≤ 3
  • X11Forwarding no (if not needed)
  • fail2ban jail enabled
  • ed25519 host key and client key
  • Modern KEX/Cipher/MAC, no legacy
  • Banner with a legal warning
  • sshd -T → check the effective configuration
  • SSH CA certificates if 5+ servers
  • auditd watch on sshd_config

§ команды

bash
sshd -t

Syntax check of the config before reload, always BEFORE systemctl

bash
sshd -T | grep -iE 'permitroot|password|kex'

Effective config including Match blocks and defaults

bash
ssh-keygen -t ed25519 -a 100 -C 'alice@laptop'

Generate a modern key with passphrase protection over 100 rounds

bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub alice@server

Copy the public key into authorized_keys with the correct mode

bash
fail2ban-client status sshd

Who is currently banned, for debugging 'cannot log in after changing IP'

bash
ssh-keygen -s ca_key -I 'alice-$(date +%F)' -n alice -V +24h pubkey.pub

Sign a user's key with the CA for one day, the SSH CA workflow

bash
journalctl -u sshd --since '10 min ago'

sshd logs: failed attempts, key events, GSSAPI errors

§ см. также

  • sshSSH: Secure ShellSSH is an encrypted channel to a remote host: shell, file copy, port-forwarding. Standard port 22, authentication by keys or password.
  • fail2banfail2ban: automatic bans from logsfail2ban reads logs (sshd, nginx, postfix), uses a regex to catch N failed attempts in a window, and adds the IP to firewall rules for bantime. It is the main tool against SSH brute-force.
  • pamPAM: Pluggable Authentication ModulesPAM is the authentication framework in Linux. Programs (sudo, login, sshd) do not check passwords themselves. They call PAM, which decides whether to let you in through a stack of modules in `/etc/pam.d/<service>`.
  • auditdauditd: syscall and file auditauditd writes kernel events to /var/log/audit/audit.log: file watches (-w), syscall rules (-a), execs. Use ausearch to search, aureport for reports. This is the basis of compliance (PCI-DSS, HIPAA, FZ-152).
  • kerberosKerberos: network single sign-onKerberos is an SSO system built on tickets and time-based cryptography. You enter your password once (`kinit`) and get a TGT. After that the KDC issues service tickets in exchange for it. Core of AD and FreeIPA.
  • secrets-managementSecrets management: Vault, k8s Secrets, sealed-secretsKeep secrets out of git and out of env vars in code. Options: HashiCorp Vault (general purpose, dynamic creds), k8s Secrets (base64, needs encryption- at-rest), sealed-secrets (commit-friendly), external-secrets (sync from a cloud vault).
  • cis-benchmark-hardeningCIS Benchmark and system hardening (lynis, OpenSCAP)CIS Benchmark is the Linux hardening standard. Lynis is a fast local audit with a score, OpenSCAP is the formal one with XCCDF profiles and a SCAP report. ansible-lockdown remediates. Keep audit and remediate separate.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies