Security isn't a one-time setup—it's ongoing maintenance. Configurations drift, new vulnerabilities emerge, and what was secure last year might not be today.
This guide uses Claude Code to generate security hardening scripts, compliance checks, and audit tools. We'll cover firewall configuration, SSH hardening, intrusion detection, security scanning, and automated compliance verification against CIS benchmarks.
Prerequisites
- Claude Code installed (see Part 1)
- Root access to your server
- Basic understanding of Linux security concepts
Comprehensive Firewall Configuration
Let's start with a robust firewall setup that goes beyond basic UFW rules:
"Create a comprehensive firewall configuration script using nftables with default deny incoming, rate limiting for SSH, port knocking, geographic blocking, Docker-aware rules, logging, whitelist for trusted IPs, and protection against common attacks."
#!/bin/bash
set -euo pipefail
# =============================================================================
# Advanced Firewall Configuration with nftables
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/firewall.conf}"
RULES_FILE="/etc/nftables.conf"
# Load configuration
source "$CONFIG_FILE"
# Defaults
SSH_PORT="${SSH_PORT:-22}"
SSH_RATE_LIMIT="${SSH_RATE_LIMIT:-4/minute}"
WHITELIST_IPS="${WHITELIST_IPS:-}"
BLOCKED_COUNTRIES="${BLOCKED_COUNTRIES:-}"
ENABLE_PORT_KNOCKING="${ENABLE_PORT_KNOCKING:-false}"
KNOCK_SEQUENCE="${KNOCK_SEQUENCE:-7000,8000,9000}"
LOG_DROPPED="${LOG_DROPPED:-true}"
MAINTENANCE_MODE="${MAINTENANCE_MODE:-false}"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
generate_nftables_config() {
cat << 'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set ssh_meter {
type ipv4_addr
flags dynamic,timeout
timeout 1m
}
set port_scanners {
type ipv4_addr
flags dynamic,timeout
timeout 1h
}
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
# Block known port scanners
ip saddr @port_scanners drop
# Rate limit ICMP
ip protocol icmp icmp type echo-request limit rate 4/second accept
# Protection against SYN floods
tcp flags syn limit rate 100/second burst 150 packets accept
tcp flags syn drop
# SSH with rate limiting
tcp dport $SSH_PORT ct state new accept
# HTTP/HTTPS
tcp dport { 80, 443 } accept
# Allow Docker networks
ip saddr 172.16.0.0/12 accept
ip saddr 10.0.0.0/8 accept
}
chain forward {
type filter hook forward priority 0; policy accept;
oifname "docker*" accept
iifname "docker*" accept
}
chain output {
type filter hook output priority 0; policy accept;
}
}
EOF
}
apply_firewall() {
log "Generating nftables configuration..."
generate_nftables_config > "$RULES_FILE"
log "Applying firewall rules..."
nft -f "$RULES_FILE"
systemctl enable nftables
log "Firewall configured successfully"
}
case "${1:-apply}" in
apply) apply_firewall ;;
status) nft list ruleset | head -100 ;;
*) echo "Usage: $0 {apply|status}" ;;
esac# =============================================================================
# Firewall Configuration
# =============================================================================
SSH_PORT=22
SSH_RATE_LIMIT="4/minute"
# Whitelist (comma-separated, always allowed)
WHITELIST_IPS="YOUR_HOME_IP, YOUR_OFFICE_IP"
# Additional allowed TCP ports
ALLOWED_TCP_PORTS="3000, 8080, 9090"
# Port Knocking
ENABLE_PORT_KNOCKING=false
KNOCK_SEQUENCE="7000,8000,9000"
# Geographic Blocking (ISO country codes)
BLOCKED_COUNTRIES=""
# Logging
LOG_DROPPED=true
# Maintenance Mode
MAINTENANCE_MODE=falseSSH Hardening
Beyond basic SSH configuration, let's implement comprehensive hardening:
"Create an SSH hardening script that generates new ED25519 host keys, configures secure ciphers and MACs, sets up 2FA with TOTP, configures fail2ban with aggressive settings, and enables detailed logging for forensics."
#!/bin/bash
set -euo pipefail
# =============================================================================
# SSH Hardening Script
# =============================================================================
BACKUP_DIR="/root/ssh-backup-$(date +%Y%m%d)"
REAL_SSH_PORT="${REAL_SSH_PORT:-2222}"
ENABLE_2FA="${ENABLE_2FA:-false}"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log_success() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] \033[0;32m✓\033[0m $*"; }
backup_config() {
log "Backing up current SSH configuration..."
mkdir -p "$BACKUP_DIR"
cp -r /etc/ssh "$BACKUP_DIR/"
log_success "Backup created at $BACKUP_DIR"
}
regenerate_host_keys() {
log "Regenerating host keys (ED25519 only)..."
rm -f /etc/ssh/ssh_host_*
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" -C "$(hostname)"
chmod 600 /etc/ssh/ssh_host_ed25519_key
chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
log_success "New ED25519 host key generated"
}
configure_sshd() {
log "Configuring SSH daemon..."
cat > /etc/ssh/sshd_config << EOF
# Hardened SSH Configuration
Port $REAL_SSH_PORT
AddressFamily inet
# Host Keys (ED25519 only)
HostKey /etc/ssh/ssh_host_ed25519_key
# Security
Protocol 2
StrictModes yes
PermitRootLogin prohibit-password
MaxAuthTries 3
MaxSessions 3
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
UsePAM yes
# Cryptography (modern, secure algorithms only)
KexAlgorithms curve25519-sha256@libssh.org,curve25519-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Disable dangerous features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
# Session
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
# Logging
SyslogFacility AUTH
LogLevel VERBOSE
# Banner
Banner /etc/ssh/banner
EOF
# Create warning banner
cat > /etc/ssh/banner << 'BANNER'
***************************************************************************
AUTHORIZED ACCESS ONLY
***************************************************************************
This system is for authorized users only. All activities are monitored.
***************************************************************************
BANNER
if sshd -t; then
log_success "SSH configuration valid"
else
log "SSH configuration invalid! Restoring backup..."
cp "$BACKUP_DIR/ssh/sshd_config" /etc/ssh/sshd_config
exit 1
fi
}
setup_fail2ban() {
log "Configuring fail2ban..."
apt-get install -y fail2ban
cat > /etc/fail2ban/jail.local << EOF
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
banaction = nftables-multiport
[sshd]
enabled = true
port = $REAL_SSH_PORT
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 24h
[sshd-aggressive]
enabled = true
port = $REAL_SSH_PORT
filter = sshd[mode=aggressive]
logpath = /var/log/auth.log
maxretry = 1
bantime = 1w
findtime = 1d
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 1w
findtime = 1d
maxretry = 3
EOF
systemctl enable fail2ban
systemctl restart fail2ban
log_success "Fail2ban configured"
}
main() {
log "=========================================="
log "SSH Hardening Script"
log "=========================================="
[[ $EUID -ne 0 ]] && { echo "Run as root"; exit 1; }
backup_config
regenerate_host_keys
configure_sshd
setup_fail2ban
systemctl restart sshd
log "=========================================="
log "SSH hardening complete!"
log "SSH is now on port $REAL_SSH_PORT"
log "=========================================="
}
main "$@"CIS Benchmark Compliance Checker
Automate compliance verification against CIS benchmarks:
"Create a CIS benchmark compliance checker for Ubuntu that checks filesystem configuration, user accounts, network settings, SSH configuration, and file permissions. Generate reports with pass/fail status, severity levels, remediation commands, and overall compliance score."
#!/bin/bash
set -uo pipefail
# =============================================================================
# CIS Benchmark Compliance Checker
# Based on CIS Ubuntu Linux Benchmark
# =============================================================================
OUTPUT_DIR="${OUTPUT_DIR:-/var/log/compliance}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPORT_JSON="$OUTPUT_DIR/cis-audit-$TIMESTAMP.json"
REPORT_HTML="$OUTPUT_DIR/cis-audit-$TIMESTAMP.html"
mkdir -p "$OUTPUT_DIR"
declare -a RESULTS=()
TOTAL_CHECKS=0
PASSED_CHECKS=0
FAILED_CHECKS=0
add_result() {
local id="$1" title="$2" status="$3" severity="$4" message="$5"
((TOTAL_CHECKS++))
[[ "$status" == "PASS" ]] && ((PASSED_CHECKS++))
[[ "$status" == "FAIL" ]] && ((FAILED_CHECKS++))
RESULTS+=("{"id":"$id","title":"$title","status":"$status","severity":"$severity","message":"$message"}")
}
check_filesystem() {
echo "Checking filesystem configuration..."
# Check unused filesystems
local unused_fs=("cramfs" "freevxfs" "jffs2" "hfs" "hfsplus")
for fs in "${unused_fs[@]}"; do
if ! lsmod | grep -q "^$fs "; then
add_result "1.1.1.$fs" "Ensure $fs is disabled" "PASS" "Low" "Not loaded"
else
add_result "1.1.1.$fs" "Ensure $fs is disabled" "FAIL" "Low" "Loaded"
fi
done
# Check /tmp mount options
if mount | grep -q " /tmp "; then
local tmp_opts=$(mount | grep " /tmp " | awk '{print $6}')
if [[ "$tmp_opts" == *"noexec"* ]] && [[ "$tmp_opts" == *"nosuid"* ]]; then
add_result "1.1.2" "Ensure /tmp has noexec,nosuid" "PASS" "Medium" "Properly configured"
else
add_result "1.1.2" "Ensure /tmp has noexec,nosuid" "FAIL" "Medium" "Missing options"
fi
fi
}
check_ssh() {
echo "Checking SSH configuration..."
local sshd_config="/etc/ssh/sshd_config"
# Check PermitRootLogin
if grep -qi "^PermitRootLogin no" "$sshd_config" || grep -qi "^PermitRootLogin prohibit-password" "$sshd_config"; then
add_result "5.2.9" "Ensure SSH root login is disabled" "PASS" "Critical" "Properly restricted"
else
add_result "5.2.9" "Ensure SSH root login is disabled" "FAIL" "Critical" "Not restricted"
fi
# Check PermitEmptyPasswords
if grep -qi "^PermitEmptyPasswords no" "$sshd_config" || ! grep -qi "^PermitEmptyPasswords yes" "$sshd_config"; then
add_result "5.2.10" "Ensure SSH empty passwords disabled" "PASS" "Critical" "Disabled"
else
add_result "5.2.10" "Ensure SSH empty passwords disabled" "FAIL" "Critical" "Enabled"
fi
}
check_file_permissions() {
echo "Checking file permissions..."
# /etc/passwd
local passwd_perms=$(stat -c "%a" /etc/passwd)
if [[ "$passwd_perms" == "644" ]]; then
add_result "6.1.2" "Ensure /etc/passwd permissions" "PASS" "High" "Correct (644)"
else
add_result "6.1.2" "Ensure /etc/passwd permissions" "FAIL" "High" "Incorrect ($passwd_perms)"
fi
# /etc/shadow
local shadow_perms=$(stat -c "%a" /etc/shadow 2>/dev/null)
if [[ "$shadow_perms" == "640" ]] || [[ "$shadow_perms" == "600" ]]; then
add_result "6.1.4" "Ensure /etc/shadow permissions" "PASS" "Critical" "Correct ($shadow_perms)"
else
add_result "6.1.4" "Ensure /etc/shadow permissions" "FAIL" "Critical" "Incorrect ($shadow_perms)"
fi
}
generate_report() {
local score=$((PASSED_CHECKS * 100 / TOTAL_CHECKS))
echo ""
echo "=========================================="
echo "Audit Complete"
echo "=========================================="
echo "Total Checks: $TOTAL_CHECKS"
echo "Passed: $PASSED_CHECKS"
echo "Failed: $FAILED_CHECKS"
echo "Score: ${score}%"
# Generate JSON
local results_json=$(printf '%s\n' "${RESULTS[@]}" | paste -sd,)
cat > "$REPORT_JSON" << EOF
{
"timestamp": "$(date -Iseconds)",
"hostname": "$(hostname)",
"summary": {"total": $TOTAL_CHECKS, "passed": $PASSED_CHECKS, "failed": $FAILED_CHECKS, "score": $score},
"results": [$results_json]
}
EOF
echo "Report saved to: $REPORT_JSON"
}
main() {
echo "=========================================="
echo "CIS Benchmark Compliance Checker"
echo "=========================================="
check_filesystem
check_ssh
check_file_permissions
generate_report
}
main "$@"Intrusion Detection with AIDE
Set up file integrity monitoring with AIDE (Advanced Intrusion Detection Environment):
"Create an AIDE configuration that monitors critical system files, runs daily checks, sends Discord/email alerts on changes, and provides detailed diff reports."
#!/bin/bash
set -euo pipefail
# =============================================================================
# AIDE Setup Script
# =============================================================================
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log "Installing AIDE..."
apt-get update && apt-get install -y aide aide-common
log "Configuring AIDE..."
cat > /etc/aide/aide.conf << 'EOF'
# Database locations
database_in=file:/var/lib/aide/aide.db
database_out=file:/var/lib/aide/aide.db.new
gzip_dbout=yes
# Rule definitions
FULL = p+i+n+u+g+s+m+c+sha256+sha512
BINLIB = p+i+n+u+g+s+b+m+c+sha256+sha512+xattrs
CONFIG = p+i+n+u+g+s+sha256
# Monitored paths
/bin BINLIB
/sbin BINLIB
/usr/bin BINLIB
/usr/sbin BINLIB
/lib BINLIB
/etc CONFIG
/boot FULL
# SSH
/etc/ssh CONFIG
/root/.ssh CONFIG
# Cron
/etc/cron.d CONFIG
/etc/cron.daily CONFIG
/var/spool/cron CONFIG
# Exclusions
!/var/log/.*
!/var/cache/.*
!/tmp/.*
!/var/tmp/.*
!/run/.*
!/proc/.*
!/sys/.*
EOF
log "Initializing AIDE database..."
aideinit
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Create check script
cat > /usr/local/bin/aide-check << 'CHECK_EOF'
#!/bin/bash
LOG_FILE="/var/log/aide/aide-$(date +%Y%m%d).log"
DISCORD_WEBHOOK="${DISCORD_WEBHOOK:-}"
mkdir -p /var/log/aide
echo "AIDE check started: $(date)" >> "$LOG_FILE"
aide --check >> "$LOG_FILE" 2>&1
AIDE_EXIT=$?
if [[ $AIDE_EXIT -eq 0 ]]; then
echo "No changes detected" >> "$LOG_FILE"
exit 0
fi
# Changes detected - send alert
if [[ -n "$DISCORD_WEBHOOK" ]]; then
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{
"embeds": [{
"title": "⚠️ AIDE: File Changes Detected",
"description": "File integrity changes on $(hostname)",
"color": 16744256
}]
}" || true
fi
exit $AIDE_EXIT
CHECK_EOF
chmod +x /usr/local/bin/aide-check
# Create update script
cat > /usr/local/bin/aide-update << 'UPDATE_EOF'
#!/bin/bash
echo "Updating AIDE database..."
aide --update
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
echo "AIDE database updated"
UPDATE_EOF
chmod +x /usr/local/bin/aide-update
# Cron job
echo "0 3 * * * root /usr/local/bin/aide-check" > /etc/cron.d/aide
log "AIDE setup complete"
log "Commands: aide-check (run check), aide-update (update after changes)"Security Scanning Script
Regular vulnerability scanning to catch security issues:
#!/bin/bash
set -uo pipefail
# =============================================================================
# Security Scanning Script
# =============================================================================
OUTPUT_DIR="${OUTPUT_DIR:-/var/log/security-scans}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPORT_FILE="$OUTPUT_DIR/security-scan-$TIMESTAMP.txt"
DISCORD_WEBHOOK="${DISCORD_WEBHOOK:-}"
mkdir -p "$OUTPUT_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$REPORT_FILE"; }
section() { echo -e "\n========== $* ==========" | tee -a "$REPORT_FILE"; }
check_updates() {
section "SECURITY UPDATES"
apt-get update -qq
local security_updates=$(apt-get -s upgrade 2>/dev/null | grep -i security | wc -l)
log "Security updates pending: $security_updates"
[[ $security_updates -gt 0 ]] && return 1
return 0
}
check_open_ports() {
section "OPEN PORTS"
local baseline_file="/etc/security/port-baseline.txt"
local current_ports=$(ss -tlnp | grep LISTEN | awk '{print $4}' | sort -u)
log "Currently listening:"
echo "$current_ports" | tee -a "$REPORT_FILE"
if [[ -f "$baseline_file" ]]; then
local new_ports=$(comm -23 <(echo "$current_ports" | sort) <(sort "$baseline_file"))
if [[ -n "$new_ports" ]]; then
log "WARNING: New ports detected!"
return 1
fi
else
echo "$current_ports" > "$baseline_file"
fi
return 0
}
check_auth_failures() {
section "AUTH FAILURES (24h)"
local failures=$(journalctl --since "24 hours ago" -u sshd 2>/dev/null | grep -i "failed\|invalid" | wc -l)
log "Failed authentication attempts: $failures"
[[ $failures -gt 100 ]] && { log "WARNING: High failure count!"; return 1; }
return 0
}
check_ssl_certs() {
section "SSL CERTIFICATES"
local domains="${SSL_DOMAINS:-}"
[[ -z "$domains" ]] && { log "No domains configured"; return 0; }
for domain in $domains; do
local expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | \
openssl x509 -noout -dates 2>/dev/null | grep notAfter | cut -d= -f2)
if [[ -n "$expiry" ]]; then
local days_left=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
[[ $days_left -lt 14 ]] && log "WARNING: $domain expires in $days_left days!"
[[ $days_left -ge 14 ]] && log "OK: $domain expires in $days_left days"
fi
done
}
check_rootkits() {
section "ROOTKIT SCAN"
command -v rkhunter &>/dev/null && rkhunter --check --skip-keypress --report-warnings-only 2>/dev/null | tee -a "$REPORT_FILE"
command -v chkrootkit &>/dev/null && chkrootkit 2>/dev/null | grep -v "not found" | tee -a "$REPORT_FILE"
}
run_lynis() {
section "LYNIS AUDIT"
command -v lynis &>/dev/null || apt-get install -y lynis
lynis audit system --quick --quiet 2>/dev/null | tail -50 >> "$REPORT_FILE"
local score=$(grep "Hardening index" /var/log/lynis.log 2>/dev/null | tail -1 | grep -oE '[0-9]+')
[[ -n "$score" ]] && log "Lynis hardening score: $score/100"
}
generate_summary() {
section "SCAN SUMMARY"
log "Scan completed at $(date)"
log "Full report: $REPORT_FILE"
if [[ -n "$DISCORD_WEBHOOK" ]]; then
local color=3066993 status="✅ Clean"
grep -q "WARNING\|ERROR" "$REPORT_FILE" && { color=16744256; status="⚠️ Issues Found"; }
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{"embeds": [{"title": "Security Scan: $status","description": "Scan on $(hostname)","color": $color}]}" || true
fi
}
main() {
log "=========================================="
log "Security Scan - $(hostname)"
log "=========================================="
check_updates || true
check_open_ports || true
check_auth_failures || true
check_ssl_certs || true
check_rootkits || true
run_lynis || true
generate_summary
}
main "$@"Scheduling Security Tasks
Use systemd timers to run security checks automatically:
[Unit]
Description=Weekly Security Scan
[Timer]
OnCalendar=Sun *-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target[Unit]
Description=Security Scan
After=network.target
[Service]
Type=oneshot
ExecStart=/opt/scripts/security-scan.sh
Environment=DISCORD_WEBHOOK=https://discord.com/api/webhooks/xxx
Environment=SSL_DOMAINS=example.com api.example.comsystemctl daemon-reload
systemctl enable --now security-scan.timer
systemctl enable --now aide-check.timerTips & Quick Reference
Security Best Practices
- Layer your defenses — No single tool catches everything
- Automate verification — Manual checks get forgotten
- Alert on changes — Unexpected changes are often first signs of compromise
- Keep baselines updated — After legitimate changes, update baselines
- Test your alerts — Make sure notifications actually reach you
- Document exceptions — Know why each security exception exists
Quick Reference: Security Prompts
| Need | Prompt Pattern |
|---|---|
| Firewall rules | "Create nftables rules for [service] with [rate limiting/geo blocking]" |
| SSH hardening | "Generate SSH config with [2FA/certificates/specific ciphers]" |
| Compliance | "Create compliance checker for [CIS/PCI-DSS/HIPAA] on [OS]" |
| IDS setup | "Configure AIDE to monitor [paths] with [exclusions]" |
| Scanning | "Create security scan script checking [updates/ports/certs/rootkits]" |
