Building an OT Traffic Baseline

Enterprise network detection is hard because enterprise networks are chaotic. A user opens forty tabs, a laptop joins from a hotel, a new SaaS tool appears without anyone telling security, and the traffic profile on Tuesday bears no useful resemblance to Monday’s. Anomaly detection in that environment generates alerts nobody can triage.

Industrial networks are the opposite, and this is the single biggest structural advantage defenders have in OT. An HMI polls the same registers on the same controller at the same interval for years. A historian collects on a fixed schedule. An engineering workstation connects occasionally and predictably. The set of devices changes only when somebody physically installs one. Traffic that would be lost in the noise of a corporate network stands out immediately against a control network that does the same thing every ninety seconds.

A baseline is what converts that predictability into detection. It is not a packet capture – a capture is raw material. A baseline is a written characterisation of what normal looks like, precise enough that a deviation from it can be recognised automatically and defended in a report.

This continues from Passive OT Asset Discovery with Wireshark, which produced the inventory, and Understanding Modbus/TCP Traffic, which explained the function codes this article treats as significant.

Safety classification: Green. Everything here is passive observation of traffic already on the network. Capture setup follows the silent-interface guidance from the discovery article – no IP address on the monitoring interface, IPv6 disabled, transmission verified as zero before connecting.

What a baseline actually contains

The mistake is to think of a baseline as a file. Ask a site for their baseline and you will sometimes be handed a PCAP, which is roughly like answering “what does your business do” with a filing cabinet.

A usable baseline is a description with six components, each of which supports a specific kind of detection.

The first is the device inventory: every host, with IP, MAC, vendor and role. Detection question answered – is this device supposed to be here?

The second is the communication matrix: which hosts talk to which, on which ports. Detection question – is this conversation supposed to happen?

The third is protocol behaviour: for industrial protocols, which function codes each relationship uses, and crucially which hosts ever write. Detection question – is this operation supposed to be performed by this host?

The fourth is timing: polling intervals, session durations, and how consistent each is. Detection question – has the rhythm changed?

The fifth is volume: packets and bytes per hour for each relationship, with the normal range rather than a single figure. Detection question – is the quantity unusual?

The sixth is periodicity: what happens daily, weekly and monthly, and when. Detection question – is this event happening at the wrong time, or failing to happen at all?

That last one is underused. A baseline that captures the nightly historian archive at 02:00 lets you alert on its absence, and a backup that stops running is a finding most sites discover months late.

How long to observe

Long enough to see every cycle you intend to detect deviations from, which is longer than most people budget.

Twenty-four hours captures the daily rhythm: shift changes, operator logins, nightly batch jobs, time synchronisation. This is the practical minimum, and it is enough to characterise steady-state polling with confidence.

Seven days adds the weekly cycle. Weekend behaviour differs from weekday behaviour at almost every site, and scheduled maintenance windows appear. A device that only transmits during Sunday-night maintenance will be flagged as an anomaly forever if your baseline never saw it.

Thirty days catches monthly reporting, archive rotations and vendor support connections. It also gives you enough samples to state a normal range rather than a point value, which matters because a baseline expressed as exact numbers will alert constantly.

There is a further consideration specific to industrial sites: process state. A plant running a different product, a different recipe or a seasonal load profile generates legitimately different traffic. If the site has distinct operating modes, the baseline needs to either cover all of them or explicitly state which one it describes. A baseline built during a summer shutdown will not survive the autumn restart.

The honest approach is to begin with twenty-four hours, get value from it immediately, and extend. A partial baseline that exists beats a complete one that is still being planned.

Extracting the baseline from capture

Wireshark is for understanding traffic; Zeek is for characterising it. For anything beyond a few hours, run Zeek over the capture and work from its logs.

zeek -r baseline-week.pcap

The communication matrix comes straight out of the connection log:

cat conn.log | zeek-cut id.orig_h id.resp_h id.resp_p proto \
  | sort | uniq -c | sort -rn

Every line is a relationship, with a count. Relationships with high counts are your polling loops. Relationships appearing once or twice are worth individual attention – they are either something scheduled and rare, or something that should not have happened.

The protocol behaviour matrix comes from the Modbus log:

cat modbus.log | zeek-cut id.orig_h id.resp_h func \
  | sort | uniq -c | sort -rn

Then isolate the writes specifically, because this list becomes the highest-value detection rule you will write:

cat modbus.log | zeek-cut ts id.orig_h id.resp_h func \
  | grep -E "WRITE" | awk '{print $2, $3, $4}' | sort -u

Every source address in that output has control authority. On a well-run network it will be one or two hosts. If it is eight, you have found something before you have written a single detection rule.

For timing, measure the interval between consecutive requests in each relationship:

