ot ics security

Implementing ICS Firewall with Tofino

Deploy and configure Tofino industrial firewalls from Belden/Hirschmann to protect SCADA systems and PLCs using deep packet inspection for OT protocols including Modbus, EtherNet/IP, OPC, and S7comm, enforcing granular access control between ICS security zones.

beldendeep-packet-inspectionfirewallicsnetwork-securityot-securityscadatofino
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When deploying zone-level firewall protection directly in front of critical PLCs or RTUs
  • When requiring deep packet inspection of industrial protocols (Modbus, EtherNet/IP, OPC, S7comm)
  • When implementing IEC 62443 zone and conduit boundaries with protocol-aware enforcement
  • When protecting legacy PLCs that cannot be patched and need compensating controls
  • When segmenting control network zones without disrupting existing industrial communications

Do not use for enterprise IT firewall deployment, for perimeter firewall between IT and OT (use Palo Alto/Fortinet at the DMZ), or for environments using only IP-based protocols without OT-specific DPI needs.

Prerequisites

  • Tofino Xenon appliance or Tofino virtual appliance with appropriate license
  • Tofino Central Management Platform (CMP) for centralized policy management
  • Network topology map showing PLC/RTU placement and communication requirements
  • Baseline of OT protocol communications (Modbus function codes, EtherNet/IP CIP services)
  • Change management approval for inline deployment between network zones

Workflow

Step 1: Design Tofino Deployment Architecture

# Tofino ICS Firewall Deployment Architecture
# Zone-level protection using deep packet inspection
 
deployment_zones:
  zone_1_reactor_control:
    tofino_appliance: "TOFINO-XN-001"
    deployment_mode: "inline_bridge"
    protected_assets:
      - name: "PLC-REACTOR-01"
        ip: "10.10.1.10"
        vendor: "Siemens S7-1500"
        protocols: ["S7comm/102", "Profinet"]
      - name: "PLC-REACTOR-02"
        ip: "10.10.1.11"
        vendor: "Siemens S7-1500"
        protocols: ["S7comm/102", "Profinet"]
    authorized_communications:
      - source: "10.10.2.50"  # Engineering workstation
        dest: "10.10.1.0/24"
        protocols: ["S7comm"]
        access_type: "engineering"
      - source: "10.10.2.10"  # HMI server
        dest: "10.10.1.0/24"
        protocols: ["S7comm"]
        access_type: "operational"
 
  zone_2_packaging:
    tofino_appliance: "TOFINO-XN-002"
    deployment_mode: "inline_bridge"
    protected_assets:
      - name: "PLC-PACK-01"
        ip: "10.10.3.10"
        vendor: "Rockwell ControlLogix"
        protocols: ["EtherNet-IP/44818", "CIP"]
    authorized_communications:
      - source: "10.10.2.20"  # HMI
        dest: "10.10.3.0/24"
        protocols: ["EtherNet-IP"]
        access_type: "operational"
 
  zone_3_utilities:
    tofino_appliance: "TOFINO-XN-003"
    deployment_mode: "inline_bridge"
    protected_assets:
      - name: "RTU-BOILER-01"
        ip: "10.10.4.10"
        vendor: "Schneider M340"
        protocols: ["Modbus-TCP/502"]
    authorized_communications:
      - source: "10.10.2.30"  # SCADA server
        dest: "10.10.4.0/24"
        protocols: ["Modbus-TCP"]
        allowed_function_codes: [1, 2, 3, 4]  # Read only from SCADA

Step 2: Configure Deep Packet Inspection Rules

#!/usr/bin/env python3
"""Tofino ICS Firewall Rule Generator.
 
Generates Tofino firewall rules with deep packet inspection for
industrial protocols based on communication baseline analysis.
"""
 
