Skip to content

Instantly share code, notes, and snippets.

@barthez-kenwou
Created June 19, 2026 14:39
Show Gist options
  • Select an option

  • Save barthez-kenwou/841dd445eefbf34cdb2b1ebd874dfe4b to your computer and use it in GitHub Desktop.

Select an option

Save barthez-kenwou/841dd445eefbf34cdb2b1ebd874dfe4b to your computer and use it in GitHub Desktop.
Linux Audit - Complete Server Security & Health Audit Script

Linux Audit — Complete Server Security & Health Audit Script

linux_audit.sh — v2.0.0 A single-file, zero-dependency Bash script that performs a comprehensive security, configuration, and health audit of any Linux server.


Table of Contents

  1. What It Does
  2. Why It Is Efficient
  3. Requirements
  4. Quick Start
  5. Usage & Options
  6. Audit Modules — Deep Dive
  7. Reading the Report
  8. Use Cases
  9. Customization Guide
  10. Security & Privacy Notes
  11. Limitations
  12. Contributing

What It Does

linux_audit.sh performs 20 distinct audit modules in a single execution pass, covering every critical aspect of a Linux server:

# Module What Is Checked
01 Pre-flight Root privileges, output file writability
02 System Identity Hostname, OS, kernel, uptime, timezone, virtualization detection
03 Hardware Resources CPU model/cores/load, RAM usage, Swap, Disk usage per partition, block devices, network interfaces
04 User Accounts UID-0 accounts, login shells, passwordless accounts, sudo config, privileged group members, login history
05 SSH Configuration Port, PermitRootLogin, PasswordAuthentication, PermitEmptyPasswords, X11Forwarding, MaxAuthTries, authorized keys & permissions
06 Firewall UFW, firewalld, iptables, nftables — active status & rules dump
07 Open Ports All listening TCP/UDP ports, sensitive port detection, established connections
08 Services & Processes Top CPU/memory consumers, running systemd services, failed services
09 File Permissions SUID/SGID binaries, world-writable files/dirs, orphaned files, critical config permissions, sticky bits
10 Cron Jobs System crontab, cron.d/hourly/daily/weekly/monthly, per-user crontabs, at jobs, systemd timers
11 Docker Version, running/stopped containers, images, privileged containers, host-network containers, volumes, networks, docker.sock exposure
12 Kubernetes Cluster info, nodes, pods, privileged pods, RBAC, NetworkPolicies, Secrets, exposed services
13 Packages Pending security updates, installed package count, security tools availability (fail2ban, rkhunter, etc.), AppArmor/SELinux status
14 Kernel Parameters IP forwarding, ICMP redirects, source routing, SYN cookies, ASLR, core dumps, dmesg restriction, ptrace scope, NX bit, Spectre/Meltdown mitigations
15 Logs & Audit Trail auditd status, journald, recent auth failures, sudo audit trail, log rotation
16 Network Configuration DNS, /etc/hosts, routing table, ARP cache, TCP wrappers, packet capture tools
17 Sensitive Files .env, .pem, private keys, SSH key permissions, shell history credentials leak detection
18 Init & Startup PID 1 identification, systemd service hardening hints, rc.local legacy scripts
19 Summary Total ALERTS / WARNINGS / OK counts, critical findings highlighted

⚡ Why It Is Efficient

Zero Dependencies

The script relies exclusively on utilities that are present by default on any standard Linux distribution: bash, ss, ps, find, awk, stat, systemctl, ip. No external packages to install.

Single-Pass Execution

All 20 modules run sequentially in one process — there is no looping back, no redundant parsing, no repeated apt or rpm invocations. The total execution time is typically 30 to 120 seconds depending on hardware and the number of running containers.

Dual-Output Architecture

Every line of output is written simultaneously to the terminal (with ANSI color) and to a plain-text report file (without escape codes) using the tee pattern. You get live feedback while the file is being built — no waiting.

Signal/Severity System

Every finding is tagged with one of four prefixes, making the report grep-friendly and CI-parseable:

[OK]    — no issue detected
[INFO]  — neutral informational data
[WARN]  — potential risk, should be reviewed
[ALERT] — critical finding requiring immediate action

Safe by Design

  • Uses set -euo pipefail — the script aborts on any unexpected error.
  • Never modifies any system configuration.
  • Never sends data over the network.
  • Filters sensitive content (e.g., history files are flagged but not printed verbatim).
  • All find calls use -xdev to avoid crossing filesystem boundaries (important on systems with NFS mounts or large /proc trees).

Requirements

Requirement Details
OS Any Linux distribution (Debian, Ubuntu, RHEL, CentOS, Fedora, Arch, Alpine, etc.)
Bash Version 4.0 or newer (bash --version)
Privileges sudo or root strongly recommended for full coverage. The script runs without root but several checks will be skipped or incomplete.
Tools ss or netstat, ps, find, awk, stat, free, df, uname, systemctl (on systemd systems). All present by default.
Optional docker, kubectl, ufw, iptables, nft, auditctl, aa-status, getenforce — used if installed

Quick Start

# 1. Download
curl -O https://gist.githubusercontent.com/YOUR_USERNAME/YOUR_GIST_ID/raw/linux_audit.sh

# 2. Make executable
chmod +x linux_audit.sh

# 3. Run as root
sudo bash linux_audit.sh

The report is automatically saved to /tmp/audit_<hostname>_<timestamp>.txt.


🔧 Usage & Options

sudo bash linux_audit.sh [OPTIONS]

Options:
  -o <file>   Write report to a custom file path
              Example: sudo bash linux_audit.sh -o /root/reports/audit.txt

  -q          Quiet mode — disables ANSI colors in terminal output
              Useful when piping output or running from CI/CD

  -h          Show usage help and exit

Examples

# Standard run — auto-generated report in /tmp
sudo bash linux_audit.sh

# Save report to a custom location
sudo bash linux_audit.sh -o /root/server_audit_$(date +%F).txt

# Run without colors (e.g., inside a CI pipeline)
sudo bash linux_audit.sh -q -o /var/log/audit_report.txt

# Run and immediately grep for critical findings
sudo bash linux_audit.sh -q 2>&1 | grep '^\[ALERT\]'

🔬 Audit Modules — Deep Dive

Module 04 — User Accounts

This is one of the most security-critical sections. The script checks:

  • UID 0 accounts — If more than one account has UID 0, all such accounts have root-equivalent access regardless of their name. This is a common backdoor technique.
  • Passwordless accounts — Reads /etc/shadow to find accounts with empty or locked password fields.
  • Sudo rules — Dumps /etc/sudoers and all files in /etc/sudoers.d/, looking for NOPASSWD rules and wildcard permissions.
  • Docker group membership — Any user in the docker group can trivially escalate to root by spawning a privileged container that mounts the host filesystem.

Module 05 — SSH Configuration

SSH is the most common attack surface on internet-facing Linux servers. The script evaluates every major sshd_config directive and flags dangerous configurations:

Directive Secure Value Risk if Misconfigured
PermitRootLogin no Direct root brute-force
PasswordAuthentication no Password-based attacks
PermitEmptyPasswords no Instant login without credential
X11Forwarding no X11 session hijacking
MaxAuthTries 3 Faster brute-force
Banner Set to a file No legal warning to attackers

Module 09 — File Permissions

The SUID/SGID scan uses find / -xdev -perm /4000 — the -xdev flag is essential to prevent crossing into virtual filesystems (/proc, /sys) or network mounts, which would cause the scan to hang indefinitely. Results are sorted for easy review against a known-good baseline.

Module 11 — Docker

Docker misconfigurations are a leading cause of container escapes. The script focuses on:

  • Privileged containers — A container running with --privileged has unrestricted access to all host devices and can trivially mount the host root filesystem.
  • Docker socket mounting — Passing /var/run/docker.sock into a container gives that container the ability to create new privileged containers, effectively granting root on the host.
  • Host network mode — Bypasses container network isolation; the container shares the host's network stack.
  • Rootless mode detection — Rootless Docker significantly reduces the blast radius of a container escape.

Module 12 — Kubernetes

The script gracefully detects whether Kubernetes is present by checking for kubectl, kubelet, and alternative distributions (k3s, microk8s, k0s). If a cluster is reachable, it audits:

  • Privileged pods — Parsed via JSON with embedded Python to identify any pod running with securityContext.privileged: true.
  • NetworkPolicies — Absence of NetworkPolicies means all pods can communicate freely, violating the principle of least privilege.
  • Public-facing servicesLoadBalancer and NodePort services expose cluster ports to external networks and must be reviewed.

