Files
linux-scripts/apache-security-auditor.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

659 lines
25 KiB
Bash

#!/usr/bin/env bash
#########################################################################################
#### apache-security-auditor.sh — Audit Apache httpd configuration for security issues####
#### Checks server info, TLS, headers, directories, modules, and file permissions ####
#### Requires: bash 4+, root access ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.01 ####
#### ####
#### Usage: ####
#### sudo ./apache-security-auditor.sh --full ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Colors (pre-initialized) ─────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
setup_colors() {
if [[ "${COLOR:-auto}" == "never" ]]; then
return
fi
if [[ "${COLOR:-auto}" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
die() { err "$*"; exit 1; }
# ── Severity counters ────────────────────────────────────────────────
TOTAL_CRIT=0
TOTAL_WARN=0
TOTAL_INFO=0
TOTAL_OK=0
flag_crit() { ((TOTAL_CRIT++)) || true; }
flag_warn() { ((TOTAL_WARN++)) || true; }
flag_info() { ((TOTAL_INFO++)) || true; }
flag_ok() { ((TOTAL_OK++)) || true; }
# ── Defaults ──────────────────────────────────────────────────────────
RUN_MODE=""
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
APACHE_CONF=""
APACHECTL=""
APACHE_CONF_DIR=""
APACHE_RUN_USER=""
PLATFORM=""
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""
# ── Platform detection ───────────────────────────────────────────────
detect_platform() {
if [[ -n "$APACHE_CONF" ]]; then
if command -v apache2ctl &>/dev/null; then
APACHECTL="apache2ctl"
APACHE_CONF_DIR="$(dirname "$APACHE_CONF")"
APACHE_RUN_USER="www-data"
PLATFORM="Debian/Ubuntu (apache2)"
elif command -v httpd &>/dev/null; then
APACHECTL="httpd"
APACHE_CONF_DIR="$(dirname "$APACHE_CONF")"
APACHE_RUN_USER="apache"
PLATFORM="RHEL/CentOS (httpd)"
else
die "Cannot find apache2ctl or httpd"
fi
return
fi
if command -v apache2ctl &>/dev/null && [[ -f /etc/apache2/apache2.conf ]]; then
APACHECTL="apache2ctl"
APACHE_CONF="/etc/apache2/apache2.conf"
APACHE_CONF_DIR="/etc/apache2"
APACHE_RUN_USER="www-data"
PLATFORM="Debian/Ubuntu (apache2)"
elif command -v httpd &>/dev/null && [[ -f /etc/httpd/conf/httpd.conf ]]; then
APACHECTL="httpd"
APACHE_CONF="/etc/httpd/conf/httpd.conf"
APACHE_CONF_DIR="/etc/httpd"
APACHE_RUN_USER="apache"
PLATFORM="RHEL/CentOS (httpd)"
else
die "Cannot detect Apache installation — use --config to specify config path"
fi
verbose "Platform: ${PLATFORM}"
verbose "Config: ${APACHE_CONF}"
verbose "Config dir: ${APACHE_CONF_DIR}"
}
# ── Get all config files ─────────────────────────────────────────────
get_config_files() {
local files=()
if [[ -f "$APACHE_CONF" ]]; then
files+=("$APACHE_CONF")
fi
local included
included=$($APACHECTL -t -D DUMP_INCLUDES 2>/dev/null | grep -oP '\(\*\) \K.*|^ *\K/.*' || true)
if [[ -n "$included" ]]; then
while IFS= read -r f; do
[[ -f "$f" ]] && files+=("$f")
done <<< "$included"
fi
for d in "${APACHE_CONF_DIR}/sites-enabled" "${APACHE_CONF_DIR}/conf-enabled" \
"${APACHE_CONF_DIR}/conf.d" "${APACHE_CONF_DIR}/conf.modules.d"; do
if [[ -d "$d" ]]; then
while IFS= read -r f; do
files+=("$f")
done < <(find "$d" -name '*.conf' -type f 2>/dev/null)
fi
done
printf '%s\n' "${files[@]}" | sort -u
}
# ── Search across all config files ───────────────────────────────────
search_config() {
local pattern="$1"
local config_files
config_files=$(get_config_files)
while IFS= read -r f; do
[[ -z "$f" ]] && continue
grep -iP "$pattern" "$f" 2>/dev/null || true
done <<< "$config_files"
}
# ── Table header ─────────────────────────────────────────────────────
print_table_header() {
printf " %-32s %-14s %s\n" "CHECK" "STATUS" "SEVERITY"
printf " %s\n" "$(printf '%.0s─' {1..65})"
}
# ── Table row ────────────────────────────────────────────────────────
print_row() {
local check="$1" status="$2" severity="$3"
local color=""
case "$severity" in
CRITICAL) color="$RED"; flag_crit ;;
WARN) color="$YELLOW"; flag_warn ;;
INFO) color="$CYAN"; flag_info ;;
OK) color="$GREEN"; flag_ok ;;
esac
printf " %-32s %-14s %b%s%b\n" "$check" "$status" "$color" "$severity" "$RESET"
}
# ══════════════════════════════════════════════════════════════════════
# SERVER INFO AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_server_info() {
log "Auditing server information exposure..."
echo ""
print_table_header
# ServerTokens
local tokens
tokens=$(search_config '^\s*ServerTokens' | tail -1 | awk '{print $2}')
if [[ -z "$tokens" ]]; then
print_row "ServerTokens" "Full (default)" "CRITICAL"
elif [[ "${tokens,,}" == "prod" || "${tokens,,}" == "productonly" ]]; then
print_row "ServerTokens" "Prod" "OK"
elif [[ "${tokens,,}" == "major" ]]; then
print_row "ServerTokens" "Major" "WARN"
else
print_row "ServerTokens" "$tokens" "CRITICAL"
fi
# ServerSignature
local sig
sig=$(search_config '^\s*ServerSignature' | tail -1 | awk '{print $2}')
if [[ -z "$sig" ]]; then
print_row "ServerSignature" "On (default)" "CRITICAL"
elif [[ "${sig,,}" == "off" ]]; then
print_row "ServerSignature" "Off" "OK"
else
print_row "ServerSignature" "$sig" "CRITICAL"
fi
# TraceEnable
local trace
trace=$(search_config '^\s*TraceEnable' | tail -1 | awk '{print $2}')
if [[ -z "$trace" ]]; then
print_row "TraceEnable" "On (default)" "WARN"
elif [[ "${trace,,}" == "off" ]]; then
print_row "TraceEnable" "Off" "OK"
else
print_row "TraceEnable" "$trace" "WARN"
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# TLS AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_tls() {
log "Auditing TLS configuration..."
echo ""
print_table_header
# Check if mod_ssl is loaded
if ! $APACHECTL -M 2>/dev/null | grep -q ssl_module; then
print_row "mod_ssl" "not loaded" "INFO"
echo ""
return
fi
print_row "mod_ssl" "loaded" "OK"
# SSLProtocol
local proto
proto=$(search_config '^\s*SSLProtocol' | tail -1)
if [[ -z "$proto" ]]; then
print_row "SSLProtocol" "not set (default)" "WARN"
else
if echo "$proto" | grep -iqP '(\+SSLv3|\+TLSv1\.0|\+TLSv1[^.]|[^-]TLSv1[^.23])'; then
print_row "SSLProtocol (legacy)" "enabled" "CRITICAL"
elif echo "$proto" | grep -iqP '(\+TLSv1\.1|[^-]TLSv1\.1)'; then
print_row "SSLProtocol (TLSv1.1)" "enabled" "CRITICAL"
else
print_row "SSLProtocol" "modern only" "OK"
fi
fi
# SSLCipherSuite
local ciphers
ciphers=$(search_config '^\s*SSLCipherSuite' | tail -1)
if [[ -z "$ciphers" ]]; then
print_row "SSLCipherSuite" "not set" "WARN"
else
print_row "SSLCipherSuite" "configured" "OK"
fi
# SSLHonorCipherOrder
local honor
honor=$(search_config '^\s*SSLHonorCipherOrder' | tail -1 | awk '{print $2}')
if [[ -z "$honor" ]]; then
print_row "SSLHonorCipherOrder" "not set" "WARN"
elif [[ "${honor,,}" == "on" ]]; then
print_row "SSLHonorCipherOrder" "on" "OK"
else
print_row "SSLHonorCipherOrder" "$honor" "WARN"
fi
# HSTS
local hsts
hsts=$(search_config 'Strict-Transport-Security')
if [[ -z "$hsts" ]]; then
print_row "HSTS Header" "missing" "WARN"
else
print_row "HSTS Header" "set" "OK"
fi
# OCSP Stapling
local ocsp
ocsp=$(search_config '^\s*SSLUseStapling' | tail -1 | awk '{print $2}')
if [[ -z "$ocsp" ]]; then
print_row "OCSP Stapling" "not configured" "WARN"
elif [[ "${ocsp,,}" == "on" ]]; then
print_row "OCSP Stapling" "on" "OK"
else
print_row "OCSP Stapling" "$ocsp" "WARN"
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# SECURITY HEADERS AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_headers() {
log "Auditing security headers..."
echo ""
print_table_header
# Check if mod_headers is loaded
if ! $APACHECTL -M 2>/dev/null | grep -q headers_module; then
print_row "mod_headers" "not loaded" "WARN"
echo ""
return
fi
local headers=(
"X-Content-Type-Options"
"X-Frame-Options"
"Content-Security-Policy"
"Referrer-Policy"
"Permissions-Policy"
)
for header in "${headers[@]}"; do
local found
found=$(search_config "Header.*set.*${header}")
if [[ -n "$found" ]]; then
print_row "$header" "set" "OK"
else
print_row "$header" "missing" "WARN"
fi
done
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# DIRECTORY AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_directories() {
log "Auditing directory and file restrictions..."
echo ""
print_table_header
# Options Indexes (enabled = bad)
local indexes_on
indexes_on=$(search_config '^\s*Options\b.*\bIndexes\b' | grep -v '\-Indexes' || true)
if [[ -n "$indexes_on" ]]; then
print_row "Options Indexes" "enabled" "WARN"
else
print_row "Options Indexes" "disabled" "OK"
fi
# AllowOverride All
local override_all
override_all=$(search_config '^\s*AllowOverride\s+All' || true)
if [[ -n "$override_all" ]]; then
print_row "AllowOverride" "All (permissive)" "WARN"
else
print_row "AllowOverride" "restricted" "OK"
fi
# Sensitive file protection (.git, .env, .htpasswd)
local sensitive_protection
sensitive_protection=$(search_config '(FilesMatch|Files|Directory).*(\\.git|\\.env|\\.htpasswd)' || true)
if [[ -n "$sensitive_protection" ]]; then
print_row "Sensitive file blocking" "configured" "OK"
else
print_row "Sensitive file blocking" "not configured" "CRITICAL"
fi
# Check for root directory restriction
local root_deny
root_deny=$(search_config '^\s*Require\s+all\s+denied' || true)
if [[ -n "$root_deny" ]]; then
print_row "Root directory denied" "yes" "OK"
else
print_row "Root directory denied" "not found" "WARN"
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# MODULES AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_modules() {
log "Auditing modules..."
echo ""
print_table_header
local loaded_modules
loaded_modules=$($APACHECTL -M 2>/dev/null || true)
# mod_security
if echo "$loaded_modules" | grep -q "security2_module"; then
print_row "mod_security" "loaded" "OK"
else
print_row "mod_security" "not loaded" "INFO"
fi
# mod_status
if echo "$loaded_modules" | grep -q "status_module"; then
local status_restricted
status_restricted=$(search_config '(<Location\s+/server-status)' || true)
if [[ -n "$status_restricted" ]]; then
local has_require
has_require=$(search_config 'Require\s+(ip|local|host)' || true)
if [[ -n "$has_require" ]]; then
print_row "mod_status" "restricted" "OK"
else
print_row "mod_status" "unrestricted" "CRITICAL"
fi
else
print_row "mod_status" "loaded (no location)" "WARN"
fi
else
print_row "mod_status" "not loaded" "OK"
fi
# mod_info
if echo "$loaded_modules" | grep -q "info_module"; then
print_row "mod_info" "loaded" "WARN"
else
print_row "mod_info" "not loaded" "OK"
fi
# mod_autoindex
if echo "$loaded_modules" | grep -q "autoindex_module"; then
print_row "mod_autoindex" "loaded" "WARN"
else
print_row "mod_autoindex" "not loaded" "OK"
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# PERMISSIONS AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_permissions() {
log "Auditing file permissions..."
echo ""
print_table_header
# Main config file
if [[ -f "$APACHE_CONF" ]]; then
local conf_perms
conf_perms=$(stat -c '%a' "$APACHE_CONF" 2>/dev/null)
if [[ "$conf_perms" -le 644 ]]; then
print_row "Config ($APACHE_CONF)" "$conf_perms" "OK"
else
print_row "Config ($APACHE_CONF)" "$conf_perms" "WARN"
fi
fi
# .htpasswd files
local htpasswd_files
htpasswd_files=$(find "$APACHE_CONF_DIR" /var/www -name '.htpasswd' -type f 2>/dev/null || true)
if [[ -n "$htpasswd_files" ]]; then
while IFS= read -r f; do
local perms
perms=$(stat -c '%a' "$f" 2>/dev/null)
if [[ "$perms" -le 640 ]]; then
print_row ".htpasswd ($f)" "$perms" "OK"
else
print_row ".htpasswd ($f)" "$perms" "WARN"
fi
done <<< "$htpasswd_files"
else
verbose "No .htpasswd files found"
fi
# Document root world-writable check
local docroots
docroots=$(search_config '^\s*DocumentRoot' | awk '{print $2}' | tr -d '"' | sort -u)
if [[ -n "$docroots" ]]; then
while IFS= read -r dr; do
[[ -z "$dr" || ! -d "$dr" ]] && continue
local dr_perms
dr_perms=$(stat -c '%a' "$dr" 2>/dev/null)
if [[ "${dr_perms: -1}" -ge 6 ]]; then
print_row "Docroot ($dr)" "${dr_perms} (world-writable)" "CRITICAL"
else
print_row "Docroot ($dr)" "$dr_perms" "OK"
fi
done <<< "$docroots"
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════
print_summary() {
local elapsed
elapsed=$(( $(date +%s) - START_TIME ))
echo ""
echo " ══════════════════════════════════════════"
echo " Apache Security Audit Summary"
echo " ══════════════════════════════════════════"
printf " %-20s %b%d%b\n" "CRITICAL:" "$RED" "$TOTAL_CRIT" "$RESET"
printf " %-20s %b%d%b\n" "WARN:" "$YELLOW" "$TOTAL_WARN" "$RESET"
printf " %-20s %b%d%b\n" "INFO:" "$CYAN" "$TOTAL_INFO" "$RESET"
printf " %-20s %b%d%b\n" "OK:" "$GREEN" "$TOTAL_OK" "$RESET"
echo " ──────────────────────────────────────────"
printf " Completed in %ds\n" "$elapsed"
echo ""
if [[ "$TOTAL_CRIT" -gt 0 ]]; then
echo -e " ${RED}${BOLD}Action required:${RESET} ${TOTAL_CRIT} critical finding(s)"
echo ""
echo " Top recommendations:"
echo " • Set ServerTokens Prod and ServerSignature Off"
echo " • Disable SSLv3, TLSv1, and TLSv1.1"
echo " • Restrict mod_status to localhost with Require ip 127.0.0.1"
echo " • Block access to .git, .env, and .htpasswd files"
echo " • Fix world-writable document root permissions"
echo ""
elif [[ "$TOTAL_WARN" -gt 0 ]]; then
echo -e " ${YELLOW}Review recommended:${RESET} ${TOTAL_WARN} warning(s)"
echo ""
echo " Suggestions:"
echo " • Add security headers (CSP, X-Frame-Options, HSTS)"
echo " • Enable OCSP stapling for TLS"
echo " • Disable mod_info and mod_autoindex in production"
echo " • Set TraceEnable Off"
echo ""
else
echo -e " ${GREEN}All checks passed${RESET}"
echo ""
fi
}
# ══════════════════════════════════════════════════════════════════════
# USAGE
# ══════════════════════════════════════════════════════════════════════
show_help() {
cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} — Apache Security Auditor
Audit Apache httpd configuration for common security misconfigurations.
Read-only — never modifies any files.
${BOLD}MODES${RESET}
--full Run all audit checks
--server-info Check ServerTokens, ServerSignature, TraceEnable
--tls Check TLS protocols, ciphers, HSTS, OCSP
--headers Check security response headers
--directories Check directory listing, AllowOverride, sensitive files
--modules Check mod_security, mod_status, mod_info, mod_autoindex
--permissions Check file permissions on configs and document roots
${BOLD}OPTIONS${RESET}
--config PATH Path to main Apache config file (auto-detect if omitted)
--verbose Debug output
--no-color Disable colored output
--help Show this help message
${BOLD}EXAMPLES${RESET}
# Full audit
sudo ${SCRIPT_NAME} --full
# Check only server info exposure
sudo ${SCRIPT_NAME} --server-info
# Custom config path
sudo ${SCRIPT_NAME} --full --config /opt/apache/conf/httpd.conf
# Pipe-friendly output
sudo ${SCRIPT_NAME} --full --no-color
${BOLD}EXIT CODES${RESET}
0 All checks passed
1 Warnings found (review recommended)
2 Critical findings (action required)
EOF
}
# ══════════════════════════════════════════════════════════════════════
# PARSE ARGS
# ══════════════════════════════════════════════════════════════════════
parse_args() {
local modes=()
while [[ $# -gt 0 ]]; do
case "$1" in
--full)
modes=(server-info tls headers directories modules permissions)
shift ;;
--server-info)
modes+=(server-info); shift ;;
--tls)
modes+=(tls); shift ;;
--headers)
modes+=(headers); shift ;;
--directories)
modes+=(directories); shift ;;
--modules)
modes+=(modules); shift ;;
--permissions)
modes+=(permissions); shift ;;
--config)
APACHE_CONF="${2:?--config requires a value}"; shift 2 ;;
--verbose)
VERBOSE="true"; shift ;;
--no-color)
COLOR="never"; shift ;;
--help|-h)
setup_colors; show_help; exit 0 ;;
*)
die "Unknown option: $1 (see --help)" ;;
esac
done
if [[ ${#modes[@]} -eq 0 ]]; then
err "No audit mode specified"
echo "Run ${SCRIPT_NAME} --help for usage" >&2
exit 1
fi
RUN_MODE="${modes[*]}"
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
setup_colors
detect_platform
START_TIME=$(date +%s)
echo ""
echo -e "${BOLD}Apache Security Auditor${RESET}"
echo -e "Host: $(hostname)"
echo -e "Config: ${APACHE_CONF}"
echo -e "Platform: ${PLATFORM}"
echo -e "Mode: ${RUN_MODE}"
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
for mode in $RUN_MODE; do
case "$mode" in
server-info) audit_server_info ;;
tls) audit_tls ;;
headers) audit_headers ;;
directories) audit_directories ;;
modules) audit_modules ;;
permissions) audit_permissions ;;
esac
done
print_summary
if [[ "$TOTAL_CRIT" -gt 0 ]]; then
exit 2
elif [[ "$TOTAL_WARN" -gt 0 ]]; then
exit 1
fi
exit 0
}
main "$@"