import json
import sys
from datetime import datetime
from typing import Dict, List
 
 
class TofinoRuleGenerator:
    """Generates Tofino ICS firewall DPI rules."""
 
    def __init__(self):
        self.rules = []
        self.rule_id = 1000
 
    def add_modbus_rule(self, src: str, dst: str, allowed_funcs: List[int],
                        allowed_registers: List[dict] = None, description: str = ""):
        """Generate Modbus DPI rule."""
        func_names = {
            1: "read_coils", 2: "read_discrete_inputs",
            3: "read_holding_registers", 4: "read_input_registers",
            5: "write_single_coil", 6: "write_single_register",
            15: "write_multiple_coils", 16: "write_multiple_registers",
        }
 
        rule = {
            "rule_id": self.rule_id,
            "protocol": "Modbus-TCP",
            "action": "ALLOW",
            "source": src,
            "destination": dst,
            "port": 502,
            "dpi_policy": {
                "allowed_function_codes": [
                    {"code": fc, "name": func_names.get(fc, f"FC{fc}")}
                    for fc in allowed_funcs
                ],
                "blocked_function_codes": [
                    fc for fc in range(1, 128) if fc not in allowed_funcs
                ],
            },
            "description": description,
            "log": True,
        }
 
        if allowed_registers:
            rule["dpi_policy"]["allowed_register_ranges"] = allowed_registers
 
        self.rules.append(rule)
        self.rule_id += 1
        return rule
 
    def add_s7comm_rule(self, src: str, dst: str, allowed_operations: List[str],
                        description: str = ""):
        """Generate S7comm DPI rule."""
        operation_map = {
            "read": {"function": 0x04, "name": "Read Variable"},
            "write": {"function": 0x05, "name": "Write Variable"},
            "setup": {"function": 0xF0, "name": "Setup Communication"},
            "download": {"function": 0x1A, "name": "Request Download"},
            "upload": {"function": 0x1D, "name": "Start Upload"},
            "cpu_stop": {"function": 0x29, "name": "PLC Stop"},
            "cpu_start": {"function": 0x28, "name": "PI Service (Start)"},
        }
 
        rule = {
            "rule_id": self.rule_id,
            "protocol": "S7comm",
            "action": "ALLOW",
            "source": src,
            "destination": dst,
            "port": 102,
            "dpi_policy": {
                "allowed_operations": [
                    operation_map[op] for op in allowed_operations if op in operation_map
                ],
                "block_cpu_stop": "cpu_stop" not in allowed_operations,
                "block_program_download": "download" not in allowed_operations,
            },
            "description": description,
            "log": True,
        }
 
        self.rules.append(rule)
        self.rule_id += 1
        return rule
 
    def add_ethernet_ip_rule(self, src: str, dst: str, allowed_services: List[str],
                              description: str = ""):
        """Generate EtherNet/IP CIP DPI rule."""
        rule = {
            "rule_id": self.rule_id,
            "protocol": "EtherNet-IP",
            "action": "ALLOW",
            "source": src,
            "destination": dst,
            "port": 44818,
            "dpi_policy": {
                "allowed_cip_services": allowed_services,
                "block_firmware_flash": True,
                "block_program_download": "program_download" not in allowed_services,
            },
            "description": description,
            "log": True,
        }
 
        self.rules.append(rule)
        self.rule_id += 1
        return rule
 
    def add_default_deny(self):
        """Add default deny rule at the end."""
        self.rules.append({
            "rule_id": 9999,
            "protocol": "ANY",
            "action": "DENY",
            "source": "ANY",
            "destination": "ANY",
            "port": "ANY",
            "description": "Default deny - block all unmatched traffic",
            "log": True,
        })
 
    def generate_config(self) -> str:
        """Generate complete Tofino firewall configuration."""
        config = {
            "tofino_configuration": {
                "generated": datetime.now().isoformat(),
                "appliance_model": "Tofino Xenon",
                "firmware_version": "4.2",
                "mode": "inline_bridge",
                "failsafe": "fail_open",
                "rules": self.rules,
            }
        }
        return json.dumps(config, indent=2)
 
    def print_summary(self):
        """Print rule summary."""
        print(f"\n{'='*65}")
        print("TOFINO ICS FIREWALL RULE SUMMARY")
        print(f"{'='*65}")
        print(f"Generated: {datetime.now().isoformat()}")
        print(f"Total Rules: {len(self.rules)}")
 
        for rule in self.rules:
            action_icon = "+" if rule["action"] == "ALLOW" else "X"
            print(f"\n  [{action_icon}] Rule {rule['rule_id']}: {rule.get('description', '')}")
            print(f"      {rule['source']} -> {rule['destination']}:{rule['port']}")
            print(f"      Protocol: {rule['protocol']}")
            if "dpi_policy" in rule:
                dpi = rule["dpi_policy"]
                if "allowed_function_codes" in dpi:
                    funcs = [f["name"] for f in dpi["allowed_function_codes"]]
                    print(f"      DPI - Allowed Modbus FCs: {', '.join(funcs)}")
                if "allowed_operations" in dpi:
                    ops = [o["name"] for o in dpi["allowed_operations"]]
                    print(f"      DPI - Allowed S7 Ops: {', '.join(ops)}")
 
 
