This project is not covered by Drupal’s security advisory policy.
Tamper-evident audit logging for Drupal, usable by any module.
Each entry's hash covers its own content and the previous entry's hash. Any later insertion, deletion or edit breaks that chain and is detectable by an independent verification pass. With an HMAC key configured, forging a repair also requires the key — so a database-level edit cannot be quietly papered over.
The difference that matters: not "the application says it logged that", but a record that can be shown not to have been altered 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', 'id' => $node->id(), 'field' => 'field_salary', ]);
entity_type, bundle, id and label become indexed columns; every other key is serialised into metadata. All of it — plus actor, timestamp, IP and user agent — is covered by the hash. The first argument is the channel (the consumer). It is bound into the hash, so an entry cannot be re-attributed after the fact.
Request-scoped collector
Do not log once per access check. Hooks like hook_entity_field_access() fire per field, per entity, per render; an entry each time floods a chain you cannot un-flood. Use the collector instead:
\Drupal::service('audit_chain.collector')->collect('personnel', 'field_read', [ 'entity_type' => 'node', 'id' => $entity->id(), 'field' => $field_name, ]);
It deduplicates per request (channel, operation, entity keys; first occurrence wins) and writes once at kernel.terminate, so the obvious call is the safe one and the chain lock stays off the request critical path.
Verifying
drush audit-chain:verifyExit code is the contract: non-zero means the chain does not verify. Two failures are reported differently:
- Tampered — a row's content or ordering no longer matches its hash.
- Unsigned / written unkeyed — rows are intact and in order, but were hashed without the configured signing key (usually an unresolvable Key entity at write time). Nothing was edited; anyone with database access can rewrite them.
Both exit non-zero. The status report flags a configured key that will not resolve while writes are still happening.
Configuration
Configuration → System → Audit Chain (/admin/config/system/audit-chain):
- Signing key — a Key entity (prefer File or Environment, outside the database). Empty means plain SHA-256.
- Retired signing keys — keys this chain was signed with previously. Verification accepts them so rotating the signing key does not make earlier rows look tampered with.
- Encryption profile — encrypts metadata at rest. Rotating orphans existing ciphertext; export or re-encrypt first. The status report warns when rows still reference a profile the site no longer uses.
- Stream entries — emit each entry to the
audit_chainlogger channel for SIEM forwarding.
Sealing an unverifiable prefix
If history was written unkeyed (or is otherwise not verifiable under today's signing keys), do not re-chain it. Recomputed hashes would paper over tampering. Instead:
drush audit-chain:seal --through=1997 --reason="pre-key unkeyed production segment" drush audit-chain:verify
The seal is a site-local genesis anchor over stored row_hash values. It proves nothing about the past; it makes any future change to that prefix detectable and lets post-seal verification exit cleanly. Only rows that do not verify under the configured keys may be sealed. See the 1.3.0 release notes.
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 one.
Origin
Extracted from MCP Sentinel, where it grew up as the trail for AI-agent traffic. It was never specific to that: personnel-record reads, permission grants, configuration changes and break-glass logins all want the same guarantee without an AI-governance module. MCP Sentinel remains its first consumer.
Requirements
Maintainers
Maintained by Jeremy Michael Cerda, sponsored by Wilkes & Liberty, LLC.
Project information
- Project categories: Administration tools, Developer tools, Security
5 sites report using this module
- Created by jmcerda on , updated
This project is not covered by the security advisory policy.
Use at your own risk! It may have publicly disclosed vulnerabilities.
Releases
Development version: 1.x-dev updated 15 Aug 2026 at 05:36 UTC