Changelog

DateChange
2026-10-12Correct Docker privilege, effective log-retention and network-audit claims.
2026-05-04Initial Version: Full audit script with comprehensive explanations.

Introduction to the Server Security Audit Script

When you provision a new server, especially in a cloud environment using tools like cloud-init, itโ€™s crucial to verify that all intended security measures have been correctly applied. Manual checks can be time-consuming and error-prone. This Bash script automates a significant portion of this auditing process, providing a quick overview of your serverโ€™s security posture on Debian or Ubuntu-based systems.

This script is designed to be run with root privileges and will:

  • Check System Fundamentals: Hostname, timezone, kernel, pending updates, and reboot status.
  • Audit User & Authentication: Verify the existence and sudo privileges of a dedicated admin user, check root password status, and identify accounts with UID 0 or empty passwords.
  • Harden SSH: Scrutinize your SSH daemonโ€™s configuration for best practices like disabled root login, password authentication, X11 forwarding, and the use of strong key algorithms. It also checks for the presence of SSH keys for your admin user.
  • Firewall Configuration (UFW): Confirm UFWโ€™s active status, default policies, specific rule sets (e.g., rate-limiting for SSH), and log redirection.
  • Intrusion Prevention (Fail2Ban): Validate that Fail2Ban is running, its SSH jail is active, and itโ€™s monitoring the correct SSH port.
  • Automated Updates: Ensure unattended-upgrades is installed and configured to keep your system patched.
  • Docker Security: If Docker is installed, it checks daemon status, Docker Compose presence, user group membership, and critically, Dockerโ€™s log rotation settings and network configuration.
  • Log Rotation: Verify persistent journaling and logrotate configurations for Traefik and UFW logs.
  • Network & Open Ports: List actively listening ports and check for known insecure legacy services.
  • Filesystem Security: Scan for world-writable files in critical directories and report disk usage.

This is a configuration checklist, not a vulnerability scan or proof of recovery. A clean summary does not establish current executable versions, complete process visibility, successful backup restores, or end-to-end alarm delivery. Treat missing permissions or unavailable commands as coverage gaps and retain separate evidence for those checks. Do not clear failed units simply to obtain a clean report.

At the end, it provides a summary of PASS, WARN, and FAIL counts, giving you an immediate understanding of areas needing attention.

Why is this important? Even with cloud-init or automated provisioning, misconfigurations can occur. This script acts as your second line of defense, ensuring that your server adheres to a baseline of security best practices before it goes into production.


How to Use the Script

  1. Save the script: Copy the entire script content into a file, for example, server-audit.sh.
  2. Make it executable:
    chmod +x server-audit.sh
  3. Run with sudo: The script requires root privileges to access system configurations and logs.
    sudo ./server-audit.sh
  4. Review the output: Pay close attention to [FAIL] and [WARN] messages, which indicate potential security vulnerabilities or areas for improvement.

The Server Security Audit Script

Here is the complete script. You can customize the ADMIN_USER variable at the beginning to match your dedicated administrative username.

#!/bin/bash
# =============================================================================
# Server Security Audit Script
# =============================================================================
# Checks if the cloud-init setup has been correctly applied and if current
# security standards are met.
#
# Usage: sudo bash server-audit.sh
# =============================================================================

set -u

# --- Colors & Symbols --------------------------------------------------------
GREEN="\033[0;32m"
RED="\033[0;31m"
YELLOW="\033[0;33m"
CYAN="\033[0;36m"
BOLD="\033[1m"
NC="\033[0m"

PASS="${GREEN}PASS${NC}"
FAIL="${RED}FAIL${NC}"
WARN="${YELLOW}WARN${NC}"
INFO="${CYAN}INFO${NC}"

pass_count=0
fail_count=0
warn_count=0

