Network Security Operations
Firewalls, IDS/IPS, VPNs, Security Monitoring, Incident Response
Introduction
Security operations is the discipline of detecting threats and responding to incidents in real time. While previous lessons focused on building secure systems, this lesson covers operating them securely, configuring firewalls, understanding intrusion detection, analyzing security logs with Python, and executing incident response playbooks. This is the day-to-day work of security engineers and SOC teams.
Firewall Concepts & iptables
A firewall is a network security control that inspects incoming and outgoing packets and decides whether to allow or block them based on a set of rules. Firewalls operate at different layers: network firewalls (Layer 3/4) filter by IP address and port; application firewalls (Layer 7) inspect the payload content (HTTP, DNS, TLS). The fundamental model is default-deny: block everything, then explicitly allow only what is needed.
iptables is the classic Linux kernel packet-filtering framework. It sits inside the kernel's Netfilter hook system and processes every packet that enters, leaves, or is forwarded through the machine. Rules are organized into tables (collections of chains) and chains (ordered lists of rules). Each rule matches packet attributes and sets a target, what to do with the packet. On modern Linux systems, nftables is the successor to iptables, but iptables remains ubiquitous and the concepts are identical.
Key Chains
INPUT: packets destined for the local machineOUTPUT: packets originating from the local machineFORWARD: packets routed through the machine (router/gateway)
Rule Targets
ACCEPT: allow the packet throughDROP: silently discard (no reply to sender)REJECT: discard and send an error backLOG: write to kernel log and continue matching
Default Policy
- Set with
-P CHAIN TARGET -P INPUT DROP: deny all inbound by default- Rules are evaluated top-to-bottom; first match wins
- The default policy fires only if no rule matched
# iptables, Linux packet filtering firewall
# ── Default policy: DENY all incoming traffic ─────────────────
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# ── Allow established connections ────────────────────────────
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# ── Allow loopback ────────────────────────────────────────────
sudo iptables -A INPUT -i lo -j ACCEPT
# ── Allow specific services ───────────────────────────────────
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT # SSH
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT # HTTP
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT # HTTPS
# ── Rate limiting, brute force protection ────────────────────
# Allow only 3 SSH connection attempts per 60 seconds per IP
sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW \
-m recent --set --name SSH
sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW \
-m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
# ── Log dropped packets (for monitoring) ─────────────────────
sudo iptables -A INPUT -j LOG --log-prefix "IPTABLES-DROP: "
# ── Save rules (persist across reboots) ──────────────────────
sudo iptables-save > /etc/iptables/rules.v4
# ── Python: Manage firewall rules ────────────────────────────
import subprocess
def block_ip(ip_address: str) -> bool:
"""Block an IP address via iptables. Returns True if successful."""
try:
result = subprocess.run(
["sudo", "iptables", "-A", "INPUT", "-s", ip_address, "-j", "DROP"],
capture_output=True, text=True, timeout=10,
)
return result.returncode == 0
except subprocess.TimeoutExpired:
return False
def list_blocked_ips() -> list[str]:
"""List all IPs blocked by DROP rules."""
result = subprocess.run(
["sudo", "iptables", "-L", "INPUT", "-n"],
capture_output=True, text=True
)
blocked = []
for line in result.stdout.split("\n"):
if "DROP" in line and "all" in line:
parts = line.split()
if len(parts) >= 4:
blocked.append(parts[3]) # Source IP
return blocked
print("Firewall rules set up with default-deny policy")
print("SSH rate-limited to 3 new connections per 60 seconds")
IDS/IPS, Intrusion Detection & Prevention
While a firewall controls which traffic is allowed in or out based on ports and addresses, it cannot tell whether allowed traffic is malicious. An SSH connection on port 22 is permitted by the firewall, but is it a legitimate admin or an attacker running a brute-force attack? This is where IDS and IPS come in.
An Intrusion Detection System (IDS) monitors network traffic or host activity, compares it against a library of known attack signatures and behavioral baselines, and raises alerts when something suspicious is detected, but it does not block traffic. An Intrusion Prevention System (IPS) does the same analysis but sits inline in the traffic path and can actively drop malicious packets in real time. The same tool (e.g. Suricata or Snort) can operate as either, depending on how it is deployed. Detection systems use two main techniques: signature-based detection (matching known attack patterns, fast but blind to novel attacks) and anomaly-based detection (flagging deviations from a learned baseline, catches zero-days but has higher false-positive rates).
IDS, Detection
Monitors and alerts on suspicious activity. Does NOT block traffic.
- Network IDS (NIDS), monitors network traffic (Suricata, Snort)
- Host IDS (HIDS), monitors system calls, file changes (OSSEC, Wazuh)
- Signature-based, matches known attack patterns
- Anomaly-based, detects deviations from baseline
IPS, Prevention
Actively blocks detected attacks in real time.
- Inline deployment, traffic passes through the IPS
- Drops packets matching rules automatically
- Higher risk of false positives blocking legitimate traffic
- Suricata and Snort can operate as IPS with nfqueue
# Snort/Suricata rule anatomy # alert tcp any any -> $HOME_NET 22 ( # msg:"SSH brute force attempt"; ← Alert message # flow:stateless; # flags:S,12; ← SYN flag (new connection) # threshold:type both, track by_src, ← Track per source IP # count 5, seconds 60; ← 5+ in 60 seconds # classtype:attempted-admin; # sid:100001; ← Unique rule ID # rev:1; # ) # Common Suricata rules for web security: # alert http any any -> any any (msg:"SQL Injection"; content:"' OR '1'='1"; sid:100002;) # alert http any any -> any any (msg:"XSS Attempt"; content:"<script>"; sid:100003;) # alert http any any -> any any (msg:"Path Traversal"; content:"../"; sid:100004;)
Python: Security Log Parser & Anomaly Detection
# log_monitor.py, Real-time security log analyzer
import re
from collections import defaultdict, deque
from datetime import datetime, timezone, timedelta
from dataclasses import dataclass, field
@dataclass
class SecurityEvent:
timestamp: datetime
source_ip: str
event_type: str
details: str
@dataclass
class SecurityMonitor:
"""Track security events and detect anomalies."""
failed_auth_window: int = 60 # seconds
brute_force_threshold: int = 5
_failed_auths: dict = field(default_factory=lambda: defaultdict(list))
_blocked_ips: set = field(default_factory=set)
_alerts: list = field(default_factory=list)
# SSH log pattern: Jan 15 14:23:01 sshd[1234]: Failed password for root from 192.168.1.100
SSH_PATTERN = re.compile(
r"(?P<date>.+?) sshd[d+]: Failed password for (?P<user>S+) from (?P<ip>[d.]+)"
)
def process_log_line(self, line: str) -> SecurityEvent | None:
"""Parse a log line and check for security events."""
match = self.SSH_PATTERN.search(line)
if not match:
return None
ip = match.group("ip")
user = match.group("user")
now = datetime.now(timezone.utc)
event = SecurityEvent(now, ip, "FAILED_AUTH", f"user={user}")
self._check_brute_force(ip, now)
return event
def _check_brute_force(self, ip: str, now: datetime) -> None:
"""Detect brute force: N failed auths within the time window."""
# Keep only recent attempts
cutoff = now - timedelta(seconds=self.failed_auth_window)
self._failed_auths[ip] = [t for t in self._failed_auths[ip] if t > cutoff]
self._failed_auths[ip].append(now)
count = len(self._failed_auths[ip])
if count >= self.brute_force_threshold and ip not in self._blocked_ips:
self._blocked_ips.add(ip)
alert = f"🚨 BRUTE FORCE DETECTED: {ip} ({count} attempts in {self.failed_auth_window}s)"
self._alerts.append(alert)
print(alert)
# Simulate log analysis
monitor = SecurityMonitor(brute_force_threshold=3, failed_auth_window=30)
fake_logs = [
"Jan 15 14:23:01 myserver sshd[1234]: Failed password for root from 10.0.0.42 port 54321 ssh2",
"Jan 15 14:23:05 myserver sshd[1234]: Failed password for admin from 10.0.0.42 port 54322 ssh2",
"Jan 15 14:23:09 myserver sshd[1234]: Failed password for ubuntu from 10.0.0.42 port 54323 ssh2",
"Jan 15 14:23:13 myserver sshd[1234]: Failed password for root from 10.0.0.42 port 54324 ssh2",
"Jan 15 14:23:17 myserver sshd[1234]: Accepted publickey for alice from 192.168.1.5 port 44100",
]
for log_line in fake_logs:
event = monitor.process_log_line(log_line)
if event:
print(f" Event: {event.event_type} from {event.source_ip} ({event.details})")
print(f"\nBlocked IPs: {monitor._blocked_ips}")
Expected Output:
Event: FAILED_AUTH from 10.0.0.42 (user=root)
Event: FAILED_AUTH from 10.0.0.42 (user=admin)
Event: FAILED_AUTH from 10.0.0.42 (user=ubuntu)
🚨 BRUTE FORCE DETECTED: 10.0.0.42 (3 attempts in 30s)
Event: FAILED_AUTH from 10.0.0.42 (user=root)
Blocked IPs: {'10.0.0.42'}NIST Incident Response Framework
1. Preparation
Policies, tools, and playbooks ready. Response team identified. Communication channels established. Regular drills.
2. Detection & Analysis
Monitor logs, alerts, and anomaly detection. Determine scope, severity, and root cause. Preserve evidence.
3. Containment
Isolate affected systems. Block attacker's access. Prevent lateral movement. Short-term vs long-term containment.
4. Eradication
Remove malware, close vulnerabilities, patch systems. Ensure the threat is fully eliminated before recovery.
5. Recovery
Restore systems from clean backups. Monitor closely for re-compromise. Validate normal operations.
6. Lessons Learned
Post-incident review within 2 weeks. Document timeline, decisions, and improvements. Update playbooks.
# incident_response.py, Automated containment script
import subprocess
import logging
from datetime import datetime, timezone
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
def quarantine_ip(ip: str) -> bool:
"""Block an IP immediately via iptables."""
cmd = ["sudo", "iptables", "-I", "INPUT", "1", "-s", ip, "-j", "DROP"]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
logging.warning(f"QUARANTINE: Blocked IP {ip}")
return True
logging.error(f"Failed to block {ip}: {result.stderr}")
return False
def send_alert(severity: str, title: str, details: str) -> None:
"""Send security alert to SIEM/Slack/PagerDuty."""
# In production: call webhook, send email, or push to SIEM
alert = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"severity": severity,
"title": title,
"details": details,
}
logging.critical(f"SECURITY ALERT [{severity}]: {title} | {details}")
# requests.post(SLACK_WEBHOOK, json={"text": f"🚨 {title}
{details}"})
def respond_to_brute_force(ip: str, attempt_count: int) -> None:
"""Automated brute force incident response."""
logging.info(f"Starting incident response for {ip} ({attempt_count} attempts)")
# Step 1: Containment, block the IP
if quarantine_ip(ip):
send_alert("HIGH", "Brute Force Detected & Blocked",
f"IP {ip} blocked after {attempt_count} failed auth attempts")
# Step 2: Collect evidence
logging.info("Evidence collected, run: sudo lastb | grep " + ip)
logging.info("Full packet capture: sudo tcpdump -i eth0 src " + ip + " -w incident.pcap")
# Step 3: Document
logging.info(f"Incident documented at {datetime.now(timezone.utc).isoformat()}")
# Simulate incident response
respond_to_brute_force("10.0.0.42", attempt_count=12)
Key Takeaways
- Default-deny firewalls, block all traffic by default; allowlist only what's needed
- Rate limiting at the firewall level, iptables can detect and block brute force without application code
- IDS detects, IPS prevents, use IDS in monitoring mode first, promote rules to IPS after tuning
- Log everything security-relevant, authentication events, access control decisions, admin actions
- Automate detection AND response, manual incident response is too slow; Python scripts can quarantine in milliseconds
- Follow NIST IR phases, Prepare → Detect → Contain → Eradicate → Recover → Learn
What's Next?
Lesson 19 is the capstone, bringing together all 18 lessons to build a complete, production-grade secure multi-tenant application.
- Secure auth, bcrypt + JWT from Lesson 3 & 10
- Tenant isolation, RBAC middleware from Lesson 11
- Encrypted storage, AES-GCM per-tenant keys from Lesson 4
- Secrets management, environment validation from Lesson 15