LinuxXXIX · Linux Security HardeningSSH sudo
SSH and sudo hardening - the privileged access surface
What you'll learn
- Apply SSH hardening in production
- Configure sudo for least privilege
- Audit sudo usage
- Recognise common SSH/sudo attack patterns
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
SSH and sudo are the two most common paths to root on Linux. Securing them is the highest-impact hardening for any host. This lesson applies the SSH and sudo lessons to a production-grade baseline.
SSH hardening recap
From the SSH hardening lesson:
- Publickey only (no password).
- No root login (
PermitRootLogin no). - Modern ciphers only (
chacha20-poly1305@openssh.cometc.). - AllowGroups for permitted users.
- fail2ban or equivalent for brute-force protection.
The production sshd_config baseline in the SSH lesson is the starting point.
SSH-specific hardening
Beyond the baseline:
# /etc/ssh/sshd_config
AllowTcpForwarding no # disable TCP forwarding by default
AllowStreamLocalForwarding no
GatewayPorts no
PermitTunnel no
X11Forwarding no
Forwarding and tunneling are convenient but expand the
attack surface. Disable by default; enable per-user with
Match blocks for those who need it.
For agents:
# /etc/ssh/sshd_config
AllowAgentForwarding no
# Permit agent forwarding for a specific group only
Match Group devs
AllowAgentForwarding yes
sudo hardening
The sudoers file is the second-most-critical config on a
host. A bad sudoers rule is a privilege escalation.
Edit safely with visudo
sudo visudo
visudo validates the file before saving. A bad sudoers
file can lock everyone out; visudo prevents that.
Use drop-in files in /etc/sudoers.d/
sudo visudo -f /etc/sudoers.d/admins
# /etc/sudoers.d/admins
%admins ALL=(ALL) ALL
Drop-in files keep the policy modular. Filenames must not
end in ~ and must be mode 440.
Least privilege
# /etc/sudoers.d/40-operations
# Specific commands with pinned arguments, not blanket ALL
%ops-team ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
# Backups run fixed, reviewed wrappers - never rsync directly.
# The "" argument means "no arguments at all are permitted".
%backup ALL=(root) /usr/local/sbin/backup-run.sh ""
%backup ALL=(root) /usr/local/sbin/zfs-snapshot-data.sh ""
# A leaf tool that must never spawn a subprocess gets NOEXEC.
%audit ALL=(root) NOEXEC: /usr/bin/less /var/log/audit/audit.log
Every rule names a command and its arguments. A compromised
operator account can restart nginx, run the backup wrappers,
or page one log file - nothing else. Validate the file with
visudo -c -f /etc/sudoers.d/40-operations before you ship
it.
Each wrapper is what makes the grant reviewable. It is root-owned, mode 0755, and takes no operator-supplied paths:
#!/bin/bash
# /usr/local/sbin/backup-run.sh - root:root 0755
set -euo pipefail
exec /usr/bin/rsync -a --delete \
--exclude-from=/etc/backup/excludes \
/srv/data/ /mnt/backup/srv-data/
The source and destination are baked in. The operator chooses
nothing, so there is nothing to abuse. If the paths must vary,
accept a name and map it to a path inside the script - never
accept a path. The script itself must be root-owned and not
writable by the granted group, otherwise the operators simply
rewrite it and the grant is ALL again.
The full treatment of shell escapes, NOEXEC:, and the
GTFOBins class of binaries is in the Part V lesson “Shell
escapes, NOEXEC, and sudoedit”.
Always log and audit
# /etc/sudoers.d/logging
Defaults log_input, log_output
Defaults iolog_dir=/var/log/sudo-io/%{user}
sudo writes a log of every command run. The I/O log
captures the command’s stdin and stdout - useful for
investigating what an operator actually did.
# Distribution-neutral: the journal has the records on every family
sudo journalctl -t sudo -f
# The text file, if your host still writes one. The path differs:
# /var/log/auth.log Debian, Ubuntu
# /var/log/secure RHEL, Rocky, Alma
sudo grep "$USER" /var/log/auth.log /var/log/secure 2>/dev/null
Prefer the journal form in anything you write down. The
authpriv facility lands in a different file on each family, so a
runbook or alert rule that hardcodes /var/log/auth.log returns
nothing on a RHEL host — and “no results” reads like “no sudo
activity”, which is exactly the wrong conclusion during an
investigation.
Common attack patterns
Pattern 1: weak SSH + sudo with NOPASSWD
user ALL=(ALL) NOPASSWD: ALL
If a user’s SSH key is leaked, the attacker has root.
Fix: remove NOPASSWD, use specific commands, require MFA.
Pattern 2: cached credentials in PAM
If pam_faillock is misconfigured (e.g. unlock_time=0),
a user stays locked out forever.
Fix: review PAM config, test lockout and unlock.
Pattern 3: world-readable SSH private keys
If a user’s ~/.ssh/id_ed25519 is mode 644, anyone on the
host can read it.
Fix: enforce chmod 600 ~/.ssh/id_*. Use configuration
management to enforce.
Pattern 4: authorised_keys with command=
A command= directive in authorized_keys runs the
specified command when the key is used. If the command is
weak (e.g. bash), the attacker gets a shell.
# Risky
command="bash"
# Better
command="/usr/bin/restricted-shell.sh"
Audit SSH and sudo
# Recent SSH logins
last -F | head
# Recent sudo usage (identifier filter - sudo is not a unit)
sudo journalctl -t sudo -n 100
# The full audit window for a quarterly review
sudo journalctl -t sudo --since '90 days ago'
# Everything on the authpriv facility (sudo, sshd, su, PAM)
sudo journalctl SYSLOG_FACILITY=10 --since today
# Authoritative record when auditd is running
sudo ausearch -m USER_CMD -ts today
# Sudo I/O log
sudo ls /var/log/sudo-io/
# Active SSH sessions
ss -tnp | grep ':22'
who
A baseline of normal activity lets you spot anomalies: logins from unexpected IPs, sudo at unusual times, etc.
Knowledge check
Knowledge check · 6 questions
Q1. What is the right command to edit sudoers safely?
Q2. "ALL=(ALL) NOPASSWD: ALL" is a safe production sudo rule.
Q3. Which of the following are SSH hardening directives? Select all that apply.
Q4. A sudoers entry that names a command but no argument list permits that command with any arguments.
Q5. You add "ForwardAgent no" to /etc/ssh/sshd_config on a remote host and run systemctl reload sshd. What happens?
Q6. During a privilege-escalation investigation you run "journalctl -u sudo --since yesterday" and get "-- No entries --". What should you conclude?
Passing score: 75%. Answers are checked in this browser.