Challenge 9: Network segmentation (IEC 62443 zones)

Objective: Implement zone-based architecture isolating corporate IT, SCADA, control systems, and safety systems using runtime firewall rules and IDS/IPS prevention.

Category: Architecture & Defence in Depth

Difficulty: Intermediate

Time Required: 45-60 minutes

Learning outcomes

By completing this challenge, you will:

  1. Understand IEC 62443 zone-based network architecture

  2. Implement firewall policies between security zones

  3. Use IDS/IPS for active threat blocking

  4. Respond to incidents using Blue Team CLI

  5. Understand config (persistent) vs runtime (temporary) changes

  6. Test attacks against your defences to verify effectiveness

Background: IEC 62443 zone model

Industrial control systems use hierarchical zones based on the Purdue Model:

┌─────────────────────────────────────────────────────┐
│  ENTERPRISE ZONE (Level 4)                          │
│  • Historians, business systems, analytics          │
│  • Security Level: Medium (IT systems)              │
└──────────────────┬──────────────────────────────────┘
                   │ Conduit (restricted)
┌──────────────────▼──────────────────────────────────┐
│  DMZ (Level 3.5)                                    │
│  • Jump servers, data diodes, protocol converters   │
│  • Security Level: High (boundary protection)       │
└──────────────────┬──────────────────────────────────┘
                   │ Conduit (restricted)
┌──────────────────▼──────────────────────────────────┐
│  OPERATIONS ZONE (Level 3)                          │
│  • SCADA servers, HMIs, engineering workstations    │
│  • Security Level: High (supervisory control)       │
└──────────────────┬──────────────────────────────────┘
                   │ Conduit (restricted)
┌──────────────────▼──────────────────────────────────┐
│  CONTROL ZONE (Levels 0-2)                          │
│  • PLCs, RTUs, safety systems, field devices        │
│  • Security Level: Critical (process control)       │
└─────────────────────────────────────────────────────┘

Key Principle: Traffic between zones must be explicitly allowed. Default deny.

Initial state (vulnerable)

Before hardening, the simulation has:

  • Zones defined in config/network.yml

  • ❌ Segmentation DISABLED: enabled: false

  • ❌ Firewall has NO RULES: Everything allowed

  • ❌ IDS in detection-only mode: Alerts but doesn’t block

  • ❌ All zones can reach all zones

Result: Attacker from enterprise zone can directly access control zone PLCs.

Part 1: Configuration changes (require restart)

Configuration changes establish baseline security that persists across restarts.

Step 1.1: Enable network segmentation

Edit config/network.yml:

segmentation:
  enabled: true   # ← Change from false
  mode: strict    # Full Purdue model enforcement

What this does:

  • Enables zone boundary checks in NetworkSimulator

  • Requires explicit inter-zone policies to allow traffic

  • Default deny for undefined zone-to-zone paths

Step 1.2: Configure baseline firewall rules

Edit config/firewall.yml:

default_action: deny  # ← Change from allow

baseline_rules:
  # Allow SCADA to Control Zone (normal operations)
  - name: "Baseline: SCADA to Control"
    action: ALLOW
    priority: 50
    source_zone: operations_zone
    dest_zone: control_zone
    enabled: true
    description: "SCADA servers must poll PLCs"

  # Allow Historian to SCADA (data collection)
  - name: "Baseline: Historian to SCADA"
    action: ALLOW
    priority: 50
    source_zone: enterprise_zone
    dest_zone: operations_zone
    enabled: true
    description: "Historian collects SCADA data"

  # Block Enterprise to Control (defence in depth)
  - name: "Baseline: Block Enterprise to Control"
    action: DROP
    priority: 100
    source_zone: enterprise_zone
    dest_zone: control_zone
    enabled: true
    description: "No direct enterprise access to control systems"

Step 1.3: Configure IDS/IPS baseline

Edit config/ids_ips.yml:

prevention_mode: false  # Keep false initially
auto_block_on_critical: true
permanent_blocked_ips: []

detection_thresholds:
  scan_threshold: 5
  scan_time_window: 60.0

