Skip to content

The Audit Chain

What the audit trail is

Each node keeps its own append-only record of what was done to it. Records are hash-chained, so altering one is detectable afterwards.

A node keeps up to two trails, shown on the Audit Log page as Security and Data:

Trail Heading on the page What it holds
Security Security trail Configuration changes, sign-ins, sessions, certificates, users, node lifecycle. Always present.
Data Data-write trail One record per OPC-UA data write. Off by default; appears only when a node has data-write auditing turned on.

Separate manual and automated write sources

Audit writes are meant to record operator actions into the system. They are not designed to handle fully automated users such as a SCADA system. If there are many writes happening per second the audit log can be filled up with all of these operations. It is recommended to separate out manual users and automated users so that there is traceability for operator actions while not degrading performance.

The two chains fail independently. A storm of data writes cannot take the security trail down with it.

The trail is local to each node. There is no fleet-wide chain.

What is recorded

Records are grouped into categories. The ones a node writes are:

config, auth, session, system, cert, user, data, audit, redundancy and snapshot.

In practice that means: every configuration write, whether made here or pushed down the chain; sign-ins, lockouts and credential changes; token issue, rotation and revocation; node start and stop and configuration load; certificate trust decisions; user and role changes; snapshot capture and restore; failover and failback; and the audit subsystem describing its own behavior.

Reads are not audited

Reading tag values, browsing the address space and running subscriptions produce no audit records, and are never blocked by the state of the trail. Data writes are recorded only when data-write auditing is turned on, and then only on the Data trail.

Each record carries, among other fields: a sequence number (seq), a UTC timestamp (ts), the node name, the chain, the category and event name, a phase (intent, outcome or event), an outcome (success, failure, denied or aborted), the actor, where the request came from, what it targeted, an optional reason-for-change, a correlation id shared by every record of one operator action, and for a configuration change the before and after values.

Two records are normal for one change: an intent written before the change is attempted, and an outcome written after. The console folds the pair together for you.

The hash chain

Every record is hashed over its own canonical bytes, and the hash covers every field, including the hash of the record before it.

  • The first record of a chain is a genesis record at sequence 1, whose previous-hash is all zeros.
  • Every record after it carries the previous record's hash.
  • Sequence numbers run without gaps. A record that fails to commit reuses its number rather than burning it, because a missing number would look like a deletion.

What tamper evidence covers

The chain alone makes these detectable:

Change to the stored trail Detected
A record's content altered Yes. Its content no longer matches its hash.
A record removed from the middle Yes. The sequence breaks and the next record's link fails.
Records reordered Yes. A record says which sequence number it is.
A record inserted Yes. It breaks its successor's link and the sequence.
Records cut from the end No, not by the chain alone.

Tail truncation is the exception because a truncated chain is still internally consistent. It is caught only by comparing the trail against knowledge held outside it: the node's separate head record, its signed segment seals, witness receipts held by a parent node, and previously exported bundles. The security chain also writes a heartbeat record every hour, so cutting the tail of even a quiet node always destroys evidence. The data chain has no heartbeat.

Tamper-evident, not tamper-proof

Nothing here prevents someone with administrative control of the host from rewriting the node's own files. The design makes such a change detectable, not impossible.

There is also no off-node replication in this release. If the disk is destroyed, the trail is lost back to the last export. Export regularly if the trail matters to you.

Chain states

The banner at the top of the Audit Log answers "can I trust what is below this" before it shows any records. It reports one of these:

State What it means
ok The chain is open and writable.
degraded The last commit failed and has stayed failed. The next record may not land.
sealed An integrity failure was detected. The trail is suspect.
disabled Auditing is off on this node. There is no trail to review.

A healthy chain still shows a warning in two cases: when the node has recorded no boot self-test result on this run, and when the open segment has gone unsealed for longer than the node's own seal policy allows.

The chain's state never changes the node's overall health status, and it never affects readiness.

What a sealed chain does