cat modbus.log | zeek-cut ts id.orig_h id.resp_h \
  | awk '{key=$2" "$3; if (key in prev) print key, $1-prev[key]; prev[key]=$1}' \
  | sort -k1,2 | awk '{s[$1" "$2]+=$3; n[$1" "$2]++} END {for (k in s) print k, s[k]/n[k]}'

That prints the mean interval per relationship. Record the variance as well as the mean – a relationship with a rock-steady 500 ms interval supports a much tighter detection threshold than one that ranges from two to thirty seconds.

For volume, hourly aggregation is usually the right granularity:

cat conn.log | zeek-cut ts id.orig_h id.resp_h orig_bytes resp_bytes \
  | awk '{h=int($1/3600); b[h" "$2" "$3]+=$4+$5} END {for (k in b) print k, b[k]}'

Convert those hourly figures into a range – minimum, maximum, and something like a 95th percentile – rather than an average. Averages hide exactly the peaks that matter.

Writing it down

The baseline that survives is the one written in a form somebody can review, challenge and update. A capture file cannot be reviewed by a process engineer; a table can.

For each device, record the address details, the role, and the times first and last observed. For each relationship, record the source, destination, port, protocol, function codes used, mean interval and its variance, hourly volume range, and whether the relationship is continuous or intermittent. For each scheduled event, record what it is, when it occurs, its expected duration, and what its absence would mean.

Then add the part most baselines omit: the justification. Beside each relationship, note why it exists in process terms. “Historian collects tank level for production reporting” is worth more than any amount of packet detail, because it lets somebody decide whether the relationship should continue to exist. A baseline is also an opportunity to ask questions the site has not been asked in years, and the relationships nobody can justify are findings in themselves.

Have the site’s engineers review it. They will identify things you misread, and they will spot absences – a device that should be talking and is not. That review converts a technical artefact into an agreed statement of normal, which is what you need when an alert eventually fires and somebody asks whether it matters.

From baseline to detection

The point of all this is that each baseline component maps directly onto a rule, and the rules are simple precisely because the baseline did the hard work.

New device detection compares observed MAC and IP addresses against the inventory. Zeek’s known_hosts.log does most of the work; anything not in the baseline warrants an alert. On a control network this is high-fidelity, because devices genuinely do not come and go.

New conversation detection compares observed source-destination-port tuples against the matrix. An HMI that begins talking to a controller it has never contacted is significant in a way that has no enterprise equivalent.

Unauthorised write detection is the highest-value rule of the set. The baseline established which hosts issue Modbus function codes 5, 6, 15 and 16. Anything outside that set issuing a write is an alert, and because writes are rare and the authorised set is small, false positives are minimal.

Timing anomaly detection watches for polling intervals departing from the recorded range. A rate that suddenly doubles suggests a reconfiguration; one that becomes erratic can indicate a device under load or a failing link.

Missing traffic detection is the one nobody builds and everybody wishes they had. A relationship that stops is either an equipment failure or an interruption, and both need attention. Alerting on absence requires the periodicity data, which is precisely why it is worth collecting.

Keeping it from going stale

A baseline reflects the network on the day it was captured. Networks change, and a baseline that has not been updated for two years is worse than none, because it generates alerts everyone has learned to ignore.

Tie updates to change control. Any approved change that affects the network – a new device, a firmware upgrade, a modified polling configuration, a new data collection requirement – should carry a step to update the baseline. This is the mechanism that works, because it puts the update where the change already is.

Review on a schedule regardless, quarterly being a reasonable default, and re-baseline fully after any significant plant modification or when the process enters an operating mode the current baseline never covered.

Version the baseline, and keep the old versions. The diff between two baselines six months apart is one of the more useful documents an OT security programme can produce: it shows what changed on the network, whether anyone intended it, and whether it went through change control. That comparison has surfaced undocumented vendor connections at more than one site.

Track alert quality as the operational signal. A specific rule that starts firing frequently usually means the network changed rather than that an attack is underway – investigate, then update the baseline rather than suppressing the rule. Suppression is how detection programmes quietly die.

What comes next

The baseline is the input to detection, and the next article uses it: writing and tuning rules that identify unauthorised Modbus commands, with the alerting logic, the tuning process and the response that should follow.

Everything in that article depends on the work here. A detection rule without a baseline is a guess about what normal looks like, and a guess is what produces the alert fatigue that stops anybody reading the alerts at all.


Do you know what normal looks like on your OT network?

Most industrial sites can describe their network as designed. Rather fewer can describe what it actually does – which hosts hold control authority, what runs at 02:00, and what it would mean if that stopped.

CyberLabs OT security assessments build verified traffic baselines from passive analysis, then examine network segmentation, remote-access pathways, industrial device exposure and monitoring controls, while accounting for operational availability and recovery requirements.