if __name__ == "__main__":
    gen = TofinoRuleGenerator()
 
    # SCADA server to Modbus RTUs: read-only
    gen.add_modbus_rule(
        src="10.10.2.30",
        dst="10.10.4.0/24",
        allowed_funcs=[1, 2, 3, 4],
        description="SCADA to utilities RTUs - read only",
    )
 
    # Engineering workstation to Siemens PLCs: full access
    gen.add_s7comm_rule(
        src="10.10.2.50",
        dst="10.10.1.0/24",
        allowed_operations=["read", "write", "setup", "download", "upload"],
        description="Engineering WS to reactor PLCs - full engineering access",
    )
 
    # HMI to Siemens PLCs: read + write only (no program download)
    gen.add_s7comm_rule(
        src="10.10.2.10",
        dst="10.10.1.0/24",
        allowed_operations=["read", "write", "setup"],
        description="HMI to reactor PLCs - operational access only",
    )
 
    # HMI to Rockwell PLCs: operational access
    gen.add_ethernet_ip_rule(
        src="10.10.2.20",
        dst="10.10.3.0/24",
        allowed_services=["read_tag", "write_tag", "get_attribute"],
        description="HMI to packaging PLCs - operational access",
    )
 
    gen.add_default_deny()
    gen.print_summary()

Key Concepts

Term Definition
Tofino Xenon Belden/Hirschmann industrial firewall appliance with deep packet inspection for OT protocols
Deep Packet Inspection (DPI) Examining message payload content beyond headers to enforce fine-grained rules on industrial protocol operations
Inline Bridge Mode Transparent deployment mode where the firewall sits between network segments without requiring IP changes
Fail-Open Safety mode where firewall passes all traffic if the appliance fails, maintaining process availability
Loadable Security Module (LSM) Tofino plugin module providing protocol-specific DPI for Modbus, EtherNet/IP, OPC, or other protocols
Central Management Platform (CMP) Tofino centralized management server for deploying and managing policies across multiple Tofino appliances

Output Format

TOFINO DEPLOYMENT REPORT
===========================
Date: YYYY-MM-DD
Appliances Deployed: [count]
 
PER-APPLIANCE SUMMARY:
  [Appliance ID]:
    Mode: Inline Bridge
    Failsafe: Fail-Open
    Protected Assets: [count]
    Rules: [count]
    DPI Protocols: [list]
 
RULE SUMMARY:
  Allow Rules: [count]
  Deny Rules: [count]
  DPI-Enforced Rules: [count]
 
MONITORING:
  Blocked Packets (24h): [count]
  DPI Violations (24h): [count]
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md1.5 KB

API Reference: Implementing ICS Firewall with Tofino

OT Protocol Ports

Protocol Port Layer DPI Support
Modbus/TCP 502 TCP Yes
EtherNet/IP 44818 TCP/UDP Yes
DNP3 20000 TCP Yes
OPC UA 4840 TCP Yes
S7comm 102 TCP Yes
BACnet 47808 UDP Yes
IEC 61850 MMS 102 TCP Yes

Risky Modbus Function Codes

Code Function Risk
5 Write Single Coil HIGH
6 Write Single Register HIGH
15 Write Multiple Coils HIGH
16 Write Multiple Registers HIGH
22 Mask Write Register HIGH

Tofino Xenon Configuration