Note: We start with IPS disabled so students see detection-only mode first.

Step 1.4: Restart simulator

python tools/simulator_manager.py

Baseline rules are now active.

Part 2: Verify baseline security

Step 2.1: Check security status

python tools/blue_team.py status

Expected output:

Firewall:
  Default Action: deny
  Active Rules: 3
  Connections Checked: 0
  Connections Blocked: 0

IDS/IPS:
  Mode: IDS (Detection Only)
  Blocked IPs: 0
  Active Alerts: 0

Step 2.2: List firewall rules

python tools/blue_team.py firewall list-rules -v

Expected: Baseline rules loaded from config.

Step 2.3: Test legitimate traffic (should work)

# SCADA → Control Zone (allowed by baseline)
python scripts/assessment/scada_polling.py

# Expected: Success - baseline rule allows this

Step 2.4: Test unauthorised traffic (should fail)

# Enterprise → Control Zone (blocked by baseline)
# Simulate attack from enterprise workstation
python scripts/vulns/modbus_write.py --target hex_turbine_plc

# Expected: Connection denied by firewall

Verify in logs:

tail -f logs/simulation.log | grep "denied by firewall"

Part 3: Runtime incident response

Runtime changes are immediate but lost on restart. Used for incident response.

Scenario: Active attack detected

Context: IDS detects port scanning from IP 192.168.1.100

Step 3.1: Enable IPS mode (immediate)

python tools/blue_team.py ids enable-ips

Effect:

  • IDS switches to prevention mode

  • Future CRITICAL alerts will auto-block source IPs

  • Change is immediate (no restart)

Step 3.2: Block attacker IP (immediate)

python tools/blue_team.py ids block-ip 192.168.1.100 "Port scanning detected"

Effect:

  • All connections from 192.168.1.100 are DROPPED

  • Attacker sees timeout or connection refused

  • Logged to audit trail

Step 3.3: Add emergency firewall rule (immediate)

python tools/blue_team.py firewall add-rule \
  --name "Emergency: Block compromised segment" \
  --action DROP \
  --source-network engineering_network \
  --dest-zone control_zone \
  --priority 1 \
  --reason "Suspected compromise in engineering network"

Effect:

  • Entire engineering network blocked from control zone

  • Rule has priority 1 (highest - checked first)

  • Immediate enforcement

Step 3.4: Verify blocks

# Check IPS blocks
python tools/blue_team.py ids list-blocked

# Check firewall rules
python tools/blue_team.py firewall list-rules

# Try attack again - should fail
python scripts/vulns/modbus_write.py --target hex_turbine_plc
# Expected: Connection refused

Step 3.5: Check statistics

python tools/blue_team.py status

Expected:

Firewall:
  Active Rules: 4 (3 baseline + 1 runtime)
  Connections Blocked: 5

IDS/IPS:
  Mode: IPS (BLOCKING)
  Blocked IPs: 1

Part 4: Zone isolation testing

Test matrix: Can zone A reach zone B?

From ↓ To →

Enterprise

Operations

Control

DMZ

Enterprise

✓ (historian)

?

Operations

✓ (SCADA)

?

Control

DMZ

?

?

?

?

Test 1: Enterprise → Control (should fail)

# Attempt Modbus write from enterprise zone
python scripts/vulns/modbus_write.py \
  --target hex_turbine_plc \
  --register 0 \
  --value 1234

# Expected: Connection denied by firewall
# Check logs:
tail -f logs/simulation.log | grep "firewall"

Test 2: Operations → Control (should work)

# SCADA polling (normal operations)
python scripts/assessment/scada_polling.py

# Expected: Success - baseline rule allows

Test 3: Control → Enterprise (should fail)

# PLCs should not initiate outbound connections
# (Normally PLCs don't initiate, but test the policy)

# Expected: Denied - no policy allows control → enterprise

Part 5: Make runtime changes permanent

After incident response, make temporary blocks permanent.

Step 5.1: Add to config files

For IP blocks:

Edit config/ids_ips.yml:

permanent_blocked_ips:
  - "192.168.1.100"  # ← Add attacker IP

