Loading_
Loading_
Audits sshd against CIS and the current Mozilla modern profile — ciphers, MACs, key exchange, root login, auth methods, idle timeouts — and prints the exact config lines to fix each gap.
#!/usr/bin/env bash## SSH hardening audit. Read-only.# Reads the EFFECTIVE config via sshd -T, so includes and Match blocks resolve# the way the daemon sees them rather than the way the file reads.## Usage: sudo ./ssh_hardening_audit.sh [--fix-file /tmp/99-hardening.conf] set -uo pipefail FIX_FILE=""[[ "${1:-}" == "--fix-file" ]] && FIX_FILE="${2:?path required}" command -v sshd >/dev/null || { echo "sshd not found" >&2; exit 2; }[[ $EUID -eq 0 ]] || { echo "Run as root: sshd -T needs it" >&2; exit 2; } PASS=0; FAIL=0; WARN=0declare -a REMEDIATION GRN=$'\033[0;32m'; RED=$'\033[0;31m'; YEL=$'\033[0;33m'; DIM=$'\033[2m'; OFF=$'\033[0m' EFFECTIVE=$(sshd -T 2>/dev/null) || { echo "sshd -T failed; config may be invalid" >&2; exit 2; } get() { echo "$EFFECTIVE" | awk -v k="$1" 'tolower($1)==tolower(k) { $1=""; sub(/^ /,""); print; exit }'; } check_equals() { local key="$1" want="$2" severity="${3:-FAIL}" local have; have=$(get "$key") if [[ "${have,,}" == "${want,,}" ]]; then printf '%s PASS%s %-28s %s\n' "$GRN" "$OFF" "$key" "$have" PASS=$((PASS + 1)) elif [[ "$severity" == "WARN" ]]; then printf '%s WARN%s %-28s is "%s", want "%s"\n' "$YEL" "$OFF" "$key" "${have:-unset}" "$want" WARN=$((WARN + 1)) REMEDIATION+=("$key $want") else printf '%s FAIL%s %-28s is "%s", want "%s"\n' "$RED" "$OFF" "$key" "${have:-unset}" "$want" FAIL=$((FAIL + 1)) REMEDIATION+=("$key $want") fi} check_max() { local key="$1" limit="$2" local have; have=$(get "$key") if [[ -n "$have" && "$have" -le "$limit" && "$have" -gt 0 ]]; then printf '%s PASS%s %-28s %s\n' "$GRN" "$OFF" "$key" "$have" PASS=$((PASS + 1)) else printf '%s FAIL%s %-28s is "%s", want <= %s\n' "$RED" "$OFF" "$key" "${have:-unset}" "$limit" FAIL=$((FAIL + 1)) REMEDIATION+=("$key $limit") fi} check_no_weak() { local key="$1"; shift local have; have=$(get "$key") local found="" for weak in "$@"; do [[ "$have" == *"$weak"* ]] && found="$found $weak" done if [[ -z "$found" ]]; then printf '%s PASS%s %-28s no weak algorithms\n' "$GRN" "$OFF" "$key" PASS=$((PASS + 1)) else printf '%s FAIL%s %-28s weak:%s\n' "$RED" "$OFF" "$key" "$found" FAIL=$((FAIL + 1)) fi} echo "SSH hardening audit - $(hostname) - $(sshd -V 2>&1 | head -1)"echo echo "Authentication"check_equals PermitRootLogin nocheck_equals PasswordAuthentication nocheck_equals PermitEmptyPasswords nocheck_equals ChallengeResponseAuthentication no WARNcheck_equals PubkeyAuthentication yescheck_equals HostbasedAuthentication nocheck_equals IgnoreRhosts yescheck_equals UsePAM yescheck_max MaxAuthTries 4check_max LoginGraceTime 60 echoecho "Session"check_max ClientAliveInterval 300check_max ClientAliveCountMax 3check_max MaxSessions 10check_equals X11Forwarding nocheck_equals AllowAgentForwarding no WARNcheck_equals PermitUserEnvironment no echoecho "Cryptography"check_no_weak ciphers 3des cbc arcfour blowfish cast128check_no_weak macs hmac-md5 hmac-sha1 umac-64 -96check_no_weak kexalgorithms diffie-hellman-group1 diffie-hellman-group14-sha1 gss-check_equals logingracetime 60 WARN echoecho "Logging"check_equals LogLevel VERBOSEcheck_equals Protocol 2 WARN # Host key permissions are the check that never appears in a config auditechoecho "Host keys"for key in /etc/ssh/ssh_host_*_key; do [[ -e "$key" ]] || continue perms=$(stat -c '%a' "$key") if [[ "$perms" == "600" || "$perms" == "640" ]]; then printf '%s PASS%s %-28s %s\n' "$GRN" "$OFF" "$(basename "$key")" "$perms" PASS=$((PASS + 1)) else printf '%s FAIL%s %-28s mode %s, want 600\n' "$RED" "$OFF" "$(basename "$key")" "$perms" FAIL=$((FAIL + 1)) fidone echoprintf 'Result: %s%d pass%s, %s%d fail%s, %s%d warn%s\n' \ "$GRN" "$PASS" "$OFF" "$RED" "$FAIL" "$OFF" "$YEL" "$WARN" "$OFF" if [[ -n "$FIX_FILE" && ${#REMEDIATION[@]} -gt 0 ]]; then { echo "# Generated by ssh_hardening_audit.sh on $(date -Is)" echo "# Drop in /etc/ssh/sshd_config.d/ and validate with: sshd -t" echo printf '%s\n' "${REMEDIATION[@]}" echo echo "Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com" echo "MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com" echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512" } > "$FIX_FILE" printf '%sRemediation written to %s%s\n' "$DIM" "$FIX_FILE" "$OFF" echo "Validate before applying: sshd -t -f /etc/ssh/sshd_config"fi [[ $FAIL -eq 0 ]] || exit 1SSH hardening guides tell you what the setting should be. They rarely tell you what yours currently is, and sshd_config is a file where an override forty lines down silently wins.
This reads the effective configuration from sshd -T rather than parsing the file, so Match blocks and includes are resolved the way the daemon actually sees them.
Every failure prints the remediation line. It changes nothing, and it validates any config you generate with sshd -t before suggesting you apply it.
| Name | Type | Required | Description |
|---|---|---|---|
--fix-file | path | Optional | Write a drop-in remediation config. |
The platform turns any script into a governed automation — versioned, gated, audited and reversible.