Skip to main content
Drupal is a registered trademark of Dries Buytaert
Release: Configuration Language Lock 1.0.2 Minor update available for module config_language_lock (1.0.2). Release: Canvas Override 1.0.1 Minor update available for module canvas_override (1.0.1). Release: Media Remote Image 8.x-1.2 Minor update available for module media_entity_remote_image (8.x-1.2). Release: IDNA Convert Service (punycode) 2.0.4 Minor update available for module idna (2.0.4). Release: Bootstrap Cloud 7.1.3 Minor update available for theme bootstrap_cloud (7.1.3). Release: synship 2.0.1 Minor update available for module synship (2.0.1). Release: Group permissions 2.0.0-rc1 First release candidate for module group_permissions (2.0.0-rc1). Release: Droost 2.0.0-alpha5 New alpha version released for module droost (2.0.0-alpha5). Usage Milestone: Term CSV Export Import Module term_csv_export_import crossed 1,000 active installs. Module Revived: Year/Month Widget 1.1.3 Module year_month_widget updated after 9 months of inactivity (1.1.3).

Audit Chain

4 sites No security coverage Drupal 10–11 PHP >=8.1

Part of the Audit ecosystem · 7 projects

View on drupal.org

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:verify

The 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_chain logger 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_profile

The 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.

Depends on

Dependencies of the latest stable release

Required by

1 tracked project depends on this one

Activity

Tracked releases
12
Tracked since
Jul 2026
Latest release
1 day ago
Releases (12 mo)
12 ▲ from 0
Maintenance
Active

Release Timeline

Releases

Version Type Core Release date
1.7.0 Stable 10–11 Sep 11, 2026
1.6.0 Stable 10–11 Sep 2, 2026
1.5.1 Stable 10–11 Aug 25, 2026
1.5.0 Stable 10–11 Aug 14, 2026
1.4.0 Stable 10–11 Aug 14, 2026
1.3.0 Stable 10–11 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 Jul 29, 2026
1.x-dev Dev 10–11 Jul 29, 2026