log_pass() { echo -e "  [${PASS}]  $1"; ((pass_count++)); }
log_fail() { echo -e "  [${FAIL}]  $1"; ((fail_count++)); }
log_warn() { echo -e "  [${WARN}]  $1"; ((warn_count++)); }
log_info() { echo -e "  [${INFO}]  $1"; }

section() { echo -e "\n${BOLD}=== $1 ===${NC}"; }

# --- Root Check --------------------------------------------------------------
if [[ $EUID -ne 0 ]]; then
    echo -e "${RED}This script must be run with sudo.${NC}"
    exit 1
fi

ADMIN_USER="exampleadmin" # <--- CUSTOMIZE THIS TO YOUR ADMIN USERNAME

echo -e "${BOLD}"
echo "============================================================"
echo "  Server Security Audit - $(date '+%Y-%m-%d %H:%M:%S')"
echo "============================================================"
echo -e "${NC}"

# =============================================================================
# 1. SYSTEM FUNDAMENTALS
# =============================================================================
section "1. System Fundamentals"

# Hostname
CURRENT_HOSTNAME=$(hostnamectl --static 2>/dev/null || hostname)
if [[ "$CURRENT_HOSTNAME" != "localhost" && -n "$CURRENT_HOSTNAME" ]]; then
    log_pass "Hostname set: ${CURRENT_HOSTNAME}"
else
    log_warn "Hostname is still on default"
fi

# Timezone
CURRENT_TZ=$(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo "unknown")
if [[ "$CURRENT_TZ" == "Europe/Berlin" ]]; then
    log_pass "Timezone: ${CURRENT_TZ}"
else
    log_warn "Timezone is ${CURRENT_TZ} (expected: Europe/Berlin)"
fi

# Kernel & OS
log_info "OS: $(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"')"
log_info "Kernel: $(uname -r)"

# Uptime
log_info "Uptime: $(uptime -p)"

# Pending Updates
UPDATES=$(apt list --upgradable 2>/dev/null | grep -c upgradable || true)
if [[ "$UPDATES" -eq 0 ]]; then
    log_pass "No pending package updates"
else
    log_warn "${UPDATES} package update(s) available"
fi

# Reboot required?
if [[ -f /var/run/reboot-required ]]; then
    log_warn "System reboot required"
else
    log_pass "No reboot required"
fi

# =============================================================================
# 2. USER & AUTHENTICATION
# =============================================================================
section "2. User & Authentication"

# Admin user exists
if id "$ADMIN_USER" &>/dev/null; then
    log_pass "User '${ADMIN_USER}' exists"
else
    log_fail "User '${ADMIN_USER}' does NOT exist"
fi

# sudo group
if groups "$ADMIN_USER" 2>/dev/null | grep -qw sudo; then
    log_pass "'${ADMIN_USER}' is in the sudo group"
else
    log_fail "'${ADMIN_USER}' is NOT in the sudo group"
fi

# NOPASSWD in sudoers (accepted for cloud-init without password)
if grep -r "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null | grep -v "^#" | grep -q "$ADMIN_USER"; then
    log_info "'${ADMIN_USER}' has NOPASSWD (intended, as cloud-init doesn't set a password)"
else
    log_pass "No NOPASSWD for '${ADMIN_USER}' - sudo requires password"
fi

# Root password (should be locked or random, not empty)
ROOT_PW_STATUS=$(passwd -S root 2>/dev/null | awk '{print $2}')
case "$ROOT_PW_STATUS" in
    L)  log_pass "Root account is locked" ;;
    P)  log_info "Root has a password set (randomized by cloud-init)" ;;
    NP) log_fail "Root has NO password!" ;;
    *)  log_info "Root password status: ${ROOT_PW_STATUS}" ;;
esac

# Accounts with empty password
EMPTY_PW=$(awk -F: '($2 == "" ) { print $1 }' /etc/shadow 2>/dev/null || true)
if [[ -z "$EMPTY_PW" ]]; then
    log_pass "No accounts with empty password"
else
    log_fail "Accounts with empty password: ${EMPTY_PW}"
fi

# UID-0 accounts (only root should have UID 0)
ROOT_ACCOUNTS=$(awk -F: '($3 == 0) { print $1 }' /etc/passwd)
if [[ "$ROOT_ACCOUNTS" == "root" ]]; then
    log_pass "Only 'root' has UID 0"
else
    log_fail "Multiple accounts with UID 0: ${ROOT_ACCOUNTS}"
fi

# =============================================================================
# 3. SSH HARDENING
# =============================================================================
section "3. SSH Hardening"

# Read effective SSH configuration
SSHD_CONFIG=$(sshd -T 2>/dev/null || true)

if [[ -z "$SSHD_CONFIG" ]]; then
    log_fail "Cannot read SSH configuration (sshd -T failed)"
else
    # Port
    SSH_PORT=$(echo "$SSHD_CONFIG" | grep "^port " | awk '{print $2}')
    if [[ "$SSH_PORT" != "22" ]]; then
        log_pass "SSH port changed: ${SSH_PORT}"
    else
        log_warn "SSH is running on default port 22"
    fi

    # Root Login
    ROOT_LOGIN=$(echo "$SSHD_CONFIG" | grep "^permitrootlogin " | awk '{print $2}')
    if [[ "$ROOT_LOGIN" == "no" ]]; then
        log_pass "Root login disabled"
    else
        log_fail "Root login is '${ROOT_LOGIN}' (should be 'no')"
    fi

    # Password Auth
    PW_AUTH=$(echo "$SSHD_CONFIG" | grep "^passwordauthentication " | awk '{print $2}')
    if [[ "$PW_AUTH" == "no" ]]; then
        log_pass "Password authentication disabled"
    else
        log_fail "Password authentication is enabled!"
    fi

    # Pubkey Auth
    PUBKEY_AUTH=$(echo "$SSHD_CONFIG" | grep "^pubkeyauthentication " | awk '{print $2}')
    if [[ "$PUBKEY_AUTH" == "yes" ]]; then
        log_pass "Public-key authentication enabled"
    else
        log_fail "Public-key authentication is disabled!"
    fi

    # MaxAuthTries
    MAX_AUTH=$(echo "$SSHD_CONFIG" | grep "^maxauthtries " | awk '{print $2}')
    if [[ "$MAX_AUTH" -le 3 ]]; then
        log_pass "MaxAuthTries: ${MAX_AUTH}"
    else
        log_warn "MaxAuthTries is ${MAX_AUTH} (recommended: <=3)"
    fi

    # LoginGraceTime
    GRACE_TIME=$(echo "$SSHD_CONFIG" | grep "^logingracetime " | awk '{print $2}')
    if [[ "$GRACE_TIME" -le 60 ]]; then
        log_pass "LoginGraceTime: ${GRACE_TIME}s"
    else
        log_warn "LoginGraceTime is ${GRACE_TIME}s (recommended: <=60)"
    fi

    # X11Forwarding
    X11=$(echo "$SSHD_CONFIG" | grep "^x11forwarding " | awk '{print $2}')
    if [[ "$X11" == "no" ]]; then
        log_pass "X11Forwarding disabled"
    else
        log_warn "X11Forwarding is enabled"
    fi

    # TCP Forwarding
    TCP_FWD=$(echo "$SSHD_CONFIG" | grep "^allowtcpforwarding " | awk '{print $2}')
    if [[ "$TCP_FWD" == "no" ]]; then
        log_pass "TCP-Forwarding disabled"
    else
        log_warn "TCP-Forwarding is enabled"
    fi

    # Agent Forwarding
    AGENT_FWD=$(echo "$SSHD_CONFIG" | grep "^allowagentforwarding " | awk '{print $2}')
    if [[ "$AGENT_FWD" == "no" ]]; then
        log_pass "Agent-Forwarding disabled"
    else
        log_warn "Agent-Forwarding is enabled"
    fi

    # AllowUsers set
    ALLOW_USERS=$(echo "$SSHD_CONFIG" | grep "^allowusers " | awk '{$1=""; print $0}' | xargs)
    if [[ -n "$ALLOW_USERS" ]]; then
        log_pass "AllowUsers restricted to: ${ALLOW_USERS}"
    else
        log_warn "AllowUsers is not set (all users can use SSH)"
    fi

    # Check if insecure Key Algorithms are accepted
    ACCEPTED_ALGOS=$(echo "$SSHD_CONFIG" | grep "^pubkeyacceptedalgorithms " | awk '{$1=""; print $0}' | xargs)
    if [[ -n "$ACCEPTED_ALGOS" ]]; then
        if echo "$ACCEPTED_ALGOS" | grep -qw "ssh-rsa"; then
            log_fail "ssh-rsa (SHA-1) is still allowed - use rsa-sha2-* only!"
        else
            log_pass "Only modern Key Algorithms allowed"
        fi
        log_info "Allowed Algorithms: ${ACCEPTED_ALGOS}"
    else
        log_info "PubkeyAcceptedAlgorithms: System Default (SHA-1 blocked since OpenSSH 8.8)"
    fi
