Skip to main content
RunBook Academy

LinuxXXIX · Linux Security HardeningSSH sudo

SSH and sudo hardening - the privileged access surface

Intermediate⏱ ~12 minsshdsudo

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

Not yet marked complete on this device.

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.com etc.).
  • 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

  1. Q1. What is the right command to edit sudoers safely?

  2. Q2. "ALL=(ALL) NOPASSWD: ALL" is a safe production sudo rule.

  3. Q3. Which of the following are SSH hardening directives? Select all that apply.

  4. Q4. A sudoers entry that names a command but no argument list permits that command with any arguments.

  5. Q5. You add "ForwardAgent no" to /etc/ssh/sshd_config on a remote host and run systemctl reload sshd. What happens?

  6. 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.