Skip to content
Commitpedia
Log in Join & edit

SSH server audit script: print risky sshd settings

A read-only bash script that asks sshd for its effective configuration, compares the important settings with safe values, flags config.d files that re-enable passwords and reports fail2ban and firewall state.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of SSH server audit script: print risky sshd settings: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
bash
Licence
CC0-1.0

What this changes

Read-only: it runs sshd -T, reads files under /etc/ssh and queries fail2ban and the firewall for status. It changes nothing and does not restart any service.

How to undo it

Nothing to undo. Any change you make afterwards is yours: edit a file in /etc/ssh/sshd_config.d/, run sshd -t and restart sshd with a second session kept open.

The question “is this server’s SSH set up safely?” has a precise answer in sshd -T, which prints every effective option after all includes and defaults. Reading 100 lines by eye is error-prone, so this script does the comparison and prints PASS, WARN or INFO per setting. It is meant to run after the hardening guide, after a cloud image update, or on a server you inherited.

The script

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
#!/usr/bin/env bash
set -uo pipefail

if [[ $EUID -ne 0 ]]; then
  echo "run as root: sshd -T needs to read the host keys" >&2
  exit 2
fi
command -v sshd >/dev/null 2>&1 || { echo "sshd not found" >&2; exit 2; }

if ! cfg=$(sshd -T 2>&1); then
  echo "sshd -T failed; the configuration has a syntax error:" >&2
  echo "$cfg" >&2
  exit 2
fi

get() { awk -v k="$1" 'tolower($1) == k { $1 = ""; sub(/^ /, ""); print; exit }' <<< "$cfg"; }

findings=0
pass() { printf '  PASS  %-28s %s\n' "$1" "$2"; }
warn() { printf '  WARN  %-28s %s\n' "$1" "$2"; findings=$((findings + 1)); }
info() { printf '  INFO  %-28s %s\n' "$1" "$2"; }

check_is() {
  local key=$1 want=$2 why=$3 val
  val=$(get "$key")
  if [[ ${val,,} == "${want,,}" ]]; then pass "$key" "$val"; else warn "$key" "is '$val', recommended '$want' ($why)"; fi
}
check_in() {
  local key=$1 why=$2; shift 2
  local val ok=0 w
  val=$(get "$key")
  for w in "$@"; do [[ ${val,,} == "${w,,}" ]] && ok=1; done
  if (( ok )); then pass "$key" "$val"; else warn "$key" "is '$val', recommended one of: $* ($why)"; fi
}
check_max() {
  local key=$1 max=$2 why=$3 val
  val=$(get "$key")
  if [[ $val =~ ^[0-9]+$ ]] && (( val <= max )); then pass "$key" "$val"; else warn "$key" "is '$val', recommended at most $max ($why)"; fi
}

echo "SSH server audit for $(hostname) ($(sshd -V 2>&1 | head -n 1))"
echo
echo "Authentication:"
check_in  permitrootlogin "log in as a user and use sudo" no prohibit-password
check_is  passwordauthentication no "keys cannot be guessed; passwords can"
check_is  kbdinteractiveauthentication no "same as passwords unless you use 2FA via PAM"
check_is  pubkeyauthentication yes "keys must stay enabled"
check_is  permitemptypasswords no "never allow empty passwords"
check_max maxauthtries 4 "limits key and password guesses per connection"
check_max logingracetime 60 "seconds an unauthenticated connection may stay open"
au=$(get allowusers); ag=$(get allowgroups)
if [[ -n $au || -n $ag ]]; then pass "allowusers/allowgroups" "${au:-} ${ag:-}"; else info "allowusers/allowgroups" "not set; every account with a shell may try to log in"; fi

echo
echo "Features:"
check_is x11forwarding no "rarely needed on servers"
check_is allowagentforwarding no "an attacker on this host could use connected clients' keys"
check_is allowtcpforwarding no "disable unless you rely on ssh -L/-R tunnels through this host"
check_is permittunnel no "layer 2/3 tunnels are almost never needed"
check_is usedns no "avoids slow logins and gives nothing security-wise"
check_in loglevel "INFO logs logins; VERBOSE also logs key fingerprints" INFO VERBOSE
info port "$(get port | paste -sd ' ' -)"
info listenaddress "$(get listenaddress | paste -sd ' ' -)"

echo
echo "Algorithms:"
for alg in sha1 cbc; do
  if grep -iE '^(ciphers|macs|kexalgorithms) ' <<< "$cfg" | grep -qi "$alg"; then
    warn "$alg algorithms" "still enabled; remove them from Ciphers/MACs/KexAlgorithms unless an old client needs them"
  else
    pass "$alg algorithms" "none enabled"
  fi
done

