Signal9Signal9 — home
online
all lessons

SSH Server Configuration: Hardening and Access Control with sshd_config

Intermediate20 min6 steps
What you'll learn
  • Locate and read the SSH server's configuration in /etc/ssh/sshd_config
  • Harden the daemon: disable root login, require keys, and change the port
  • Restrict who may connect with AllowUsers and lower MaxAuthTries
  • Validate the config and apply it safely without locking yourself out
  1. sshd_config is the server's rulebook

    Separate the client config from the server config and know which file to edit.

    SSH has two sides and two config files, and mixing them up is the classic first mistake:

    • `/etc/ssh/ssh_config` configures the client (ssh when you connect out).
    • `/etc/ssh/sshd_config` configures the server — the sshd daemon that accepts incoming connections.

    The d is for daemon. To harden the box against inbound SSH, you edit `sshd_config`. A directive is one Keyword value per line; lines starting with # are comments showing the compiled-in default.

    grep -vE '^\s*(#|$)' /etc/ssh/sshd_config
    Include /etc/ssh/sshd_config.d/*.conf
    Subsystem sftp /usr/lib/openssh/sftp-server
    -vE '^\s*(#|$)'
    Invert-match comment lines and blank lines, leaving the active directives.
    Strips comments and blank lines to show only the directives actually in effect — often just a couple, because most settings run on their commented defaults. To change a default you uncomment its line (or add a new one) and set the value you want.
  2. Shut the front door: root login and passwords

    Disable direct root SSH and force key-based authentication.

    Two directives do most of the hardening work:

    • `PermitRootLogin no` — nobody logs in directly as root. Admins log in as themselves and escalate with sudo, so every action is attributable.
    • `PasswordAuthentication no` — refuse password logins entirely and require SSH keys. This kills password brute-forcing outright.
    grep -E '^(PermitRootLogin|PasswordAuthentication)' /etc/ssh/sshd_config
    PermitRootLogin no
    PasswordAuthentication no
    After editing, this confirms both directives are set and uncommented. If a line still starts with #, it isn't in effect — sshd is using the default, not your intended value.

    Check yourself

    You add PasswordAuthentication no to sshd_config but the line still begins with a #. Is password login disabled?

  3. Set up the key that replaces the password

    Create an SSH keypair and install the public half so key auth works before you disable passwords.

    PasswordAuthentication no only works if a key is already in place — so here's the mechanism it depends on. SSH key authentication uses a keypair: a private key that never leaves your client, and a matching public key you copy to the server. At login the server challenges the client to prove it holds the private key; no secret ever crosses the wire, so there's nothing to brute-force or phish.

    The workflow is three steps:

    • Generate a pair on your client with `ssh-keygen`. Prefer `-t ed25519` (a modern, short, fast key) over old RSA. It writes ~/.ssh/id_ed25519 (private — guard it) and ~/.ssh/id_ed25519.pub (public — safe to share).
    • Install the public key into the target account's `~/.ssh/authorized_keys` on the server. ssh-copy-id user@host does this for you (over a password login, the last time you'll need one); or append the .pub file's contents by hand.
    • Test a key login in a second session — then set PasswordAuthentication no.

    One detail bites everyone: permissions. sshd's StrictModes (on by default) refuses to use a key if the directory or file is group/world-writable. ~/.ssh must be 700 and authorized_keys 600, owned by the user. A key that silently 'doesn't work' is almost always a permissions problem.

    ssh-keygen -t ed25519 -C 'ada@laptop' -f ~/.ssh/id_ed25519 -N ''
    Generating public/private ed25519 key pair.
    Your identification has been saved in /home/ada/.ssh/id_ed25519
    Your public key has been saved in /home/ada/.ssh/id_ed25519.pub
    -t ed25519
    Use the modern Ed25519 algorithm (short, fast, secure) instead of RSA.
    Create an ed25519 keypair: -t picks the algorithm, -C adds a comment to identify it, -f names the output file, and -N '' sets an empty passphrase (for a demo — on a real key use a passphrase and an agent). The .pub file is the half you install on servers; the other file stays secret on this machine.
    ssh-copy-id deploy@server && ssh -o PasswordAuthentication=no deploy@server 'echo key login works'
    Number of key(s) added: 1
    key login works
    ssh-copy-id appends your public key to deploy's ~/.ssh/authorized_keys on the server (with the right permissions). The second command then forces a key-only login and runs a command — proving the key works BEFORE you disable passwords. If this prints the message, it's safe to set PasswordAuthentication no.

    Check yourself

    Which half of the keypair do you copy into the server's ~/.ssh/authorized_keys — the public key or the private key?

  4. Least privilege: who may connect, and how hard they may try

    Restrict logins to named accounts and tighten per-connection limits.

    Beyond how people authenticate, control who may even attempt it:

    • `AllowUsers alice deploy` — only these accounts may authenticate; every other user is refused before a password/key is even checked. (There's also AllowGroups, DenyUsers, DenyGroups.)
    • `MaxAuthTries 3` — cap authentication attempts per connection (default 6). Lower means a brute-forcer gets fewer guesses per TCP connection.
    • `X11Forwarding no` — turn off GUI forwarding you almost never need on a server; less attack surface.
    • `ClientAliveInterval 300` with `ClientAliveCountMax 2` — drop idle sessions: the server pings a quiet client every 300s and disconnects after 2 unanswered pings (~10 minutes), so abandoned terminals don't linger open.

    For policy that differs per account, a `Match` block applies directives only to some connections. For example, Match User backup followed by ForceCommand /usr/bin/rrsync and PermitTTY no can pin the backup account to one command with no interactive shell — least privilege enforced by the daemon itself. Match blocks go at the end of the file (they apply until the next Match or end-of-file).

    grep -E '^(AllowUsers|MaxAuthTries|X11Forwarding)' /etc/ssh/sshd_config
    AllowUsers deploy
    MaxAuthTries 3
    X11Forwarding no
    AllowUsers
    Space-separated allowlist of accounts permitted to log in via SSH.
    Confirms the access-control trio is in place. AllowUsers is an allowlist — once it exists, anyone not on it is rejected, so double-check the account you actually use is listed before you rely on it.

    Check yourself

    sshd_config contains AllowUsers deploy. Can the user backup log in over SSH?

  5. Change the port, then validate before you reload

    Move the service and apply changes without breaking sshd.

    Port 2222 moves the daemon off 22. It won't stop a determined attacker, but it dramatically cuts drive-by scan noise in your logs. One subtlety trips people up: for most keywords, sshd uses the FIRST value it finds in the file. A stray earlier Port 22 line will win over a later Port 2222, so don't leave a conflicting duplicate.

    sshd -t && echo 'config OK'
    config OK
    -t
    Test mode: validate the configuration and exit; changes nothing.
    sshd -t does a syntax/sanity check of the config WITHOUT touching the running service. Run it after every edit: a typo caught here is harmless, but the same typo discovered during a restart can leave the daemon down and you locked out.
    systemctl reload sshd
    reload
    Re-read config without killing current sessions (vs restart, which bounces the daemon).
    Applies the validated config. reload re-reads sshd_config while keeping EXISTING sessions alive — safer than restart, which drops connections. Existing sessions also give you a safety net: stay logged in and test a NEW connection before closing the working one.
  6. Try it in your live terminal

    Inspect a real sshd_config safely (read-only).

    These commands only read and validate — nothing here changes the running service, so run them freely on the box to your right.

    grep -vE '^\s*(#|$)' /etc/ssh/sshd_config
    Shows the directives actually in effect. Note how few there are — everything else is running on defaults.
    grep -nE '^\s*Port' /etc/ssh/sshd_config
    Lists any Port lines with line numbers — the way you'd hunt for a conflicting duplicate that a first-value-wins keyword would honor.
    sshd -t && echo OK
    Validates the current config. If it prints OK, the file is syntactically sound; if not, it names the offending line.
    sudo journalctl -q -u ssh -n 5 --no-pager 2>/dev/null | grep . || sudo tail -n 5 /var/log/auth.log
    Read the SSH daemon's recent log lines — accepted and failed logins, and any config-reload messages. This is where you confirm a change took effect and watch for brute-force attempts (repeated 'Failed password' or 'Invalid user'). On systemd hosts it's journalctl -u ssh (or sshd); on others, /var/log/auth.log. journalctl exits 0 even when it has no journal to read, so -q | grep . is what makes the || fallback fire instead of silently printing nothing.
    sshd -T 2>/dev/null | grep -iE '^(permitrootlogin|passwordauthentication|maxauthtries|port)' 
    sshd -T prints the FULLY resolved, effective configuration — every setting sshd will actually use, defaults included. Unlike grepping the file (which misses defaults and can be fooled by duplicates), this is the ground truth of how the daemon is configured right now.
Recap
  • Edit the SERVER file /etc/ssh/sshd_config (not the client ssh_config); a leading # means the directive is a default, not active.
  • Key auth uses a keypair: ssh-keygen -t ed25519 makes it, the PUBLIC half goes in the server account's ~/.ssh/authorized_keys (ssh-copy-id installs it), and StrictModes needs ~/.ssh 700 / authorized_keys 600. Prove a key login works before disabling passwords.
  • Harden with PermitRootLogin no and PasswordAuthentication no; apply least privilege with AllowUsers (an allowlist), a lower MaxAuthTries, X11Forwarding no, ClientAliveInterval for idle timeouts, and Match blocks for per-account policy.
  • sshd uses the FIRST value for a keyword; validate with sshd -t, see the effective config with sshd -T, apply with systemctl reload sshd, and test a new session before closing the old one.
Directive → effect / watch for
DirectiveEffectWatch for
PermitRootLogin noNo direct root SSHLog in as a user, sudo up
PasswordAuthentication noKeys onlyConfirm your key works FIRST
ssh-copy-id user@hostInstall your public keyNeeds ~/.ssh 700, authorized_keys 600
AllowUsers <list>Allowlist of accountsAnyone not listed is refused
MaxAuthTries <n>Attempts per connectionDefault is 6; lower it
ClientAliveInterval <s>Drop idle sessionsPair with ClientAliveCountMax
Match User <u>Per-account overridesGoes at the END of the file
Port <n>Move off 22First Port line wins — no duplicates
sshd -t / sshd -TValidate / show effective configRun -t after every edit
systemctl reload sshdApply without dropping sessionsTest a new session before logging out