For firewall rules:

Edit config/firewall.yml:

baseline_rules:
  # ... existing rules ...

  # Add emergency rule as baseline
  - name: "Block compromised segment"
    action: DROP
    source_network: engineering_network
    dest_zone: control_zone
    priority: 1
    enabled: true
    description: "Permanent block after incident #2026-042"

Step 5.2: Restart and verify

# Restart simulator
python tools/simulator_manager.py

# In another terminal, check rules loaded
python tools/blue_team.py firewall list-rules

# Verify permanent block
python tools/blue_team.py ids list-blocked

Expected: Runtime rules now appear as baseline rules after restart.

Part 6: Advanced scenarios

Scenario A: Lateral movement prevention

Attack: Attacker compromises HMI workstation, attempts to reach PLCs.

Response:

# 1. Block compromised workstation
python tools/blue_team.py ids block-ip 10.20.2.15 "Compromised HMI"

# 2. Isolate entire HMI network temporarily
python tools/blue_team.py firewall add-rule \
  --name "Isolate HMI network" \
  --action DROP \
  --source-network hmi_network \
  --dest-zone control_zone \
  --priority 1 \
  --reason "HMI compromise containment"

# 3. Allow SCADA exception (keep operations running)
python tools/blue_team.py firewall add-rule \
  --name "SCADA exception" \
  --action ALLOW \
  --source-ip 10.20.1.10 \
  --dest-zone control_zone \
  --priority 10 \
  --reason "SCADA must continue operations"

# 4. Verify containment
python scripts/vulns/modbus_write.py --target hex_turbine_plc
# Expected: Connection denied

Scenario B: Emergency access

Need: Engineer needs temporary access from enterprise zone for emergency maintenance.

Response:

# 1. Create temporary allow rule (high priority)
python tools/blue_team.py firewall add-rule \
  --name "TEMP: Emergency engineering access" \
  --action ALLOW \
  --source-ip 10.50.1.42 \
  --dest-zone control_zone \
  --priority 5 \
  --user alice \
  --reason "Emergency turbine maintenance - ticket INC-123"

# 2. After maintenance, remove rule
python tools/blue_team.py firewall list-rules
# Note rule ID (e.g., FW-00042)

python tools/blue_team.py firewall remove-rule FW-00042 \
  --reason "Maintenance complete"

Scenario C: Progressive lockdown

Context: Suspected widespread compromise.

Response:

# Phase 1: Block known attackers
python tools/blue_team.py ids block-ip 192.168.1.100 "Attacker 1"
python tools/blue_team.py ids block-ip 192.168.1.101 "Attacker 2"

# Phase 2: Lockdown entire enterprise zone
python tools/blue_team.py firewall add-rule \
  --name "LOCKDOWN: Block all enterprise" \
  --action DROP \
  --source-zone enterprise_zone \
  --dest-zone control_zone \
  --priority 1 \
  --reason "Widespread compromise suspected"

# Phase 3: Enable IPS for auto-blocking
python tools/blue_team.py ids enable-ips

# Phase 4: Allow critical services only
python tools/blue_team.py firewall add-rule \
  --name "Critical: Historian only" \
  --action ALLOW \
  --source-ip 10.50.1.10 \
  --dest-zone operations_zone \
  --priority 5 \
  --reason "Historian must collect data"

# Verify lockdown
python tools/blue_team.py status

Part 7: Config vs runtime changes

Understanding the difference

Aspect

Config Changes

Runtime Changes

File Location

