Audit Tamper Evidence
Audience: DevOps / Platform engineers, compliance officers, auditors
Time: ~15 min
Control Core provides cryptographic assurance that audit records have not been altered after they were written. This capability is designed to satisfy the tamper-evidence requirements of SOX, FFIEC, and SOC 2 Type II without requiring a third-party audit database.
How the hash chain works
Every row written to the audit log is linked to the row before it using a SHA-256 hash chain:
row N-1 ─────┐
├─ SHA-256(prev_hash || 0x1f || canonical_fields) ──► row_hash(N)
row N fields ┘
Each row stores two integrity columns:
| Column | Contents |
|---|---|
prev_hash | The row_hash value from the immediately preceding row (or a genesis sentinel for the first row) |
row_hash | SHA-256 of prev_hash, a separator byte, and the canonical field set for this row |
To verify that a sequence of rows is unmodified, recompute row_hash for each row in ascending ID order and confirm that each prev_hash matches the row_hash of the row before it. A mismatch at row N means that row N (or a row before it) was altered or deleted after it was written.
Troubleshooting: If a chain verification reports a break, do not delete or modify any audit rows. Preserve the state and escalate immediately — see Breach response below. Common causes: a database restore from a backup that predates the chain; an unauthorized
UPDATEorDELETEdirectly against the audit table. Full reference: Troubleshooting.
KMS-signed WORM checkpoints
In addition to the row-level hash chain, Control Core writes a signed checkpoint of the chain tip at regular intervals. The checkpoint contains the most recent row_hash value, a timestamp, and the row count.
Signing preference (in order):
- AWS KMS asymmetric key (
AUDIT_CHECKPOINT_KMS_KEY_ID, algorithmRSASSA_PSS_SHA_256) — recommended for regulated environments. - Local HMAC-SHA256 (
AUDIT_CHECKPOINT_HMAC_KEY) — acceptable when KMS is not available. - Unsigned fallback — checkpoints are still written but carry no cryptographic signature; not recommended for compliance-critical deployments.
Storage target: S3 Object Lock (WORM_EXPORT_S3_BUCKET) with object lock mode configured by WORM_EXPORT_OBJECT_LOCK_MODE (default GOVERNANCE). Object Lock prevents deletion or overwrite for the duration of the retention period — even by the bucket owner.
Each checkpoint write also records an AUDIT_CHECKPOINT event in the audit log itself, so the checkpoint history is part of the tamper-evident chain.
Deploying WORM storage
AWS S3 Object Lock
- Create an S3 bucket with Object Lock enabled (cannot be enabled after bucket creation).
- Set the default retention period to match your regulatory requirement (7 years for FFIEC; 5 years for SOC 2).
- Configure environment variables on the Control Plane:
WORM_EXPORT_S3_BUCKET=my-audit-worm-bucket
WORM_EXPORT_OBJECT_LOCK_MODE=COMPLIANCE # Use GOVERNANCE for operational flexibility
AUDIT_CHECKPOINT_S3_PREFIX=checkpoints/
AUDIT_CHECKPOINT_KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/mrk-...
AUDIT_CHECKPOINT_INTERVAL_MIN=5
- Attach an IAM policy to the Control Plane task role that grants
s3:PutObjectands3:GetObjecton the bucket, andkms:Signon the KMS key.
Troubleshooting: If checkpoints stop appearing in the bucket, check the Control Plane logs for
AUDIT_CHECKPOINTevents. Common causes: IAM permission gap on the task role; KMS key disabled or key policy not grantingkms:Sign; S3 bucket policy denying the task role. Full reference: Troubleshooting.
Azure Immutable Blob Storage
Azure Blob Storage with time-based immutability provides the equivalent of S3 Object Lock on Azure:
- Create an Azure Blob Storage container and enable an Immutability policy (time-based retention).
- Set the retention interval to match your regulatory requirement.
- Configure the Control Plane to write checkpoints to the Azure Blob endpoint using the S3-compatible API or by deploying a bridge service. Contact your platform team for the exact configuration pattern in your environment.
Retention and the production purge rule
| Environment | Auto-purge behaviour |
|---|---|
production | Disabled. Deleting rows breaks the hash chain and violates 7-year retention obligations under SOX and FFIEC. The Control Plane will not run scheduled purges in a production environment. |
All others (sandbox, staging, etc.) | Rows older than AUDIT_RETENTION_DAYS are purged on a daily schedule. Default: 2555 days (7 years). |
Key environment variables:
| Variable | Default | Description |
|---|---|---|
AUDIT_RETENTION_DAYS | 2555 (7 years) | Purge threshold for non-production environments |
AUDIT_CHECKPOINT_INTERVAL_MIN | 5 | Minutes between checkpoint writes |
AUDIT_CHECKPOINT_KMS_KEY_ID | (unset) | AWS KMS key ARN for signed checkpoints |
AUDIT_CHECKPOINT_HMAC_KEY | (unset) | HMAC-SHA256 key for locally signed checkpoints |
WORM_EXPORT_S3_BUCKET | (unset) | S3 bucket name for checkpoint storage |
WORM_EXPORT_OBJECT_LOCK_MODE | GOVERNANCE | S3 Object Lock mode (GOVERNANCE or COMPLIANCE) |
AUDIT_CHECKPOINT_S3_PREFIX | checkpoints/ | S3 key prefix for checkpoint objects |
Verifying the chain
Use the Control Plane API to trigger a chain verification. The verification recomputes row_hash for every row in ascending ID order and reports the first break detected.
# Python snippet — run from within the Control Plane service container
from app.services.audit_hash_chain import verify_chain, get_chain_tip
tip = get_chain_tip()
print(f"Chain tip: row {tip.id}, hash {tip.row_hash[:16]}…")
result = verify_chain()
if result.is_valid:
print(f"Chain intact — {result.rows_verified} rows verified.")
else:
print(f"BREAK DETECTED at row {result.break_at_id}!")
A future GET /audit/chain/verify REST endpoint is planned; until then, use the Python helper above or the Control Plane admin panel (if available in your version).
Breach response
If verify_chain() reports a break, follow these steps:
- Do not alter the database. Any further writes will make forensic recovery harder.
- Capture the break point. Record
result.break_at_idand the surroundingprev_hash/row_hashvalues from the database. - Pull the nearest prior checkpoint. Retrieve the signed checkpoint object from WORM storage that is immediately before the break point. Compare its embedded
row_hashto the database value at that row ID. - Determine the scope. The chain is valid up to the checkpoint; only rows written after the checkpoint are in doubt.
- Escalate to your security and compliance teams. Provide the break point, the checkpoint comparison, and the raw database rows for the affected range.
- Open a support case with Control Core if the break cannot be explained by a known database event (restore, migration).
Troubleshooting: Unexpected chain breaks after a database restore are expected — a restore replaces rows with an older state, invalidating the chain from the restore point forward. Re-run
verify_chain()from the last good checkpoint to confirm the scope. Full reference: Troubleshooting.
Secret access audit events
Control Core also records an audit event for every operation on stored credentials (integration keys, signing keys):
| Event | Operation |
|---|---|
SECRET_STORED | A new credential was stored |
SECRET_READ | A credential was retrieved |
SECRET_ROTATED | A credential was rotated |
SECRET_DELETED | A credential was deleted |
The event_context.secret_id_hash field contains a SHA-256 hash of the credential identifier. The raw identifier and the secret material are never written to the audit log.
These events are hash-chained alongside all other audit rows and flow to the SIEM outbox like any other event type.
Next steps
- AI event catalog —
AI_TRAFFIC_LOG,AI_POLICY_VIOLATION,AI_PII_REDACTIONfield reference - Audit & SIEM integration — configure Splunk, Sentinel, Elastic, and other targets
- How logging works — end-to-end capture architecture
- Troubleshooting — diagnostic reference