Comprehensive security audit script that checks for common Linux security vulnerabilities and misconfigurations.

Overview

This script performs essential security checks on Linux systems, identifying potential vulnerabilities like root-equivalent users, world-writable files, SUID/SGID binaries, weak password configurations, and firewall status.

Use Case: Regular security audits, compliance checks, and hardening verification.

Platform: Linux (all distributions) Requirements: Root/sudo privileges for complete audit Execution Time: 2-10 minutes (depending on filesystem size)

The Script

Lang: bash
  1#!/bin/bash
  2
  3#
  4# security-audit.sh
  5#
  6# Comprehensive Linux security audit script
  7# Checks for common vulnerabilities and security misconfigurations
  8#
  9# Author: glyph.sh
 10# Usage: sudo ./security-audit.sh [options]
 11# Options:
 12#   -o, --output FILE    Save report to file
 13#   -v, --verbose        Show detailed output
 14#   -h, --help          Show this help message
 15#
 16
 17set -euo pipefail
 18
 19# Colors for output
 20RED='\033[0;31m'
 21YELLOW='\033[1;33m'
 22GREEN='\033[0;32m'
 23BLUE='\033[0;34m'
 24CYAN='\033[0;36m'
 25NC='\033[0m' # No Color
 26
 27# Variables
 28OUTPUT_FILE=""
 29VERBOSE=false
 30ISSUES_FOUND=0
 31
 32# Parse command line arguments
 33while [[ $# -gt 0 ]]; do
 34    case $1 in
 35        -o|--output)
 36            OUTPUT_FILE="$2"
 37            shift 2
 38            ;;
 39        -v|--verbose)
 40            VERBOSE=true
 41            shift
 42            ;;
 43        -h|--help)
 44            echo "Usage: sudo $0 [options]"
 45            echo "Options:"
 46            echo "  -o, --output FILE    Save report to file"
 47            echo "  -v, --verbose        Show detailed output"
 48            echo "  -h, --help          Show this help message"
 49            exit 0
 50            ;;
 51        *)
 52            echo "Unknown option: $1"
 53            exit 1
 54            ;;
 55    esac
 56done
 57
 58# Function to print colored messages
 59print_header() {
 60    echo -e "\n${CYAN}========================================${NC}"
 61    echo -e "${CYAN} $1${NC}"
 62    echo -e "${CYAN}========================================${NC}\n"
 63}
 64
 65print_section() {
 66    echo -e "\n${BLUE}[+] $1${NC}"
 67}
 68
 69print_pass() {
 70    echo -e "  ${GREEN}${NC} $1"
 71}
 72
 73print_warning() {
 74    echo -e "  ${YELLOW}${NC} $1"
 75    ((ISSUES_FOUND++))
 76}
 77
 78print_fail() {
 79    echo -e "  ${RED}${NC} $1"
 80    ((ISSUES_FOUND++))
 81}
 82
 83print_info() {
 84    echo -e "  ${CYAN}${NC} $1"
 85}
 86
 87# Check if running as root
 88check_root() {
 89    if [[ $EUID -ne 0 ]]; then
 90        echo -e "${YELLOW}Warning: Not running as root. Some checks may be incomplete.${NC}"
 91        echo ""
 92    fi
 93}
 94
 95# Check for users with UID 0 (root privileges)
 96check_uid_zero() {
 97    print_section "Checking for users with UID 0 (root privileges)"
 98
 99    local uid_zero_users=$(awk -F: '$3 == 0 {print $1}' /etc/passwd)
100    local count=$(echo "$uid_zero_users" | wc -l)
101
102    if [[ $count -eq 1 ]] && [[ "$uid_zero_users" == "root" ]]; then
103        print_pass "Only root user has UID 0"
104    else
105        print_fail "Multiple users with UID 0 found:"
106        echo "$uid_zero_users" | while read -r user; do
107            if [[ "$user" != "root" ]]; then
108                echo "      - $user"
109            fi
110        done
111    fi
112}
113
114# Check for world-writable files
115check_world_writable() {
116    print_section "Checking for world-writable files (excluding /tmp, /proc, /sys)"
117
118    print_info "Scanning filesystem (this may take a few minutes)..."
119
120    local writable_files=$(find / -xdev -type f -perm -0002 \
121        ! -path "/proc/*" \
122        ! -path "/sys/*" \
123        ! -path "/tmp/*" \
124        ! -path "/var/tmp/*" \
125        ! -path "/run/*" \
126        2>/dev/null | head -20)
127
128    if [[ -z "$writable_files" ]]; then
129        print_pass "No suspicious world-writable files found"
130    else
131        print_warning "World-writable files found:"
132        echo "$writable_files" | while read -r file; do
133            echo "      - $file"
134        done
135        echo "      (showing first 20 results)"
136    fi
137}
138
139# Check for SUID/SGID files
140check_suid_sgid() {
141    print_section "Checking for SUID/SGID files"
142
143    print_info "Scanning for SUID files..."
144
145    local suid_files=$(find / -xdev \( -perm -4000 -o -perm -2000 \) -type f \
146        2>/dev/null | wc -l)
147
148    print_info "Found $suid_files SUID/SGID files"
149
150    # Check for suspicious SUID files
151    local suspicious_suid=$(find / -xdev -perm -4000 -type f \
152        2>/dev/null | grep -E "(nmap|perl|python|ruby|find|vim|less|more|awk)" || true)
153
154    if [[ -z "$suspicious_suid" ]]; then
155        print_pass "No suspicious SUID binaries found"
156    else
157        print_warning "Suspicious SUID binaries found (potential privilege escalation):"
158        echo "$suspicious_suid" | while read -r file; do
159            echo "      - $file"
160        done
161    fi
162
163    if [[ "$VERBOSE" == true ]]; then
164        print_info "All SUID/SGID files:"
165        find / -xdev \( -perm -4000 -o -perm -2000 \) -type f \
166            -exec ls -lh {} \; 2>/dev/null | while read -r line; do
167            echo "      $line"
168        done
169    fi
170}
171
172# Check for empty passwords in /etc/shadow
173check_empty_passwords() {
174    print_section "Checking for empty password fields in /etc/shadow"
175
176    if [[ ! -r /etc/shadow ]]; then
177        print_warning "Cannot read /etc/shadow (need root privileges)"
178        return
179    fi
180
181    local empty_pass=$(awk -F: '$2 == "" || $2 == "!" || $2 == "*" {print $1}' /etc/shadow)
182    local locked_count=$(echo "$empty_pass" | wc -l)
183
184    # Check for truly empty passwords (not locked accounts)
185    local truly_empty=$(awk -F: '$2 == "" {print $1}' /etc/shadow)
186
187    if [[ -z "$truly_empty" ]]; then
188        print_pass "No accounts with empty passwords found"
189    else
190        print_fail "Accounts with empty passwords found:"
191        echo "$truly_empty" | while read -r user; do
192            echo "      - $user"
193        done
194    fi
195
196    if [[ "$VERBOSE" == true ]] && [[ -n "$empty_pass" ]]; then
197        print_info "Locked/disabled accounts: $locked_count"
198    fi
199}
200
201# Check firewall status
202check_firewall() {
203    print_section "Checking firewall status"
204
205    # Check iptables
206    if command -v iptables &> /dev/null; then
207        print_info "Checking iptables..."
208
209        local iptables_rules=$(iptables -L -n 2>/dev/null | wc -l || echo "0")
210
211        if [[ $iptables_rules -gt 8 ]]; then
212            print_pass "iptables is configured with rules"
213
214            if [[ "$VERBOSE" == true ]]; then
215                echo "      Current iptables rules:"
216                iptables -L -n 2>/dev/null | head -20 | while read -r line; do
217                    echo "        $line"
218                done
219            fi
220        else
221            print_warning "iptables appears to have minimal or no rules"
222        fi
223    fi
224
225    # Check firewalld
226    if command -v firewall-cmd &> /dev/null; then
227        print_info "Checking firewalld..."
228
229        if systemctl is-active --quiet firewalld 2>/dev/null; then
230            print_pass "firewalld is active and running"
231
232            if [[ "$VERBOSE" == true ]]; then
233                local default_zone=$(firewall-cmd --get-default-zone 2>/dev/null || echo "unknown")
234                local active_zones=$(firewall-cmd --get-active-zones 2>/dev/null | grep -v "interfaces:" || echo "none")
235                echo "      Default zone: $default_zone"
236                echo "      Active zones: $active_zones"
237            fi
238        else
239            print_warning "firewalld is installed but not active"
240        fi
241    fi
242
243    # Check ufw (Ubuntu/Debian)
244    if command -v ufw &> /dev/null; then
245        print_info "Checking ufw..."
246
247        local ufw_status=$(ufw status 2>/dev/null | grep -i "Status:" | awk '{print $2}')
248
249        if [[ "$ufw_status" == "active" ]]; then
250            print_pass "ufw is active"
251
252            if [[ "$VERBOSE" == true ]]; then
253                echo "      UFW rules:"
254                ufw status numbered 2>/dev/null | tail -n +4 | while read -r line; do
255                    echo "        $line"
256                done
257            fi
258        else
259            print_warning "ufw is installed but not active"
260        fi
261    fi
262
263    # If no firewall found
264    if ! command -v iptables &> /dev/null && \
265       ! command -v firewall-cmd &> /dev/null && \
266       ! command -v ufw &> /dev/null; then
267        print_fail "No firewall tools found (iptables, firewalld, ufw)"
268    fi
269}
270
271# Check SSH configuration
272check_ssh_config() {
273    print_section "Checking SSH configuration security"
274
275    local ssh_config="/etc/ssh/sshd_config"
276
277    if [[ ! -r "$ssh_config" ]]; then
278        print_warning "Cannot read $ssh_config (need root privileges)"
279        return
280    fi
281
282    # Check PermitRootLogin
283    local root_login=$(grep -i "^PermitRootLogin" "$ssh_config" | awk '{print $2}')
284    if [[ "$root_login" == "no" ]] || [[ "$root_login" == "without-password" ]] || [[ "$root_login" == "prohibit-password" ]]; then
285        print_pass "Root login is restricted: $root_login"
286    elif [[ -z "$root_login" ]]; then
287        print_warning "PermitRootLogin not explicitly set (may default to 'yes')"
288    else
289        print_fail "Root login is enabled: $root_login"
290    fi
291
292    # Check PasswordAuthentication
293    local pass_auth=$(grep -i "^PasswordAuthentication" "$ssh_config" | awk '{print $2}')
294    if [[ "$pass_auth" == "no" ]]; then
295        print_pass "Password authentication is disabled (key-only)"
296    elif [[ -z "$pass_auth" ]]; then
297        print_info "PasswordAuthentication not explicitly set (may default to 'yes')"
298    else
299        print_info "Password authentication is enabled: $pass_auth"
300    fi
301
302    # Check Protocol
303    local protocol=$(grep -i "^Protocol" "$ssh_config" | awk '{print $2}')
304    if [[ "$protocol" == "2" ]]; then
305        print_pass "SSH Protocol 2 is enforced"
306    elif [[ -z "$protocol" ]]; then
307        print_info "SSH Protocol not explicitly set (modern systems default to 2)"
308    else
309        print_warning "SSH Protocol setting: $protocol"
310    fi
311
312    # Check PermitEmptyPasswords
313    local empty_pass=$(grep -i "^PermitEmptyPasswords" "$ssh_config" | awk '{print $2}')
314    if [[ "$empty_pass" == "no" ]]; then
315        print_pass "Empty passwords are not permitted"
316    elif [[ -z "$empty_pass" ]]; then
317        print_info "PermitEmptyPasswords not explicitly set (should default to 'no')"
318    else
319        print_fail "Empty passwords are permitted!"
320    fi
321
322    # Check X11Forwarding
323    local x11=$(grep -i "^X11Forwarding" "$ssh_config" | awk '{print $2}')
324    if [[ "$x11" == "no" ]]; then
325        print_pass "X11 forwarding is disabled"
326    else
327        print_info "X11 forwarding is enabled or not explicitly disabled"
328    fi
329
330    # Check for custom port
331    local port=$(grep -i "^Port" "$ssh_config" | awk '{print $2}')
332    if [[ -n "$port" ]] && [[ "$port" != "22" ]]; then
333        print_pass "SSH is running on non-standard port: $port"
334    else
335        print_info "SSH is running on default port 22"
336    fi
337}
338
339# Main execution
340main() {
341    print_header "Linux Security Audit Script"
342    echo "Starting security audit at $(date)"
343    echo ""
344
345    check_root
346
347    check_uid_zero
348    check_world_writable
349    check_suid_sgid
350    check_empty_passwords
351    check_firewall
352    check_ssh_config
353
354    # Summary
355    print_header "Security Audit Complete"
356
357    if [[ $ISSUES_FOUND -eq 0 ]]; then
358        echo -e "${GREEN}No security issues found!${NC}"
359    elif [[ $ISSUES_FOUND -le 3 ]]; then
360        echo -e "${YELLOW}Found $ISSUES_FOUND potential security issue(s)${NC}"
361    else
362        echo -e "${RED}Found $ISSUES_FOUND potential security issues${NC}"
363    fi
364
365    echo ""
366    echo "Audit completed at $(date)"
367    echo ""
368
369    if [[ -n "$OUTPUT_FILE" ]]; then
370        print_info "Report saved to: $OUTPUT_FILE"
371    fi
372
373    echo -e "${CYAN}Recommendations:${NC}"
374    echo "  1. Review and address any warnings or failures"
375    echo "  2. Ensure all software is up to date"
376    echo "  3. Enable and configure firewall rules"
377    echo "  4. Use SSH key authentication when possible"
378    echo "  5. Regularly audit file permissions and SUID binaries"
379    echo "  6. Monitor system logs for suspicious activity"
380    echo ""
381}
382
383# Execute main function
384if [[ -n "$OUTPUT_FILE" ]]; then
385    main 2>&1 | tee "$OUTPUT_FILE"
386else
387    main
388fi