echo
echo "Configuration files:"
main=/etc/ssh/sshd_config
printf '  INFO  %-28s %s\n' "$main" "$(stat -c '%U:%G %a' "$main" 2>/dev/null)"
if [[ -d /etc/ssh/sshd_config.d ]]; then
  for f in /etc/ssh/sshd_config.d/*.conf; do
    [[ -e $f ]] || continue
    printf '  INFO  %-28s %s\n' "$f" "$(grep -vE '^\s*(#|$)' "$f" | paste -sd ';' -)"
    if grep -qiE '^\s*passwordauthentication\s+yes' "$f"; then
      warn "$f" "re-enables password authentication (common in cloud images); sshd uses the first value it reads"
    fi
  done
fi
if [[ -d /etc/ssh/sshd_config.d ]] && ! grep -qiE '^\s*include\s+/etc/ssh/sshd_config.d' "$main"; then
  info "Include" "sshd_config does not include sshd_config.d; files there are ignored"
fi
for k in /etc/ssh/ssh_host_*_key; do
  [[ -e $k ]] || continue
  mode=$(stat -c '%a' "$k")
  if [[ $mode == 600 || $mode == 640 ]]; then :; else warn "$k" "mode $mode, should be 600"; fi
done

echo
echo "Related:"
if command -v fail2ban-client >/dev/null 2>&1 && fail2ban-client status sshd >/dev/null 2>&1; then
  pass fail2ban "sshd jail active"
else
  info fail2ban "not installed or sshd jail not active"
fi
if command -v ufw >/dev/null 2>&1; then
  info ufw "$(ufw status | head -n 1)"
elif command -v firewall-cmd >/dev/null 2>&1; then
  info firewalld "$(firewall-cmd --state 2>&1)"
fi
for h in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
  [[ -e $h ]] || continue
  info "authorized_keys" "$h: $(grep -cvE '^\s*(#|$)' "$h") key(s)"
done

echo
if (( findings > 0 )); then
  echo "RESULT: $findings finding(s). Nothing was changed. Review each WARN, keep a second SSH session open while you edit, run 'sshd -t', then restart sshd."
  exit 1
fi
echo "RESULT: no findings"

How to use it

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
sudo bash sshaudit.sh
sudo bash sshaudit.sh && echo "clean" || echo "review findings"
SSH server audit for web1 (OpenSSH_9.6p1 Ubuntu-3ubuntu13, OpenSSL 3.0.13 30 Jan 2024)

Authentication:
  PASS  permitrootlogin              no
  WARN  passwordauthentication       is 'yes', recommended 'no' (keys cannot be guessed; passwords can)
  PASS  kbdinteractiveauthentication no
  PASS  pubkeyauthentication         yes
  PASS  permitemptypasswords         no
  WARN  maxauthtries                 is '6', recommended at most 4 (limits key and password guesses per connection)
  ...
Configuration files:
  INFO  /etc/ssh/sshd_config.d/50-cloud-init.conf PasswordAuthentication yes
  WARN  /etc/ssh/sshd_config.d/50-cloud-init.conf re-enables password authentication (common in cloud images); sshd uses the first value it reads

RESULT: 2 finding(s). Nothing was changed. ...

What the checks mean

  • Authentication: the settings from the hardening guide. PermitRootLogin prohibit-password passes because root with a key is acceptable on single-admin machines; no is the stricter choice.
  • Features: forwarding options are reported as WARN when enabled because each one is a capability an attacker on the box or a compromised client could use. If you deliberately use ssh -L tunnels through this server, leave AllowTcpForwarding on and treat that WARN as expected.
  • Algorithms: current OpenSSH defaults contain no SHA-1 or CBC algorithms; a hit means someone added them for an old client.
  • Configuration files: the most useful part on cloud servers. A file in sshd_config.d with PasswordAuthentication yes silently wins over the main file if it is included first, which is exactly how servers end up with passwords on despite a careful sshd_config.
  • Related: whether fail2ban watches sshd, whether a firewall is active, and how many keys each account accepts. An authorized_keys with keys you cannot account for deserves a look.

The script only reports. To act on a finding, create or edit a file such as /etc/ssh/sshd_config.d/00-hardening.conf, run sudo sshd -t, and restart the service. Careful: keep your current SSH session open and log in from a second terminal before you close it; a wrong setting locks you out, and the open session is your way back.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

  • 0276 What is SSH? SSH is a secure way to open a command line on another computer over the network, with everything encrypted. Beginner · Explains an idea · Any system Article SSH 15 Oct 2026
  • 0322 Fix: user is not in the sudoers file sudo refuses because your account is not allowed to use it. Add the user to the sudo group (Debian, Ubuntu) or wheel… Beginner · Fixes an error · Linux, macOS Article Server hardening · Users, sudo & hardening 15 Oct 2026
  • 0345 SSH keys: create, copy and use them Generate an ed25519 key pair, install the public key on a server with ssh-copy-id, keep the private key protected with a passphrase… Beginner · Step by step · Linux, macOS, Windows Article Keys & config · SSH 15 Oct 2026
  • 0346 The SSH config file: aliases, jump hosts and keys per server Reference for ~/.ssh/config: short names for servers, the right user, port and key per host, ProxyJump through a bastion, connection reuse and… Intermediate · Cheat sheet · Linux, macOS, Windows Article Keys & config · SSH 15 Oct 2026
  • 0352 Harden a fresh Linux server in 30 minutes The baseline for any new Debian, Ubuntu or RHEL-family server: updates on, a non-root user with sudo, SSH keys only, a firewall… Beginner · Step by step · Linux Article Server hardening · Users, sudo & hardening 15 Oct 2026
  • 0353 fail2ban: block repeated failed logins Install fail2ban, enable the sshd jail with sane ban times, whitelist your own address, read the ban list, unban someone, and add… Intermediate · Step by step · Linux Article Server hardening · SSH 15 Oct 2026

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +8,798 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?