# Server Security Scan

> Use when given an IP address (and optionally SSH credentials) to run a non-destructive security audit — external attack-surface analysis from IP alone, or internal privilege/configuration scan when SSH access credentials are provided.

- Skill: `tommasinigiovanni/server-security-scan` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tommasinigiovanni/server-security-scan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tommasinigiovanni/server-security-scan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: tommasinigiovanni (https://skillmd.com/u/tommasinigiovanni)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tommasinigiovanni/server-security-scan

---


# 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:

```bash
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:
```bash
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:

```bash
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

```bash
# 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 `-Pn` if nmap reports "host seems down".

```bash
# 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)

```bash
nuclei -update-templates -silent
```

### Step 4 — Web scanning (if port 80/443/8080/8443 is open)

```bash
# 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

```bash
# 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

```bash
# With private key
ssh -i /path/to/key user@<IP>

# With password
ssh user@<IP>
```

### Step 1 — System information

```bash
uname -a && cat /etc/os-release
hostname && id && whoami
uptime && last -n 20
```

### Step 2 — Users, authentication, and SSH hardening

```bash
# 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

```bash
# 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.

```bash
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

```bash
# 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):**

```bash
# 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

```bash
# 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

```bash
# 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)

```bash
which lynis && sudo lynis audit system --quick 2>/dev/null | tail -50
```

### Step 9 — World-writable sensitive files

```bash
# 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:

```bash
#!/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:**
1. Extract from nuclei output if present (nuclei embeds CVSS in `[cvss-score]` tag)
2. If missing, look up the CVE-ID on [nvd.nist.gov](https://nvd.nist.gov/vuln/search)
3. 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 `-T5` on 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