Usage

Basic Audit

Lang: bash
1sudo ./security-audit.sh

Verbose Output

Lang: bash
1sudo ./security-audit.sh --verbose

Save Report to File

Lang: bash
1sudo ./security-audit.sh --output security-report-$(date +%Y%m%d).txt

Combined Options

Lang: bash
1sudo ./security-audit.sh --verbose --output audit.txt

What It Checks

1. Users with UID 0

  • Identifies all users with root-equivalent privileges
  • Only the root user should have UID 0
  • Additional UID 0 users are a serious security risk

2. World-Writable Files

  • Scans filesystem for files writable by anyone
  • Excludes temporary directories (/tmp, /proc, /sys)
  • World-writable files can be exploited for privilege escalation

3. SUID/SGID Files

  • Lists files with Set-UID and Set-GID permissions
  • Identifies suspicious SUID binaries (perl, python, nmap, etc.)
  • These files run with elevated privileges and can be exploited

4. Empty Password Fields

  • Checks /etc/shadow for accounts without passwords
  • Distinguishes between empty and locked accounts
  • Empty passwords allow login without authentication

5. Firewall Status

  • Checks iptables, firewalld, and ufw status
  • Verifies that firewall rules are configured
  • Shows active zones and rule counts

6. SSH Configuration

  • PermitRootLogin: Should be ’no’ or ‘without-password’
  • PasswordAuthentication: Preferably ’no’ (key-only)
  • PermitEmptyPasswords: Must be ’no'
  • Protocol: Should be ‘2’ (or default on modern systems)
  • X11Forwarding: Consider disabling if not needed
  • Port: Non-standard port reduces automated attacks