fi

# SSH Key present for Admin User
ADMIN_HOME=$(eval echo ~"$ADMIN_USER")
if [[ -f "${ADMIN_HOME}/.ssh/authorized_keys" ]]; then
    KEY_COUNT=$(grep -c "^ssh-" "${ADMIN_HOME}/.ssh/authorized_keys" 2>/dev/null || true)
    if [[ "$KEY_COUNT" -gt 0 ]]; then
        log_pass "${KEY_COUNT} SSH key(s) stored for '${ADMIN_USER}'"
        while IFS= read -r line; do
            KEY_TYPE=$(echo "$line" | awk '{print $1}')
            KEY_COMMENT=$(echo "$line" | awk '{print $3}')
            KEY_BITS=$(ssh-keygen -l -f /dev/stdin <<< "$line" 2>/dev/null | awk '{print $1}' || true)
            log_info "  Key: ${KEY_TYPE} ${KEY_BITS:-?} bit (${KEY_COMMENT:-no comment})"
        done < <(grep "^ssh-" "${ADMIN_HOME}/.ssh/authorized_keys")
    else
        log_fail "No SSH keys in authorized_keys!"
    fi
else
    log_fail "No authorized_keys file for '${ADMIN_USER}'!"
fi

SSH_VERSION=$(ssh -V 2>&1)
log_info "SSH Version: ${SSH_VERSION}"

# =============================================================================
# 4. FIREWALL (UFW)
# =============================================================================
section "4. Firewall (UFW)"

# SSH_PORT for later use (if sshd -T failed)
SSH_PORT=${SSH_PORT:-8496} # Fallback to a common custom port

if command -v ufw &>/dev/null; then
    UFW_STATUS=$(ufw status 2>/dev/null || true)
    if echo "$UFW_STATUS" | grep -q "Status: active"; then
        log_pass "UFW is active"

        # Default Policies
        UFW_VERBOSE=$(ufw status verbose 2>/dev/null || true)
        if echo "$UFW_VERBOSE" | grep -q "deny (incoming)"; then
            log_pass "Default policy incoming: deny"
        else
            log_fail "Default policy incoming is NOT deny!"
        fi

        if echo "$UFW_VERBOSE" | grep -q "allow (outgoing)"; then
            log_pass "Default policy outgoing: allow"
        else
            log_info "Default policy outgoing is not 'allow'"
        fi

        # List open ports
        echo ""
        log_info "Active rules:"
        ufw status numbered 2>/dev/null | grep -E "^\[" | while IFS= read -r rule; do
            echo -e "         ${rule}"
        done

        # SSH port with rate-limit?
        if echo "$UFW_STATUS" | grep -q "${SSH_PORT}.*LIMIT"; then
            log_pass "SSH port ${SSH_PORT} has Rate-Limiting"
        elif echo "$UFW_STATUS" | grep -q "${SSH_PORT}"; then
            log_warn "SSH port ${SSH_PORT} is open, but WITHOUT Rate-Limiting"
        else
            log_warn "SSH port ${SSH_PORT} is not explicitly in UFW"
        fi
    else
        log_fail "UFW is NOT active"
    fi

    # UFW Logging Redirect (VNC Console fix)
    if [[ -f /etc/rsyslog.d/20-ufw.conf ]]; then
        log_pass "UFW logs are redirected to /var/log/ufw.log (console remains clean)"
    else
        log_warn "UFW logs not redirected - VNC console will be flooded with [UFW BLOCK]"
    fi
