Skip to content

Audit Trail (audit_trail)

Tamper-evident database logging for Drupal 11.3 and later.

Every entry that lands in the chain hashes its own payload together with the hash of the entry before it in the same chain, and an operator-held secret signs that hash. Insert, modify or delete a record after the fact and the chain breaks at every entry downstream: verification pinpoints the first broken row, and the alteration is provably detectable.

Why

Drupal's dblog module records every event you tell it to, but the rows are plain database writes. Anyone with database write access (a sysadmin, a compromised app account, an attacker who escalated through some other vulnerability) can edit a row, drop a row, or insert a fake row without a trace. For most sites that is acceptable. For audit-worthy applications: anything subject to regulated retention (notarial acts, financial transactions, medical records), or any system that needs a defensible answer to "who did what, when, with what content": it is not.

audit_trail adds a cryptographic chain on top of selected log entries. It does not stop tampering (no logging mechanism can do that without external storage) but it makes tampering provably detectable, which is the property regulators and auditors care about.

What it does

  • Plugs into Drupal's standard logger pipeline (PSR-3 + the logger service tag), so any module that calls \Drupal::logger($channel)->info(...) can land in the chain without code changes.
  • Chains entries selectively based on either a per-call flag ('audit_trail' => TRUE in the PSR-3 context), or mode: auto chains that capture every entry on their claimed channels automatically. mode: auto on the default chain claims only the channels it explicitly names: it does NOT wildcard every other channel, so unrelated dblog noise stays out.
  • Stores each chained entry in a dedicated audit_trail table with a publicly-verifiable SHA-256 hash chain plus an operator HMAC layer.
  • Groups entries by chain id (defaults to channel; can map multiple channels into one chain via an audit_trail_chain config entity) so verification is independent per chain.
  • Exposes a AuditTrailVerifier service to walk a chain in id order and report every contiguous broken range in a single pass, and signs checkpoints so repeat verification is incremental rather than a cold walk from genesis.
  • Rotates signing secrets without breaking history. Secrets are audit_trail_secret config entities backed by drupal/key, and every row records which one signed it.
  • Enforces retention without breaking the chain, through a five-stage segment lifecycle (transient-purge, archive to NDJSON, live-purge, file-purge, compaction) driven by cron with per-chain overrides. A purged range stays verifiable because the segment row attests the purge.
  • Anchors chain heads to a qualified RFC 3161 Time-Stamping Authority, through the audit_trail_tsa submodule.
  • Ships admin pages for entries (with a before/after diff), chains, secrets, segments and acknowledgments, and drush commands for every lifecycle and verification operation. See Verifying integrity and Configuration.

What it does not do

  • It is tamper-evident, not tamper-proof. Anyone with the HMAC secret and database write access can forge a fresh chain from scratch. What they cannot do is rewrite the existing chain retroactively without breaking it, nor reproduce the TSA timestamps already anchored to the head they are replacing.
  • It does not move archives off the machine. The archive step writes the NDJSON file and records its SHA-256; copying that file to Write-Once-Read-Many storage (S3 Object Lock, an institutional archival service) is an operator step, and it is what turns tamper-evidence into tamper-resistance.
  • It does not sign exported batches with an eIDAS-qualified key. See the roadmap.
  • It does not install on Drupal 12 yet. The code supports core 12 and the next-major CI lane is green, but drupal/key is a hard dependency and no published key release allows core 12, so composer cannot resolve the install and Drupal refuses to enable key. The gate is key's core compatibility, not this module's.

Comparison with existing modules

Module Scope Tamper-evident?
Core dblog All log calls to DB table No
Core syslog All log calls to syslog No (file)
audit_log Entity events (Node, Taxonomy, User) to DB / pluggable No
audit_trail Selected PSR-3 calls to DB with HMAC chain Yes

The three approaches complement each other. audit_log answers "what entity changed?" with a rich entity-aware UI. audit_trail answers "did the log itself get tampered with?" with a low-level cryptographic guarantee. They can run side-by-side.

Status

Under active development. Project home is drupal.org/project/audit_trail; the canonical repository is at https://git.drupalcode.org/project/audit_trail. Published documentation: project.pages.drupalcode.org/audit_trail. Issues, merge requests and security disclosures go through the drupal.org project page.

Documentation

  • architecture.md: chain construction, cryptographic primitives, canonical form, segment lifecycle, forensic envelope.
  • configuration.md: chain / secret / settings configuration, retention policy, bucket granularity, filter plugins.
  • consumers.md: guide for code that LOGS into the chain (bridges, contributors, private routing keys).
  • verification.md: what the verifier proves and does not prove; incremental walks and signed checkpoints.
  • commands.md: every drush command, what it does and the options that change what it does.
  • security.md: operator deployment guidance (secret rotation, key providers, retention policy, two-tier retention model).
  • threat-model.md: consolidated threat model for security reviewers and compliance assessors (actor matrix, defenses, non-goals, cryptographic assumptions, operator responsibilities).
  • roadmap.md: shipped scope + post-1.0 intent.