Understanding Results

Pass ✓

Configuration follows security best practices.

Warning ⚠

Potential security concern that should be reviewed.

Fail ✗

Security issue that requires immediate attention.

Info ℹ

Informational message about configuration state.

Common Security Issues

IssueRisk LevelRemediation
Multiple UID 0 usersCriticalRemove non-root UID 0 users
Empty passwordsCriticalSet passwords or lock accounts
World-writable filesHighChange permissions to 644 or 600
Suspicious SUID binariesHighRemove SUID bit: chmod u-s file
Root SSH login enabledHighSet PermitRootLogin no
No firewall activeMediumEnable and configure firewall
Default SSH port (22)LowChange to non-standard port

Remediation Examples

Remove SUID Bit

Lang: bash
1sudo chmod u-s /path/to/binary

Disable Root SSH Login

Lang: bash
1sudo sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
2sudo systemctl restart sshd

Enable Firewall (Ubuntu/Debian)

Lang: bash
1sudo ufw enable
2sudo ufw default deny incoming
3sudo ufw default allow outgoing
4sudo ufw allow ssh

Enable Firewall (RHEL/CentOS)

Lang: bash
1sudo systemctl start firewalld
2sudo systemctl enable firewalld
3sudo firewall-cmd --set-default-zone=public

Fix World-Writable File

Lang: bash
1sudo chmod 644 /path/to/file

Lock Account with Empty Password

Lang: bash
1sudo passwd -l username

Scheduling Regular Audits

Add to crontab for weekly audits:

Lang: bash
1# Run security audit every Sunday at 2 AM
20 2 * * 0 /path/to/security-audit.sh --output /var/log/security-audit-$(date +\%Y\%m\%d).txt

Limitations

  • Does not check for rootkits or malware
  • Does not analyze running processes or network connections
  • Does not verify software patch levels
  • Filesystem scan can be time-consuming on large systems
  • Some checks require root privileges

See Also

Download

Lang: bash
1curl -O https://glyph.sh/scripts/security-audit.sh
2chmod +x security-audit.sh
3sudo ./security-audit.sh