else
    log_fail "UFW is not installed"
fi

# =============================================================================
# 5. FAIL2BAN
# =============================================================================
section "5. Fail2Ban"

if command -v fail2ban-client &>/dev/null; then
    if systemctl is-active --quiet fail2ban; then
        log_pass "Fail2Ban is running"

        # SSH Jail active?
        if fail2ban-client status sshd &>/dev/null; then
            log_pass "SSH Jail is active"
            F2B_STATUS=$(fail2ban-client status sshd 2>/dev/null || true)
            BANNED=$(echo "$F2B_STATUS" | grep "Currently banned" | awk '{print $NF}')
            TOTAL_BANNED=$(echo "$F2B_STATUS" | grep "Total banned" | awk '{print $NF}')
            log_info "Currently banned: ${BANNED:-0}, Total banned: ${TOTAL_BANNED:-0}"
        else
            log_fail "SSH Jail is NOT active"
        fi

        # Check configuration
        F2B_PORT=$(grep -r "^port" /etc/fail2ban/jail.local 2>/dev/null | head -1 | awk '{print $NF}' || true)
        if [[ "$F2B_PORT" == "$SSH_PORT" ]]; then
            log_pass "Fail2Ban monitors correct SSH port (${F2B_PORT})"
        elif [[ -n "$F2B_PORT" ]]; then
            log_fail "Fail2Ban monitors port ${F2B_PORT}, SSH runs on ${SSH_PORT}!"
        fi
    else
        log_fail "Fail2Ban is installed, but NOT active"
    fi
else
    log_fail "Fail2Ban is not installed"
fi

# =============================================================================
# 6. AUTOMATIC UPDATES
# =============================================================================
section "6. Automatic Updates"

if dpkg -l unattended-upgrades &>/dev/null; then
    log_pass "unattended-upgrades is installed"

    if [[ -f /etc/apt/apt.conf.d/20auto-upgrades ]]; then
        if grep -q 'Unattended-Upgrade "1"' /etc/apt/apt.conf.d/20auto-upgrades; then
            log_pass "Automatic upgrades are enabled"
        else
            log_warn "20auto-upgrades exists, but upgrades appear disabled"
        fi
    else
        log_warn "20auto-upgrades configuration is missing"
    fi
else
    log_fail "unattended-upgrades is not installed"
fi

# =============================================================================
# 7. DOCKER
# =============================================================================
section "7. Docker"