config/*.yml

Command-line (CLI)

Takes Effect

After restart

Immediately

Persistence

Permanent

Lost on restart

Use Case

Baseline security

Incident response

Example

Default deny policy

Block attacker IP

Audit Trail

Config file changes

ICSLogger events

When to use what

Use Config (Baseline Security):

  • Default firewall policy (allow/deny)

  • Normal operational rules (SCADA → PLCs)

  • Permanent blocks (known bad IPs)

  • IPS mode default (detection vs prevention)

  • Network segmentation enabled/disabled

Use Runtime (Incident Response):

  • Block attacker during active attack

  • Emergency firewall rules

  • Temporary access grants

  • Quick containment actions

  • Testing security changes

Making runtime → permanent

Workflow:

  1. Respond to incident using CLI (immediate)

  2. Verify effectiveness

  3. Add to config file (permanent)

  4. Restart simulator

  5. Verify config loaded

Example:

# Runtime (immediate)
python tools/blue_team.py ids block-ip 192.168.1.100 "Attacker"

# Later, make permanent:
# Edit config/ids_ips.yml, add to permanent_blocked_ips
# Restart: python tools/simulator_manager.py

Testing defences

Test 1: Unauthorised zone access

Objective: Verify enterprise zone cannot reach control zone.

# From enterprise zone perspective
python scripts/vulns/modbus_write.py --target hex_turbine_plc

# Expected: Connection denied
# Verify: grep "denied by firewall" logs/simulation.log

Test 2: IPS auto-block

Objective: Verify IPS auto-blocks on CRITICAL alerts.

# Enable IPS
python tools/blue_team.py ids enable-ips

# Trigger detection (port scan)
nmap -p 10502-10510 localhost

# Check if auto-blocked
python tools/blue_team.py ids list-blocked

# Try connecting again
python scripts/vulns/modbus_write.py --target hex_turbine_plc
# Expected: Connection refused (IP blocked)

Test 3: Firewall rule priority

Objective: Verify higher priority rules checked first.

# Add allow rule (priority 50)
python tools/blue_team.py firewall add-rule \
  --name "Allow test" \
  --action ALLOW \
  --source-ip 127.0.0.1 \
  --priority 50

# Add deny rule (priority 1 - higher)
python tools/blue_team.py firewall add-rule \
  --name "Deny test" \
  --action DROP \
  --source-ip 127.0.0.1 \
  --priority 1

# Test connection
python scripts/vulns/modbus_write.py --target hex_turbine_plc

# Expected: DROPPED (priority 1 rule wins)

Common issues and solutions

Issue 1: “Connection denied by firewall” but should be allowed

Cause: No rule allowing the traffic.

Solution:

# Check existing rules
python tools/blue_team.py firewall list-rules -v

# Add allow rule with correct zones
python tools/blue_team.py firewall add-rule \
  --name "Allow legitimate traffic" \
  --action ALLOW \
  --source-zone operations_zone \
  --dest-zone control_zone \
  --priority 50

Issue 2: Runtime changes lost after restart

Cause: This is expected behaviour.

Solution: Add to config files for persistence.

Issue 3: Legitimate operations blocked

Cause: Default deny policy or overly broad block rule.

Solution:

# Add exception with high priority
python tools/blue_team.py firewall add-rule \
  --name "SCADA exception" \
  --action ALLOW \
  --source-ip 10.20.1.10 \
  --priority 5

TL;DR

  1. Defence in Depth: Multiple layers (zones, firewall, IPS) provide redundancy

  2. Default Deny: Explicitly allow only required traffic

  3. Zone Isolation: Control zone is most critical, most isolated

  4. Incident Response: Runtime changes provide immediate protection

  5. Config Management: Baseline security persists across restarts

  6. Audit Trail: All changes logged for forensics

  7. Testing Required: Always verify defences actually block attacks

Assessment checklist

  • Network segmentation enabled in config

  • Firewall default action set to deny

  • Baseline rules configured for normal operations

  • IPS mode configurable via CLI

  • Runtime IP blocking works immediately

  • Emergency firewall rules can be added

  • Attacks from enterprise zone blocked

  • SCADA operations still functional

  • Runtime changes tested and verified

  • Permanent rules added to config files

  • Audit trail shows all security actions

  • Documentation written for incident response

Next steps

After completing this challenge:

  1. Combine with Challenge 2 (RBAC) - Who can modify firewall rules?

  2. Add Challenge 5 (Protocol Filtering) - Block specific Modbus function codes

  3. Implement Challenge 6 (Dual Authorisation) - Critical changes need two approvals

  4. Review audit logs for security events

  5. Create incident response playbooks

Additional resources