docs(security): add audit log integrity to threat model#2010
Conversation
Adds a cross-cutting concern section on audit log integrity, covering hash chaining, write-once sinks, post-run verification, and signed entries as defense options. References issue fullsend-ai#1685 (commit signing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Benjamin Kapner <bkapner@redhat.com>
ReviewFindingsMedium
Low
Labels: PR adds SHA-256 hash chaining to the security trace subsystem with harness integration. Previous runReviewFindingsMedium
Low
Previous run (2)ReviewFindingsMedium
Low
Labels: PR adds security infrastructure (SHA-256 hash chaining for audit logs) with harness integration Labels: PR adds SHA-256 hash chaining for audit log integrity with harness integration Previous run (3)ReviewFindingsMedium
Low
Previous run (4)ReviewFindingsMedium
Low
Previous run (5)ReviewFindingsMedium
Low
Previous run (6)ReviewFindingsMedium
Low
Previous run (7)ReviewFindingsMedium
Low
Info
Previous run (8)ReviewFindingsMedium
Low
Labels: PR adds security infrastructure (SHA-256 hash chaining for audit logs) with harness integration Previous run (9)ReviewFindingsMedium
Low
Previous run (10)ReviewFindingsMedium
Low
Previous run (11)ReviewFindingsMedium
Low
Previous run (12)ReviewFindingsMedium
Low
Info
|
Add tamper-evident hash chaining to TracedFinding and AppendFinding. Each JSONL entry now includes prev_hash and hash fields forming a SHA-256 chain. Modifying or deleting any entry breaks the chain from that point forward. Add VerifyChain() to validate chain integrity, and tests covering chain construction, tampering detection, and deletion detection. Backward compatible: entries without hash fields are skipped during verification. No changes needed to existing callers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Benjamin Kapner <bkapner@redhat.com>
Add tail truncation limitation note to hash chaining defense. Add cross-reference to agent self-report unreliability section. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Benjamin Kapner <bkapner@redhat.com>
|
Thanks for the review. Addressed the findings in cfe2e01 and updated the PR description. Here's a response to each: [missing-authorization] This PR is motivated by #1685 (commit signing). The doc section positions audit log integrity as a prerequisite for commit signing: signing the output commit provides provenance, but if the process log cannot be trusted, provenance alone does not give you accountability. Happy to open a dedicated issue if the maintainers prefer a separate tracking item. [edge-case-correctness] Good catch. Added an explicit limitation note to the hash chaining defense: "hash chaining alone does not detect tail truncation; detecting that requires an external record of the expected chain length or latest hash." This is also why the doc lists write-once external sinks and post-run verification as complementary defenses rather than positioning hash chaining as sufficient on its own. [internal-consistency] Added a cross-reference to the agent self-report unreliability section in the opening paragraph: "if an agent can both misrepresent its own actions and tamper with the log that would catch the misrepresentation, the combination defeats both controls." [technical-accuracy] Agreed. The parenthetical "the harness signs entries before writing" is the intended model, keeping signing keys outside the sandbox consistent with ADR 0017. Clarified this in the updated PR description as well. |
Call VerifyChain() on the findings JSONL after extracting sandbox security findings. Logs a failure if the hash chain is broken, confirming the audit trail was not tampered with during the run. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Benjamin Kapner <bkapner@redhat.com>
|
Added post-run verification in 5e3d64b.
The verification runs only when security is enabled and a findings file exists. If the chain is intact, it logs the entry count. If broken, it logs exactly which entry failed and why. |
| if verifyErr != nil { | ||
| printer.StepWarn("Audit log verification error: " + verifyErr.Error()) | ||
| } else if !cv.Valid { | ||
| printer.StepFail(fmt.Sprintf("Audit log integrity check FAILED: %s", cv.BrokenMsg)) |
There was a problem hiding this comment.
[medium] logic-error
VerifyChain failure calls printer.StepFail but does not return an error or set a non-zero exit code. Every other StepFail call in run.go is followed by return fmt.Errorf(...) to actually halt the run. The threat model document added in this PR states post-run verification should flag the run for investigation if verification fails. The current implementation contradicts this — a tampered log produces a print-only message that does not affect the run outcome.
Suggested fix: Return an error when cv.Valid is false, consistent with the pattern used by all other StepFail calls in this function. If soft-fail is intentional for the initial rollout, add a code comment and a TODO referencing the threat model open question about synchronous vs. asynchronous verification.
There was a problem hiding this comment.
Fixed in 8abd8e2. StepFail is now followed by return fmt.Errorf(...), consistent with the rest of run.go. A tampered log halts the run
| payload := struct { | ||
| TraceID string `json:"trace_id"` | ||
| Timestamp string `json:"timestamp"` | ||
| Phase string `json:"phase"` |
There was a problem hiding this comment.
[low] error-handling-gap
computeHash discards the json.Marshal error with data, _ := json.Marshal(payload). Deviates from the package established error-handling pattern.
There was a problem hiding this comment.
The payload struct contains only string fields. json.Marshal cannot fail on a struct of strings (no channels, functions, or unsupported types). Accepting the _ here avoids an error path that can never trigger.
| } | ||
|
|
||
| // lastHash reads the final line of the JSONL file and extracts the hash field. | ||
| // Returns seedHash if the file does not exist or is empty. |
There was a problem hiding this comment.
[low] edge-case
lastHash uses bufio.Scanner with the default 64KB buffer. Lines exceeding this limit cause scanner.Scan() to return false, and lastHash silently returns seedHash — forking the chain. scanner.Err() is not checked after the scan loop.
There was a problem hiding this comment.
valid point, i think in practice, a single JSONL finding entry is unlikely to exceed 64KB (typical entries are under 1KB), but adding scanner.Err() check and a larger buffer would make this more robust. Happy to add if the maintainers want it
| scanner := bufio.NewScanner(f) | ||
| prev := seedHash | ||
| idx := 0 | ||
|
|
There was a problem hiding this comment.
[low] edge-case
VerifyChain skips legacy entries (both Hash and PrevHash empty) without updating prev. Legacy entries can be deleted, inserted, or modified without detection — a known trade-off worth documenting in code comments.
There was a problem hiding this comment.
Intentional. Legacy entries are skipped for backward compatibility so that existing logs from before hash chaining was added don't break verification. The trade-off is documented in the threat model's open questions section (per-run vs. global chains). Once all entries are hashed, this path becomes dead code
VerifyChain failure now returns an error and halts the run, consistent with the StepFail pattern used elsewhere in run.go. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Benjamin Kapner <bkapner@redhat.com>
Site previewPreview: https://83265bb9-site.fullsend-ai.workers.dev Commit: |
|
Hello! Sorry for the delay. Taking a look (also locally). |
|
I'm wondering how do I test this locally... do you have a way to generate findings? |
|
Thanks @rh-hemartin. you can view it with unit tests: you can run go test ./internal/security/... to see it in action. the tests create fake findings, chain them, tamper with them, and verify the chain catches it. you don't need a real agent run to validate the behavior. |
|
I wanted to see an agent actively trying to forge the chain and fail to do it because of the safeguards you introduced, but I guess it is OK. I'm not very familiar with this part and wanted to give it a go. |
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
|
🤖 Review · ❌ Terminated · Started 8:09 AM UTC · Ended 8:27 AM UTC |
|
ok, @rh-hemartin, should be clean now |
|
🤖 Review · ❌ Terminated · Started 12:07 PM UTC · Ended 12:23 PM UTC |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
🤖 Finished Review · ❌ Failure · Started 12:07 PM UTC · Completed 12:23 PM UTC |
|
🤖 Finished Review · ❌ Failure · Started 2:32 PM UTC · Completed 2:48 PM UTC |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Benjamin Kapner <bkapner@redhat.com>
c07c7f7 to
627e603
Compare
|
@ralphbean fixed, changed to chore(security):. (thanks for that) |
|
🤖 Finished Review · ❌ Failure · Started 6:11 PM UTC · Completed 6:30 PM UTC |
|
🤖 Finished Retro · ✅ Success · Started 7:00 PM UTC · Completed 7:11 PM UTC |
Retro: PR #2010 — Audit log hash chainingThis was a human-authored PR (Benkapner, with Claude co-authoring) adding SHA-256 hash chaining to the security audit log. The review agent performed well overall. What went well
What could go better
Proposals filed
|
The security architecture lists auditability as a cross-cutting principle: every agent action is logged, attributable, and reviewable. But the audit log itself is not protected against tampering.
The current
findings.jsonlaudit trail is append-only by convention, not by enforcement. In the sandbox model (ADR 0017), the agent has write access to the workspace, which includes the audit log directory. A compromised agent that cannot exfiltrate credentials (because credential isolation works as designed) can still cover its tracks by deleting entries that record suspicious activity, modifying findings to downgrade severity, inserting fabricated entries to create a false trail, or truncating the log to remove evidence of a compromise window.This compounds with agent self-report unreliability: if an agent can both misrepresent its own actions and tamper with the log that would catch the misrepresentation, the combination defeats both controls.
This matters most for forensics. When investigating a security incident, the first question is "what did the agent actually do?" If the answer depends on a log the agent could have tampered with, the investigation starts from zero.
What this PR does
1. Threat model addition (
docs/problems/security-threat-model.md): Adds a new "Cross-cutting concern: audit log integrity" section with four defense options and their trade-offs:2. Implementation (
internal/security/trace.go): Adds SHA-256 hash chaining toTracedFindingandAppendFinding. Each JSONL entry now includesprev_hashandhashfields forming a chain. Modifying or deleting any entry breaks the chain from that point forward. AddsVerifyChain()for post-run integrity verification.Backward compatible: entries without hash fields (from before this change) are skipped during verification. No changes needed to existing callers since
AppendFindingcomputes the hashes internally.3. Tests (
internal/security/trace_test.go): Covers chain construction, tampering detection, entry deletion detection, and empty file handling.Relates to #1685 (commit signing with gitsign). Audit log integrity is a prerequisite: if the process log cannot be trusted, signing the output commit provides provenance but not accountability. Hash-chained audit logs are a simpler first step that does not require external signing infrastructure.