if command -v docker &>/dev/null; then
    log_pass "Docker is installed: $(docker --version 2>/dev/null | head -1)"

    if systemctl is-active --quiet docker; then
        log_pass "Docker daemon is running"
    else
        log_warn "Docker daemon is not active"
    fi

    # Docker Compose
    if docker compose version &>/dev/null; then
        log_pass "Docker Compose: $(docker compose version 2>/dev/null | head -1)"
    else
        log_warn "Docker Compose Plugin not found"
    fi

    # User in docker group
    if groups "$ADMIN_USER" 2>/dev/null | grep -qw docker; then
        log_warn "'${ADMIN_USER}' can control Docker; review root-equivalent daemon access"
    else
        log_info "'${ADMIN_USER}' is not in the docker group; membership is not required for a secure setup"
    fi

    # Inspect effective settings without printing environment variables or secrets.
    if docker info --format '{{.LoggingDriver}}' 2>/dev/null; then
        log_info "Default logging driver shown above; inspect each container separately"
        if docker ps -aq >/dev/null 2>&1; then
            while IFS= read -r container_id; do
                [[ -n "$container_id" ]] || continue
                if ! docker inspect --format '{{.Name}} {{json .HostConfig.LogConfig}}' "$container_id"; then
                    log_warn "Cannot inspect container log configuration; coverage incomplete"
                fi
            done < <(docker ps -aq)
        else
            log_warn "Cannot enumerate containers; log-retention coverage incomplete"
        fi
        log_warn "Review driver-specific retention and application-file logs; daemon defaults do not update existing containers"
    else
        log_warn "Cannot read Docker logging configuration; coverage incomplete"
    fi

    # Proxy network
    if docker network ls 2>/dev/null | grep -q "proxy"; then
        log_pass "Docker network 'proxy' exists"
    else
        log_warn "Docker network 'proxy' is missing"
    fi
else
    log_fail "Docker is not installed"
fi

# =============================================================================
# 8. TRAEFIK PREPARATION
# =============================================================================
section "8. Traefik Stack Preparation"

TRAEFIK_DIR="/opt/containers/traefik-stack"
if [[ -d "$TRAEFIK_DIR" ]]; then
    log_pass "Directory ${TRAEFIK_DIR} exists"

    # Subdirectories
    for dir in traefik/dynamic traefik/logs traefik/certs crowdsec/config crowdsec/data; do
        if [[ -d "${TRAEFIK_DIR}/${dir}" ]]; then
            log_pass "  ${dir}/ is present"
        else
            log_fail "  ${dir}/ is missing"
        fi
    done

    # acme.json Permissions
    if [[ -f "${TRAEFIK_DIR}/traefik/certs/acme.json" ]]; then
        ACME_PERMS=$(stat -c "%a" "${TRAEFIK_DIR}/traefik/certs/acme.json")
        if [[ "$ACME_PERMS" == "600" ]]; then
            log_pass "acme.json has correct permissions (600)"
        else
            log_fail "acme.json has permissions ${ACME_PERMS} (should be 600)"
        fi
    else
        log_warn "acme.json does not exist yet"
    fi
else
    log_fail "Directory ${TRAEFIK_DIR} is missing"
fi

# =============================================================================
# 9. LOG ROTATION
# =============================================================================
section "9. Log Rotation"

# Persistent Journal
if [[ -d /var/log/journal ]]; then
    log_pass "Persistent Journal activated"
    JOURNAL_SIZE=$(journalctl --disk-usage 2>/dev/null | grep -oP '[\d.]+\w+' | head -1 || true)
    log_info "Journal size: ${JOURNAL_SIZE:-unknown}"
else
    log_warn "Journal is NOT persistent (logs are lost on reboot)"
fi

# Logrotate: Traefik
if [[ -f /etc/logrotate.d/traefik ]]; then
    log_pass "Logrotate for Traefik configured"
else
    log_warn "Logrotate for Traefik is missing"
fi

# Logrotate: UFW
if [[ -f /etc/logrotate.d/ufw-custom ]]; then
    log_pass "Logrotate for UFW logs configured"
elif [[ -f /etc/logrotate.d/ufw ]]; then
    log_pass "Logrotate for UFW logs configured (system default)"
else
    log_warn "Logrotate for UFW logs is missing - /var/log/ufw.log can grow indefinitely!"
fi

# Configuration-file presence is not proof of effective retention or a successful run.
log_warn "Verify last logrotate result, application log reopen and bounded growth separately"

