Audit Chain
Part of the Audit ecosystem · 7 projects
This module provides tamper-evident audit logging by creating a chain of records where each entry's hash includes the previous one, making any alteration detectable. It can be configured with a signing key to prevent unauthorized database edits and includes options for encrypting metadata and streaming entries to external logging systems.
Tamper-evident audit logging for Drupal, usable by any module.
Each entry's hash covers its own content and the previous entry's hash. A later insertion, deletion, or edit breaks that chain and is detectable by an independent verification pass. With an HMAC key configured, repairing the chain also requires the key, so a database-level edit cannot be quietly papered over.
The useful distinction is not “the application says it logged that,” but “this record can be checked for changes since it was written.”
Using it
Inject Drupal\audit_chain\AuditChainLoggerInterface and write an entry:
\Drupal::service('audit_chain.logger')->log('personnel', 'field_read', [ 'entity_type' => 'node', 'bundle' => 'person', 'id' => $node->id(), 'label' => $node->label(), 'field' => 'field_salary', ]);
entity_type, bundle, id, and label become indexed columns. Every other key is serialized into metadata. The hash also covers the actor, timestamp, IP address, user agent, channel, and operation.
Evidence-required consumers
Call logKeyed() instead of log() when an unsigned row is unacceptable. It throws AuditChainSigningUnavailableException and writes nothing if the signing key will not resolve. signingStatus() returns {keyed, key_id} for precondition checks. Ordinary auditing should keep using log(), which prefers an unsigned row over a dropped one.
Request-scoped collector
Do not log once per access check. Hooks such as hook_entity_field_access() run per field, entity, and render. Logging every call floods a chain that cannot later be reduced without breaking it. Use the collector instead:
\Drupal::service('audit_chain.collector')->collect('personnel', 'field_read', [ 'entity_type' => 'node', 'id' => $entity->id(), 'field' => $field_name, ]);
The collector deduplicates within the request and writes once at kernel.terminate. The first occurrence wins.
Verifying
drush audit-chain:verifyThe exit code is the contract: non-zero means the chain does not verify. The result identifies the condition that needs attention:
- BROKEN — a row's content or ordering no longer matches its hash.
- UNSIGNED — rows are intact and ordered but were written without the configured signing key. Anyone with database access could recompute them.
- SEAL BROKEN — a stored hash in a sealed historical prefix no longer matches the seal digest.
- SEAL FOREIGN — the sealed-prefix digest still matches, but its MAC cannot be authenticated with this environment's current or retired keys.
A foreign seal remains fail-closed: verification exits non-zero and evidence export stays blocked. It is reported as an operational warning without dispatching the integrity-failure event because unchanged copied hashes are not, by themselves, evidence of tampering.
Scheduled verification and alerting
Scheduled verification runs a full check on cron at the configured interval. Each run records a durable verdict for the status report. An integrity failure logs an error to the audit_chain channel and dispatches AuditChainVerificationFailedEvent; the check never modifies the chain.
The Require keyed verification option refuses unkeyed operation when no signing key resolves or when history was written unsigned. Use it where plain SHA-256 does not meet the assurance requirement.
Exporting evidence off-system
drush audit-chain:export --destination=https://evidence.example.com/ingest drush audit-chain:export --destination=/var/evidence/chain.ndjson --from-id=1
The exporter sends versioned NDJSON to an HTTPS endpoint or appends it to a server file under an exclusive lock. Plain HTTP is refused except to loopback. Per-destination checkpoints advance only after successful delivery, so delivery is at least once and consumers must deduplicate on row id.
The export is data-minimized: it includes identifiers and hash-chain columns, but excludes metadata, IP addresses, user agents, and entity labels. Because the exported subset cannot recompute row_hash, verification remains an on-system duty. Export refuses while the latest scheduled verification is failing.
Configuration
Configuration → System → Audit Chain (/admin/config/system/audit-chain) provides:
- Signing key — a Key entity, preferably stored outside the database with the File or Environment provider. Empty means plain SHA-256.
- Retired signing keys — keys accepted when verifying older rows and seals after rotation.
- Encryption profile — encrypts metadata at rest.
- Stream entries — emits structured records to the
audit_chainlogger channel for SIEM forwarding. - Scheduled verification interval and Require keyed verification — control cron verification and its assurance floor.
- Export evidence off-system on cron, destination, and channel filter — control scheduled evidence delivery.
Rotating the encryption profile
Metadata encrypted under one profile cannot be read with another profile alone, and verification needs the plaintext covered by each row's hash. Keep the old profile available and re-encrypt existing rows before removing it:
drush audit-chain:reencrypt --from=old_profile --to=new_profileThe command rewrites ciphertext without changing the hash chain. The status report warns while rows still name an old profile. Evidence export is not a substitute because exported records deliberately exclude metadata.
Sealing an unverifiable prefix
If historical rows were written unkeyed or are otherwise unverifiable under the configured signing keys, do not re-chain them. Recomputed hashes would erase the evidence of a prior change. Create a keyed anchor over the stored hashes instead:
drush audit-chain:seal --through=1997 --reason="pre-key unkeyed production segment" drush audit-chain:verify
The seal proves nothing about the prefix before it was created. It makes future changes to those stored hashes detectable and lets verification continue from the next row. Seal creation requires a resolvable active signing key.
Database refreshes and foreign seals
A database refresh copies the seal but should not copy the source environment's signing key. The target therefore reports SEAL FOREIGN when the copied prefix hashes still match but the seal MAC cannot be authenticated locally. Verify the prefix on the source environment before relying on it. Do not copy a production key merely to make a refreshed environment pass.
What it does not do
- It does not make deletion impossible; it makes deletion evident.
- It does not order events across servers.
- It is not a replacement for dblog or syslog. Those are operational logs; this is an evidentiary record.
Origin
Audit Chain was extracted from MCP Sentinel, where it records AI-agent traffic. The same integrity requirement applies to personnel-record reads, permission grants, configuration changes, and break-glass logins. MCP Sentinel remains its first consumer.
Requirements
Maintainers
Maintained by Jeremy Michael Cerda, sponsored by Wilkes & Liberty, LLC.
Required by
1 tracked project depends on this one
- MCP Sentinel 4 sites
More in the Audit ecosystem
Most installed first
Activity
Release Timeline
Releases
| Version | Type | Core | Notes | Release date | |
|---|---|---|---|---|---|
| 1.7.0 | Stable | 10–11 | Integrity reports page with optional Charts API upgrade. | Sep 11, 2026 | |
| 1.6.0 | Stable | 10–11 | Strict keyed append (logKeyed) and signing-status API for evidence-required consumers. | Sep 2, 2026 | |
| 1.5.1 | Stable | 10–11 | Distinguishes foreign prefix seals from tampering while preserving fail-closed verification and export behavior. | Aug 25, 2026 | |
| 1.5.0 | Stable | 10–11 | Aug 14, 2026 | ||
| 1.4.0 | Stable | 10–11 | Scheduled keyed verification, integrity-failure alerting, and the enterprise assurance profile. | Aug 14, 2026 | |
| 1.3.0 | Stable | 10–11 | Prefix seal for unverifiable history (audit-chain:seal); verify reports sealed_through / seal_intact / verified_from. | Aug 1, 2026 | |
| 1.2.0 | Stable | 10–11 | Jul 31, 2026 | ||
| 1.1.0 | Stable | 10–11 | Jul 31, 2026 | ||
| 1.0.2 | Stable | 10–11 | Jul 30, 2026 | ||
| 1.0.1 | Stable | 10–11 | Jul 29, 2026 | ||
| 1.0.0 | Stable | 10–11 | First stable release of Audit Chain | Jul 29, 2026 | |
| 1.x-dev | Dev | 10–11 | First published snapshot of Audit Chain. | Jul 29, 2026 |