fix: write-ahead audit entry so calls stop when the audit log stops accepting writes (SEC-03)

This commit is contained in:
Latte committed 2026-10-14 17:11:53 +02:00
1 parent 5b622e64e2
commit 43eebd3089
8 files changed
+134 -15

No files matched your search

+6 -3
View File
@@ -115,8 +115,9 @@ One token verifier accepts either kind. The dispatcher turns the verified token
## Audit
Every tool call, including failed and invalid ones, produces exactly one event in the
append-only, hash-chained audit log. Only argument names are recorded, never values. Details and
Every tool call, including failed and invalid ones, produces exactly one result event in the
append-only, hash-chained audit log, after a write-ahead event that makes a call fail closed when
the log stops accepting writes. Only argument names are recorded, never values. Details and
integrity validation are in [audit-logging.md](audit-logging.md).
## Request flow
@@ -127,7 +128,9 @@ integrity validation are in [audit-logging.md](audit-logging.md).
schema, and resolves the identity.
4. The executor calls Gitea with the identity's token.
5. The response is bounded, redacted and masked, and wrapped in the envelope.
6. One audit event is written and the result is returned.
6. One audit result event is written and the result is returned. (A write-ahead
`tool_call_started` event is written right after the tool lookup in step 3; when it cannot be
written the call is refused.)
## Module map
+13 -9
View File
@@ -1,8 +1,10 @@
# Audit logging
Every tool call produces exactly one event in an append-only, hash-chained JSON-lines log,
whether it succeeded, failed at Gitea, failed validation or hit an internal error. Security
events are logged in the same chain.
Every tool call produces exactly one result event (`tool_invocation`) in an append-only,
hash-chained JSON-lines log, whether it succeeded, failed at Gitea, failed validation or hit an
internal error. A call to a known tool is preceded by a small write-ahead event
(`tool_call_started`, see Location and permissions). Security events are logged in the same
chain.
## What an event contains
@@ -11,7 +13,7 @@ Each line is one JSON object:
| Field | Meaning |
|---|---|
| `timestamp` | UTC, ISO 8601 |
| `event_type` | `tool_invocation` or `security_event` |
| `event_type` | `tool_invocation`, `tool_call_started`, `access_denied` or `security_event` |
| `request_id` | Correlation id of the request |
| `payload` | The event data, see below |
| `prev_hash` | `entry_hash` of the previous line (`GENESIS` for the first) |
@@ -56,11 +58,13 @@ The log is written to `AUDIT_LOG_PATH`, default `DATA_DIR/audit.log` (`/var/lib/
image). Keep it on durable storage, readable and writable only by the service account.
The server opens the file for append at startup and exits with code 2 (message on stderr) when
that fails, so it never runs unaudited. Each call also obtains the audit logger before it runs;
if that fails the call is refused with `audit_unavailable` and never reaches Gitea. A write
failure after a call has executed is only logged to stderr: the Gitea side effect already
happened and the caller must still receive the result. The `correlation_id` of a tool entry is
the request id, which is also the entry's `request_id`.
that fails, so it never runs unaudited. The check does not stop at startup: each call to a known
tool first writes a `tool_call_started` entry (tool name, domain and correlation id), and when
that write fails (full disk, revoked mount, closed file) the call is refused with
`audit_unavailable` and never reaches Gitea. Calls resume as soon as writes work again. A write
failure of the final `tool_invocation` entry after a call has executed is only logged to stderr:
the Gitea side effect already happened and the caller must still receive the result. The
`correlation_id` of both entries is the request id, which is also the entry's `request_id`.
## Rotation
+1 -1
View File
@@ -78,7 +78,7 @@ Repudiation
| Threat | Mitigation | Implemented in |
|---|---|---|
| Unrecorded tool calls | Exactly one hash-chained audit entry per call; a call is refused when the audit log cannot be opened; startup fails on an unwritable log | `server.py`, `audit.py`, `transports.py` (write failures after startup: SEC-03) |
| Unrecorded tool calls | Exactly one hash-chained result entry per call, preceded by a write-ahead `tool_call_started` entry; a call is refused when either the logger or that write fails; startup fails on an unwritable log | `server.py`, `audit.py`, `transports.py` |
| Undetected edits of the log | SHA-256 hash chain, `scripts/validate_audit_log.py` | `audit.py` (unkeyed chain: SEC-11) |
Information disclosure