DevOps & Workflow7 min read

    How to Secure SSH on CentOS, AlmaLinux and Rocky Linux

    By Amir— Reliability & Network Engineering
    Share

    SSH is the one service that is open on almost every Linux server and the one whose compromise hands over everything else. The defaults on a fresh RHEL-family install are reasonable but not strict: root can log in with a key, passwords are accepted, idle sessions live forever, and any local account is a valid SSH user.

    This guide is a hardening pass for CentOS Stream 9, AlmaLinux 8/9 and Rocky Linux 8/9 (and RHEL itself). The original version targeted CentOS 7 and 8; both are end-of-life now (CentOS 8 in December 2021, CentOS 7 in June 2024) and should be migrated, but every step still works on them — differences are noted inline. Each step ends with how to verify it, and the whole thing is designed so that you never close a working session until the next one is proven.

    Rule for the whole guide: keep your current SSH session open. After every change, open a new terminal and confirm you can still log in. Only then continue.

    Where the configuration lives

    • RHEL/Alma/Rocky 9, CentOS Stream 9: /etc/ssh/sshd_config includes /etc/ssh/sshd_config.d/*.conf. Put your changes in a drop-in such as /etc/ssh/sshd_config.d/10-hardening.conf. Files are read in lexical order and the first occurrence of a directive wins, so a low number beats the vendor's 50-redhat.conf.
    • RHEL/Alma/Rocky 8, CentOS 7/8: no drop-in directory by default; edit /etc/ssh/sshd_config directly (take a backup: sudo cp /etc/ssh/sshd_config{,.bak}).

    Two commands you will use after every step:

    sudo sshd -t                 # syntax check: silent = OK
    sudo sshd -T | grep -i <key> # the *effective* value after all includes
    sudo systemctl reload sshd   # apply without dropping existing sessions
    

    Step 1: Create a dedicated login user with sudo

    Log in as root only via the console. Day-to-day access goes through a personal account that can sudo:

    sudo useradd -m -G wheel deploy
    sudo passwd deploy
    

    wheel is the sudo group on RHEL-family systems; the stock /etc/sudoers already grants it. Details and the NOPASSWD variants are in Add a user to sudoers on AlmaLinux/Rocky Linux.

    Verify: ssh deploy@server and sudo -v both succeed.

    Step 2: Install your SSH key for that user

    From your workstation:

    ssh-keygen -t ed25519 -a 64 -C "you@workstation"
    ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@server
    

    ssh-copy-id creates ~/.ssh (700) and authorized_keys (600) with the right modes and, on SELinux systems, the right context. If you copy the key by hand, run restorecon -Rv ~deploy/.ssh afterwards — a wrong SELinux label is the most common "key works for root but not for the new user" cause on RHEL.

    Verify: ssh deploy@server logs in without asking for the account password.

    Step 3: Disable root login

    sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'CONF'
    PermitRootLogin no
    CONF
    sudo sshd -t && sudo systemctl reload sshd
    

    prohibit-password (the upstream default) still allows root with a key; no closes it entirely. If an automation account genuinely needs root over SSH, give it its own user with a narrow sudoers rule instead.

    Verify: ssh root@server → Permission denied.

    Step 4: Disable password and empty-password authentication

    Append to the same drop-in:

    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PermitEmptyPasswords no
    PubkeyAuthentication yes
    

    (ChallengeResponseAuthentication is the old name for KbdInteractiveAuthentication; use the old name on CentOS 7.) Reload and verify from a second terminal:

    ssh -o PubkeyAuthentication=no deploy@server
    # Permission denied (publickey).
    

    This step alone ends SSH brute-forcing. The remaining steps limit blast radius and noise; see 5 ways to stop SSH brute-force attacks for fail2ban and firewall rate-limiting on top.

    Step 5: Limit who can log in at all

    AllowGroups ssh-users
    
    sudo groupadd ssh-users
    sudo usermod -aG ssh-users deploy
    sudo sshd -t && sudo systemctl reload sshd
    

    Now postgres, nginx, forgotten test accounts and anything an attacker creates later are not valid SSH users, regardless of their keys or passwords. AllowUsers deploy backup@203.0.113.0/24 is the per-user form, with an optional source restriction.

    Verify: sudo sshd -T | grep allowgroups, and a login as a user not in the group is refused.

    Step 6: Idle timeout and connection limits

    ClientAliveInterval 300
    ClientAliveCountMax 2
    LoginGraceTime 20
    MaxAuthTries 3
    MaxSessions 4
    
    • ClientAliveInterval 300 + ClientAliveCountMax 2: the server probes an idle client every 5 minutes and drops it after two unanswered probes (≈10 minutes idle). This closes sessions on unattended laptops; it does not log out an active vim.
    • LoginGraceTime 20: unauthenticated connections are dropped after 20 s.
    • MaxAuthTries 3: three attempts per connection.

    Step 7: Trim the feature set

    X11Forwarding no
    AllowAgentForwarding no
    AllowTcpForwarding no
    PermitTunnel no
    Banner /etc/issue.net
    

    Turn AllowTcpForwarding back on for specific users with a Match User block if they need port forwarding (database tunnels, ProxyJump through a bastion):

    Match User deploy
        AllowTcpForwarding yes
    

    Match blocks must come last in the file; everything after a Match line belongs to it.

    Protocol 2 and ciphers

    Protocol 2 is the only option in every OpenSSH release of the last decade; the directive is accepted but meaningless on 8/9 (on CentOS 7 it is already the default). Cipher and key-exchange policy on RHEL 8/9 is managed system-wide by crypto-policies — sudo update-crypto-policies --set FUTURE tightens SSH, TLS and Kerberos together — rather than by hand-editing Ciphers and KexAlgorithms, which the policy would override anyway.

    Step 8 (optional): Change the port — properly

    Moving off port 22 quiets the logs and defeats the laziest scanners. It is not a security control, and on RHEL it breaks in two places if done naively.

    # 1. sshd
    echo 'Port 2222' | sudo tee -a /etc/ssh/sshd_config.d/10-hardening.conf
    
    # 2. SELinux must allow sshd to bind the new port
    sudo dnf install -y policycoreutils-python-utils     # provides semanage
    sudo semanage port -a -t ssh_port_t -p tcp 2222      # use -m instead of -a if it says the port is already defined
    
    # 3. firewalld must let it in (keep 22 open until the new port is verified!)
    sudo firewall-cmd --permanent --add-port=2222/tcp
    sudo firewall-cmd --reload
    
    sudo sshd -t && sudo systemctl restart sshd
    

    Verify from a second terminal with ssh -p 2222 deploy@server. Only then remove port 22: sudo firewall-cmd --permanent --remove-service=ssh && sudo firewall-cmd --reload. If sshd fails to start after adding the port, sudo journalctl -u sshd -n 20 will show Permission denied from SELinux — step 2 was skipped. On RHEL 9, sshd is socket-activated on some images; if Port seems ignored, check systemctl status sshd.socket.

    The finished drop-in

    # /etc/ssh/sshd_config.d/10-hardening.conf
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PermitEmptyPasswords no
    PubkeyAuthentication yes
    AllowGroups ssh-users
    ClientAliveInterval 300
    ClientAliveCountMax 2
    LoginGraceTime 20
    MaxAuthTries 3
    MaxSessions 4
    X11Forwarding no
    AllowAgentForwarding no
    AllowTcpForwarding no
    PermitTunnel no
    Banner /etc/issue.net
    # Port 2222
    
    Match User deploy
        AllowTcpForwarding yes
    

    Final check that the running daemon agrees with the file:

    sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|allowgroups|clientalive|maxauthtries|^port'
    

    Troubleshooting

    • sshd -t is silent but a setting has no effect — another file sets it first. sudo sshd -T | grep -i <key> shows the effective value; grep -ri <key> /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ shows where.
    • Key refused for the new user: Authentication refused: bad ownership or modes — chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys; chown -R deploy:deploy ~deploy/.ssh, and the home directory itself must not be group- or world-writable.
    • Key refused only on RHEL, modes are correct — SELinux label: restorecon -Rv ~deploy/.ssh. sudo ausearch -m avc -ts recent confirms.
    • Locked out — use the provider console, log in as root locally, and relax the offending line. Then re-read the rule at the top of this guide.
    • Connection refused after a port change — SELinux or firewalld, see step 8. sudo ss -tlnp | grep sshd shows whether sshd is actually listening on the new port.
    • CentOS 7: KbdInteractiveAuthentication unknown — use ChallengeResponseAuthentication no; sshd -t tells you about unknown directives.

    Summary

    1. Personal user in wheel; root stays on the console.
    2. Ed25519 key installed with ssh-copy-id.
    3. PermitRootLogin no, PasswordAuthentication no, PermitEmptyPasswords no.
    4. AllowGroups ssh-users.
    5. Idle timeout, LoginGraceTime, MaxAuthTries.
    6. Forwarding and X11 off, re-enabled per user with Match.
    7. Port change only with semanage and firewall-cmd; keep 22 open until verified.
    8. sshd -t before every reload, sshd -T to confirm, a second terminal before you close the first.