├── img └── output.png ├── LICENSE ├── README.md └── audit.sh /img/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healthyhost/audit-vps-script/HEAD/img/output.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kyri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # audit-vps-script 2 | 3 | Scans your server to find common issues like weak SSH settings, missing firewalls, lack of updates, and more. Perfect for getting your VPS ready for production. 4 | 5 | ![Output](./img/output.png) 6 | 7 | ## Usage 8 | 9 | 1. Download the script 10 | 11 | ```bash 12 | $ curl -O https://raw.githubusercontent.com/healthyhost/audit-vps-script/main/audit.sh 13 | ``` 14 | 15 | 2. Make it executable: 16 | 17 | ```bash 18 | $ chmod +x audit.sh 19 | ``` 20 | 21 | 3. Run the security audit: 22 | 23 | ```bash 24 | $ sudo ./audit.sh 25 | ``` 26 | 27 | ### Web 28 | 29 | Visit [https://auditpvps.com](https://auditvps.com) to run checks through our web app for free and render the results in your browser. 30 | 31 | ## Requirements 32 | 33 | - Ubuntu/Debian-based distributions 34 | - `jq` (temporary dependency, planned for removal) 35 | - Root or sudo privileges 36 | 37 | ## Security checks 38 | 39 | The scripts perform the following checks: 40 | 41 | - Root 42 | - Check that non-root sudo user exists 43 | - Firewall (UFW) 44 | - Check that UFW is installed 45 | - Check that UFW is enabled 46 | - Check that incoming traffic is blocked 47 | - SSH 48 | - Check that SSH is enabled 49 | - Check that key-based authentication is enabled 50 | - Check that root login is disabled 51 | - Check that password authentication is disabled 52 | - Check that SSH is listening on port 22 53 | - Fail2Ban 54 | - Check that Fail2ban is installed 55 | - Check that Fail2ban is enabled 56 | - Check that Fail2ban is configured correctly 57 | - Check that Fail2ban SSH is enabled 58 | - Check that Fail2ban SSH is in aggressive mode 59 | - Access control 60 | - Check that `/etc/passwd` is readable by everyone 61 | - Check that `/etc/shadow` is readable by root only 62 | - System updates 63 | - Check that automatic system updates are enabled 64 | - Check that automatic system upgrades are enabled 65 | - Port security 66 | - Checks that insecure ports are not open 67 | 68 | ## Contributing 69 | 70 | We welcome contributions! If you have any suggestions or improvements, please open an issue or submit a pull request. 71 | 72 | ## License 73 | 74 | MIT 75 | 76 | ## Roadmap 77 | 78 | - [ ] Support for additional Linux distributions: 79 | - [ ] RHEL/CentOS 80 | - [ ] Fedora 81 | - [ ] Alpine Linux 82 | - [ ] Removal of `jq` dependency 83 | - [ ] Warning states (WARN) for non-critical security recommendations 84 | - [ ] Code refactoring to improve readability 85 | -------------------------------------------------------------------------------- /audit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # MIT License 4 | 5 | # Copyright (c) 2024 Kyriacos Kyriacou (@kkyrio) 6 | 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # audit.sh 26 | # 27 | # Author: Kyri (https://x.com/kkyrio) 28 | # 29 | # This script performs security checks on an Ubuntu/Debian VPS, ensuring it follows 30 | # good security practices. It checks for: 31 | # * Non-root user setup 32 | # * UFW firewall configuration 33 | # * SSH hardening 34 | # * Fail2ban configuration 35 | # * Access control and permissions 36 | # * Port security 37 | # * Automatic updates 38 | # 39 | # Usage: 40 | # Local reporting only: 41 | # ./audit.sh 42 | # 43 | # Report to remote service: 44 | # ./audit.sh 45 | # 46 | # Note: Certain commands require sudo privileges. 47 | # When no session id is provided, results are only printed to terminal. 48 | # When session id is set, results are sent to API_ENDPOINT and also printed to terminal. 49 | # Each check's status (running/pass/fail/error) is reported progressively in both modes. 50 | 51 | set -u 52 | 53 | VERSION="0.4.0" 54 | API_ENDPOINT="https://api.auditvps.com/audit-step" 55 | AUDIT_ID=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 10 | head -n 1) 56 | SESSION="${1:-}" # Get first parameter or empty string if not provided 57 | 58 | RED='\033[1;31m' 59 | GREEN='\033[1;32m' 60 | YELLOW='\033[1;33m' 61 | CYAN='\033[1;36m' 62 | NC='\033[0m' # No Color 63 | 64 | check_os() { 65 | local os="" 66 | if [ -f /etc/lsb-release ]; then 67 | os="ubuntu" 68 | elif [ -f /etc/debian_version ]; then 69 | os="debian" 70 | fi 71 | 72 | if [ -z "$os" ]; then 73 | echo -e "${RED}This script only supports Ubuntu/Debian systems. Exiting.${NC}" 74 | echo "Please ensure you're running this script on a supported operating system." 75 | exit 1 76 | fi 77 | 78 | echo -e "${GREEN}Detected supported OS: ${os}${NC}\n" 79 | } 80 | 81 | check_dependencies() { 82 | echo -e "${CYAN}Checking required dependencies...${NC}" 83 | 84 | local required_commands=("curl" "jq" "systemctl" "apt-get") 85 | local missing_commands=() 86 | 87 | for cmd in "${required_commands[@]}"; do 88 | if ! command -v "$cmd" >/dev/null 2>&1; then 89 | missing_commands+=("$cmd") 90 | fi 91 | done 92 | 93 | if [ ${#missing_commands[@]} -ne 0 ]; then 94 | echo -e "${RED}The following required commands are missing:${NC}" 95 | for cmd in "${missing_commands[@]}"; do 96 | echo " - $cmd" 97 | done 98 | echo 99 | echo -e "${YELLOW}Please install these commands before running this script.${NC}" 100 | exit 1 101 | fi 102 | 103 | echo -e "${GREEN}All required dependencies are installed${NC}\n" 104 | return 0 105 | } 106 | 107 | send_to_api() { 108 | # Only proceed if SESSION is set 109 | if [ -n "${SESSION:-}" ]; then 110 | local category="$1" 111 | local status="$2" 112 | local message="$3" 113 | local check="${4:-}" 114 | 115 | local data 116 | if [ -n "$check" ]; then 117 | data=$(jq -n \ 118 | --arg session "$SESSION" \ 119 | --arg category "$category" \ 120 | --arg status "$status" \ 121 | --arg msg "$message" \ 122 | --arg check "$check" \ 123 | --arg id "$AUDIT_ID" \ 124 | --arg version "$VERSION" \ 125 | '{session_id: $session, id: $id, category: $category, status: $status, message: $msg, check: $check, version: $version}') 126 | else 127 | data=$(jq -n \ 128 | --arg session "$SESSION" \ 129 | --arg category "$category" \ 130 | --arg status "$status" \ 131 | --arg msg "$message" \ 132 | --arg id "$AUDIT_ID" \ 133 | --arg version "$VERSION" \ 134 | '{session_id: $session, id: $id, category: $category, status: $status, message: $msg, version: $version}') 135 | fi 136 | 137 | local response 138 | response=$(curl -s -w "\n%{http_code}" -X POST -H "Content-Type: application/json" -d "$data" "$API_ENDPOINT") 139 | 140 | local http_code 141 | http_code=$(echo "$response" | tail -n1) 142 | 143 | if [ "$http_code" != "200" ]; then 144 | echo "$response" | sed '$d' 145 | echo -e "${RED}Failed to send status to API (HTTP $http_code)${NC}" 146 | return 1 147 | fi 148 | fi 149 | return 0 150 | } 151 | 152 | print_status() { 153 | local category="$1" 154 | local status="$2" 155 | local message="$3" 156 | local check="${4:-}" 157 | 158 | local indent="" 159 | local status_color="" 160 | 161 | if [ -n "$check" ]; then 162 | indent=" ├─ " 163 | category="$check" # Use check as the category for checks 164 | fi 165 | 166 | case "$status" in 167 | "running") 168 | status_color="$CYAN" 169 | ;; 170 | "pass") 171 | status_color="$GREEN" 172 | ;; 173 | "fail") 174 | status_color="$RED" 175 | ;; 176 | "error") 177 | status_color="$RED" 178 | ;; 179 | "skip") 180 | status_color="$YELLOW" 181 | ;; 182 | *) 183 | status_color="$YELLOW" 184 | ;; 185 | esac 186 | 187 | echo -e "${indent}${status_color}[${status^^}]${NC} ${category}: ${message}" 188 | } 189 | 190 | send_status() { 191 | local category="$1" 192 | local status="$2" 193 | local message="$3" 194 | local check="${4:-}" 195 | 196 | print_status "$category" "$status" "$message" "$check" 197 | send_to_api "$category" "$status" "$message" "$check" 198 | } 199 | 200 | check_ufw() { 201 | local category="ufw_security" 202 | local failed=false 203 | 204 | send_status "$category" "running" "Starting UFW security check" 205 | 206 | # Check if UFW is installed 207 | if ! command -v ufw >/dev/null 2>&1; then 208 | send_status "$category" "fail" "UFW is not installed" "installation" 209 | failed=true 210 | else 211 | send_status "$category" "pass" "UFW is installed" "installation" 212 | fi 213 | 214 | # Check if UFW is active 215 | if ! $failed; then 216 | if ! sudo ufw status | grep -qw "active"; then 217 | send_status "$category" "fail" "UFW is not active" "active_status" 218 | failed=true 219 | else 220 | send_status "$category" "pass" "UFW is active" "active_status" 221 | fi 222 | else 223 | send_status "$category" "skip" "UFW not installed - skipping" "active_status" 224 | fi 225 | 226 | # Check default policies 227 | if ! $failed; then 228 | local default_incoming 229 | if ! default_incoming=$(sudo ufw status verbose | grep "Default:" | grep "incoming" | awk '{print $2}'); then 230 | send_status "$category" "error" "Failed to retrieve UFW default policy" "default_policy" 231 | failed=true 232 | elif [ "$default_incoming" != "deny" ]; then 233 | send_status "$category" "fail" "Default incoming policy is not set to deny" "default_policy" 234 | failed=true 235 | else 236 | send_status "$category" "pass" "Default incoming policy is properly set to deny" "default_policy" 237 | fi 238 | else 239 | send_status "$category" "skip" "UFW not active - skipping" "default_policy" 240 | fi 241 | 242 | # Final status 243 | if $failed; then 244 | send_status "$category" "fail" "Some UFW security checks failed" 245 | return 1 246 | else 247 | send_status "$category" "pass" "All UFW security checks passed" 248 | return 0 249 | fi 250 | } 251 | 252 | check_ssh() { 253 | local category="ssh_security" 254 | local final_status="pass" 255 | local ssh_enabled=false 256 | 257 | send_status "$category" "running" "Starting SSH security check" 258 | 259 | # Check if SSH is enabled 260 | if systemctl is-active --quiet sshd || systemctl is-active --quiet ssh.service; then 261 | send_status "$category" "pass" "SSH service is enabled" "service_status" 262 | ssh_enabled=true 263 | else 264 | send_status "$category" "pass" "SSH service is disabled" "service_status" 265 | send_status "$category" "skip" "SSH service disabled - skipping" "key_auth" 266 | send_status "$category" "skip" "SSH service disabled - skipping" "config_PermitRootLogin" 267 | send_status "$category" "skip" "SSH service disabled - skipping" "config_KbdInteractiveAuthentication" 268 | send_status "$category" "skip" "SSH service disabled - skipping" "config_PasswordAuthentication" 269 | send_status "$category" "skip" "SSH service disabled - skipping" "config_UsePAM" 270 | send_status "$category" "skip" "SSH service disabled - skipping" "port" 271 | send_status "$category" "pass" "All SSH security checks passed" 272 | return 0 273 | fi 274 | 275 | # Only continue if SSH is enabled 276 | if $ssh_enabled; then 277 | # Check if key-based auth is setup (look for authorized_keys) 278 | if ! find "$HOME/.ssh" -type f -name "authorized_keys" 2>/dev/null | grep -q .; then 279 | send_status "$category" "fail" "No authorized_keys found in home directory" "key_auth" 280 | final_status="fail" 281 | else 282 | send_status "$category" "pass" "Key-based authentication is set up" "key_auth" 283 | fi 284 | 285 | # Check SSH config settings 286 | local config_checks=( 287 | "PermitRootLogin no" 288 | "KbdInteractiveAuthentication no" 289 | "PasswordAuthentication no" 290 | "UsePAM no" 291 | ) 292 | 293 | for check in "${config_checks[@]}"; do 294 | local key="${check% *}" # Get the key part (before the space) 295 | local expected="${check#* }" # Get the value part (after the space) 296 | local actual 297 | 298 | # Get actual value from sshd -T output 299 | actual=$(sudo sshd -T | grep -i "^${key}" | awk '{print $2}') 300 | 301 | if [ -z "$actual" ]; then 302 | send_status "$category" "fail" "${key} is not configured" "config_${key}" 303 | final_status="fail" 304 | elif [ "$actual" != "$expected" ]; then 305 | send_status "$category" "fail" "${key} is set to '$actual' (should be '$expected')" "config_${key}" 306 | final_status="fail" 307 | else 308 | send_status "$category" "pass" "${key} is correctly set to '$expected'" "config_${key}" 309 | fi 310 | done 311 | fi 312 | 313 | # Final status 314 | if [ "$final_status" = "fail" ]; then 315 | send_status "$category" "fail" "Some SSH security checks failed" 316 | return 1 317 | else 318 | send_status "$category" "pass" "All SSH security checks passed" 319 | return 0 320 | fi 321 | } 322 | 323 | check_non_root_user() { 324 | local category="non_root_user" 325 | local final_status="pass" 326 | local sudo_users 327 | local admin_users 328 | local privileged_users 329 | 330 | send_status "$category" "running" "Checking for properly configured non-root user" 331 | 332 | # Look for users with sudo privileges (in sudo or admin group) 333 | sudo_users=$(grep -Po '^sudo:.*:\K.*$' /etc/group | tr ',' '\n' | grep -v root) 334 | admin_users=$(grep -Po '^admin:.*:\K.*$' /etc/group | tr ',' '\n' | grep -v root) 335 | 336 | if [ -z "$sudo_users" ] && [ -z "$admin_users" ]; then 337 | send_status "$category" "fail" "No non-root users found with sudo privileges" "sudo_access" 338 | final_status="fail" 339 | else 340 | # Combine and deduplicate users 341 | privileged_users=$(echo -e "${sudo_users}\n${admin_users}" | sort -u | grep -v '^$') 342 | 343 | # Check if any of these users have a valid shell 344 | local valid_user_found=false 345 | local user_shell 346 | 347 | while IFS= read -r user; do 348 | user_shell=$(getent passwd "$user" | cut -d: -f7) 349 | if [[ "$user_shell" != "/usr/sbin/nologin" && "$user_shell" != "/bin/false" ]]; then 350 | valid_user_found=true 351 | send_status "$category" "pass" "Found valid non-root sudo user" "sudo_access" 352 | break 353 | fi 354 | done <<< "$privileged_users" 355 | 356 | if ! $valid_user_found; then 357 | send_status "$category" "fail" "No non-root sudo users found" "sudo_access" 358 | final_status="fail" 359 | fi 360 | fi 361 | 362 | # Final status 363 | if [ "$final_status" = "fail" ]; then 364 | send_status "$category" "fail" "Non-root sudo user check failed" 365 | return 1 366 | else 367 | send_status "$category" "pass" "Valid non-root sudo user exists" 368 | return 0 369 | fi 370 | } 371 | 372 | check_access_control() { 373 | local category="access_control" 374 | local failed=false 375 | 376 | send_status "$category" "running" "Starting access control checks" 377 | 378 | declare -A critical_files=( 379 | ["/etc/passwd"]="644" 380 | ["/etc/shadow"]="600" 381 | ) 382 | 383 | declare -A check_names=( 384 | ["/etc/passwd"]="passwd_file" 385 | ["/etc/shadow"]="shadow_file" 386 | ) 387 | 388 | for file in "${!critical_files[@]}"; do 389 | if [ ! -e "$file" ]; then 390 | send_status "$category" "fail" "$file does not exist" "${check_names[$file]}" 391 | failed=true 392 | continue 393 | fi 394 | 395 | local actual_perms 396 | actual_perms=$(stat -c "%a" "$file") 397 | 398 | if [ "$actual_perms" != "${critical_files[$file]}" ]; then 399 | send_status "$category" "fail" "$file permissions are incorrectly set to $actual_perms (expected ${critical_files[$file]})" "${check_names[$file]}" 400 | failed=true 401 | else 402 | send_status "$category" "pass" "$file permissions are correctly set to ${critical_files[$file]}" "${check_names[$file]}" 403 | fi 404 | done 405 | 406 | # Final status 407 | if $failed; then 408 | send_status "$category" "fail" "Some access control checks failed" 409 | return 1 410 | else 411 | send_status "$category" "pass" "All access control checks passed" 412 | return 0 413 | fi 414 | } 415 | 416 | check_unattended_upgrades() { 417 | local category="unattended_upgrades" 418 | local final_status="pass" 419 | local auto_upgrades_file="/etc/apt/apt.conf.d/20auto-upgrades" 420 | local update_enabled 421 | local upgrade_enabled 422 | 423 | send_status "$category" "running" "Checking automatic upgrades configuration" 424 | 425 | # Check if package is installed 426 | if ! dpkg -l | grep -q "unattended-upgrades"; then 427 | send_status "$category" "fail" "unattended-upgrades package is not installed" "installation" 428 | final_status="fail" 429 | return 1 430 | fi 431 | send_status "$category" "pass" "unattended-upgrades package is installed" "installation" 432 | 433 | # Check if service is running 434 | if ! systemctl is-active --quiet unattended-upgrades.service; then 435 | send_status "$category" "fail" "unattended-upgrades service is not running" "service_status" 436 | final_status="fail" 437 | else 438 | send_status "$category" "pass" "unattended-upgrades service is running" "service_status" 439 | fi 440 | 441 | # Check if automatic updates are enabled in /etc/apt/apt.conf.d/20auto-upgrades 442 | if [ ! -f "$auto_upgrades_file" ]; then 443 | send_status "$category" "fail" "Auto-upgrades config file not found" "config_file" 444 | send_status "$category" "skip" "Auto-upgrades file not present - skipping" "auto_update" 445 | send_status "$category" "skip" "Auto-upgrades file not present - skipping" "auto_upgrade" 446 | final_status="fail" 447 | else 448 | send_status "$category" "pass" "Auto-upgrades configuration file exists" "config_file" 449 | 450 | update_enabled=$(grep "APT::Periodic::Update-Package-Lists" "$auto_upgrades_file" | grep -o '[0-9]\+' || echo "0") 451 | upgrade_enabled=$(grep "APT::Periodic::Unattended-Upgrade" "$auto_upgrades_file" | grep -o '[0-9]\+' || echo "0") 452 | 453 | if [ "$update_enabled" = "0" ]; then 454 | send_status "$category" "fail" "Automatic package list updates are disabled" "auto_update" 455 | final_status="fail" 456 | else 457 | send_status "$category" "pass" "Automatic updates are enabled" "auto_update" 458 | fi 459 | 460 | if [ "$upgrade_enabled" = "0" ]; then 461 | send_status "$category" "fail" "Automatic upgrades are disabled" "auto_upgrade" 462 | final_status="fail" 463 | else 464 | send_status "$category" "pass" "Automatic upgrades are enabled" "auto_upgrade" 465 | fi 466 | fi 467 | 468 | # Final status 469 | if [ "$final_status" = "fail" ]; then 470 | send_status "$category" "fail" "Some automatic upgrades check failed" 471 | return 1 472 | else 473 | send_status "$category" "pass" "All automatic upgrade checks passed" 474 | return 0 475 | fi 476 | } 477 | 478 | check_port_security() { 479 | local category="port_security" 480 | local description="Checking open ports" 481 | send_status "$category" "running" "$description" 482 | 483 | local cmd 484 | local column 485 | if command -v ss >/dev/null 2>&1; then 486 | cmd="ss -tuln" 487 | column=5 488 | elif command -v netstat >/dev/null 2>&1; then 489 | cmd="netstat -tuln" 490 | column=4 491 | else 492 | send_status "$category" "error" "Neither 'ss' nor 'netstat' command found" 493 | return 1 494 | fi 495 | 496 | # Get list of listening ports 497 | local ports 498 | ports=$($cmd | grep 'LISTEN' | awk "{print \$$column}" | awk -F: '{print $NF}' | sort -u) 499 | 500 | declare -A insecure_ports=( 501 | [21]="FTP - Unencrypted file transfer" 502 | [23]="Telnet - Unencrypted remote access" 503 | [25]="SMTP - Unencrypted email transfer" 504 | [69]="TFTP - Trivial FTP, unencrypted" 505 | [111]="RPC - Remote procedure call" 506 | [135]="RPC - Windows RPC" 507 | [445]="SMB - File sharing" 508 | [3389]="RDP - Remote Desktop" 509 | ) 510 | local -a ordered_ports=(21 23 25 69 111 135 445 3389) 511 | 512 | local failed=0 513 | 514 | for port in "${ordered_ports[@]}"; do 515 | local subcategory="port_${port}" 516 | local port_description="Checking port ${port} (${insecure_ports[$port]})" 517 | 518 | if echo "$ports" | grep -q "^${port}$"; then 519 | failed=1 520 | send_status "$category" "fail" "Port ${port} (${insecure_ports[$port]}) is open" "$subcategory" 521 | else 522 | send_status "$category" "pass" "Port ${port} is not open" "$subcategory" 523 | fi 524 | done 525 | 526 | if [ $failed -eq 1 ]; then 527 | send_status "$category" "fail" "Some port security checks failed" 528 | return 1 529 | else 530 | send_status "$category" "pass" "All port security checks passed" 531 | return 0 532 | fi 533 | } 534 | 535 | check_fail2ban() { 536 | local category="fail2ban" 537 | local failed=false 538 | local installation_failed=false 539 | local config_file_missing=false 540 | local ssh_enabled 541 | local ssh_mode 542 | 543 | send_status "$category" "running" "Checking fail2ban installation and configuration" 544 | 545 | # Check if package is installed - all other checks depend on this 546 | if ! dpkg -l | grep -q "fail2ban"; then 547 | send_status "$category" "fail" "fail2ban package is not installed" "installation" 548 | installation_failed=true 549 | failed=true 550 | else 551 | send_status "$category" "pass" "fail2ban package is installed" "installation" 552 | fi 553 | 554 | if ! $installation_failed; then 555 | # Check if service is enabled 556 | if ! systemctl is-enabled --quiet fail2ban.service; then 557 | send_status "$category" "fail" "fail2ban service is not enabled" "service_enabled" 558 | failed=true 559 | else 560 | send_status "$category" "pass" "fail2ban service is enabled" "service_enabled" 561 | fi 562 | 563 | # Check if service is running 564 | if ! systemctl is-active --quiet fail2ban.service; then 565 | send_status "$category" "fail" "fail2ban service is not running" "service_active" 566 | failed=true 567 | else 568 | send_status "$category" "pass" "fail2ban service is running" "service_active" 569 | fi 570 | 571 | # Check if jail.local exists - jail config depends on this 572 | if [ ! -f "/etc/fail2ban/jail.local" ]; then 573 | send_status "$category" "fail" "jail.local configuration file not found" "config_file" 574 | config_file_missing=true 575 | failed=true 576 | else 577 | send_status "$category" "pass" "jail.local configuration file exists" "config_file" 578 | fi 579 | 580 | # Check SSH jail configuration only if jail.local exists 581 | if ! $config_file_missing; then 582 | # Check if SSH jail is enabled 583 | ssh_enabled=$(grep -A10 "^\[sshd\]" /etc/fail2ban/jail.local | grep -m 1 "enabled" | awk '{print $NF}' | tr -d '[:space:]') 584 | if [ "$ssh_enabled" != "true" ]; then 585 | send_status "$category" "fail" "SSH jail is not enabled" "ssh_jail_enabled" 586 | failed=true 587 | else 588 | send_status "$category" "pass" "SSH jail is enabled" "ssh_jail_enabled" 589 | fi 590 | 591 | # Check if mode is aggressive 592 | ssh_mode=$(grep -A10 "^\[sshd\]" /etc/fail2ban/jail.local | grep -m 1 "^mode[[:space:]]*=[[:space:]]*aggressive" >/dev/null && echo "aggressive" || echo "") 593 | if [ "$ssh_mode" != "aggressive" ]; then 594 | send_status "$category" "fail" "SSH jail is not in aggressive mode" "ssh_jail_mode" 595 | failed=true 596 | else 597 | send_status "$category" "pass" "SSH jail is in aggressive mode" "ssh_jail_mode" 598 | fi 599 | else 600 | send_status "$category" "skip" "jail.local file not present - skipping" "ssh_jail_enabled" 601 | send_status "$category" "skip" "jail.local file not present - skipping" "ssh_jail_mode" 602 | fi 603 | else 604 | # Skip all remaining checks if installation failed 605 | send_status "$category" "skip" "fail2ban not installed - skipping" "service_enabled" 606 | send_status "$category" "skip" "fail2ban not installed - skipping" "service_active" 607 | send_status "$category" "skip" "fail2ban not installed - skipping" "config_file" 608 | send_status "$category" "skip" "fail2ban not installed - skipping" "ssh_jail_enabled" 609 | send_status "$category" "skip" "fail2ban not installed - skipping" "ssh_jail_mode" 610 | fi 611 | 612 | # Final status 613 | if $failed; then 614 | send_status "$category" "fail" "Some fail2ban security checks failed" 615 | return 1 616 | else 617 | send_status "$category" "pass" "All fail2ban checks passed" 618 | return 0 619 | fi 620 | } 621 | 622 | main() { 623 | check_os 624 | check_dependencies 625 | 626 | if [ -n "${SESSION:-}" ]; then 627 | echo -e "Session ID: ${SESSION:-}" 628 | else 629 | echo -e "${YELLOW}Running in local mode (no SESSION provided)${NC}" 630 | fi 631 | echo 632 | 633 | send_status "audit" "running" "Starting security audit v${VERSION}" 634 | 635 | local failed=0 636 | 637 | check_non_root_user || failed=1 638 | check_ufw || failed=1 639 | check_ssh || failed=1 640 | check_fail2ban || failed=1 641 | check_access_control || failed=1 642 | check_port_security || failed=1 643 | check_unattended_upgrades || failed=1 644 | 645 | 646 | send_status "audit" "pass" "Security audit complete" 647 | 648 | if [ $failed -eq 1 ]; then 649 | echo -e "\n${RED}Audit completed with failures${NC}" 650 | exit 1 651 | else 652 | echo -e "\n${GREEN}All checks passed!${NC}" 653 | exit 0 654 | fi 655 | } 656 | 657 | main "$@" --------------------------------------------------------------------------------