Skip to content

Audit Chain Integrity

Arbitex uses an HMAC-SHA256 hash chain to provide tamper evidence for the audit trail. Any modification, deletion, or reordering of audit entries breaks the chain and is reported by the verification API. This page describes how the chain works, how to verify it, and how to manage the signing key.

For the full audit log schema and event type taxonomy, see Audit Data Model Reference.


Standard database audit logs are susceptible to tampering by privileged users: an attacker with database write access can modify, delete, or reorder entries without leaving a trace. An HMAC chain anchors each entry to the content of all previous entries. Changing any entry changes its HMAC, which then invalidates every subsequent entry’s previous_hmac linkage.

The chain does not prevent deletion — it detects it. If entries are deleted from the middle of the sequence, the gap is visible when adjacent entries are verified because the previous_hmac of the entry after the gap no longer matches the hmac of the entry before it.


The chain starts with a fixed sentinel value:

GENESIS_HMAC = "0000000000000000000000000000000000000000000000000000000000000000"

This is a 64-character hex string (256 zero bits). The first entry in every org’s audit chain has previous_hmac = GENESIS_HMAC. Any audit chain that doesn’t start with this value has been tampered with or was not properly initialized.

For each audit event, the platform computes an HMAC-SHA256 over a deterministic message:

message = hmac_key_id + ":" + json_canonical(event_fields) + previous_hmac
hmac = HMAC-SHA256(AUDIT_HMAC_KEY, message.encode("utf-8"))

Where:

  • hmac_key_id is the key version identifier (default: "default")
  • json_canonical(event_fields) is json.dumps(event, sort_keys=True, default=str) over the event content, excluding the hmac, previous_hmac, and hmac_key_id fields
  • previous_hmac is the HMAC of the preceding entry (or GENESIS_HMAC for entry 0)
  • AUDIT_HMAC_KEY is the secret signing key (from environment or Azure Key Vault)

Including hmac_key_id as a prefix ensures that entries signed under different keys produce different HMACs, preventing cross-key splicing attacks.

Three fields are written to each audit entry after computation:

FieldValue
hmac_key_idKey version used to sign this entry
previous_hmacHMAC of the immediately preceding entry
hmacHMAC-SHA256 digest for this entry
Entry 0: previous_hmac = GENESIS_HMAC
hmac = H(key, "default:" + json(e0) + GENESIS_HMAC)
Entry 1: previous_hmac = hmac(Entry 0)
hmac = H(key, "default:" + json(e1) + hmac(Entry 0))
Entry 2: previous_hmac = hmac(Entry 1)
hmac = H(key, "default:" + json(e2) + hmac(Entry 1))
...

Each entry is cryptographically linked to its predecessor. Any modification to entry N will produce a different hmac(Entry N), making previous_hmac on entry N+1 incorrect, which cascades through all subsequent entries.


The verify_chain function checks three invariants across an ordered sequence of events:

The first event’s previous_hmac must equal GENESIS_HMAC. A mismatch indicates that the start of the chain has been tampered with or that the events were not retrieved in full sequence order.

For each event, the verifier recomputes the HMAC from the event’s content and checks it against the stored hmac field:

verifier = HMACChain(key=AUDIT_HMAC_KEY, key_id=event["hmac_key_id"])
verifier._previous_hmac = event["previous_hmac"]
expected_hmac = verifier.compute_hmac(event)
if event["hmac"] != expected_hmac:
# Content has been modified
errors.append(f"Event {idx}: HMAC mismatch")

A mismatch means the event content has been modified after the chain was written.

Each event’s previous_hmac must equal the hmac of the immediately preceding event:

if event["previous_hmac"] != expected_prev_hmac:
errors.append(f"Event {idx}: previous_hmac mismatch")

A mismatch indicates a gap (missing entries) or a reordering attack.

The verifier continues checking after finding errors (rather than stopping at the first failure). This means a single call returns all integrity violations found in the checked range. Each error includes the event index and a description of the specific invariant that failed.


POST /api/admin/audit/verify
Authorization: Bearer arb_live_your-api-key-here
Content-Type: application/json
{
"start": "2026-01-01T00:00:00Z",
"end": "2026-01-31T23:59:59Z"
}

Success response:

{
"valid": true,
"events_checked": 4821,
"errors": []
}

Integrity failure response:

{
"valid": false,
"events_checked": 4821,
"errors": [
"Event 142: HMAC mismatch (expected 'a3f...', got 'c7d...')",
"Event 143: previous_hmac mismatch (expected 'a3f...', got 'c7d...')"
]
}

The verification API requires Org Admin role. It verifies all entries for the calling admin’s org within the specified time range, ordered by created_at ascending.


The HMAC signing key is configured via the AUDIT_HMAC_KEY environment variable. In production, this is sourced from Azure Key Vault via the platform secrets backend.

When AUDIT_HMAC_KEY is empty or unset, HMAC chaining is silently disabled — entries are written without hmac, previous_hmac, or hmac_key_id fields. This is acceptable for development deployments. Production deployments must configure this key.

To rotate the signing key:

  1. Generate a new 256-bit (32-byte) random key: openssl rand -hex 32
  2. Deploy the new key with a new hmac_key_id value (e.g., "v2")
  3. New entries are signed with the new key and hmac_key_id = "v2"
  4. Existing entries retain their original hmac_key_id = "default" and continue to verify against the old key

The chain verifier reads hmac_key_id from each entry and uses the corresponding key for verification. Verification of entries across a key rotation requires access to both the old and new keys. Arbitex stores both keys simultaneously during transition periods.

There is no need to re-sign old entries during key rotation — the hmac_key_id field provides per-entry key identification.

The HMAC key must be kept secret. An attacker who possesses the HMAC key can forge signatures and create an internally consistent tampered chain. Key storage in Azure Key Vault with access logging provides the audit trail for key access.


Observed network facts (src_ip, dst_ip) are included in the HMAC chain. They are immutable facts about the connection captured at request time.

GeoIP enrichment fields (country, city, ISP, ASN) are excluded from the HMAC chain. GeoIP databases are updated periodically — if enrichment fields were part of the chain, re-enriching entries (e.g., after a database update) would break the chain. Only the raw IP is signed.

This means GeoIP enrichment data can be re-derived without invalidating the audit chain, but the IP address itself cannot be changed.


For Hybrid Outpost deployments, audit events are generated on-premises and synced to the Cloud control plane. Synced entries are included in the org’s audit chain:

  • The chain is per-org across all event sources (cloud and outpost)
  • Outpost entries are distinguished by source="outpost" and a non-null outpost_id
  • HMAC computation is identical to cloud entries — the same algorithm applies

The chain is contiguous for events inserted in created_at order. Out-of-order insertions (e.g., delayed outpost sync) will produce chain verification errors for the out-of-order entries. The Outpost sync mechanism is designed to deliver events in order.


FrameworkHow HMAC chain satisfies the requirement
PCI-DSS Req 10.5Tamper-evident audit trail; HMAC detects modification of stored log entries
SOX ICFRHMAC chain provides evidence that IT general controls logs have not been altered
SEC 17a-4(f)Non-rewritable record property — HMAC detects any modification
HIPAA 45 CFR § 164.312(b)Audit control integrity — detection of unauthorized modification of audit records

See Compliance Frameworks for the full regulatory mapping.