Module 14 — Kernel Parameters

Critical sysctl security values are checked individually with a clear pass/fail decision:

Parameter Expected Impact
net.ipv4.ip_forward 0 (unless router/k8s) Packet routing between interfaces
net.ipv4.conf.all.accept_source_route 0 Routing table manipulation
net.ipv4.tcp_syncookies 1 SYN flood DDoS protection
kernel.randomize_va_space 2 Memory exploitation via ASLR
kernel.yama.ptrace_scope 1 or higher Process tracing attacks
kernel.dmesg_restrict 1 Kernel memory address leakage

The Spectre/Meltdown section reads /sys/devices/system/cpu/vulnerabilities/*, which is maintained by the kernel and reflects whether hardware and kernel mitigations are active.


Reading the Report

The generated text report is structured identically to the terminal output, without ANSI escape codes. You can work with it using standard tools:

# Show only critical alerts
grep '^\[ALERT\]' /tmp/audit_*.txt

# Show all warnings and alerts
grep -E '^\[(ALERT|WARN)\]' /tmp/audit_*.txt

# Count findings by severity
echo "Alerts : $(grep -c '^\[ALERT\]' /tmp/audit_*.txt)"
echo "Warns  : $(grep -c '^\[WARN\]'  /tmp/audit_*.txt)"
echo "OK     : $(grep -c '^\[OK\]'    /tmp/audit_*.txt)"

# Jump to a specific section
grep -A 50 'SSH Configuration Audit' /tmp/audit_*.txt

Sample Report Snippet

════════════════════════════════════════════════════════════════
  ► SSH Configuration Audit
════════════════════════════════════════════════════════════════
[INFO]  SSH Port               : 22
[WARN]  Default SSH port 22 in use. Consider a non-standard port.
[ALERT] PermitRootLogin is 'yes' — direct root SSH login is dangerous!
[WARN]  PasswordAuthentication is enabled. Prefer key-based authentication only.
[OK]    PermitEmptyPasswords is disabled.
[OK]    PubkeyAuthentication is enabled.
[OK]    X11Forwarding is disabled.
[WARN]  MaxAuthTries is 6 — consider reducing to 3.
[WARN]  No login banner configured (Banner directive missing or 'none').

Use Cases

1. New Server Baseline

Run the audit immediately after provisioning a new server to establish a security baseline before deploying applications.

sudo bash linux_audit.sh -o /root/baseline_$(date +%F).txt

2. Monthly Security Review

Schedule the audit via cron and retain reports for comparison:

0 3 1 * * root /opt/scripts/linux_audit.sh -q -o /var/log/audits/audit_$(date +\%Y\%m).txt

3. Pre-Production Checklist

Run before deploying to production to catch common misconfigurations (open ports, weak SSH, missing firewall).

4. Incident Response

After a suspected breach, run the audit to quickly inventory open ports, unusual processes, unexpected users, and scheduled tasks that may have been planted.

5. CI/CD Pipeline Integration

Use the -q flag and grep for [ALERT] to fail a pipeline if critical issues are found:

#!/bin/bash
sudo bash linux_audit.sh -q -o /tmp/audit.txt
if grep -q '^\[ALERT\]' /tmp/audit.txt; then
  echo "Critical security issues found — see /tmp/audit.txt"
  exit 1
fi

6. Compliance Evidence

The timestamped, structured report can serve as evidence for security audits (ISO 27001, SOC 2, PCI DSS) demonstrating that regular system hardening reviews are performed.


Customization Guide

The script is designed to be extended. Each audit module is a self-contained audit_<name>() function.

Add a Custom Module

# Add this function anywhere in the script:
audit_my_app() {
  section "My Application Audit"

  local config="/etc/myapp/config.yml"
  if [[ -f "$config" ]]; then
    ok "Configuration file found: ${config}"
    # Check a specific setting
    if grep -q 'debug: true' "$config"; then
      alert "MyApp is running in DEBUG mode — disable in production!"
    fi
  else
    warn "MyApp configuration not found."
  fi
}

# Then call it in main():
main() {
  ...
  audit_my_app  # ← Add your module here
  audit_summary
}

Change Sensitive Port Detection Thresholds

In audit_ports(), the KNOWN_RISKY associative array maps port numbers to descriptions. You can add, remove, or modify entries:

declare -A KNOWN_RISKY=(
  [8888]="Jupyter Notebook (often unauthenticated)"
  [5000]="Flask dev server (never expose to internet)"
  [4444]="Metasploit default listener"
  # ... add your own
)

Adjust Disk Alert Threshold

In audit_hardware(), the df processing awk block uses >=90 for critical and >=80 for warnings. Change these to fit your environment:

# Original:
if(used>=90) print "[ALERT] ..."
else if(used>=80) print "[WARN] ..."

# Custom (tighter thresholds):
if(used>=85) print "[ALERT] ..."
else if(used>=70) print "[WARN] ..."

Run Only Specific Modules

Comment out unwanted audit_* calls in main():

main() {
  preflight_checks
  audit_system_info
  audit_hardware
  # audit_users        ← skip this module
  audit_ssh
  audit_firewall
  audit_ports
  # audit_docker       ← skip if no Docker
  audit_summary
}

Disable Color Output Permanently

Change the QUIET default at the top of the script:

QUIET=true   # was: false

Security & Privacy Notes

  • The script is read-only. It never writes to any system configuration, never installs software, never opens network connections, and never modifies user data.
  • Shell history inspection is limited to flagging files that contain sensitive keywords (password, token, secret). The actual content is never printed in the report.
  • Report files contain sensitive system information. Store them securely: restrict permissions to root-only and avoid storing them in web-accessible directories.
  • Applying the principle of least privilege: run the script as root only for the initial baseline audit. Subsequent runs can use a dedicated read-only audit user for most checks (some /etc/shadow and iptables checks will be skipped).

Limitations

Limitation Details
Not a replacement for dedicated tools For production-grade continuous monitoring, complement with tools like Lynis, OpenSCAP, Falco, Wazuh, or Tenable.
Static snapshot The report reflects the state of the system at execution time. Dynamic threats (malware that hides from ps, kernel rootkits) require real-time agents.
Kubernetes access The K8s audit depends on the presence of a valid kubectl context. If the cluster is remote, configure KUBECONFIG before running.
Alpine / BusyBox Some checks use GNU-specific flags (stat -c, free -h, df -T). On Alpine/BusyBox, a few lines may produce warnings — the audit still completes.
Read-only kernel parameters Some sysctl values are only writable at boot time and cannot be changed at runtime even with root.

Contributing

Pull requests and improvements are welcome. When extending the script, please follow these conventions:

  1. Each new check must use the ok(), warn(), alert(), or info() log functions — never echo directly.
  2. Each new block of checks must be wrapped in a descriptive log "${BOLD}── Title ──${RESET}" subheader.
  3. Long-running operations (file searches, update checks) must emit an info "Scanning..." message beforehand.
  4. New features must degrade gracefully: check if the relevant command exists with cmd_exists before calling it.

License

MIT License — free to use, modify, and distribute with attribution.


linux_audit.sh — Because security is not an afterthought.

#!/usr/bin/env bash
# =============================================================================
# linux_audit.sh — Complete Linux Server Security & Health Audit
# Version : 2.0.0
# Author : Security Audit Framework
# License : MIT
# =============================================================================
#
# USAGE:
# sudo bash linux_audit.sh [OPTIONS]
#
# OPTIONS:
# -o <file> Write report to a specific file (default: auto-generated)
# -q Quiet mode (no colors in terminal output)
# -h Show this help message
#
# REQUIREMENTS:
# - Bash 4.0+
# - Root or sudo privileges (strongly recommended for full coverage)
# - Standard Linux utilities (ss, ps, find, awk, etc.)
#
# =============================================================================
set -euo pipefail
IFS=$'\n\t'
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 0 — CLI Arguments & Global Configuration
# ─────────────────────────────────────────────────────────────────────────────
REPORT_DIR="/tmp"
REPORT_FILE=""
QUIET=false
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
HOSTNAME_SHORT=$(hostname -s 2>/dev/null || echo "unknown")
usage() {
grep '^# ' "$0" | sed 's/^# //'
exit 0
}
while getopts ":o:qh" opt; do
case $opt in
o) REPORT_FILE="$OPTARG" ;;
q) QUIET=true ;;
h) usage ;;
*) echo "Unknown option: -$OPTARG" >&2; exit 1 ;;
esac
done
[[ -z "$REPORT_FILE" ]] && REPORT_FILE="${REPORT_DIR}/audit_${HOSTNAME_SHORT}_${TIMESTAMP}.txt"
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 1 — Color & Formatting Helpers
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$QUIET" == false ]] && [[ -t 1 ]]; then
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
BLUE='\033[0;34m'; MAGENTA='\033[0;35m'
else
RED=''; YELLOW=''; GREEN=''; CYAN=''; BOLD=''; RESET=''; BLUE=''; MAGENTA=''
fi
# Outputs to both terminal and the report file simultaneously
log() { printf "%b\n" "$*" | tee -a "$REPORT_FILE"; }
warn() { printf "%b\n" "${YELLOW}[WARN]${RESET} $*" | tee -a "$REPORT_FILE"; }
alert() { printf "%b\n" "${RED}[ALERT]${RESET} $*" | tee -a "$REPORT_FILE"; }
ok() { printf "%b\n" "${GREEN}[OK]${RESET} $*" | tee -a "$REPORT_FILE"; }
info() { printf "%b\n" "${CYAN}[INFO]${RESET} $*" | tee -a "$REPORT_FILE"; }
# Print a clearly visible section header
section() {
local title="$1"
local line="════════════════════════════════════════════════════════════════"
log ""
log "${BOLD}${BLUE}${line}${RESET}"
log "${BOLD}${BLUE} ► $title${RESET}"
log "${BOLD}${BLUE}${line}${RESET}"
}
# Check if a command exists on the system
cmd_exists() { command -v "$1" &>/dev/null; }
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 2 — Privilege & Environment Check
# ─────────────────────────────────────────────────────────────────────────────
preflight_checks() {
section "Pre-flight Checks"
# Root check — many checks silently fail without root
if [[ $EUID -ne 0 ]]; then
warn "Script is NOT running as root. Some checks may return incomplete results."
warn "Re-run with: sudo bash $0"
else
ok "Running as root — full audit coverage enabled."
fi
# Ensure the report file is writable
touch "$REPORT_FILE" 2>/dev/null || {
echo "Cannot write to $REPORT_FILE. Check permissions." >&2
exit 1
}
info "Report will be saved to: ${BOLD}${REPORT_FILE}${RESET}"
info "Audit started at: $(date '+%A %d %B %Y — %H:%M:%S %Z')"
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 3 — System Identity & OS Information
# ─────────────────────────────────────────────────────────────────────────────
audit_system_info() {
section "System Identity & OS Information"
info "Hostname : $(hostname -f 2>/dev/null || hostname)"
info "Operating System: $(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"' || uname -o)"
info "Kernel Version : $(uname -r)"
info "Architecture : $(uname -m)"
info "Uptime : $(uptime -p 2>/dev/null || uptime)"
info "System Date/Time: $(date)"
info "Timezone : $(timedatectl 2>/dev/null | grep 'Time zone' | awk '{print $3}' || cat /etc/timezone 2>/dev/null || echo 'Unknown')"
info "Last Reboot : $(who -b 2>/dev/null | awk '{print $3, $4}' || last reboot | head -1)"
# Virtualization detection (bare metal vs VM vs container)
if cmd_exists systemd-detect-virt; then
local virt
virt=$(systemd-detect-virt 2>/dev/null || echo "unknown")
info "Virtualization : ${virt}"
[[ "$virt" == "none" ]] && ok "Bare-metal server detected." || warn "Running inside a virtualized environment: ${virt}"
fi
# Check if running inside a container
if [[ -f /.dockerenv ]]; then
warn "Docker container environment detected."
fi
if grep -qE 'docker|lxc|kubepods' /proc/1/cgroup 2>/dev/null; then
warn "Container cgroup markers found in /proc/1/cgroup."
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 4 — Hardware Resources (CPU / RAM / Disk / Swap)
# ─────────────────────────────────────────────────────────────────────────────
audit_hardware() {
section "Hardware Resources"
# CPU
log "${BOLD}── CPU ──────────────────────────────────────────────${RESET}"
local cpu_model cpu_cores cpu_threads
cpu_model=$(grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)
cpu_cores=$(grep -c '^processor' /proc/cpuinfo)
cpu_threads=$(nproc --all 2>/dev/null || echo "$cpu_cores")
info "Model : ${cpu_model:-Unknown}"
info "Cores : ${cpu_cores}"
info "Threads : ${cpu_threads}"
# Load average vs core count — flag if load is critically high
local load1 load5 load15
read -r load1 load5 load15 _ < /proc/loadavg
info "Load Average (1m/5m/15m): ${load1} / ${load5} / ${load15}"
if (( $(echo "$load1 > $cpu_cores" | bc -l 2>/dev/null || echo 0) )); then
alert "1-minute load (${load1}) exceeds CPU core count (${cpu_cores}) — possible overload!"
else
ok "Load average is within acceptable range."
fi
# RAM
log ""
log "${BOLD}── Memory (RAM) ─────────────────────────────────────${RESET}"
if cmd_exists free; then
free -h | tee -a "$REPORT_FILE"
local mem_used mem_total mem_pct
mem_used=$(free -m | awk '/^Mem:/{print $3}')
mem_total=$(free -m | awk '/^Mem:/{print $2}')
if [[ $mem_total -gt 0 ]]; then
mem_pct=$(( mem_used * 100 / mem_total ))
info "RAM Usage: ${mem_pct}% (${mem_used} MB / ${mem_total} MB)"
[[ $mem_pct -gt 90 ]] && alert "RAM usage is critically high: ${mem_pct}%"
[[ $mem_pct -gt 75 && $mem_pct -le 90 ]] && warn "RAM usage is elevated: ${mem_pct}%"
[[ $mem_pct -le 75 ]] && ok "RAM usage is normal: ${mem_pct}%"
fi
fi
# Swap
log ""
log "${BOLD}── Swap ──────────────────────────────────────────────${RESET}"
local swap_total
swap_total=$(free -m | awk '/^Swap:/{print $2}')
if [[ "${swap_total:-0}" -eq 0 ]]; then
warn "No swap space configured. This may be intentional (e.g., Kubernetes nodes) or a risk."
else
free -h | awk '/^Swap:/' | tee -a "$REPORT_FILE"
ok "Swap is configured (${swap_total} MB total)."
fi
# Disk
log ""
log "${BOLD}── Disk Usage ───────────────────────────────────────${RESET}"
df -hT --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs 2>/dev/null | tee -a "$REPORT_FILE"
# Alert on partitions above 85%
df -P --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs 2>/dev/null | awk 'NR>1{
used=$5+0
if(used>=90) print "[ALERT] Partition "$6" is "$5" full — CRITICAL!"
else if(used>=80) print "[WARN] Partition "$6" is "$5" full."
else print "[OK] Partition "$6" usage: "$5
}' | tee -a "$REPORT_FILE"
# Block devices
log ""
log "${BOLD}── Block Devices ────────────────────────────────────${RESET}"
if cmd_exists lsblk; then
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE 2>/dev/null | tee -a "$REPORT_FILE"
fi
# Network interfaces
log ""
log "${BOLD}── Network Interfaces ───────────────────────────────${RESET}"
if cmd_exists ip; then
ip -brief address show 2>/dev/null | tee -a "$REPORT_FILE"
else
ifconfig 2>/dev/null | grep -E 'inet|^[a-z]' | tee -a "$REPORT_FILE"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 5 — User Account & Privilege Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_users() {
section "User Accounts & Privilege Audit"
# Root-equivalent accounts (UID 0)
log "${BOLD}── Accounts with UID 0 (root-equivalent) ────────────${RESET}"
local uid0_accounts
uid0_accounts=$(awk -F: '$3==0{print $1}' /etc/passwd)
if [[ $(echo "$uid0_accounts" | wc -l) -gt 1 ]]; then
alert "Multiple UID 0 accounts detected:"
echo "$uid0_accounts" | tee -a "$REPORT_FILE"
else
ok "Only one UID 0 account: ${uid0_accounts}"
fi
# Accounts with interactive login shells
log ""
log "${BOLD}── Accounts with Interactive Login Shells ───────────${RESET}"
grep -E '/bin/(bash|sh|zsh|fish|dash|ksh)$' /etc/passwd | tee -a "$REPORT_FILE"
# Accounts with no password set
log ""
log "${BOLD}── Accounts without a Password ──────────────────────${RESET}"
local no_pass
no_pass=$(awk -F: '($2==""||$2=="!"|$2=="*"){print $1}' /etc/shadow 2>/dev/null || echo "Cannot read /etc/shadow — re-run as root")
if [[ -n "$no_pass" ]]; then
warn "Accounts with no valid password:"
echo "$no_pass" | tee -a "$REPORT_FILE"
else
ok "All accounts have passwords set."
fi
# Password expiry & aging policy
log ""
log "${BOLD}── Password Aging Policy (/etc/login.defs) ──────────${RESET}"
grep -E '^(PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_WARN_AGE)' /etc/login.defs 2>/dev/null | tee -a "$REPORT_FILE"
# Sudo privileges — who can run what
log ""
log "${BOLD}── Sudo Configuration ───────────────────────────────${RESET}"
if [[ -f /etc/sudoers ]]; then
grep -v '^#\|^$' /etc/sudoers 2>/dev/null | tee -a "$REPORT_FILE" || warn "Cannot read /etc/sudoers (need root)."
fi
if [[ -d /etc/sudoers.d ]]; then
info "Additional sudoers.d files:"
ls /etc/sudoers.d/ 2>/dev/null | tee -a "$REPORT_FILE"
fi
# Members of privileged groups
log ""
log "${BOLD}── Members of Privileged Groups ─────────────────────${RESET}"
for grp in root sudo wheel docker admin; do
local members
members=$(getent group "$grp" 2>/dev/null | cut -d: -f4)
if [[ -n "$members" ]]; then
info "Group '${grp}': ${members}"
[[ "$grp" == "docker" ]] && warn "Docker group members can escalate to root via container escape."
fi
done
# Last login activity
log ""
log "${BOLD}── Last 10 User Logins ───────────────────────────────${RESET}"
last -n 10 2>/dev/null | tee -a "$REPORT_FILE"
# Currently logged-in users
log ""
log "${BOLD}── Currently Logged-in Users ────────────────────────${RESET}"
who 2>/dev/null | tee -a "$REPORT_FILE"
# Failed login attempts
log ""
log "${BOLD}── Failed Login Attempts (last 20) ──────────────────${RESET}"
if cmd_exists lastb; then
lastb -n 20 2>/dev/null | tee -a "$REPORT_FILE" || warn "lastb unavailable or insufficient privileges."
else
warn "lastb command not found. Check auth logs manually."
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 6 — SSH Configuration Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_ssh() {
section "SSH Configuration Audit"
local sshd_config="/etc/ssh/sshd_config"
if [[ ! -f "$sshd_config" ]]; then
warn "SSH daemon config not found at $sshd_config — SSH may not be installed."
return
fi
# Helper: extract value from sshd_config (last uncommented directive wins)
get_ssh_val() {
grep -iE "^\s*$1\s" "$sshd_config" | tail -1 | awk '{print $2}'
}
local port root_login pw_auth permit_empty pubkey x11 max_auth max_sess banner
port=$(get_ssh_val Port); port=${port:-22}
root_login=$(get_ssh_val PermitRootLogin); root_login=${root_login:-yes}
pw_auth=$(get_ssh_val PasswordAuthentication); pw_auth=${pw_auth:-yes}
permit_empty=$(get_ssh_val PermitEmptyPasswords); permit_empty=${permit_empty:-no}
pubkey=$(get_ssh_val PubkeyAuthentication); pubkey=${pubkey:-yes}
x11=$(get_ssh_val X11Forwarding); x11=${x11:-no}
max_auth=$(get_ssh_val MaxAuthTries); max_auth=${max_auth:-6}
max_sess=$(get_ssh_val MaxSessions); max_sess=${max_sess:-10}
banner=$(get_ssh_val Banner); banner=${banner:-none}
info "SSH Port : ${port}"
[[ "$port" == "22" ]] && warn "Default SSH port 22 in use. Consider a non-standard port." || ok "Non-default SSH port: ${port}"
[[ "$root_login" =~ ^(yes|without-password|forced-commands-only)$ ]] && \
alert "PermitRootLogin is '${root_login}' — direct root SSH login is dangerous!" || \
ok "PermitRootLogin is '${root_login}'"
[[ "$pw_auth" =~ ^[Yy]es$ ]] && \
warn "PasswordAuthentication is enabled. Prefer key-based authentication only." || \
ok "PasswordAuthentication is disabled — key-based auth only."
[[ "$permit_empty" =~ ^[Yy]es$ ]] && \
alert "PermitEmptyPasswords is ENABLED — critical security risk!" || \
ok "PermitEmptyPasswords is disabled."
[[ "$pubkey" =~ ^[Yy]es$ ]] && ok "PubkeyAuthentication is enabled." || warn "PubkeyAuthentication is disabled."
[[ "$x11" =~ ^[Yy]es$ ]] && warn "X11Forwarding is enabled. Disable if not needed." || ok "X11Forwarding is disabled."
local max_auth_int=${max_auth//[^0-9]/}
[[ -n "$max_auth_int" && "$max_auth_int" -gt 4 ]] && \
warn "MaxAuthTries is ${max_auth} — consider reducing to 3." || \
ok "MaxAuthTries: ${max_auth}"
info "MaxSessions : ${max_sess}"
[[ "$banner" == "none" ]] && warn "No login banner configured (Banner directive missing or 'none')." || ok "Login banner: ${banner}"
# Check authorized_keys for all users
log ""
log "${BOLD}── Authorized SSH Keys ───────────────────────────────${RESET}"
while IFS=: read -r user _ uid _ _ home _; do
local ak="${home}/.ssh/authorized_keys"
if [[ -f "$ak" ]]; then
local key_count
key_count=$(grep -c '^ssh-' "$ak" 2>/dev/null || echo 0)
info "User '${user}' (UID ${uid}): ${key_count} authorized key(s) in ${ak}"
# Warn about world-readable auth key files
if [[ -r "$ak" ]]; then
local perms
perms=$(stat -c '%a' "$ak" 2>/dev/null)
[[ "$perms" != "600" && "$perms" != "640" ]] && warn " Permissions on ${ak}: ${perms} (should be 600)"
fi
fi
done < /etc/passwd
# Check SSH host keys
log ""
log "${BOLD}── SSH Host Keys ─────────────────────────────────────${RESET}"
for key in /etc/ssh/ssh_host_*_key; do
[[ -f "$key" ]] && info "Found host key: ${key} ($(stat -c '%a' "$key" 2>/dev/null))"
done
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 7 — Firewall Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_firewall() {
section "Firewall Configuration"
local fw_found=false
# UFW (Uncomplicated Firewall — common on Debian/Ubuntu)
if cmd_exists ufw; then
fw_found=true
log "${BOLD}── UFW Status ────────────────────────────────────────${RESET}"
local ufw_status
ufw_status=$(ufw status verbose 2>/dev/null || echo "Unable to query UFW")
echo "$ufw_status" | tee -a "$REPORT_FILE"
echo "$ufw_status" | grep -qi "inactive" && alert "UFW is installed but INACTIVE — server has no firewall protection!" || ok "UFW is active."
fi
# firewalld (common on RHEL/CentOS/Fedora)
if cmd_exists firewall-cmd; then
fw_found=true
log "${BOLD}── firewalld Status ──────────────────────────────────${RESET}"
if systemctl is-active firewalld &>/dev/null; then
ok "firewalld is running."
firewall-cmd --list-all 2>/dev/null | tee -a "$REPORT_FILE"
else
alert "firewalld is installed but NOT running."
fi
fi
# iptables — raw rules dump
if cmd_exists iptables; then
fw_found=true
log "${BOLD}── iptables Rules ────────────────────────────────────${RESET}"
iptables -L -n -v --line-numbers 2>/dev/null | tee -a "$REPORT_FILE" || warn "Cannot read iptables rules."
# Check for a default DROP/REJECT policy on INPUT chain
local input_policy
input_policy=$(iptables -L INPUT 2>/dev/null | head -1 | awk '{print $NF}')
[[ "$input_policy" == "ACCEPT" ]] && alert "iptables INPUT chain default policy is ACCEPT — not restrictive!" || ok "iptables INPUT default policy: ${input_policy}"
fi
# nftables
if cmd_exists nft; then
fw_found=true
log "${BOLD}── nftables Ruleset ──────────────────────────────────${RESET}"
nft list ruleset 2>/dev/null | tee -a "$REPORT_FILE" || warn "Cannot read nftables ruleset."
fi
[[ "$fw_found" == false ]] && alert "No firewall detected (UFW, firewalld, iptables, nftables)!"
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 8 — Open Ports & Network Connections
# ─────────────────────────────────────────────────────────────────────────────
audit_ports() {
section "Open Ports & Network Connections"
log "${BOLD}── Listening TCP/UDP Ports ──────────────────────────${RESET}"
if cmd_exists ss; then
ss -tulnp 2>/dev/null | tee -a "$REPORT_FILE"
elif cmd_exists netstat; then
netstat -tulnp 2>/dev/null | tee -a "$REPORT_FILE"
else
warn "Neither 'ss' nor 'netstat' found. Install iproute2 or net-tools."
fi
# Highlight known dangerous / unexpected ports
log ""
log "${BOLD}── Sensitive Port Check ──────────────────────────────${RESET}"
declare -A KNOWN_RISKY=(
[21]="FTP (cleartext — use SFTP instead)"
[23]="Telnet (cleartext — critical risk)"
[25]="SMTP (open relay risk)"
[110]="POP3 (cleartext mail)"
[143]="IMAP (cleartext mail)"
[512]="rexec (remote exec — legacy & dangerous)"
[513]="rlogin (remote login — legacy & dangerous)"
[514]="rsh/syslog (dangerous if exposed)"
[5900]="VNC (remote desktop — ensure tunneled)"
[3389]="RDP (Windows Remote Desktop)"
[6379]="Redis (often unauthenticated)"
[27017]="MongoDB (check auth)"
[5432]="PostgreSQL"
[3306]="MySQL/MariaDB"
[11211]="Memcached (often unauthenticated)"
[9200]="Elasticsearch (REST API — often open)"
[8080]="HTTP alternate"
[8443]="HTTPS alternate"
)
for port in "${!KNOWN_RISKY[@]}"; do
if ss -tuln 2>/dev/null | grep -qE ":${port}\b"; then
warn "Port ${port} is OPEN — ${KNOWN_RISKY[$port]}"
fi
done
# Established connections (who is currently talking to us)
log ""
log "${BOLD}── Established Network Connections ──────────────────${RESET}"
ss -tnp state established 2>/dev/null | tee -a "$REPORT_FILE"
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 9 — Running Processes & Active Services
# ─────────────────────────────────────────────────────────────────────────────
audit_services() {
section "Running Processes & Active Services"
# Top CPU & memory consumers
log "${BOLD}── Top 15 Processes by CPU ───────────────────────────${RESET}"
ps aux --sort=-%cpu 2>/dev/null | head -16 | tee -a "$REPORT_FILE"
log ""
log "${BOLD}── Top 15 Processes by Memory ───────────────────────${RESET}"
ps aux --sort=-%mem 2>/dev/null | head -16 | tee -a "$REPORT_FILE"
# Systemd services
log ""
log "${BOLD}── Active systemd Services ──────────────────────────${RESET}"
if cmd_exists systemctl; then
systemctl list-units --type=service --state=running --no-pager 2>/dev/null | tee -a "$REPORT_FILE"
log ""
log "${BOLD}── Failed systemd Services ──────────────────────────${RESET}"
local failed
failed=$(systemctl list-units --type=service --state=failed --no-pager 2>/dev/null)
if echo "$failed" | grep -q '\.service'; then
alert "There are FAILED systemd services:"
echo "$failed" | tee -a "$REPORT_FILE"
else
ok "No failed systemd services detected."
fi
else
warn "systemctl not found — system may use SysV init or another init system."
service --status-all 2>/dev/null | tee -a "$REPORT_FILE" || true
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 10 — Dangerous File Permissions
# ─────────────────────────────────────────────────────────────────────────────
audit_permissions() {
section "Dangerous File Permissions"
# SUID / SGID binaries — can be exploited for privilege escalation
log "${BOLD}── SUID Binaries ──────────────────────────────────────${RESET}"
info "Searching for SUID binaries (this may take a moment)..."
find / -xdev -perm /4000 -type f 2>/dev/null | sort | tee -a "$REPORT_FILE"
log ""
log "${BOLD}── SGID Binaries ──────────────────────────────────────${RESET}"
find / -xdev -perm /2000 -type f 2>/dev/null | sort | tee -a "$REPORT_FILE"
# World-writable files (any user can modify)
log ""
log "${BOLD}── World-Writable Files (excl. /proc, /sys, /dev) ───${RESET}"
info "Searching for world-writable files..."
find / -xdev -not \( -path '/proc/*' -o -path '/sys/*' -o -path '/dev/*' -o -path '/run/*' \) \
-perm -o+w -type f 2>/dev/null | head -50 | tee -a "$REPORT_FILE"
# World-writable directories without sticky bit
log ""
log "${BOLD}── World-Writable Directories (no sticky bit) ───────${RESET}"
find / -xdev -not \( -path '/proc/*' -o -path '/sys/*' -o -path '/dev/*' \) \
-perm -0002 -not -perm -1000 -type d 2>/dev/null | tee -a "$REPORT_FILE"
# Files with no owner
log ""
log "${BOLD}── Files with No Owner (orphaned) ───────────────────${RESET}"
find / -xdev -not \( -path '/proc/*' -o -path '/sys/*' \) \
-nouser -o -nogroup 2>/dev/null | head -30 | tee -a "$REPORT_FILE"
# Critical config files — check permissions
log ""
log "${BOLD}── Critical Config File Permissions ────────────────${RESET}"
for f in /etc/passwd /etc/shadow /etc/group /etc/sudoers /etc/ssh/sshd_config; do
if [[ -f "$f" ]]; then
local perm owner
perm=$(stat -c '%a' "$f" 2>/dev/null)
owner=$(stat -c '%U:%G' "$f" 2>/dev/null)
info "${f}: permissions=${perm}, owner=${owner}"
# shadow should be 000 or 640
[[ "$f" == "/etc/shadow" && "$perm" != "000" && "$perm" != "640" ]] && \
alert "/etc/shadow permissions are ${perm} — should be 000 or 640!"
fi
done
# /tmp and /var/tmp sticky bits
log ""
log "${BOLD}── Sticky Bit on /tmp /var/tmp ──────────────────────${RESET}"
for tmpdir in /tmp /var/tmp; do
if [[ -d "$tmpdir" ]]; then
local sticky
sticky=$(stat -c '%a' "$tmpdir" 2>/dev/null)
[[ "${sticky: -1}" == "1" || "${sticky: -1}" == "7" ]] && \
ok "${tmpdir} has sticky bit set (${sticky})." || \
alert "${tmpdir} is MISSING the sticky bit (${sticky}) — privilege escalation risk!"
fi
done
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 11 — Cron Jobs Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_cron() {
section "Cron Jobs Audit"
# System-wide crontab
log "${BOLD}── /etc/crontab ─────────────────────────────────────${RESET}"
[[ -f /etc/crontab ]] && cat /etc/crontab | tee -a "$REPORT_FILE" || info "/etc/crontab not found."
# Cron directories
log ""
log "${BOLD}── /etc/cron.* Directories ──────────────────────────${RESET}"
for crondir in /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly; do
if [[ -d "$crondir" ]]; then
info "Contents of ${crondir}:"
ls -la "$crondir" 2>/dev/null | tee -a "$REPORT_FILE"
# Show content of each file
for f in "$crondir"/*; do
[[ -f "$f" ]] && { echo " → ${f}:"; cat "$f" | tee -a "$REPORT_FILE"; }
done
fi
done
# Per-user crontabs
log ""
log "${BOLD}── Per-User Crontabs ─────────────────────────────────${RESET}"
local cron_spool="/var/spool/cron"
[[ -d "/var/spool/cron/crontabs" ]] && cron_spool="/var/spool/cron/crontabs"
if [[ -d "$cron_spool" ]]; then
for f in "$cron_spool"/*; do
if [[ -f "$f" ]]; then
info "Crontab for user '$(basename "$f")':"
cat "$f" | tee -a "$REPORT_FILE"
fi
done
else
info "No per-user cron spool directory found."
fi
# at jobs
log ""
log "${BOLD}── at / batch Jobs ──────────────────────────────────${RESET}"
if cmd_exists atq; then
atq 2>/dev/null | tee -a "$REPORT_FILE" || info "No 'at' jobs found or atd not running."
else
info "'at' command not installed."
fi
# systemd timers (modern replacement for cron)
log ""
log "${BOLD}── systemd Timers ────────────────────────────────────${RESET}"
if cmd_exists systemctl; then
systemctl list-timers --all --no-pager 2>/dev/null | tee -a "$REPORT_FILE"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 12 — Docker Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_docker() {
section "Docker Security Audit"
if ! cmd_exists docker; then
info "Docker is not installed on this system. Skipping."
return
fi
if ! docker info &>/dev/null; then
warn "Docker is installed but the daemon is not running or access is denied."
return
fi
# Docker version
log "${BOLD}── Docker Version ─────────────────────────────────────${RESET}"
docker version 2>/dev/null | tee -a "$REPORT_FILE"
# Docker daemon info (security options, rootless, etc.)
log ""
log "${BOLD}── Docker Daemon Info ────────────────────────────────${RESET}"
docker info 2>/dev/null | grep -E 'Security|Root|User|Debug|Experimental|Cgroup|Insecure' | tee -a "$REPORT_FILE"
# Check if Docker is rootless
local rootless
rootless=$(docker info 2>/dev/null | grep -i 'rootless' || echo "")
[[ -z "$rootless" ]] && warn "Docker does not appear to be running in rootless mode." || ok "Docker rootless mode detected."
# Running containers
log ""
log "${BOLD}── Running Containers ───────────────────────────────${RESET}"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null | tee -a "$REPORT_FILE"
# All containers (including stopped)
log ""
log "${BOLD}── All Containers (inc. stopped) ────────────────────${RESET}"
docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null | tee -a "$REPORT_FILE"
# Docker images
log ""
log "${BOLD}── Docker Images ─────────────────────────────────────${RESET}"
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" 2>/dev/null | tee -a "$REPORT_FILE"
# Privileged containers — critical risk!
log ""
log "${BOLD}── Privileged Containers ─────────────────────────────${RESET}"
local privileged_containers
privileged_containers=$(docker ps -q 2>/dev/null | xargs -I{} docker inspect {} --format '{{.Name}}: Privileged={{.HostConfig.Privileged}}' 2>/dev/null | grep 'Privileged=true' || true)
if [[ -n "$privileged_containers" ]]; then
alert "Privileged containers found (full host access — critical risk):"
echo "$privileged_containers" | tee -a "$REPORT_FILE"
else
ok "No privileged containers running."
fi
# Containers with host network mode
log ""
log "${BOLD}── Containers with Host Network ─────────────────────${RESET}"
docker ps -q 2>/dev/null | xargs -I{} docker inspect {} --format '{{.Name}}: Network={{.HostConfig.NetworkMode}}' 2>/dev/null | grep 'NetworkMode=host' | tee -a "$REPORT_FILE" || ok "No containers using host network mode."
# Docker volumes
log ""
log "${BOLD}── Docker Volumes ─────────────────────────────────────${RESET}"
docker volume ls 2>/dev/null | tee -a "$REPORT_FILE"
# Docker networks
log ""
log "${BOLD}── Docker Networks ───────────────────────────────────${RESET}"
docker network ls 2>/dev/null | tee -a "$REPORT_FILE"
# Docker socket exposure (major privilege escalation vector)
log ""
log "${BOLD}── Docker Socket (/var/run/docker.sock) ─────────────${RESET}"
if [[ -S /var/run/docker.sock ]]; then
local sock_perms
sock_perms=$(stat -c '%a %U:%G' /var/run/docker.sock 2>/dev/null)
warn "Docker socket is present: /var/run/docker.sock (${sock_perms})"
warn "Mounting this socket into a container grants root-equivalent access to the host."
# Check which containers mount the socket
local sock_containers
sock_containers=$(docker ps -q 2>/dev/null | xargs -I{} docker inspect {} --format '{{.Name}}: {{.HostConfig.Binds}}' 2>/dev/null | grep 'docker.sock' || true)
[[ -n "$sock_containers" ]] && alert "Containers with docker.sock mounted: ${sock_containers}"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 13 — Kubernetes Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_kubernetes() {
section "Kubernetes Audit"
local k8s_found=false
# Check for kubectl
if cmd_exists kubectl; then
k8s_found=true
log "${BOLD}── kubectl Version & Cluster Info ───────────────────${RESET}"
kubectl version --short 2>/dev/null | tee -a "$REPORT_FILE" || true
kubectl cluster-info 2>/dev/null | tee -a "$REPORT_FILE" || warn "Cannot reach Kubernetes cluster."
log ""
log "${BOLD}── Node Status ───────────────────────────────────────${RESET}"
kubectl get nodes -o wide 2>/dev/null | tee -a "$REPORT_FILE" || true
log ""
log "${BOLD}── Pods Across All Namespaces ───────────────────────${RESET}"
kubectl get pods --all-namespaces 2>/dev/null | tee -a "$REPORT_FILE" || true
log ""
log "${BOLD}── Privileged Pods ───────────────────────────────────${RESET}"
kubectl get pods --all-namespaces -o json 2>/dev/null | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
for item in data.get('items', []):
ns = item['metadata']['namespace']
name = item['metadata']['name']
for c in item['spec'].get('containers', []):
sc = c.get('securityContext', {})
if sc.get('privileged'):
print(f'[ALERT] Privileged pod: {ns}/{name} (container: {c[\"name\"]})')
" 2>/dev/null | tee -a "$REPORT_FILE" || true
log ""
log "${BOLD}── RBAC — ClusterRoleBindings ───────────────────────${RESET}"
kubectl get clusterrolebindings -o wide 2>/dev/null | head -30 | tee -a "$REPORT_FILE" || true
log ""
log "${BOLD}── Network Policies ──────────────────────────────────${RESET}"
local np_count
np_count=$(kubectl get networkpolicies --all-namespaces 2>/dev/null | wc -l)
if [[ "$np_count" -lt 2 ]]; then
warn "No NetworkPolicies found. All pod-to-pod traffic is allowed by default."
else
ok "NetworkPolicies are configured."
kubectl get networkpolicies --all-namespaces 2>/dev/null | tee -a "$REPORT_FILE"
fi
log ""
log "${BOLD}── Secrets Count per Namespace ──────────────────────${RESET}"
kubectl get secrets --all-namespaces 2>/dev/null | tee -a "$REPORT_FILE" || true
log ""
log "${BOLD}── Services (LoadBalancer / NodePort — public exposure) ──${RESET}"
kubectl get svc --all-namespaces 2>/dev/null | grep -E 'LoadBalancer|NodePort' | tee -a "$REPORT_FILE" || info "No LoadBalancer or NodePort services found."
fi
# kubelet process
if pgrep -x kubelet &>/dev/null; then
k8s_found=true
ok "kubelet process is running on this node."
fi
# k3s / k0s / microk8s
for alt in k3s microk8s k0s; do
if cmd_exists "$alt"; then
k8s_found=true
info "Lightweight Kubernetes distribution detected: ${alt}"
fi
done
[[ "$k8s_found" == false ]] && info "Kubernetes does not appear to be installed or running on this system."
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 14 — Package & Software Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_packages() {
section "Package & Software Audit"
# Pending security updates
log "${BOLD}── Pending Security Updates ─────────────────────────${RESET}"
if cmd_exists apt; then
apt-get update -qq 2>/dev/null
local upgradeable
upgradeable=$(apt list --upgradeable 2>/dev/null | grep -c '\[upgradeable' || echo 0)
info "Packages with available updates: ${upgradeable}"
[[ "$upgradeable" -gt 0 ]] && warn "Run 'apt upgrade' to apply updates." || ok "System is fully up-to-date."
apt list --upgradeable 2>/dev/null | head -20 | tee -a "$REPORT_FILE"
elif cmd_exists yum; then
yum check-update --quiet 2>/dev/null | tee -a "$REPORT_FILE" || true
elif cmd_exists dnf; then
dnf check-update --quiet 2>/dev/null | tee -a "$REPORT_FILE" || true
fi
# Installed package count
log ""
log "${BOLD}── Installed Package Count ──────────────────────────${RESET}"
if cmd_exists dpkg; then
info "dpkg packages installed: $(dpkg -l 2>/dev/null | grep -c '^ii')"
elif cmd_exists rpm; then
info "RPM packages installed: $(rpm -qa 2>/dev/null | wc -l)"
fi
# Check for known security-relevant tools
log ""
log "${BOLD}── Security Tool Availability ───────────────────────${RESET}"
for tool in fail2ban rkhunter chkrootkit aide auditd lynis apparmor selinux; do
cmd_exists "$tool" && ok "${tool} is installed." || info "${tool} not found."
done
# AppArmor status
log ""
log "${BOLD}── AppArmor Status ───────────────────────────────────${RESET}"
if cmd_exists aa-status; then
aa-status 2>/dev/null | tee -a "$REPORT_FILE"
elif cmd_exists apparmor_status; then
apparmor_status 2>/dev/null | tee -a "$REPORT_FILE"
else
info "AppArmor not found."
fi
# SELinux status
log ""
log "${BOLD}── SELinux Status ────────────────────────────────────${RESET}"
if cmd_exists getenforce; then
local selinux_mode
selinux_mode=$(getenforce 2>/dev/null)
info "SELinux mode: ${selinux_mode}"
[[ "$selinux_mode" == "Enforcing" ]] && ok "SELinux is enforcing." || warn "SELinux is ${selinux_mode} — not actively enforcing policies."
elif cmd_exists sestatus; then
sestatus 2>/dev/null | tee -a "$REPORT_FILE"
else
info "SELinux not found on this system."
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 15 — Kernel Parameters & Security Hardening (sysctl)
# ─────────────────────────────────────────────────────────────────────────────
audit_kernel() {
section "Kernel Parameters & Security Hardening"
# Helper: read a sysctl value
sysctl_get() { sysctl -n "$1" 2>/dev/null || echo "N/A"; }
# IP forwarding (required for routers/NAT — risky on regular servers)
local ipfwd
ipfwd=$(sysctl_get net.ipv4.ip_forward)
[[ "$ipfwd" == "1" ]] && warn "IP forwarding is ENABLED (net.ipv4.ip_forward=1) — acceptable for routers/k8s nodes only." || ok "IP forwarding is disabled."
# ICMP redirects
local icmp_redir
icmp_redir=$(sysctl_get net.ipv4.conf.all.accept_redirects)
[[ "$icmp_redir" == "1" ]] && warn "ICMP redirects accepted (net.ipv4.conf.all.accept_redirects=1) — risk of routing manipulation." || ok "ICMP redirects disabled."
# Source routing
local src_route
src_route=$(sysctl_get net.ipv4.conf.all.accept_source_route)
[[ "$src_route" == "1" ]] && alert "Source routing ENABLED (net.ipv4.conf.all.accept_source_route=1) — disable immediately!" || ok "Source routing disabled."
# SYN cookies (DDoS protection)
local syn_cookies
syn_cookies=$(sysctl_get net.ipv4.tcp_syncookies)
[[ "$syn_cookies" == "1" ]] && ok "TCP SYN cookies enabled (DDoS protection)." || warn "TCP SYN cookies disabled — vulnerable to SYN flood attacks."
# ASLR (Address Space Layout Randomization)
local aslr
aslr=$(sysctl_get kernel.randomize_va_space)
case "$aslr" in
2) ok "ASLR is fully enabled (kernel.randomize_va_space=2)." ;;
1) warn "ASLR is partially enabled (=1). Set to 2 for full protection." ;;
0) alert "ASLR is DISABLED (=0) — serious memory exploitation risk!" ;;
*) info "ASLR status unknown." ;;
esac
# Core dumps
local core
core=$(sysctl_get kernel.core_pattern)
info "Core dump pattern: ${core}"
local core_limit
core_limit=$(ulimit -c 2>/dev/null || echo "unknown")
[[ "$core_limit" == "unlimited" ]] && warn "Core dumps are unlimited — may expose sensitive memory data."
# Dmesg restriction
local dmesg_restrict
dmesg_restrict=$(sysctl_get kernel.dmesg_restrict)
[[ "$dmesg_restrict" == "1" ]] && ok "dmesg output is restricted to root (kernel.dmesg_restrict=1)." || warn "dmesg is readable by non-root users (kernel.dmesg_restrict=0)."
# ptrace scope
local ptrace
ptrace=$(sysctl_get kernel.yama.ptrace_scope)
case "$ptrace" in
0) warn "ptrace scope is 0 — any process can be traced by any other." ;;
1) ok "ptrace scope is 1 — only parent processes can trace children." ;;
2) ok "ptrace scope is 2 — only admin can use ptrace." ;;
3) ok "ptrace is fully disabled (scope=3)." ;;
*) info "ptrace scope: ${ptrace:-not configured}" ;;
esac
# NX/DEP (Execute Disable — hardware level)
if grep -q ' nx ' /proc/cpuinfo 2>/dev/null; then
ok "NX/XD (No-Execute / Execute Disable) bit is supported by CPU."
else
warn "NX bit not detected in CPU flags."
fi
# Spectre/Meltdown mitigations
log ""
log "${BOLD}── CPU Vulnerability Mitigations ────────────────────${RESET}"
if [[ -d /sys/devices/system/cpu/vulnerabilities ]]; then
for v in /sys/devices/system/cpu/vulnerabilities/*; do
local vuln_status
vuln_status=$(cat "$v" 2>/dev/null)
local vuln_name
vuln_name=$(basename "$v")
if echo "$vuln_status" | grep -qi 'vulnerable'; then
alert "${vuln_name}: ${vuln_status}"
else
ok "${vuln_name}: ${vuln_status}"
fi
done
else
info "CPU vulnerability information not available at /sys/devices/system/cpu/vulnerabilities"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 16 — Log & Audit Trail Inspection
# ─────────────────────────────────────────────────────────────────────────────
audit_logs() {
section "Log & Audit Trail Inspection"
# Check auditd
log "${BOLD}── Auditd (Linux Audit Daemon) ──────────────────────${RESET}"
if cmd_exists auditctl; then
if systemctl is-active auditd &>/dev/null || service auditd status &>/dev/null 2>&1; then
ok "auditd is running."
auditctl -l 2>/dev/null | tee -a "$REPORT_FILE"
else
warn "auditd is installed but not running."
fi
else
warn "auditd not installed. Security events may not be logged."
fi
# Syslog / journald
log ""
log "${BOLD}── Logging Daemon ────────────────────────────────────${RESET}"
if cmd_exists journalctl; then
ok "systemd-journald is available."
journalctl --disk-usage 2>/dev/null | tee -a "$REPORT_FILE"
fi
for log_daemon in rsyslog syslog syslog-ng; do
systemctl is-active "$log_daemon" &>/dev/null 2>&1 && ok "${log_daemon} is active." || true
done
# Recent auth failures (from journal or auth.log)
log ""
log "${BOLD}── Recent Authentication Failures (last 25) ─────────${RESET}"
if cmd_exists journalctl; then
journalctl -u ssh -u sshd --since "24 hours ago" --no-pager 2>/dev/null | \
grep -i 'failed\|invalid\|error' | tail -25 | tee -a "$REPORT_FILE" || info "No recent SSH failures in journal."
elif [[ -f /var/log/auth.log ]]; then
grep -i 'failed\|invalid' /var/log/auth.log 2>/dev/null | tail -25 | tee -a "$REPORT_FILE"
elif [[ -f /var/log/secure ]]; then
grep -i 'failed\|invalid' /var/log/secure 2>/dev/null | tail -25 | tee -a "$REPORT_FILE"
fi
# Sudo usage audit
log ""
log "${BOLD}── Recent sudo Usage (last 20) ──────────────────────${RESET}"
if cmd_exists journalctl; then
journalctl -u sudo --no-pager --since "7 days ago" 2>/dev/null | tail -20 | tee -a "$REPORT_FILE" || true
fi
grep 'sudo' /var/log/auth.log 2>/dev/null | tail -20 | tee -a "$REPORT_FILE" || true
# Log rotation config
log ""
log "${BOLD}── Log Rotation (logrotate) ─────────────────────────${RESET}"
[[ -f /etc/logrotate.conf ]] && ok "logrotate.conf found." || warn "No logrotate.conf found."
if [[ -d /etc/logrotate.d ]]; then
info "logrotate.d entries: $(ls /etc/logrotate.d/ | wc -l)"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 17 — Network Configuration & Security
# ─────────────────────────────────────────────────────────────────────────────
audit_network() {
section "Network Configuration & Security"
# DNS configuration
log "${BOLD}── DNS Configuration ─────────────────────────────────${RESET}"
cat /etc/resolv.conf 2>/dev/null | tee -a "$REPORT_FILE"
# Hosts file
log ""
log "${BOLD}── /etc/hosts ────────────────────────────────────────${RESET}"
cat /etc/hosts 2>/dev/null | tee -a "$REPORT_FILE"
# Routing table
log ""
log "${BOLD}── Routing Table ─────────────────────────────────────${RESET}"
if cmd_exists ip; then
ip route show 2>/dev/null | tee -a "$REPORT_FILE"
else
route -n 2>/dev/null | tee -a "$REPORT_FILE"
fi
# ARP cache
log ""
log "${BOLD}── ARP Cache ─────────────────────────────────────────${RESET}"
if cmd_exists ip; then
ip neigh show 2>/dev/null | tee -a "$REPORT_FILE"
else
arp -n 2>/dev/null | tee -a "$REPORT_FILE"
fi
# hosts.allow / hosts.deny (TCP Wrappers)
log ""
log "${BOLD}── TCP Wrappers (hosts.allow / hosts.deny) ──────────${RESET}"
for f in /etc/hosts.allow /etc/hosts.deny; do
if [[ -f "$f" ]]; then
info "Contents of ${f}:"
grep -v '^#\|^$' "$f" 2>/dev/null | tee -a "$REPORT_FILE" || true
fi
done
# Wireshark / tcpdump check (capture tools)
log ""
log "${BOLD}── Packet Capture Tools ──────────────────────────────${RESET}"
for tool in tcpdump wireshark tshark; do
cmd_exists "$tool" && warn "${tool} is installed — ensure it is authorized." || info "${tool}: not installed."
done
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 18 — Sensitive File & Directory Audit
# ─────────────────────────────────────────────────────────────────────────────
audit_sensitive_files() {
section "Sensitive Files & Directories"
# Environment files that might contain secrets
log "${BOLD}── Potential Secret Files ────────────────────────────${RESET}"
local secret_patterns=(".env" "*.pem" "*.key" "id_rsa" "id_dsa" "id_ecdsa" "id_ed25519" "*.p12" "*.pfx" "*.kdbx")
for pattern in "${secret_patterns[@]}"; do
find /home /root /etc /var /opt 2>/dev/null -name "$pattern" -not -path '*/proc/*' 2>/dev/null | head -10 | while read -r f; do
warn "Potentially sensitive file found: ${f} ($(stat -c '%a %U:%G' "$f" 2>/dev/null))"
done
done
# Private SSH keys with bad permissions
log ""
log "${BOLD}── Private SSH Keys Permission Check ────────────────${RESET}"
find /home /root 2>/dev/null -name 'id_*' -not -name '*.pub' 2>/dev/null | while read -r key; do
local perm
perm=$(stat -c '%a' "$key" 2>/dev/null)
[[ "$perm" != "600" && "$perm" != "400" ]] && alert "SSH key ${key} has insecure permissions: ${perm} (should be 600)" || ok "Key ${key}: ${perm}"
done
# .bash_history with credentials
log ""
log "${BOLD}── Shell History Files ───────────────────────────────${RESET}"
find /home /root 2>/dev/null -name '.*history' 2>/dev/null | while read -r hist; do
info "Found: ${hist} ($(wc -l < "$hist" 2>/dev/null) lines)"
# Hint at potential credential leaks without printing actual content
if grep -qiE '(password|passwd|secret|token|api_key|auth)' "$hist" 2>/dev/null; then
alert "${hist} may contain sensitive keywords (password/token/secret). Review manually."
fi
done
# World-readable /root
log ""
log "${BOLD}── /root Directory Permissions ──────────────────────${RESET}"
if [[ -d /root ]]; then
local root_perm
root_perm=$(stat -c '%a' /root)
[[ "$root_perm" != "700" && "$root_perm" != "750" ]] && \
alert "/root directory permissions: ${root_perm} (should be 700)." || \
ok "/root directory permissions: ${root_perm}"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 19 — Systemd & Init Security
# ─────────────────────────────────────────────────────────────────────────────
audit_init() {
section "Init System & Startup Security"
log "${BOLD}── Init System ───────────────────────────────────────${RESET}"
local init_cmd
init_cmd=$(readlink /proc/1/exe 2>/dev/null || ls -la /proc/1/exe 2>/dev/null)
info "PID 1 (init): ${init_cmd}"
# Systemd security hardening for service units
log ""
log "${BOLD}── systemd Service Security Hardening (sample) ──────${RESET}"
if cmd_exists systemctl; then
info "Checking for services without security sandboxing (first 15 running services):"
systemctl list-units --type=service --state=running --no-pager 2>/dev/null | awk '/\.service/{print $1}' | head -15 | while read -r svc; do
local priv_users protect_sys
priv_users=$(systemctl show "$svc" -p PrivateUsers --value 2>/dev/null)
protect_sys=$(systemctl show "$svc" -p ProtectSystem --value 2>/dev/null)
if [[ "$priv_users" == "no" && "$protect_sys" == "no" ]]; then
warn "${svc}: No PrivateUsers or ProtectSystem hardening."
fi
done
fi
# rc.local / legacy init scripts
log ""
log "${BOLD}── Legacy Startup Scripts ────────────────────────────${RESET}"
for rc in /etc/rc.local /etc/rc.d/rc.local; do
if [[ -f "$rc" && -x "$rc" ]]; then
warn "${rc} exists and is executable — review its content:"
cat "$rc" | tee -a "$REPORT_FILE"
fi
done
}
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 20 — Final Report Summary
# ─────────────────────────────────────────────────────────────────────────────
audit_summary() {
local line="════════════════════════════════════════════════════════════════"
log ""
log "${BOLD}${MAGENTA}${line}${RESET}"
log "${BOLD}${MAGENTA} AUDIT COMPLETE${RESET}"
log "${BOLD}${MAGENTA}${line}${RESET}"
log ""
log "${BOLD}Host :${RESET} $(hostname -f 2>/dev/null || hostname)"
log "${BOLD}Date :${RESET} $(date '+%A %d %B %Y — %H:%M:%S')"
log "${BOLD}Report :${RESET} ${REPORT_FILE}"
log ""
# Count findings by severity
local alerts warns oks
alerts=$(grep -c '^\[ALERT\]' "$REPORT_FILE" 2>/dev/null || echo 0)
warns=$(grep -c '^\[WARN\]' "$REPORT_FILE" 2>/dev/null || echo 0)
oks=$(grep -c '^\[OK\]' "$REPORT_FILE" 2>/dev/null || echo 0)
log "${BOLD}${RED} ALERTS : ${alerts}${RESET}"
log "${BOLD}${YELLOW} WARNINGS: ${warns}${RESET}"
log "${BOLD}${GREEN} OK : ${oks}${RESET}"
log ""
if [[ "$alerts" -gt 0 ]]; then
log "${BOLD}${RED} ► CRITICAL FINDINGS (review immediately):${RESET}"
grep '^\[ALERT\]' "$REPORT_FILE" | tee /dev/tty
fi
log ""
log "${BOLD}${MAGENTA}${line}${RESET}"
log "Full report saved to: ${REPORT_FILE}"
log "${BOLD}${MAGENTA}${line}${RESET}"
}
# ─────────────────────────────────────────────────────────────────────────────
# MAIN — Run all audit modules in order
# ─────────────────────────────────────────────────────────────────────────────
main() {
# Print the audit banner to terminal
printf "%b\n" "${BOLD}${CYAN}"
printf "%b\n" " ██╗ ██╗███╗ ██╗██╗ ██╗██╗ ██╗ █████╗ ██╗ ██╗██████╗ ██╗████████╗"
printf "%b\n" " ██║ ██║████╗ ██║██║ ██║╚██╗██╔╝ ██╔══██╗██║ ██║██╔══██╗██║╚══██╔══╝"
printf "%b\n" " ██║ ██║██╔██╗ ██║██║ ██║ ╚███╔╝ ███████║██║ ██║██║ ██║██║ ██║ "
printf "%b\n" " ██║ ██║██║╚██╗██║██║ ██║ ██╔██╗ ██╔══██║██║ ██║██║ ██║██║ ██║ "
printf "%b\n" " ███████╗██║██║ ╚████║╚██████╔╝██╔╝ ██╗ ██║ ██║╚██████╔╝██████╔╝██║ ██║ "
printf "%b\n" " ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ "
printf "%b\n" "${RESET}"
printf "%b\n" "${BOLD} Linux Server Audit — v2.0.0${RESET}"
printf "%b\n" " $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# Initialize the report file header
{
echo "==============================================================================="
echo " Linux Server Security & Health Audit Report"
echo " Generated by: linux_audit.sh v2.0.0"
echo " Host: $(hostname -f 2>/dev/null || hostname)"
echo " Date: $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo " OS : $(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"')"
echo "==============================================================================="
} > "$REPORT_FILE"
preflight_checks
audit_system_info
audit_hardware
audit_users
audit_ssh
audit_firewall
audit_ports
audit_services
audit_permissions
audit_cron
audit_docker
audit_kubernetes
audit_packages
audit_kernel
audit_logs
audit_network
audit_sensitive_files
audit_init
audit_summary
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment