📑 Table of Contents
- Why Server Logs Are Your First Line of Defense
- Common Attack Patterns Visible in Logs
- Setting Up Log-Based Intrusion Detection
- Identifying Vulnerability Scanners and Their Signatures
- SSH and Authentication Log Analysis
- Web Application Firewall (WAF) Log Correlation
- Detecting Data Exfiltration Through Log Anomalies
- Automated Alerting for Security Events
- Compliance and Audit Logging Requirements
- Using LogBeast for Real-Time Security Monitoring
Why Server Logs Are Your First Line of Defense
Every attack leaves a trail. Whether it is a brute force attempt against your SSH daemon, a SQL injection probe against your web application, or a slow-and-low reconnaissance scan mapping your infrastructure, the evidence lives in your server logs. The question is not whether the attack was logged -- it almost certainly was -- but whether anyone is watching.
Most organizations discover breaches weeks or months after initial compromise. According to industry reports, the average time to detect a breach exceeds 200 days. The irony is that the signs were present in the logs from day one. A failed login spike here, an unusual outbound data transfer there, a burst of 404 errors targeting paths that do not exist on your server -- each of these signals is a thread that, when pulled, unravels the full story of an intrusion.
🔑 Key Insight: Firewalls and intrusion prevention systems block known threats at the perimeter. But attackers who bypass those defenses -- through zero-day exploits, stolen credentials, or social engineering -- are only visible in your logs. Log analysis is not a supplement to perimeter security; it is the layer that catches everything the perimeter misses.
Server logs come in many forms. Web server access logs (Nginx, Apache) record every HTTP request. Authentication logs (/var/log/auth.log, /var/log/secure) track login attempts. System logs (/var/log/syslog, journald) capture kernel events, service starts, and privilege escalations. Application logs record business-logic events. Together, these logs form a complete audit trail of everything that happens on your server.
In this guide, we will walk through the specific attack patterns you can detect through log analysis, how to set up automated detection pipelines, and how tools like LogBeast can transform raw log data into actionable security intelligence. If you are new to log analysis, start with our complete server logs guide for a primer on formats and parsing techniques.
Common Attack Patterns Visible in Logs
Before you can defend against attacks, you need to recognize what they look like in your logs. Different attack categories produce distinct signatures, and understanding these patterns is the foundation of log-based security monitoring.
Brute Force Attacks
Brute force attacks are the simplest and most common form of intrusion attempt. Attackers systematically try username/password combinations against login endpoints, hoping to find weak credentials. They are loud in logs and easy to detect.
# Detect SSH brute force in auth.log
# Look for repeated "Failed password" entries from the same IP
grep "Failed password" /var/log/auth.log | \
awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20
# Sample output showing brute force:
# 4827 185.220.101.42
# 3291 45.148.10.174
# 2847 193.106.191.52
# 12 10.0.1.15 # This is probably a legitimate user who forgot their password
# Detect HTTP login brute force
grep "POST /login" /var/log/nginx/access.log | \
awk '{print $1}' | sort | uniq -c | sort -rn | head -20
⚠️ Warning: Modern brute force attacks distribute attempts across thousands of IPs using botnets or residential proxies. No single IP may trigger a threshold, but the aggregate pattern -- hundreds of unique IPs all hitting /login with failed attempts -- is unmistakable when you analyze at the endpoint level rather than per-IP.
SQL Injection Attempts
SQL injection probes appear in web server access logs as requests containing SQL syntax in URL parameters, headers, or POST bodies. Even when your application is properly parameterized and immune to SQLi, the attempts themselves reveal that someone is actively probing your attack surface.
# Find SQL injection attempts in access logs
grep -iE "(union\+select|or\+1=1|drop\+table|insert\+into|select\+from|' or '|%27|%22|--)" \
/var/log/nginx/access.log | awk '{print $1, $7}' | head -30
# Common SQLi signatures to watch for:
# UNION SELECT - Data extraction attempt
# OR 1=1 - Authentication bypass
# DROP TABLE - Destructive attack
# ' OR ''=' - Classic bypass
# %27 (url-encoded ') - Obfuscated single quote
# ; -- - Comment injection
# SLEEP(5) - Blind SQLi timing probe
# BENCHMARK() - Blind SQLi via CPU load
Path Traversal Attacks
Path traversal (directory traversal) attacks attempt to access files outside the web root by using ../ sequences. They target sensitive files like /etc/passwd, /etc/shadow, application configuration files, and environment variables.
# Detect path traversal attempts
grep -E "(\.\./|\.\.\\\\|%2e%2e|%252e%252e)" /var/log/nginx/access.log | \
awk '{print $1, $7, $9}' | head -20
# Common traversal targets:
# ../../etc/passwd
# ../../etc/shadow
# ../../proc/self/environ
# ../../var/log/auth.log
# ../.env
# ../../wp-config.php
Cross-Site Scripting (XSS) Probes
XSS probes inject JavaScript into URL parameters, form fields, and headers. Even reflected XSS attempts that fail leave clear signatures in access logs:
# Detect XSS probes in access logs
grep -iE "(
| Attack Type | Log Source | Key Signature | Risk Level |
|---|---|---|---|
| Brute Force (SSH) | auth.log | Repeated "Failed password" from same IP/subnet | 🔴 Critical |
| Brute Force (HTTP) | access.log | POST floods to /login with 401/403 responses | 🔴 Critical |
| SQL Injection | access.log | UNION SELECT, OR 1=1, single quotes in URLs | 🔴 Critical |
| Path Traversal | access.log | ../ sequences targeting /etc/passwd, .env | 🟠 High |
| XSS Probes | access.log | <script> tags, javascript: URIs in parameters | 🟠 High |
| Command Injection | access.log | ; ls, | cat, backticks, $() in parameters | 🔴 Critical |
| File Inclusion | access.log | php://filter, data://, expect:// in URLs | 🔴 Critical |
| Privilege Escalation | auth.log, syslog | Unexpected sudo usage, uid=0 processes | 🔴 Critical |
Setting Up Log-Based Intrusion Detection
A proper log-based intrusion detection system (IDS) goes beyond ad-hoc grep commands. It requires structured collection, pattern matching, and alerting. Here is how to build one from the ground up.
Step 1: Centralize Your Logs
The first requirement is getting all logs into a single location where they can be correlated. Attackers often touch multiple services -- an SSH brute force may precede a web application exploit, which may precede privilege escalation.
# /etc/rsyslog.d/50-remote.conf
# Forward all logs to a central syslog server
*.* @@logserver.internal:514
# Or use journald to forward to a central location
# /etc/systemd/journal-upload.conf
[Upload]
URL=https://logserver.internal:19532
ServerKeyFile=/etc/ssl/private/journal-upload.pem
ServerCertificateFile=/etc/ssl/certs/journal-upload.pem
TrustedCertificateFile=/etc/ssl/ca/ca.pem
Step 2: Define Detection Rules
Create a structured rule set that maps log patterns to security events. Here is a Python-based detection engine that processes multiple log sources:
#!/usr/bin/env python3
"""Simple log-based intrusion detection system."""
import re
import sys
from collections import defaultdict
from datetime import datetime, timedelta
# Detection rules: (name, log_pattern, threshold, window_seconds, severity)
RULES = [
{
'name': 'SSH Brute Force',
'pattern': re.compile(r'Failed password.*from (\d+\.\d+\.\d+\.\d+)'),
'threshold': 10,
'window': 300,
'severity': 'CRITICAL',
'group_by': 'ip', # group by captured IP
},
{
'name': 'HTTP Login Brute Force',
'pattern': re.compile(r'(\d+\.\d+\.\d+\.\d+).*POST.*/(?:login|signin|wp-login).*(?:401|403)'),
'threshold': 20,
'window': 60,
'severity': 'CRITICAL',
'group_by': 'ip',
},
{
'name': 'SQL Injection Attempt',
'pattern': re.compile(r'(\d+\.\d+\.\d+\.\d+).*(?:union.*select|or\+1=1|drop\+table|sleep\(\d)', re.I),
'threshold': 3,
'window': 600,
'severity': 'HIGH',
'group_by': 'ip',
},
{
'name': 'Path Traversal',
'pattern': re.compile(r'(\d+\.\d+\.\d+\.\d+).*\.\.(?:/|%2f|%252f)', re.I),
'threshold': 5,
'window': 300,
'severity': 'HIGH',
'group_by': 'ip',
},
{
'name': 'Vulnerability Scanner',
'pattern': re.compile(r'(\d+\.\d+\.\d+\.\d+).*(?:\.env|wp-admin|phpmyadmin|actuator|\.git/config)', re.I),
'threshold': 5,
'window': 600,
'severity': 'HIGH',
'group_by': 'ip',
},
{
'name': 'Privilege Escalation',
'pattern': re.compile(r'sudo:.*COMMAND=.*(chmod\+s|chown.*root|passwd|useradd|visudo)'),
'threshold': 1,
'window': 60,
'severity': 'CRITICAL',
'group_by': 'all',
},
]
class LogIDS:
def __init__(self):
self.events = defaultdict(list) # rule_name -> [(timestamp, group_key)]
self.alerts = []
def process_line(self, line, timestamp=None):
if timestamp is None:
timestamp = datetime.now()
for rule in RULES:
match = rule['pattern'].search(line)
if match:
group_key = match.group(1) if rule['group_by'] == 'ip' else 'global'
event_key = f"{rule['name']}:{group_key}"
# Add event and prune old entries
cutoff = timestamp - timedelta(seconds=rule['window'])
self.events[event_key] = [
t for t in self.events[event_key] if t > cutoff
]
self.events[event_key].append(timestamp)
# Check threshold
if len(self.events[event_key]) >= rule['threshold']:
alert = {
'rule': rule['name'],
'severity': rule['severity'],
'source': group_key,
'count': len(self.events[event_key]),
'window': rule['window'],
'timestamp': timestamp,
}
self.alerts.append(alert)
self.events[event_key] = [] # reset after alert
print(f"[{rule['severity']}] {rule['name']}: "
f"{group_key} - {alert['count']} events in "
f"{rule['window']}s", file=sys.stderr)
def report(self):
print(f"\n=== INTRUSION DETECTION REPORT ===")
print(f"Total alerts: {len(self.alerts)}\n")
for alert in sorted(self.alerts, key=lambda a: a['timestamp']):
print(f" [{alert['severity']}] {alert['rule']}")
print(f" Source: {alert['source']}")
print(f" Events: {alert['count']} in {alert['window']}s window")
print(f" Time: {alert['timestamp']}")
print()
if __name__ == "__main__":
ids = LogIDS()
for log_file in sys.argv[1:]:
with open(log_file) as f:
for line in f:
ids.process_line(line.strip())
ids.report()
💡 Pro Tip: LogBeast includes a built-in security analysis engine that applies over 200 detection rules across all your log sources simultaneously. It correlates events across web, auth, system, and application logs to surface attack chains that single-source analysis would miss.
Step 3: Establish Baselines
Detection rules need context. A server that normally sees 5 failed SSH logins per day should alert at 20. A server that handles 500 legitimate failures per day (large user base) might need a threshold of 2,000. Baseline your normal activity before setting thresholds:
# Baseline SSH failures per day over the last 30 days
for i in $(seq 0 29); do
date_str=$(date -d "$i days ago" +%b\ %e)
count=$(grep "$date_str" /var/log/auth.log | grep -c "Failed password")
echo "$(date -d "$i days ago" +%Y-%m-%d): $count failures"
done
# Baseline HTTP error rates per hour
awk '{print substr($4, 2, 14)}' /var/log/nginx/access.log | \
sort | uniq -c | awk '{print $2, $1}' > /tmp/hourly_baseline.txt
# Calculate mean and standard deviation for anomaly detection
awk '{sum+=$2; sumsq+=$2*$2; n++}
END {mean=sum/n; sd=sqrt(sumsq/n - mean*mean);
print "Mean:", mean, "StdDev:", sd, "Alert threshold (3-sigma):", mean+3*sd}' \
/tmp/hourly_baseline.txt
Identifying Vulnerability Scanners and Their Signatures
Vulnerability scanners are among the noisiest attackers in your logs. Automated tools like Nikto, Nmap, Nessus, OpenVAS, and custom scripts systematically probe your server for known weaknesses. Detecting them early tells you someone is mapping your attack surface -- an intrusion may follow.
Scanner Fingerprints in Access Logs
Each scanner leaves characteristic patterns. Here are the most common signatures:
| Scanner | User-Agent Signature | Behavioral Pattern |
|---|---|---|
| Nikto | Contains "Nikto" | Rapid requests to /cgi-bin/, /admin/, /.env, /phpinfo.php |
| Nmap HTTP scripts | Contains "Nmap Scripting Engine" | HEAD requests to identify server version, probes common ports |
| sqlmap | Contains "sqlmap" | Parameterized SQLi payloads appended to every URL parameter |
| WPScan | Contains "WPScan" | WordPress-specific paths: /wp-json/, /xmlrpc.php, /wp-content/plugins/ |
| Acunetix | Contains "Acunetix" | acunetix-wvs-test paths, systematic parameter fuzzing |
| ZAP (OWASP) | Contains "ZAP" | Proxy-style scanning with injected headers |
| Custom/Unknown | Varies or spoofed | Rapid 404 rate, requests to non-existent admin panels |
# Detect known vulnerability scanners
grep -iE "(nikto|nmap|sqlmap|wpscan|acunetix|nessus|openvas|ZAP)" \
/var/log/nginx/access.log | awk '{print $1}' | sort -u
# Detect scanner behavior: IPs with high 404 rates on admin paths
awk '$9 == 404 && $7 ~ /(\.env|wp-admin|phpmyadmin|\.git|actuator|config|backup|admin)/ {
print $1
}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# Comprehensive scanner detection: IPs requesting many non-existent paths
awk '$9 == 404 {ips[$1]++} END {for(ip in ips) if(ips[ip]>50) print ips[ip], ip}' \
/var/log/nginx/access.log | sort -rn
🔑 Key Insight: Professional attackers do not use off-the-shelf scanners with default User-Agent strings. They write custom tools or modify scanner fingerprints. The reliable detection method is behavioral: any IP that generates a high volume of 404 errors across diverse administrative paths is scanning, regardless of its User-Agent.
Building a Scanner Detection Script
#!/usr/bin/env python3
"""Detect vulnerability scanner activity from access logs."""
import re
import sys
from collections import defaultdict
# Paths that scanners commonly probe
SCANNER_PATHS = {
'/.env', '/.git/config', '/.git/HEAD', '/.svn/entries',
'/wp-admin/', '/wp-login.php', '/xmlrpc.php', '/wp-config.php',
'/phpmyadmin/', '/pma/', '/myadmin/', '/mysql/',
'/admin/', '/administrator/', '/manager/', '/console/',
'/actuator/', '/actuator/health', '/actuator/env',
'/solr/', '/jenkins/', '/jmx-console/',
'/server-status', '/server-info', '/.htaccess', '/.htpasswd',
'/backup.sql', '/dump.sql', '/database.sql', '/db.sql',
'/config.php', '/configuration.php', '/settings.php',
'/phpinfo.php', '/info.php', '/test.php',
'/api/v1/admin', '/graphql', '/api/swagger',
'/cgi-bin/', '/shell', '/cmd', '/command',
}
LOG_RE = re.compile(r'(\S+) \S+ \S+ \[.+?\] "(\S+) (\S+) \S+" (\d+)')
def detect_scanners(log_file):
ip_scanner_hits = defaultdict(int)
ip_total = defaultdict(int)
ip_paths = defaultdict(set)
ip_errors = defaultdict(int)
with open(log_file) as f:
for line in f:
m = LOG_RE.search(line)
if not m:
continue
ip, method, path, status = m.groups()
ip_total[ip] += 1
if int(status) >= 400:
ip_errors[ip] += 1
# Check against scanner paths
path_lower = path.lower().split('?')[0]
for sp in SCANNER_PATHS:
if path_lower.startswith(sp) or path_lower == sp:
ip_scanner_hits[ip] += 1
ip_paths[ip].add(sp)
break
print(f"{'IP':<20} {'Scanner Hits':>12} {'Total Reqs':>10} {'Errors':>8} {'Unique Paths':>12} {'Verdict'}")
print("-" * 85)
for ip in sorted(ip_scanner_hits, key=ip_scanner_hits.get, reverse=True)[:30]:
hits = ip_scanner_hits[ip]
total = ip_total[ip]
errors = ip_errors[ip]
unique = len(ip_paths[ip])
verdict = "SCANNER" if hits >= 10 or unique >= 5 else "SUSPECT" if hits >= 3 else "MONITOR"
print(f"{ip:<20} {hits:>12} {total:>10} {errors:>8} {unique:>12} {verdict}")
if __name__ == "__main__":
detect_scanners(sys.argv[1])
SSH and Authentication Log Analysis
SSH remains the primary administrative access method for Linux servers, making it the number one target for automated attacks. Your authentication logs (/var/log/auth.log on Debian/Ubuntu, /var/log/secure on RHEL/CentOS) contain a wealth of security intelligence.
Key Events to Monitor
- Failed password attempts: The most obvious brute force indicator
- Invalid user attempts: Attackers trying common usernames (root, admin, test, guest, oracle, postgres)
- Accepted logins from unusual IPs: Legitimate-looking access from unexpected locations
- Key authentication failures: Could indicate compromised key distribution
- Session opened/closed patterns: Short sessions may indicate automated access
- sudo usage: Privilege escalation attempts after initial access
# Failed password analysis - who is being targeted?
grep "Failed password" /var/log/auth.log | \
awk '{for(i=1;i<=NF;i++) if($i=="for") print $(i+1)}' | \
sort | uniq -c | sort -rn | head -20
# Invalid user attempts (non-existent accounts)
grep "Invalid user" /var/log/auth.log | \
awk '{print $8}' | sort | uniq -c | sort -rn | head -20
# Sample output:
# 847 admin
# 623 root
# 412 test
# 298 user
# 187 oracle
# 145 postgres
# 134 ftpuser
# 98 ubuntu
# Successful logins - verify each one is legitimate
grep "Accepted" /var/log/auth.log | \
awk '{print $1, $2, $3, $9, $11}' | tail -20
# Detect successful login after many failures (possible compromise!)
awk '/Failed password/ {fails[$NF]++}
/Accepted/ {if(fails[$NF]>10) print "ALERT: Login success after", fails[$NF], "failures from", $NF}' \
/var/log/auth.log
⚠️ Warning: A successful login following hundreds of failed attempts from the same IP is the single most critical alert in SSH log analysis. It likely means an attacker cracked a password. Investigate immediately: check what commands the session executed, what files were accessed, and whether any backdoors were planted.
Hardening SSH Based on Log Intelligence
Use the patterns you find in your logs to tighten your SSH configuration:
# /etc/ssh/sshd_config - Hardened configuration based on log analysis
# Disable password authentication entirely (use keys only)
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
# Restrict to specific users
AllowUsers deploy monitoring admin
# Disable root login
PermitRootLogin no
# Limit authentication attempts per connection
MaxAuthTries 3
# Set idle timeout
ClientAliveInterval 300
ClientAliveCountMax 2
# Use only strong key exchange algorithms
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com
# Log verbosely for security monitoring
LogLevel VERBOSE
# Change default port (reduces noise, not security)
Port 2222
# Fail2ban jail for SSH - configure based on your baseline
# /etc/fail2ban/jail.d/ssh.conf
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-multiport[name=sshd, port="2222"]
sendmail-whois[name=sshd, dest=security@example.com]
Web Application Firewall (WAF) Log Correlation
A Web Application Firewall generates its own logs when it blocks or flags requests. Correlating WAF logs with web server access logs and application logs reveals the complete attack picture -- what was attempted, what was blocked, and critically, what may have slipped through.
ModSecurity Audit Log Analysis
ModSecurity is the most widely deployed open-source WAF. Its audit log contains detailed information about every intercepted request:
# Parse ModSecurity audit log for blocked attacks
# Audit log format: section A (headers), B (request body), F (response headers), H (audit info)
# Count blocked requests by rule ID
grep -oP 'id "\K\d+' /var/log/modsec_audit.log | sort | uniq -c | sort -rn | head -20
# Common ModSecurity rule IDs and what they detect:
# 942100 - SQL Injection attack detected
# 941100 - XSS attack detected
# 930100 - Path traversal attack
# 913100 - Scanner detection
# 920350 - IP not allowed by geo-restriction
# 949110 - Inbound anomaly score exceeded
# Extract source IPs from blocked requests
grep -B5 'Action: Intercepted' /var/log/modsec_audit.log | \
grep -oP '\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn | head -20
# Find requests that scored high but were NOT blocked (detection-only mode)
grep 'Anomaly Score Exceeded' /var/log/modsec_audit.log | \
grep -v 'Action: Intercepted'
Cross-Correlating WAF and Access Logs
The real power of WAF logs comes from correlating them with your access logs. This reveals gaps in WAF coverage:
#!/usr/bin/env python3
"""Correlate WAF blocks with access log entries to find gaps."""
import re
import sys
from collections import defaultdict
def load_waf_blocked_ips(waf_log):
"""Extract IPs that the WAF has blocked."""
blocked = set()
with open(waf_log) as f:
for line in f:
match = re.search(r'(\d+\.\d+\.\d+\.\d+)', line)
if match and 'Intercepted' in line:
blocked.add(match.group(1))
return blocked
def find_unblocked_attacks(access_log, blocked_ips):
"""Find attack signatures in access log from IPs NOT blocked by WAF."""
attack_patterns = [
(re.compile(r'(?:union.*select|or\+1=1|sleep\(\d)', re.I), 'SQLi'),
(re.compile(r'(?: