#!/bin/sh # # POSIX-Compliant SSH Security Auditing and Management Script # Purpose: Audit and manage SSH security configurations across multiple hosts # Compatible with: sh, dash, bash, ksh, zsh # # Author: Ismo Vuorinen # Copyright: MIT # Version: 2.0-POSIX # Date: 2025-10-17 set -eu # ============================================================================ # CONFIGURATION # ============================================================================ SCRIPT_NAME=$(basename "$0") # SCRIPT_DIR=$(dirname "$(readlink -f "$0" 2> /dev/null || pwd)") # Reserved for future use TIMESTAMP=$(date +%Y%m%d_%H%M%S) AUDIT_BASE_DIR="./ssh-audit/${TIMESTAMP}" BACKUP_DIR="${AUDIT_BASE_DIR}/backup" LOG_FILE="${AUDIT_BASE_DIR}/log.log" REPORT_FILE="${AUDIT_BASE_DIR}/report.csv" TEMP_DIR="${AUDIT_BASE_DIR}/tmp" # SSH connection parameters SSH_TIMEOUT=10 SSH_RETRIES=3 # Fallback usernames for SSH connections (in order of priority) FALLBACK_USERS="root ubuntu ivuorinen" # Common default SSH key locations (checked in order) DEFAULT_SSH_KEYS=" ~/.ssh/id_ed25519 ~/.ssh/id_rsa ~/.ssh/id_ecdsa ~/.ssh/id_dsa " # Color codes (using tput for POSIX compliance) if [ -t 1 ]; then RED=$(tput setaf 1 2> /dev/null || echo '') GREEN=$(tput setaf 2 2> /dev/null || echo '') YELLOW=$(tput setaf 3 2> /dev/null || echo '') BLUE=$(tput setaf 4 2> /dev/null || echo '') RESET=$(tput sgr0 2> /dev/null || echo '') BOLD=$(tput bold 2> /dev/null || echo '') else RED='' GREEN='' YELLOW='' BLUE='' RESET='' BOLD='' fi # Global state files (replacing bash associative arrays) HOST_STATUS_FILE="${TEMP_DIR}/host-status" HOST_RESULTS_FILE="${TEMP_DIR}/host-results" HOSTS_LIST_FILE="${TEMP_DIR}/hosts-list" FAILED_HOSTS_FILE="${TEMP_DIR}/failed-hosts" # ============================================================================ # HELPER FUNCTIONS FOR STATE MANAGEMENT # ============================================================================ # Initialize state files init_state_files() { : > "$HOST_STATUS_FILE" : > "$HOST_RESULTS_FILE" : > "$HOSTS_LIST_FILE" : > "$FAILED_HOSTS_FILE" } # Detect available default SSH keys detect_available_ssh_keys() { available_keys="" for key in $DEFAULT_SSH_KEYS; do # Expand tilde to home directory expanded_key=$(eval echo "$key") if [ -f "$expanded_key" ] && [ -r "$expanded_key" ]; then available_keys="${available_keys}${available_keys:+ }${expanded_key}" fi done printf '%s' "$available_keys" } # Trim whitespace from string (POSIX compatible) trim_whitespace() { printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' } # Extract SSH config value from sshd -T output get_sshd_value() { config_file="$1" key="$2" grep -i "^${key}" "$config_file" | awk '{print $2}' } # Check security setting and log if it matches bad value check_security_issue() { host="$1" # $2 is setting_name (unused but kept for API clarity) actual_value="$3" bad_value="$4" level="${5:-WARNING}" message="$6" if [ "$actual_value" = "$bad_value" ]; then log_message "$level" "$host: $message" return 0 # Issue found fi return 1 # No issue } # Set a key-value pair in state file set_state() { state_file="$1" key="$2" value="$3" # Remove existing entry if present if [ -f "$state_file" ]; then grep -v "^${key}=" "$state_file" > "${state_file}.tmp" 2> /dev/null || true mv "${state_file}.tmp" "$state_file" fi # Add new entry printf '%s=%s\n' "$key" "$value" >> "$state_file" } # Get a value from state file get_state() { state_file="$1" key="$2" default="${3:-unknown}" if [ -f "$state_file" ]; then value=$(grep "^${key}=" "$state_file" 2> /dev/null | cut -d= -f2- | head -n1) printf '%s' "${value:-$default}" else printf '%s' "$default" fi } # ============================================================================ # ERROR HANDLING AND CLEANUP # ============================================================================ cleanup() { exit_code=$? printf '\n%sCleaning up...%s\n' "$YELLOW" "$RESET" # Clean up temporary directory (keep logs, reports, and backups) if [ -d "$TEMP_DIR" ]; then rm -rf "$TEMP_DIR" fi if [ $exit_code -ne 0 ]; then printf '%s[ERROR] Script failed with exit code: %d%s\n' "$RED" "$exit_code" "$RESET" >&2 fi exit $exit_code } trap cleanup EXIT INT TERM # ============================================================================ # LOGGING FUNCTIONS # ============================================================================ setup_logging() { # Create all necessary directories mkdir -p "$BACKUP_DIR" "$TEMP_DIR" 2> /dev/null || { printf '%s[ERROR] Failed to create audit directories%s\n' "$RED" "$RESET" >&2 exit 1 } { echo "==========================================" echo "SSH Security Audit Log" echo "Started: $(date)" echo "Script: $SCRIPT_NAME" echo "User: $(whoami)" echo "Working Directory: $(pwd)" echo "Output Directory: $AUDIT_BASE_DIR" echo "==========================================" } >> "$LOG_FILE" } log_message() { level="$1" message="$2" timestamp=$(date '+%Y-%m-%d %H:%M:%S') printf '[%s] [%s] %s\n' "$timestamp" "$level" "$message" >> "$LOG_FILE" case "$level" in ERROR) printf '%s[ERROR] %s%s\n' "$RED" "$message" "$RESET" >&2 ;; WARNING) printf '%s[WARNING] %s%s\n' "$YELLOW" "$message" "$RESET" >&2 ;; INFO) printf '%s[INFO] %s%s\n' "$BLUE" "$message" "$RESET" ;; SUCCESS) printf '%s[SUCCESS] %s%s\n' "$GREEN" "$message" "$RESET" ;; esac } # ============================================================================ # INPUT VALIDATION # ============================================================================ validate_hostname() { hostname="$1" # Basic hostname validation case "$hostname" in *[!a-zA-Z0-9.-]*) log_message "ERROR" "Invalid hostname format: $hostname" return 1 ;; .*) log_message "ERROR" "Invalid hostname format: $hostname" return 1 ;; esac return 0 } validate_username() { username="$1" # Basic username validation case "$username" in *[!a-zA-Z0-9_-]*) log_message "ERROR" "Invalid username format: $username" return 1 ;; [0-9]*) log_message "ERROR" "Invalid username format: $username" return 1 ;; esac return 0 } parse_host_list() { input_file="$1" if [ ! -f "$input_file" ]; then log_message "ERROR" "Input file not found: $input_file" exit 1 fi while IFS=':' read -r host username ssh_key; do # Skip empty lines and comments case "$host" in '' | '#'*) continue ;; esac # Trim whitespace (POSIX compatible) host=$(trim_whitespace "$host") username=$(trim_whitespace "$username") ssh_key=$(trim_whitespace "$ssh_key") if validate_hostname "$host" && validate_username "$username"; then # Store with optional SSH key (empty if not provided) printf '%s:%s:%s\n' "$host" "$username" "$ssh_key" >> "$HOSTS_LIST_FILE" if [ -n "$ssh_key" ]; then log_message "INFO" "Added host: $host with user: $username and SSH key: $ssh_key" else log_message "INFO" "Added host: $host with user: $username" fi else log_message "WARNING" "Skipping invalid entry: ${host}:${username}" fi done < "$input_file" if [ ! -s "$HOSTS_LIST_FILE" ]; then log_message "ERROR" "No valid hosts found in input file" exit 1 fi host_count=$(wc -l < "$HOSTS_LIST_FILE") log_message "INFO" "Loaded $host_count hosts for auditing" } # ============================================================================ # SSH CONNECTION FUNCTIONS # ============================================================================ ssh_with_retry() { host="$1" username="$2" command="$3" ssh_key="${4:-}" max_retries="${5:-$SSH_RETRIES}" # Build list of keys to try keys_to_try="" # Priority 1: Specific key if provided if [ -n "$ssh_key" ] && [ -f "$ssh_key" ]; then keys_to_try="$ssh_key" log_message "INFO" "Will try specific SSH key: $ssh_key" fi # Priority 2: Detect and add available default keys available_default_keys=$(detect_available_ssh_keys) if [ -n "$available_default_keys" ]; then keys_to_try="${keys_to_try}${keys_to_try:+ }${available_default_keys}" fi # Priority 3: Try without explicit key (agent/default) keys_to_try="${keys_to_try}${keys_to_try:+ }NO_KEY" attempt=1 while [ "$attempt" -le "$max_retries" ]; do log_message "INFO" "Connecting to $host as $username (attempt $attempt/$max_retries)" # Try each key in sequence for try_key in $keys_to_try; do ssh_opts="-o ConnectTimeout=$SSH_TIMEOUT -o BatchMode=yes -o StrictHostKeyChecking=yes" if [ "$try_key" != "NO_KEY" ]; then ssh_opts="$ssh_opts -i $try_key" log_message "INFO" "Trying SSH key: $try_key" else log_message "INFO" "Trying SSH agent/default authentication" fi # shellcheck disable=SC2086,SC2029 if ssh $ssh_opts "${username}@${host}" "$command" 2> /dev/null; then if [ "$try_key" != "NO_KEY" ]; then log_message "SUCCESS" "Connected using SSH key: $try_key" else log_message "SUCCESS" "Connected using SSH agent/default authentication" fi return 0 fi done status=$? log_message "WARNING" "SSH connection failed to $host with all authentication methods (status: $status)" if [ "$attempt" -lt "$max_retries" ]; then sleep $((attempt * 2)) fi attempt=$((attempt + 1)) done return 1 } test_ssh_connectivity() { host="$1" username="$2" ssh_key="${3:-}" if ssh_with_retry "$host" "$username" "echo 'SSH_OK'" "$ssh_key" | grep -q "SSH_OK"; then log_message "SUCCESS" "SSH connectivity verified for ${username}@${host}" return 0 else log_message "ERROR" "Cannot establish SSH connection to ${username}@${host}" return 1 fi } # ============================================================================ # SSH SECURITY AUDIT FUNCTIONS # ============================================================================ check_sshd_config() { host="$1" username="$2" ssh_key="${3:-}" temp_file="${TEMP_DIR}/sshd-config-${host}" log_message "INFO" "Checking SSH configuration on $host" # Get sshd configuration if ! ssh_with_retry "$host" "$username" "sudo sshd -T 2>/dev/null || sshd -T 2>/dev/null" "$ssh_key" > "$temp_file"; then log_message "ERROR" "Failed to retrieve SSH configuration from $host" return 1 fi # Parse configuration (POSIX compatible) password_auth=$(get_sshd_value "$temp_file" "passwordauthentication") pubkey_auth=$(get_sshd_value "$temp_file" "pubkeyauthentication") permit_root=$(get_sshd_value "$temp_file" "permitrootlogin") protocol=$(get_sshd_value "$temp_file" "protocol") x11_forwarding=$(get_sshd_value "$temp_file" "x11forwarding") permit_empty=$(get_sshd_value "$temp_file" "permitemptypasswords") # Store results set_state "$HOST_RESULTS_FILE" "${host}_password_auth" "${password_auth:-unknown}" set_state "$HOST_RESULTS_FILE" "${host}_pubkey_auth" "${pubkey_auth:-unknown}" set_state "$HOST_RESULTS_FILE" "${host}_permit_root" "${permit_root:-unknown}" set_state "$HOST_RESULTS_FILE" "${host}_protocol" "${protocol:-unknown}" set_state "$HOST_RESULTS_FILE" "${host}_x11_forwarding" "${x11_forwarding:-unknown}" set_state "$HOST_RESULTS_FILE" "${host}_permit_empty_passwords" "${permit_empty:-unknown}" # Security assessment security_issues=0 check_security_issue "$host" "password_auth" "$password_auth" "yes" \ "WARNING" "Password authentication is enabled" && security_issues=$((security_issues + 1)) # Special handling for permit_root (check if NOT "no") if [ "$permit_root" != "no" ]; then log_message "WARNING" "$host: Root login is not disabled (current: $permit_root)" security_issues=$((security_issues + 1)) fi check_security_issue "$host" "permit_empty" "$permit_empty" "yes" \ "ERROR" "Empty passwords are permitted!" && security_issues=$((security_issues + 1)) check_security_issue "$host" "x11_forwarding" "$x11_forwarding" "yes" \ "WARNING" "X11 forwarding is enabled" && security_issues=$((security_issues + 1)) set_state "$HOST_RESULTS_FILE" "${host}_security_issues" "$security_issues" rm -f "$temp_file" return 0 } # ============================================================================ # AUTOMATED UPDATES DETECTION # ============================================================================ check_automated_updates() { host="$1" username="$2" ssh_key="${3:-}" log_message "INFO" "Checking automated updates on $host" # Detect distribution distro=$(ssh_with_retry "$host" "$username" " if [ -f /etc/os-release ]; then . /etc/os-release printf '%s' \"\$ID\" elif [ -f /etc/debian_version ]; then printf 'debian' elif [ -f /etc/redhat-release ]; then printf 'rhel' else printf 'unknown' fi " "$ssh_key") auto_updates="disabled" case "$distro" in ubuntu | debian) unattended_status=$(ssh_with_retry "$host" "$username" " if dpkg -l unattended-upgrades >/dev/null 2>&1; then if [ -f /etc/apt/apt.conf.d/20auto-upgrades ]; then if grep '^APT::Periodic::Unattended-Upgrade' /etc/apt/apt.conf.d/20auto-upgrades | grep -q '\"1\"'; then printf 'enabled' else printf 'disabled' fi else printf 'not-configured' fi else printf 'not-installed' fi " "$ssh_key") auto_updates="$unattended_status" ;; rhel | centos | rocky | almalinux | fedora) dnf_status=$(ssh_with_retry "$host" "$username" " if command -v dnf >/dev/null 2>&1 && rpm -q dnf-automatic >/dev/null 2>&1; then if systemctl is-enabled dnf-automatic.timer >/dev/null 2>&1; then printf 'enabled' else printf 'disabled' fi elif rpm -q yum-cron >/dev/null 2>&1; then if systemctl is-enabled yum-cron >/dev/null 2>&1; then printf 'enabled' else printf 'disabled' fi else printf 'not-installed' fi " "$ssh_key") auto_updates="$dnf_status" ;; *) auto_updates="unknown" ;; esac set_state "$HOST_RESULTS_FILE" "${host}_auto_updates" "$auto_updates" set_state "$HOST_RESULTS_FILE" "${host}_distro" "$distro" log_message "INFO" "$host: Automated updates status: $auto_updates (distro: $distro)" return 0 } # ============================================================================ # PENDING REBOOT DETECTION # ============================================================================ check_pending_reboot() { host="$1" username="$2" ssh_key="${3:-}" log_message "INFO" "Checking pending reboot status on $host" # Multiple detection methods (POSIX compliant) check_result=$(ssh_with_retry "$host" "$username" ' REBOOT_NEEDED=0 REASONS="" # Method 1: Check /var/run/reboot-required if [ -f /var/run/reboot-required ]; then REBOOT_NEEDED=1 REASONS="${REASONS:+$REASONS,}reboot-required" fi # Method 2: Check kernel version mismatch if command -v rpm >/dev/null 2>&1; then CURRENT_KERNEL=$(uname -r) LATEST_KERNEL=$(rpm -q kernel --last 2>/dev/null | head -1 | sed "s/^kernel-//;s/ .*//") if [ -n "$LATEST_KERNEL" ] && [ "$CURRENT_KERNEL" != "$LATEST_KERNEL" ]; then REBOOT_NEEDED=1 REASONS="${REASONS:+$REASONS,}kernel-mismatch" fi fi # Method 3: Check needs-restarting if command -v needs-restarting >/dev/null 2>&1; then if ! needs-restarting -r >/dev/null 2>&1; then REBOOT_NEEDED=1 REASONS="${REASONS:+$REASONS,}needs-restarting" fi fi # Method 4: Check deleted libraries DELETED_LIBS=$(lsof 2>/dev/null | grep -c "DEL.*deleted" || echo 0) if [ "$DELETED_LIBS" -gt 10 ]; then REBOOT_NEEDED=1 REASONS="${REASONS:+$REASONS,}deleted-libraries" fi if [ $REBOOT_NEEDED -eq 1 ]; then printf "yes|%s" "${REASONS:-unknown}" else printf "no|none" fi ' "$ssh_key") reboot_required=$(printf '%s' "$check_result" | cut -d'|' -f1) reboot_reason=$(printf '%s' "$check_result" | cut -d'|' -f2) set_state "$HOST_RESULTS_FILE" "${host}_reboot_required" "$reboot_required" set_state "$HOST_RESULTS_FILE" "${host}_reboot_reason" "$reboot_reason" if [ "$reboot_required" = "yes" ]; then log_message "WARNING" "$host: Reboot required (reason: $reboot_reason)" else log_message "INFO" "$host: No reboot required" fi return 0 } # ============================================================================ # REMEDIATION FUNCTIONS # ============================================================================ backup_sshd_config() { host="$1" username="$2" ssh_key="${3:-}" log_message "INFO" "Creating backup of sshd_config on $host" ssh_with_retry "$host" "$username" " sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.${TIMESTAMP} printf 'Backup created: /etc/ssh/sshd_config.backup.%s\n' '${TIMESTAMP}' " "$ssh_key" } disable_password_auth() { host="$1" username="$2" ssh_key="${3:-}" log_message "INFO" "Disabling password authentication on $host" # Create backup first backup_sshd_config "$host" "$username" "$ssh_key" # Create hardening configuration config_content='# SSH Hardening - Automated Security Audit # Generated: '"$(date)"' # Disable password authentication PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no UsePAM no # Ensure key-based authentication PubkeyAuthentication yes AuthenticationMethods publickey # Additional security settings PermitRootLogin no PermitEmptyPasswords no MaxAuthTries 3 MaxSessions 2 ClientAliveInterval 300 ClientAliveCountMax 2 ' # Apply configuration if ssh_with_retry "$host" "$username" " printf '%s\n' '$config_content' | sudo tee /etc/ssh/sshd_config.d/99-hardening.conf >/dev/null sudo sshd -t && sudo systemctl reload sshd " "$ssh_key"; then log_message "SUCCESS" "Password authentication disabled on $host" set_state "$HOST_RESULTS_FILE" "${host}_remediation" "success" return 0 else log_message "ERROR" "Failed to disable password authentication on $host" set_state "$HOST_RESULTS_FILE" "${host}_remediation" "failed" return 1 fi } # ============================================================================ # REPORTING FUNCTIONS # ============================================================================ generate_csv_report() { report_file="$1" # Header printf 'Timestamp,Hostname,Username,SSH Status,Password Auth,Key Auth,Root Login,Auto Updates,Reboot Required,Security Issues,Remediation\n' > "$report_file" # Data rows while IFS=':' read -r host username ssh_key; do { printf '%s,' "$(date '+%Y-%m-%d %H:%M:%S')" printf '%s,' "$host" printf '%s,' "$username" printf '%s,' "$(get_state "$HOST_STATUS_FILE" "$host" "unknown")" printf '%s,' "$(get_state "$HOST_RESULTS_FILE" "${host}_password_auth" "unknown")" printf '%s,' "$(get_state "$HOST_RESULTS_FILE" "${host}_pubkey_auth" "unknown")" printf '%s,' "$(get_state "$HOST_RESULTS_FILE" "${host}_permit_root" "unknown")" printf '%s,' "$(get_state "$HOST_RESULTS_FILE" "${host}_auto_updates" "unknown")" printf '%s,' "$(get_state "$HOST_RESULTS_FILE" "${host}_reboot_required" "unknown")" printf '%s,' "$(get_state "$HOST_RESULTS_FILE" "${host}_security_issues" "0")" printf '%s\n' "$(get_state "$HOST_RESULTS_FILE" "${host}_remediation" "none")" } >> "$report_file" done < "$HOSTS_LIST_FILE" } display_summary() { printf '\n' printf '%s==========================================\n' "$BOLD" printf ' SSH Security Audit Summary\n' printf '==========================================%s\n' "$RESET" printf '\n' total_hosts=$(wc -l < "$HOSTS_LIST_FILE") successful_audits=0 security_issues_found=0 reboots_required=0 password_auth_enabled=0 while IFS=':' read -r host username ssh_key; do status=$(get_state "$HOST_STATUS_FILE" "$host" "unknown") [ "$status" = "audited" ] && successful_audits=$((successful_audits + 1)) password_auth=$(get_state "$HOST_RESULTS_FILE" "${host}_password_auth" "unknown") [ "$password_auth" = "yes" ] && password_auth_enabled=$((password_auth_enabled + 1)) reboot_req=$(get_state "$HOST_RESULTS_FILE" "${host}_reboot_required" "unknown") [ "$reboot_req" = "yes" ] && reboots_required=$((reboots_required + 1)) issues=$(get_state "$HOST_RESULTS_FILE" "${host}_security_issues" "0") [ "$issues" -gt 0 ] && security_issues_found=$((security_issues_found + 1)) done < "$HOSTS_LIST_FILE" printf '%sTotal Hosts:%s %d\n' "$BLUE" "$RESET" "$total_hosts" printf '%sSuccessfully Audited:%s %d\n' "$GREEN" "$RESET" "$successful_audits" printf '%sPassword Auth Enabled:%s %d\n' "$YELLOW" "$RESET" "$password_auth_enabled" printf '%sSecurity Issues Found:%s %d\n' "$YELLOW" "$RESET" "$security_issues_found" printf '%sReboots Required:%s %d\n' "$YELLOW" "$RESET" "$reboots_required" if [ -s "$FAILED_HOSTS_FILE" ]; then printf '\n%sFailed Hosts:%s\n' "$RED" "$RESET" while read -r failed; do printf ' - %s\n' "$failed" done < "$FAILED_HOSTS_FILE" fi printf '\n' printf '%sReport saved to:%s %s\n' "$BOLD" "$RESET" "$REPORT_FILE" printf '%sLog file:%s %s\n' "$BOLD" "$RESET" "$LOG_FILE" } # ============================================================================ # MAIN AUDIT FUNCTION # ============================================================================ audit_host() { host_entry="$1" host=$(printf '%s' "$host_entry" | cut -d':' -f1) primary_user=$(printf '%s' "$host_entry" | cut -d':' -f2) ssh_key=$(printf '%s' "$host_entry" | cut -d':' -f3) log_message "INFO" "Starting audit of $host" # Try multiple usernames (primary user first, then fallbacks) connected=0 connected_user="" for username in "$primary_user" $FALLBACK_USERS; do if test_ssh_connectivity "$host" "$username" "$ssh_key"; then connected=1 connected_user="$username" log_message "SUCCESS" "Connected to $host as $username" break fi done if [ $connected -eq 0 ]; then log_message "ERROR" "Could not connect to $host with any username" set_state "$HOST_STATUS_FILE" "$host" "connection_failed" printf '%s\n' "$host" >> "$FAILED_HOSTS_FILE" return 1 fi # Perform audit checks check_sshd_config "$host" "$connected_user" "$ssh_key" check_automated_updates "$host" "$connected_user" "$ssh_key" check_pending_reboot "$host" "$connected_user" "$ssh_key" set_state "$HOST_STATUS_FILE" "$host" "audited" set_state "$HOST_RESULTS_FILE" "${host}_connected_user" "$connected_user" log_message "SUCCESS" "Completed audit of $host" return 0 } # ============================================================================ # MAIN EXECUTION # ============================================================================ main() { input_file="${1:-}" auto_remediate="${2:-no}" # Validate arguments if [ -z "$input_file" ]; then printf 'Usage: %s [auto-remediate:yes|no]\n' "$SCRIPT_NAME" printf '\n' printf 'Host list file format (one per line):\n' printf ' hostname1:username1\n' printf ' hostname2:username2\n' printf ' hostname3:username3:/path/to/ssh_key # Optional SSH key\n' printf '\n' printf 'Format: hostname:username[:ssh_key]\n' printf ' - hostname: The host to connect to\n' printf ' - username: SSH username\n' printf ' - ssh_key: (Optional) Path to SSH private key file\n' printf '\n' printf 'SSH Key Authentication Priority:\n' printf ' 1. Specific key (if provided in host file)\n' printf ' 2. Auto-detected default keys (~/.ssh/id_ed25519, id_rsa, etc.)\n' printf ' 3. SSH agent or system default authentication\n' printf '\n' printf 'Example:\n' printf ' %s hosts.txt\n' "$SCRIPT_NAME" printf ' %s hosts.txt yes # Auto-remediate security issues\n' "$SCRIPT_NAME" exit 1 fi # Setup setup_logging init_state_files log_message "INFO" "Starting SSH Security Audit" # Detect available SSH keys and log available_keys=$(detect_available_ssh_keys) if [ -n "$available_keys" ]; then key_count=$(printf '%s\n' "$available_keys" | wc -w | tr -d ' ') log_message "INFO" "Detected $key_count default SSH key(s) for authentication" for key in $available_keys; do log_message "INFO" " - $key" done else log_message "INFO" "No default SSH keys detected, will use SSH agent/default authentication" fi # Parse input file parse_host_list "$input_file" # Audit each host while IFS=':' read -r host username ssh_key; do audit_host "${host}:${username}:${ssh_key}" done < "$HOSTS_LIST_FILE" # Generate report generate_csv_report "$REPORT_FILE" # Display summary display_summary # Offer remediation if [ "$auto_remediate" != "yes" ]; then printf '\n%sWould you like to automatically remediate security issues? (y/N)%s ' "$YELLOW" "$RESET" read -r response case "$response" in [Yy]*) auto_remediate="yes" ;; esac fi if [ "$auto_remediate" = "yes" ]; then printf '\n%sStarting automatic remediation...%s\n' "$BOLD" "$RESET" while IFS=':' read -r host username ssh_key; do password_auth=$(get_state "$HOST_RESULTS_FILE" "${host}_password_auth" "unknown") pubkey_auth=$(get_state "$HOST_RESULTS_FILE" "${host}_pubkey_auth" "unknown") if [ "$password_auth" = "yes" ] && [ "$pubkey_auth" = "yes" ]; then printf '\n%sDisabling password authentication on %s...%s\n' "$YELLOW" "$host" "$RESET" connected_user=$(get_state "$HOST_RESULTS_FILE" "${host}_connected_user" "root") if disable_password_auth "$host" "$connected_user" "$ssh_key"; then printf '%s✓ Successfully disabled password authentication on %s%s\n' "$GREEN" "$host" "$RESET" else printf '%s✗ Failed to disable password authentication on %s%s\n' "$RED" "$host" "$RESET" fi fi done < "$HOSTS_LIST_FILE" # Regenerate report after remediation remediated_report="${REPORT_FILE%.csv}-remediated.csv" generate_csv_report "$remediated_report" printf '\n%sRemediation complete. Updated report: %s%s\n' "$BOLD" "$remediated_report" "$RESET" fi log_message "INFO" "SSH Security Audit completed" } # Run main function main "$@"