While a chain is sealed, the node refuses every change that would write to it. Configuration edits and sign-ins are turned away with a service-unavailable response naming the reason. Reading records, reading data, browsing, subscriptions and store and forward all keep working.

The way out is for an Administrator to record a discontinuity acknowledgement on that node. That documents the break. It never repairs one, and the answer says so. A sealed node is acknowledged at that node; there is no way to do it from a parent.

Two different meanings of "sealed"

The word is used for two unrelated things. Keep them apart.

The node closes the open segment and signs it once it reaches its record or age limit. This is normal, expected housekeeping. Every seal is announced into the security trail. Sealed segments are what an export and a prune are measured against.

An integrity failure found by the node's own self-test at start-up. This is the state described above, and it stops every write to that chain.

Verifying

You can ask a node to re-hash a range of its own trail from the Audit Log. The result reports whether the range verified, the range checked, the first bad sequence number if any, a list of anomalies, and how many sealed segments it covered. Every run is itself recorded, and the record's outcome is the verdict rather than "the command finished".

The check a node runs against itself is bounded, and it is not the strongest evidence available. The credible verification is done offline, against an exported bundle, with no access to the running node.

An export produces a signed archive containing the exact stored records, a manifest naming the range and the seals that cover it, a detached signature over that manifest, and written instructions for verifying it. Offline verification answers OK, NOT PROVEN or FAILED. "NOT PROVEN" means a seal was checked against a key the bundle itself carried rather than one you had archived independently.

The console's NDJSON download is not an export

Download this view (NDJSON) gives you the records currently listed, as a convenience copy. It carries no manifest and no signature, and nothing in it can be verified offline. Use a signed export when you need evidence.

Retention

Automatic cleanup is off by default and stays off until someone turns it on.

When it is on, it removes whole sealed segments from the front of the chain only. What remains is still one contiguous chain: the node records the hash of the record that used to sit just before the new front, so a verifier can still check the front link. A pruned trail verifies; it simply starts later than it used to.

Two limits drive it, and either can be left off:

  • Maximum age: how long records are kept.
  • Maximum record count: how many records a chain may hold.

A separate minimum age acts as a floor, and the floor outranks the ceiling. If the floor is longer than the age limit, the floor wins and the records stay. The shipped floor is one year. Turning on automatic cleanup with neither limit set is refused: it would read as a retention control while dropping nothing.

Automatic cleanup does not wait for an archive

The sweeper does not check whether the records it removes were ever exported or witnessed by a parent node. It reports how many of them were not, but it removes them anyway. If you need records archived before they go, export on a schedule, or leave automatic cleanup off and prune by hand.

Removing records by hand is a different act, with stricter conditions. Among them: the range must sit wholly inside sealed segments, be covered by an export that verifies, and, on a node that has a parent, be witnessed upstream.

Disk space is not returned

Cleanup caps growth. It does not shrink the file on disk, and the storage budget itself never deletes anything: it warns.

Reading a trail across the chain

A parent can read a child node's trail without signing in to that node directly, using the node picker in the title bar.

The two halves are gated differently, on purpose:

  • The records themselves need an Administrator role scoped to that node. They carry configuration diffs, the addresses a node dials, and the name of everyone who signed in.
  • The chain status banner is visible to Engineers as well, locally and across the chain, so an engineer can see that a downstream trail is degraded or sealed without reading what is in it.

When the trail cannot be written

Auditing blocks rather than dropping. If a node cannot record a change, it refuses the change:

  • Configuration writes, locally and across the chain, are refused before anything happens.
  • A successful sign-in is refused, with one deliberate exception: sign-in proceeds while a chain is sealed, and the node records who signed in during that window once the chain reopens. A wrong password is still refused.
  • Reads, browse, subscriptions, data flow and store and forward are never blocked, under any audit condition.

A refusal of this kind is reported as a service problem, never as a permissions problem, so you look at disk space and chain condition rather than at your own roles.