SSH Server Configuration: Hardening and Access Control with sshd_config
- 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
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 (
sshwhen you connect out). - `/etc/ssh/sshd_config` configures the server — the
sshddaemon that accepts incoming connections.
The
dis for daemon. To harden the box against inbound SSH, you edit `sshd_config`. A directive is oneKeyword valueper line; lines starting with#are comments showing the compiled-in default.grep -vE '^\s*(#|$)' /etc/ssh/sshd_configInclude /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. - `/etc/ssh/ssh_config` configures the client (
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 withsudo, 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_configPermitRootLogin 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 noto sshd_config but the line still begins with a#. Is password login disabled?- `PermitRootLogin no` — nobody logs in directly as
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 noonly 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@hostdoes this for you (over a password login, the last time you'll need one); or append the.pubfile'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.~/.sshmust be700andauthorized_keys600, 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?
- Generate a pair on your client with `ssh-keygen`. Prefer `-t ed25519` (a modern, short, fast key) over old RSA. It writes
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 backupfollowed byForceCommand /usr/bin/rrsyncandPermitTTY nocan pin thebackupaccount 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 nextMatchor end-of-file).grep -E '^(AllowUsers|MaxAuthTries|X11Forwarding)' /etc/ssh/sshd_configAllowUsers 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 userbackuplog in over SSH?- `AllowUsers alice deploy` — only these accounts may authenticate; every other user is refused before a password/key is even checked. (There's also
Change the port, then validate before you reload
Move the service and apply changes without breaking sshd.
Port 2222moves 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 earlierPort 22line will win over a laterPort 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 -tdoes 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. reloadre-reads sshd_config while keeping EXISTING sessions alive — safer thanrestart, which drops connections. Existing sessions also give you a safety net: stay logged in and test a NEW connection before closing the working one.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_configShows the directives actually in effect. Note how few there are — everything else is running on defaults. grep -nE '^\s*Port' /etc/ssh/sshd_configLists 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 OKValidates 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.logRead 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. journalctlexits 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.
- 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 |
|---|---|---|
PermitRootLogin no | No direct root SSH | Log in as a user, sudo up |
PasswordAuthentication no | Keys only | Confirm your key works FIRST |
ssh-copy-id user@host | Install your public key | Needs ~/.ssh 700, authorized_keys 600 |
AllowUsers <list> | Allowlist of accounts | Anyone not listed is refused |
MaxAuthTries <n> | Attempts per connection | Default is 6; lower it |
ClientAliveInterval <s> | Drop idle sessions | Pair with ClientAliveCountMax |
Match User <u> | Per-account overrides | Goes at the END of the file |
Port <n> | Move off 22 | First Port line wins — no duplicates |
sshd -t / sshd -T | Validate / show effective config | Run -t after every edit |
systemctl reload sshd | Apply without dropping sessions | Test a new session before logging out |