# =============================================================================
# 10. NETWORK & OPEN PORTS
# =============================================================================
section "10. Network & Open Ports"

log_info "Local TCP listeners (reachability requires a separate network check):"
ss -tlnp 2>/dev/null | grep -v "127.0.0" | grep "LISTEN" | while IFS= read -r line; do
    PORT=$(echo "$line" | awk '{print $4}' | rev | cut -d: -f1 | rev)
    PROC=$(echo "$line" | grep -oP 'users:\(\("\K[^"]+' || echo "unknown")
    echo -e "         Port ${PORT} (${PROC})"
done

# Check for known insecure services
INSECURE_FOUND=0
for svc in telnetd rshd rlogind vsftpd proftpd; do
    if systemctl is-active --quiet "$svc" 2>/dev/null; then
        log_fail "Insecure service running: ${svc}"
        INSECURE_FOUND=1
    fi
done
if [[ "$INSECURE_FOUND" -eq 0 ]]; then
    log_pass "No insecure legacy services active"
fi

# =============================================================================
# 11. FILESYSTEM SECURITY
# =============================================================================
section "11. Filesystem"

# World-writable files in critical directories
WW_COUNT=$(find /etc /usr -xdev -type f -perm -o+w 2>/dev/null | wc -l)
if [[ "$WW_COUNT" -eq 0 ]]; then
    log_pass "No world-writable files in /etc and /usr"
else
    log_warn "${WW_COUNT} world-writable file(s) in /etc or /usr"
fi

# SUID Binaries (informative)
SUID_COUNT=$(find / -xdev -type f -perm -4000 2>/dev/null | wc -l)
log_info "SUID binaries found: ${SUID_COUNT} (manual review if needed)"

# Disk Usage
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ "$DISK_USAGE" -lt 80 ]]; then
    log_pass "Disk usage: ${DISK_USAGE}%"
elif [[ "$DISK_USAGE" -lt 90 ]]; then
    log_warn "Disk usage: ${DISK_USAGE}% (getting low)"
else
    log_fail "Disk usage: ${DISK_USAGE}% (critical!)"
fi

# =============================================================================
# SUMMARY
# =============================================================================
echo ""
echo -e "${BOLD}============================================================${NC}"
echo -e "${BOLD}  SUMMARY${NC}"
echo -e "${BOLD}============================================================${NC}"
echo -e "  ${GREEN}Passed:${NC}   ${pass_count}"
echo -e "  ${YELLOW}Warnings:${NC} ${warn_count}"
echo -e "  ${RED}Failed:${NC}    ${fail_count}"
echo -e "${BOLD}============================================================${NC}"

if [[ "$fail_count" -eq 0 && "$warn_count" -eq 0 ]]; then
    echo -e "\n${GREEN}${BOLD}Excellent! All checks passed.${NC}"
elif [[ "$fail_count" -eq 0 ]]; then
    echo -e "\n${YELLOW}${BOLD}Good - no critical failures, but review warnings.${NC}"
else
    echo -e "\n${RED}${BOLD}Attention - there are critical failures that should be resolved!${NC}"
fi

echo ""
exit "$fail_count"

Interpreting Docker and network results

Membership in the Docker group grants control over a normally privileged daemon; it is an access decision to review, not a hardening requirement. Restrict daemon access to trusted administrators. Docker daemon attack surface.

Review the effective logging driver and retention options for each container. Docker daemon default changes apply to newly created containers; existing containers retain their creation-time configuration. Application logs written into bind mounts or volumes need their own retention and reopen procedure. A configuration file or timer alone does not prove that rotation succeeded. Docker logging configuration.

A local listener listing does not establish internet reachability or prove the reverse proxy is the only entry point. Compare Dockerโ€™s published bindings with the intended access policy and test from the relevant external network, including IPv6 where enabled. A directly published application port can bypass proxy authentication. Docker port publishing.

Sources