Server Security Scan
Overview
Dual-mode security audit: external (IP only → simulate attacker perspective) and internal (IP + SSH → scan from inside). Never destructive. Always requires explicit authorization confirmation before starting.
Authorization Gate (MANDATORY)
Before ANY scan, check the allowlist first:
ALLOWLIST="$HOME/.config/server-security-scan/allowlist.txt"
if [ -f "$ALLOWLIST" ] && grep -qxF "<IP>" "$ALLOWLIST"; then
echo "IP <IP> found in allowlist — proceeding."
else
# Ask explicit confirmation
echo "IP <IP> not in allowlist."
echo "Confirm you are the authorized owner/administrator and have explicit permission to scan this target."
# If not confirmed → stop immediately
fi
To pre-authorize IPs and skip the prompt:
mkdir -p ~/.config/server-security-scan
echo "192.168.1.10" >> ~/.config/server-security-scan/allowlist.txt
If not in allowlist and not confirmed → stop immediately.
Output directory
All scan files go into a dated subdirectory to avoid collisions across multiple scans:
SCAN_DIR="./security-scan/$(date +%Y-%m-%d)-<IP>"
mkdir -p "$SCAN_DIR"
Use $SCAN_DIR as base for all -oN / -output / -o flags below.
At the end, generate $SCAN_DIR/REPORT.md from findings.
Mode 1: External Scan (IP only)
Goal: reproduce what an external attacker sees from the internet.
Step 1 — Passive reconnaissance
# Reverse DNS
dig -x <IP> +short
host <IP>
# ASN / geolocation
curl -s "https://ipinfo.io/<IP>/json"
Step 2 — Port scan
Note: many cloud providers (Hetzner, DigitalOcean, OVH) filter ICMP — add
-Pnif nmap reports "host seems down".
# Full TCP scan with version and OS detection (non-aggressive)
nmap -Pn -sV -sC -O -p- --open -T3 --reason <IP> -oN "$SCAN_DIR/01-nmap.txt"
# Faster alternative (top 1000 ports)
nmap -Pn -sV -sC --open -T3 <IP> -oN "$SCAN_DIR/01-nmap.txt"
Step 3 — Update nuclei templates (run once per session)
nuclei -update-templates -silent
Step 4 — Web scanning (if port 80/443/8080/8443 is open)
# Nikto on each web port found
nikto -h <IP> -p <PORT> -output "$SCAN_DIR/02-nikto-<PORT>.txt"
# Nuclei CVE and misconfiguration templates
nuclei -u http://<IP>:<PORT> -t cves/ -t misconfiguration/ \
-severity medium,high,critical -o "$SCAN_DIR/03-nuclei.txt"
Step 5 — Service-specific checks
# SSH: check version and algorithms
nmap --script ssh2-enum-algos,ssh-auth-methods -p 22 <IP>
# SMB (if port 445 is open)
nmap --script smb-vuln* -p 445 <IP>
# SSL/TLS (if HTTPS)
nmap --script ssl-enum-ciphers -p 443 <IP>
Step 6 — External report
Include in the report:
- Open ports, services, and detected versions
- CVEs identified (CVSS score from nuclei output; flag as "self-assigned" if not from NVD)
- Weak configurations (deprecated SSH algorithms, obsolete TLS, etc.)
- Remediation for each finding
Mode 2: Internal Scan (IP + SSH credentials)
Goal: audit from inside — configurations, privilege escalation paths, hidden services.
SSH access
# With private key
ssh -i /path/to/key user@<IP>
# With password
ssh user@<IP>
Step 1 — System information
uname -a && cat /etc/os-release
hostname && id && whoami
uptime && last -n 20
Step 2 — Users, authentication, and SSH hardening
# Users with a shell
grep -E '/bin/(bash|sh|zsh|fish)$' /etc/passwd
# Users with UID 0 (root equivalents)
awk -F: '($3 == 0) {print}' /etc/passwd
# Authorized SSH keys
find / -name "authorized_keys" 2>/dev/null
find / -name "*.pem" -o -name "id_rsa" 2>/dev/null | grep -v proc
# Last login per user
lastlog | grep -v 'Never'
# SSH daemon effective config (resolves drop-in files, more reliable than grep on sshd_config)
sshd -T 2>/dev/null | grep -E 'passwordauthentication|permitrootlogin|pubkeyauthentication'
# Brute-force protection
systemctl is-active fail2ban 2>/dev/null
fail2ban-client status sshd 2>/dev/null
# Failed login rate (last 100 lines)
journalctl -u ssh --no-pager -n 100 2>/dev/null | grep -i 'failed\|invalid' | wc -l
Step 3 — Sudo and SUID
# Sudo rules
sudo -l 2>/dev/null
# Non-standard SUID binaries (potential privilege escalation paths)
find / -perm -4000 -type f 2>/dev/null | grep -v -E '(/bin/|/sbin/|/usr/bin/|/usr/sbin/)' | sort
# SGID binaries
find / -perm -2000 -type f 2>/dev/null | sort
Step 4 — Firewall state
Run this BEFORE concluding on severity of 0.0.0.0 binds — a bound port may still be blocked by the firewall.
ufw status verbose 2>/dev/null
iptables -L INPUT -v -n 2>/dev/null
firewall-cmd --list-all 2>/dev/null
Step 5 — Services, internal ports, and Docker
# Listening ports (local-only = hidden from external scan)
ss -tulpn 2>/dev/null || netstat -tulpn 2>/dev/null
# Running services
systemctl list-units --type=service --state=running 2>/dev/null
# Running processes
ps aux --sort=-%cpu | head -30
Docker audit (if Docker is present):
# Container port bindings — flag any 0.0.0.0 that should be 127.0.0.1
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}" 2>/dev/null
# Docker daemon socket exposure
ss -tlnp | grep -E '2375|2376'
# Privileged containers (critical finding if present)
docker inspect $(docker ps -q) 2>/dev/null \
| jq '.[] | select(.HostConfig.Privileged==true) | .Name'
Step 6 — Cron and scheduled tasks
# System cron
cat /etc/crontab 2>/dev/null
ls -la /etc/cron.* 2>/dev/null
# Per-user crontabs
for user in $(cut -f1 -d: /etc/passwd); do
crontab -u "$user" -l 2>/dev/null | grep -v '^#' | grep -v '^$' \
&& echo " ^ user: $user"
done
Step 7 — Package updates
# Debian/Ubuntu
apt list --upgradable 2>/dev/null | head -30
# RHEL/CentOS/Fedora
yum check-update 2>/dev/null | head -30 || dnf check-update 2>/dev/null | head -30
Step 8 — Lynis (if available)
which lynis && sudo lynis audit system --quick 2>/dev/null | tail -50
Step 9 — World-writable sensitive files
# World-writable files in /etc
find /etc -writable -type f 2>/dev/null
# World-writable scripts executed by cron
find /etc/cron* /var/spool/cron -writable 2>/dev/null
Step 10 — Internal report
Include in the report:
- Unexpected users or accounts with UID 0
- Non-standard SUID/SGID binaries (privilege escalation paths)
- Internal ports not exposed externally (hidden services)
- Docker containers binding 0.0.0.0 or running privileged
- Docker daemon socket exposed on TCP
- Cron jobs executing world-writable scripts
- Packages with known vulnerabilities
- Overly permissive sudo rules
- Exposed SSH keys
- SSH with PasswordAuthentication enabled or fail2ban absent
- Firewall state vs open binds (cross-reference Step 4 with Step 5)
- Prioritized remediation by severity
Report Generation
After all scan steps are complete, run this script to generate a pre-populated REPORT.md from the scan output files:
#!/usr/bin/env bash
# Usage: bash generate-report.sh <IP> <SCAN_DIR> [external|internal|both]
IP="$1"; SCAN_DIR="$2"; TYPE="${3:-both}"
REPORT="$SCAN_DIR/REPORT.md"
echo "# Security Report — $IP" > "$REPORT"
echo "**Date:** $(date +%Y-%m-%d) **Type:** $TYPE **Author:** " >> "$REPORT"
echo "" >> "$REPORT"
# --- Open ports from nmap ---
echo "## Detected Ports and Services" >> "$REPORT"
if [ -f "$SCAN_DIR/01-nmap.txt" ]; then
echo '```' >> "$REPORT"
grep -E '^[0-9]+/(tcp|udp)' "$SCAN_DIR/01-nmap.txt" >> "$REPORT"
echo '```' >> "$REPORT"
else
echo "_nmap output not found_" >> "$REPORT"
fi
echo "" >> "$REPORT"
# --- Nuclei findings by severity ---
echo "## Findings" >> "$REPORT"
for sev in critical high medium low info; do
label=$(echo "$sev" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
case $sev in
critical) icon="🔴" ;; high) icon="🟠" ;; medium) icon="🟡" ;;
low|info) icon="🟢" ;;
esac
findings=""
if [ -f "$SCAN_DIR/03-nuclei.txt" ]; then
findings=$(grep -i "\[$sev\]" "$SCAN_DIR/03-nuclei.txt" 2>/dev/null)
fi
echo "### $icon $label" >> "$REPORT"
echo "| # | Vulnerability | Service/Path | CVSS | Source | Remediation |" >> "$REPORT"
echo "|---|---|---|---|---|---|" >> "$REPORT"
if [ -n "$findings" ]; then
i=1
while IFS= read -r line; do
template=$(echo "$line" | grep -oP '(?<=\[)[^\]]+(?=\])' | head -1)
target=$(echo "$line" | grep -oP 'https?://\S+' | head -1)
echo "| $i | $template | ${target:--} | — | nuclei | TODO |" >> "$REPORT"
i=$((i+1))
done <<< "$findings"
else
echo "| — | _none_ | | | | |" >> "$REPORT"
fi
echo "" >> "$REPORT"
done
# --- Nikto findings (appended as raw block) ---
if ls "$SCAN_DIR"/02-nikto-*.txt 1>/dev/null 2>&1; then
echo "## Nikto Findings" >> "$REPORT"
for f in "$SCAN_DIR"/02-nikto-*.txt; do
port=$(basename "$f" | grep -oP '\d+(?=\.txt)')
echo "### Port $port" >> "$REPORT"
echo '```' >> "$REPORT"
grep -v '^-' "$f" | grep -v '^$' >> "$REPORT"
echo '```' >> "$REPORT"
echo "" >> "$REPORT"
done
fi
echo "> CVSS scores sourced from nuclei/NVD where available; marked "self-assigned" otherwise." >> "$REPORT"
echo "" >> "$REPORT"
echo "## Executive Summary" >> "$REPORT"
echo "_TODO: 2-3 lines — finding count by severity, overall risk assessment_" >> "$REPORT"
echo "" >> "$REPORT"
echo "## Next Steps" >> "$REPORT"
echo "_TODO: prioritized action list_" >> "$REPORT"
echo "Report generated: $REPORT"
The script pre-populates ports and nuclei/nikto findings. Fill in remediation notes and Executive Summary manually.
CVSS score resolution order:
- Extract from nuclei output if present (nuclei embeds CVSS in
[cvss-score]tag) - If missing, look up the CVE-ID on nvd.nist.gov
- If no CVE-ID exists, assign manually and mark as
(self-assigned)
Hard Limits — Never do
- Active exploits or destructive payloads
- Denial of service (no
-T5on production, no stress testing) - Exfiltrate real data from the server
- Modify files or configurations
- Create backdoors or accounts
- Share the report with unauthorized third parties