<TofinoRule>
  <Action>Allow</Action>
  <Source>192.168.1.10</Source>
  <Destination>192.168.1.50</Destination>
  <Protocol>Modbus</Protocol>
  <Port>502</Port>
  <DPI enabled="true">
    <AllowedFunctions>1,2,3,4</AllowedFunctions>
  </DPI>
  <Logging>true</Logging>
</TofinoRule>

Rule Audit Checks

Check Severity Description
Allow-any-any CRITICAL Overly permissive rule
No default deny CRITICAL Missing deny-all at end
No DPI for OT protocol HIGH Missing deep packet inspection
Write functions allowed HIGH Modbus write codes permitted
Allow without logging MEDIUM No audit trail

References

Scripts 1

agent.py8.0 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for auditing and configuring Tofino ICS firewall rules."""

import json
import argparse
from datetime import datetime
from collections import Counter


OT_PROTOCOLS = {
    "modbus": {"port": 502, "layer": "TCP", "dpi": True},
    "enip": {"port": 44818, "layer": "TCP/UDP", "dpi": True},
    "dnp3": {"port": 20000, "layer": "TCP", "dpi": True},
    "opc_ua": {"port": 4840, "layer": "TCP", "dpi": True},
    "s7comm": {"port": 102, "layer": "TCP", "dpi": True},
    "bacnet": {"port": 47808, "layer": "UDP", "dpi": True},
    "iec61850_mms": {"port": 102, "layer": "TCP", "dpi": True},
    "profinet": {"port": 34964, "layer": "UDP", "dpi": False},
}

RISKY_MODBUS_FUNCTIONS = {
    5: "Write Single Coil",
    6: "Write Single Register",
    15: "Write Multiple Coils",
    16: "Write Multiple Registers",
    22: "Mask Write Register",
    23: "Read/Write Multiple Registers",
}


def audit_firewall_rules(rules_path):
    """Audit Tofino firewall rules for security issues."""
    with open(rules_path) as f:
        rules = json.load(f)
    rule_list = rules if isinstance(rules, list) else rules.get("rules", [])
    findings = []

    for rule in rule_list:
        action = rule.get("action", "").lower()
        src = rule.get("source", rule.get("src", "any"))
        dst = rule.get("destination", rule.get("dst", "any"))
        protocol = rule.get("protocol", "").lower()
        port = rule.get("port", rule.get("dst_port", "any"))

        if src == "any" and dst == "any" and action == "allow":
            findings.append({
                "rule": rule.get("id", rule.get("name", "")),
                "issue": "Allow-any-any rule detected",
                "severity": "CRITICAL",
                "recommendation": "Restrict to specific source/destination",
            })

        if protocol in ("modbus", "enip", "s7comm") and not rule.get("dpi_enabled", False):
            findings.append({
                "rule": rule.get("id", ""),
                "issue": f"DPI not enabled for {protocol}",
                "severity": "HIGH",
                "recommendation": f"Enable deep packet inspection for {protocol}",
            })

        if protocol == "modbus":
            allowed_funcs = rule.get("allowed_functions", [])
            risky = [f for f in allowed_funcs if f in RISKY_MODBUS_FUNCTIONS]
            if risky:
                findings.append({
                    "rule": rule.get("id", ""),
                    "issue": f"Write functions allowed: {risky}",
                    "severity": "HIGH",
                    "detail": {fc: RISKY_MODBUS_FUNCTIONS[fc] for fc in risky},
                })

        if action == "allow" and not rule.get("logging", False):
            findings.append({
                "rule": rule.get("id", ""),
                "issue": "Allow rule without logging",
                "severity": "MEDIUM",
            })

    has_deny_all = any(r.get("action", "").lower() == "deny" and
                       r.get("source", "any") == "any" and
                       r.get("destination", "any") == "any" for r in rule_list)
    if not has_deny_all:
        findings.append({
            "issue": "No default deny-all rule found",
            "severity": "CRITICAL",
            "recommendation": "Add deny-all as last rule",
        })

    return findings


def analyze_ot_traffic_log(log_path):
    """Analyze OT network traffic log for anomalies."""
    with open(log_path) as f:
        entries = json.load(f)
    items = entries if isinstance(entries, list) else entries.get("flows", [])

    by_protocol = Counter()
    by_src = Counter()
    anomalies = []

    for entry in items:
        proto = entry.get("protocol", entry.get("app_protocol", "unknown")).lower()
        by_protocol[proto] += 1
        by_src[entry.get("src_ip", "unknown")] += 1

        port = entry.get("dst_port", 0)
        if proto == "modbus" and port != 502:
            anomalies.append({"type": "non_standard_port", "protocol": proto,
                              "port": port, "severity": "HIGH"})

        if entry.get("action", "").lower() == "denied":
            anomalies.append({
                "type": "denied_connection",
                "src": entry.get("src_ip", ""),
                "dst": entry.get("dst_ip", ""),
                "protocol": proto,
                "severity": "MEDIUM",
            })

    return {
        "total_flows": len(items),
        "by_protocol": dict(by_protocol),
        "unique_sources": len(by_src),
        "anomalies": anomalies[:50],
        "anomaly_count": len(anomalies),
    }


def generate_zone_rules(zone_config):
    """Generate Tofino firewall rules from zone configuration."""
    rules = []
    zones = zone_config.get("zones", [])
    for zone in zones:
        zone_name = zone.get("name", "")
        allowed_protocols = zone.get("allowed_protocols", [])
        plc_ips = zone.get("plc_ips", [])
        hmi_ips = zone.get("hmi_ips", [])
        eng_ips = zone.get("engineering_ips", [])

        for proto in allowed_protocols:
            proto_info = OT_PROTOCOLS.get(proto, {})
            port = proto_info.get("port", "any")
            for hmi in hmi_ips:
                for plc in plc_ips:
                    rules.append({
                        "zone": zone_name,
                        "action": "allow",
                        "source": hmi,
                        "destination": plc,
                        "protocol": proto,
                        "port": port,
                        "dpi_enabled": proto_info.get("dpi", False),
                        "logging": True,
                    })
            if proto == "modbus":
                for eng in eng_ips:
                    for plc in plc_ips:
                        rules.append({
                            "zone": zone_name,
                            "action": "allow",
                            "source": eng,
                            "destination": plc,
                            "protocol": "modbus",
                            "port": 502,
                            "dpi_enabled": True,
                            "allowed_functions": [1, 2, 3, 4],
                            "logging": True,
                            "comment": "Read-only Modbus for engineering",
                        })

    rules.append({"action": "deny", "source": "any", "destination": "any",
                  "protocol": "any", "logging": True, "comment": "Default deny-all"})
    return rules


def main():
    parser = argparse.ArgumentParser(description="Tofino ICS Firewall Agent")
    parser.add_argument("--rules", help="Firewall rules JSON to audit")
    parser.add_argument("--traffic-log", help="OT traffic log JSON")
    parser.add_argument("--zone-config", help="Zone config JSON for rule generation")
    parser.add_argument("--action", choices=["audit", "traffic", "generate", "full"],
                        default="full")
    parser.add_argument("--output", default="tofino_ics_firewall_report.json")
    args = parser.parse_args()

    report = {"generated_at": datetime.utcnow().isoformat(), "results": {}}

    if args.action in ("audit", "full") and args.rules:
        findings = audit_firewall_rules(args.rules)
        report["results"]["audit"] = findings
        critical = sum(1 for f in findings if f.get("severity") == "CRITICAL")
        print(f"[+] Audit: {len(findings)} findings, {critical} critical")

    if args.action in ("traffic", "full") and args.traffic_log:
        result = analyze_ot_traffic_log(args.traffic_log)
        report["results"]["traffic"] = result
        print(f"[+] Traffic: {result['total_flows']} flows, {result['anomaly_count']} anomalies")

    if args.action in ("generate", "full") and args.zone_config:
        with open(args.zone_config) as f:
            zc = json.load(f)
        rules = generate_zone_rules(zc)
        report["results"]["generated_rules"] = rules
        print(f"[+] Generated {len(rules)} firewall rules")

    with open(args.output, "w") as f:
        json.dump(report, f, indent=2, default=str)
    print(f"[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
Keep exploring