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:

ColumnContents
prev_hashThe row_hash value from the immediately preceding row (or a genesis sentinel for the first row)
row_hashSHA-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 UPDATE or DELETE directly 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):

  1. AWS KMS asymmetric key (AUDIT_CHECKPOINT_KMS_KEY_ID, algorithm RSASSA_PSS_SHA_256) — recommended for regulated environments.
  2. Local HMAC-SHA256 (AUDIT_CHECKPOINT_HMAC_KEY) — acceptable when KMS is not available.
  3. 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

  1. Create an S3 bucket with Object Lock enabled (cannot be enabled after bucket creation).
  2. Set the default retention period to match your regulatory requirement (7 years for FFIEC; 5 years for SOC 2).
  3. 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
  1. Attach an IAM policy to the Control Plane task role that grants s3:PutObject and s3:GetObject on the bucket, and kms:Sign on the KMS key.

Troubleshooting: If checkpoints stop appearing in the bucket, check the Control Plane logs for AUDIT_CHECKPOINT events. Common causes: IAM permission gap on the task role; KMS key disabled or key policy not granting kms: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:

  1. Create an Azure Blob Storage container and enable an Immutability policy (time-based retention).
  2. Set the retention interval to match your regulatory requirement.
  3. 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

EnvironmentAuto-purge behaviour
productionDisabled. 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:

VariableDefaultDescription
AUDIT_RETENTION_DAYS2555 (7 years)Purge threshold for non-production environments
AUDIT_CHECKPOINT_INTERVAL_MIN5Minutes 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_MODEGOVERNANCES3 Object Lock mode (GOVERNANCE or COMPLIANCE)
AUDIT_CHECKPOINT_S3_PREFIXcheckpoints/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:

  1. Do not alter the database. Any further writes will make forensic recovery harder.
  2. Capture the break point. Record result.break_at_id and the surrounding prev_hash / row_hash values from the database.
  3. Pull the nearest prior checkpoint. Retrieve the signed checkpoint object from WORM storage that is immediately before the break point. Compare its embedded row_hash to the database value at that row ID.
  4. Determine the scope. The chain is valid up to the checkpoint; only rows written after the checkpoint are in doubt.
  5. Escalate to your security and compliance teams. Provide the break point, the checkpoint comparison, and the raw database rows for the affected range.
  6. 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):

EventOperation
SECRET_STOREDA new credential was stored
SECRET_READA credential was retrieved
SECRET_ROTATEDA credential was rotated
SECRET_DELETEDA 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