|
3 | 3 | # ShipNode Doctor - Pre-flight diagnostic checks |
4 | 4 |
|
5 | 5 | cmd_doctor() { |
| 6 | + # Check for --security flag |
| 7 | + local security_mode=false |
| 8 | + if [ "$1" = "--security" ]; then |
| 9 | + security_mode=true |
| 10 | + fi |
| 11 | + |
| 12 | + if [ "$security_mode" = true ]; then |
| 13 | + cmd_doctor_security |
| 14 | + return |
| 15 | + fi |
| 16 | + |
6 | 17 | info "Running ShipNode diagnostics..." |
7 | 18 | echo "" |
8 | 19 |
|
@@ -52,6 +63,49 @@ cmd_doctor() { |
52 | 63 | fi |
53 | 64 | } |
54 | 65 |
|
| 66 | +# Security audit - non-destructive checks only |
| 67 | +cmd_doctor_security() { |
| 68 | + info "Running security audit..." |
| 69 | + echo "" |
| 70 | + |
| 71 | + local has_warnings=false |
| 72 | + local has_info=false |
| 73 | + |
| 74 | + # Local security checks |
| 75 | + info "Local security checks:" |
| 76 | + check_local_file_permissions || has_warnings=true |
| 77 | + echo "" |
| 78 | + |
| 79 | + # SSH connectivity required for remote checks |
| 80 | + if ! check_ssh_connection_quiet; then |
| 81 | + warn "Cannot perform remote security checks - SSH connection failed" |
| 82 | + echo "" |
| 83 | + else |
| 84 | + echo "" |
| 85 | + |
| 86 | + # Remote security checks |
| 87 | + info "Remote security checks:" |
| 88 | + check_ssh_security || has_warnings=true |
| 89 | + check_firewall_status || has_info=true |
| 90 | + check_fail2ban_status || has_info=true |
| 91 | + echo "" |
| 92 | + fi |
| 93 | + |
| 94 | + # Summary |
| 95 | + echo "" |
| 96 | + if [ "$has_warnings" = true ]; then |
| 97 | + warn "Security audit completed with warnings. Review the issues above." |
| 98 | + echo "" |
| 99 | + info "Run 'shipnode harden' for interactive security hardening." |
| 100 | + elif [ "$has_info" = true ]; then |
| 101 | + info "Security audit completed with informational notices." |
| 102 | + echo "" |
| 103 | + info "No critical issues found. Review the notices above." |
| 104 | + else |
| 105 | + success "Security audit passed! No issues found." |
| 106 | + fi |
| 107 | +} |
| 108 | + |
55 | 109 | # Check if shipnode.conf exists and required vars are set |
56 | 110 | check_local_config() { |
57 | 111 | if [ ! -f "shipnode.conf" ]; then |
@@ -295,3 +349,255 @@ REMOTE_CHECKS |
295 | 349 | [ "$has_errors" = true ] && return 1 |
296 | 350 | return 0 |
297 | 351 | } |
| 352 | + |
| 353 | +# Quiet SSH connection check (no output) |
| 354 | +check_ssh_connection_quiet() { |
| 355 | + if [ ! -f "shipnode.conf" ]; then |
| 356 | + return 1 |
| 357 | + fi |
| 358 | + |
| 359 | + set +e |
| 360 | + source shipnode.conf 2>/dev/null |
| 361 | + set -e |
| 362 | + |
| 363 | + if [ -z "$SSH_USER" ] || [ -z "$SSH_HOST" ]; then |
| 364 | + return 1 |
| 365 | + fi |
| 366 | + |
| 367 | + remote_exec "exit" &>/dev/null |
| 368 | +} |
| 369 | + |
| 370 | +# Check local file permissions for sensitive files |
| 371 | +check_local_file_permissions() { |
| 372 | + local has_issues=false |
| 373 | + |
| 374 | + # Check shipnode.conf permissions |
| 375 | + if [ -f "shipnode.conf" ]; then |
| 376 | + local conf_perms |
| 377 | + conf_perms=$(stat -c "%a" shipnode.conf 2>/dev/null || stat -f "%Lp" shipnode.conf 2>/dev/null) |
| 378 | + if [ -n "$conf_perms" ]; then |
| 379 | + # Check if permissions are too permissive (readable by group/others) |
| 380 | + local other_read=$((conf_perms % 10 / 1)) |
| 381 | + local group_read=$(((conf_perms / 10) % 10 / 1)) |
| 382 | + |
| 383 | + if [ "$other_read" -ge 4 ] || [ "$group_read" -ge 4 ]; then |
| 384 | + echo " ⚠ shipnode.conf has overly permissive permissions ($conf_perms)" |
| 385 | + echo " Recommendation: Run 'chmod 600 shipnode.conf'" |
| 386 | + has_issues=true |
| 387 | + else |
| 388 | + echo " ✓ shipnode.conf permissions are secure ($conf_perms)" |
| 389 | + fi |
| 390 | + fi |
| 391 | + fi |
| 392 | + |
| 393 | + # Check .env file permissions |
| 394 | + if [ -f ".env" ]; then |
| 395 | + local env_perms |
| 396 | + env_perms=$(stat -c "%a" .env 2>/dev/null || stat -f "%Lp" .env 2>/dev/null) |
| 397 | + if [ -n "$env_perms" ]; then |
| 398 | + local other_read=$((env_perms % 10 / 1)) |
| 399 | + local group_read=$(((env_perms / 10) % 10 / 1)) |
| 400 | + |
| 401 | + if [ "$other_read" -ge 4 ] || [ "$group_read" -ge 4 ]; then |
| 402 | + echo " ⚠ .env file has overly permissive permissions ($env_perms)" |
| 403 | + echo " Recommendation: Run 'chmod 600 .env'" |
| 404 | + has_issues=true |
| 405 | + else |
| 406 | + echo " ✓ .env file permissions are secure ($env_perms)" |
| 407 | + fi |
| 408 | + fi |
| 409 | + fi |
| 410 | + |
| 411 | + # Check users.yml file permissions |
| 412 | + if [ -f "users.yml" ]; then |
| 413 | + local users_perms |
| 414 | + users_perms=$(stat -c "%a" users.yml 2>/dev/null || stat -f "%Lp" users.yml 2>/dev/null) |
| 415 | + if [ -n "$users_perms" ]; then |
| 416 | + local other_read=$((users_perms % 10 / 1)) |
| 417 | + local group_read=$(((users_perms / 10) % 10 / 1)) |
| 418 | + |
| 419 | + if [ "$other_read" -ge 4 ] || [ "$group_read" -ge 4 ]; then |
| 420 | + echo " ⚠ users.yml has overly permissive permissions ($users_perms)" |
| 421 | + echo " Recommendation: Run 'chmod 600 users.yml'" |
| 422 | + has_issues=true |
| 423 | + else |
| 424 | + echo " ✓ users.yml permissions are secure ($users_perms)" |
| 425 | + fi |
| 426 | + fi |
| 427 | + fi |
| 428 | + |
| 429 | + [ "$has_issues" = true ] && return 1 |
| 430 | + return 0 |
| 431 | +} |
| 432 | + |
| 433 | +# Check SSH configuration security |
| 434 | +check_ssh_security() { |
| 435 | + local has_issues=false |
| 436 | + |
| 437 | + # Fetch SSH configuration from remote server |
| 438 | + local ssh_config |
| 439 | + ssh_config=$(remote_exec "cat /etc/ssh/sshd_config 2>/dev/null || echo 'SSHD_CONFIG_MISSING'") |
| 440 | + |
| 441 | + if [ "$ssh_config" = "SSHD_CONFIG_MISSING" ]; then |
| 442 | + echo " ⚠ Could not read /etc/ssh/sshd_config" |
| 443 | + return 1 |
| 444 | + fi |
| 445 | + |
| 446 | + # Check PermitRootLogin |
| 447 | + local root_login |
| 448 | + root_login=$(echo "$ssh_config" | grep -i "^PermitRootLogin" | tail -1 | awk '{print $2}') |
| 449 | + if [ -z "$root_login" ]; then |
| 450 | + root_login="prohibit-password" # Default in modern OpenSSH |
| 451 | + fi |
| 452 | + |
| 453 | + case "$root_login" in |
| 454 | + yes|prohibit-password) |
| 455 | + echo " ⚠ PermitRootLogin is enabled ($root_login)" |
| 456 | + echo " Recommendation: Set 'PermitRootLogin no' in /etc/ssh/sshd_config" |
| 457 | + has_issues=true |
| 458 | + ;; |
| 459 | + no) |
| 460 | + echo " ✓ PermitRootLogin is disabled" |
| 461 | + ;; |
| 462 | + *) |
| 463 | + echo " ℹ PermitRootLogin setting: $root_login" |
| 464 | + ;; |
| 465 | + esac |
| 466 | + |
| 467 | + # Check PasswordAuthentication |
| 468 | + local pass_auth |
| 469 | + pass_auth=$(echo "$ssh_config" | grep -i "^PasswordAuthentication" | tail -1 | awk '{print $2}') |
| 470 | + if [ -z "$pass_auth" ]; then |
| 471 | + pass_auth="yes" # Default |
| 472 | + fi |
| 473 | + |
| 474 | + case "$pass_auth" in |
| 475 | + yes) |
| 476 | + echo " ⚠ PasswordAuthentication is enabled" |
| 477 | + echo " Recommendation: Set 'PasswordAuthentication no' in /etc/ssh/sshd_config" |
| 478 | + has_issues=true |
| 479 | + ;; |
| 480 | + no) |
| 481 | + echo " ✓ PasswordAuthentication is disabled" |
| 482 | + ;; |
| 483 | + esac |
| 484 | + |
| 485 | + # Check SSH Port |
| 486 | + local ssh_port |
| 487 | + ssh_port=$(echo "$ssh_config" | grep -i "^Port" | tail -1 | awk '{print $2}') |
| 488 | + if [ -z "$ssh_port" ]; then |
| 489 | + ssh_port="22" # Default |
| 490 | + fi |
| 491 | + |
| 492 | + if [ "$ssh_port" = "22" ]; then |
| 493 | + echo " ℹ SSH is running on default port 22" |
| 494 | + echo " Recommendation: Consider changing to a non-standard port for security by obscurity" |
| 495 | + else |
| 496 | + echo " ✓ SSH is running on non-default port $ssh_port" |
| 497 | + fi |
| 498 | + |
| 499 | + [ "$has_issues" = true ] && return 1 |
| 500 | + return 0 |
| 501 | +} |
| 502 | + |
| 503 | +# Check firewall status |
| 504 | +check_firewall_status() { |
| 505 | + local has_info=false |
| 506 | + |
| 507 | + # Check if ufw is available and active |
| 508 | + local ufw_status |
| 509 | + ufw_status=$(remote_exec "sudo ufw status 2>/dev/null || echo 'UFW_NOT_FOUND'") |
| 510 | + |
| 511 | + if [ "$ufw_status" != "UFW_NOT_FOUND" ]; then |
| 512 | + if echo "$ufw_status" | grep -q "Status: active"; then |
| 513 | + echo " ✓ UFW firewall is active" |
| 514 | + |
| 515 | + # Check if SSH port is allowed |
| 516 | + if echo "$ufw_status" | grep -q "22/tcp\|SSH"; then |
| 517 | + echo " ✓ SSH port is allowed in UFW" |
| 518 | + else |
| 519 | + echo " ⚠ SSH port may not be explicitly allowed in UFW" |
| 520 | + has_info=true |
| 521 | + fi |
| 522 | + |
| 523 | + # Check if HTTP/HTTPS are allowed |
| 524 | + if echo "$ufw_status" | grep -q "80/tcp\|80,443/tcp\|Nginx Full\|Apache Full"; then |
| 525 | + echo " ✓ HTTP/HTTPS ports are allowed in UFW" |
| 526 | + else |
| 527 | + echo " ℹ HTTP/HTTPS ports not explicitly allowed (may use different firewall)" |
| 528 | + has_info=true |
| 529 | + fi |
| 530 | + else |
| 531 | + echo " ⚠ UFW is installed but not active" |
| 532 | + echo " Recommendation: Run 'sudo ufw enable' to activate the firewall" |
| 533 | + has_info=true |
| 534 | + fi |
| 535 | + return 0 |
| 536 | + fi |
| 537 | + |
| 538 | + # Check if firewalld is running |
| 539 | + local firewalld_status |
| 540 | + firewalld_status=$(remote_exec "systemctl is-active firewalld 2>/dev/null || echo 'inactive'") |
| 541 | + |
| 542 | + if [ "$firewalld_status" = "active" ]; then |
| 543 | + echo " ✓ firewalld is active" |
| 544 | + return 0 |
| 545 | + fi |
| 546 | + |
| 547 | + # Check iptables directly |
| 548 | + local iptables_rules |
| 549 | + iptables_rules=$(remote_exec "sudo iptables -L -n 2>/dev/null | head -5 || echo 'IPTABLES_NOT_AVAILABLE'") |
| 550 | + |
| 551 | + if [ "$iptables_rules" != "IPTABLES_NOT_FOUND" ] && echo "$iptables_rules" | grep -q "Chain INPUT"; then |
| 552 | + if echo "$iptables_rules" | grep -q "DROP"; then |
| 553 | + echo " ✓ iptables has active rules with DROP policies" |
| 554 | + else |
| 555 | + echo " ℹ iptables has rules but no explicit DROP policy detected" |
| 556 | + has_info=true |
| 557 | + fi |
| 558 | + else |
| 559 | + echo " ⚠ No active firewall detected (UFW, firewalld, or iptables)" |
| 560 | + echo " Recommendation: Enable UFW with 'shipnode harden' or manually configure iptables" |
| 561 | + has_info=true |
| 562 | + fi |
| 563 | + |
| 564 | + [ "$has_info" = true ] && return 1 |
| 565 | + return 0 |
| 566 | +} |
| 567 | + |
| 568 | +# Check fail2ban status |
| 569 | +check_fail2ban_status() { |
| 570 | + local has_info=false |
| 571 | + |
| 572 | + # Check if fail2ban is installed |
| 573 | + local fail2ban_installed |
| 574 | + fail2ban_installed=$(remote_exec "command -v fail2ban-server 2>/dev/null || echo 'NOT_INSTALLED'") |
| 575 | + |
| 576 | + if [ "$fail2ban_installed" = "NOT_INSTALLED" ]; then |
| 577 | + echo " ℹ fail2ban is not installed" |
| 578 | + echo " Recommendation: Install fail2ban for brute-force protection (use 'shipnode harden')" |
| 579 | + has_info=true |
| 580 | + else |
| 581 | + # Check if fail2ban service is running |
| 582 | + local fail2ban_status |
| 583 | + fail2ban_status=$(remote_exec "sudo systemctl is-active fail2ban 2>/dev/null || sudo service fail2ban status 2>/dev/null | grep -i running || echo 'NOT_RUNNING'") |
| 584 | + |
| 585 | + if [ "$fail2ban_status" = "active" ] || echo "$fail2ban_status" | grep -q "running"; then |
| 586 | + echo " ✓ fail2ban is installed and running" |
| 587 | + |
| 588 | + # Get list of active jails |
| 589 | + local jails |
| 590 | + jails=$(remote_exec "sudo fail2ban-client status 2>/dev/null | grep 'Jail list' || echo ''") |
| 591 | + if [ -n "$jails" ]; then |
| 592 | + echo " $jails" |
| 593 | + fi |
| 594 | + else |
| 595 | + echo " ℹ fail2ban is installed but not running" |
| 596 | + echo " Recommendation: Start fail2ban with 'sudo systemctl start fail2ban'" |
| 597 | + has_info=true |
| 598 | + fi |
| 599 | + fi |
| 600 | + |
| 601 | + [ "$has_info" = true ] && return 1 |
| 602 | + return 0 |
| 603 | +} |
0 commit comments