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 machine
  • OUTPUT: packets originating from the local machine
  • FORWARD: packets routed through the machine (router/gateway)
Rule Targets
  • ACCEPT: allow the packet through
  • DROP: silently discard (no reply to sender)
  • REJECT: discard and send an error back
  • LOG: 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