Enterprise
Tamper-evident audit log for your wiki
Wikantik records authentication, content, and administrative events in an append-only, SHA-256 hash-chained audit log. Every privileged action is traceable — who did it, when, from where, and whether it succeeded — and the integrity of that record is cryptographically verifiable.
Why a tamper-evident trail matters
A log you can modify isn't a log — it's a ledger that favours whoever controls it last. Compliance frameworks like SOC 2 and ISO 27001 require audit records that are demonstrably unaltered. Most wiki platforms write events to a database table with no integrity guarantee: an admin with database access can delete an inconvenient row and nobody will know.
Wikantik uses a hash chain: each row's hash includes the hash of the row before it. Changing any single row changes its hash, which invalidates every subsequent row's prev_hash. Tampering is detectable instantly, even if the attacker has direct database access. The application role is also granted only INSERT and SELECT on audit_log — UPDATE and DELETE are explicitly revoked at the PostgreSQL level.
What gets logged
Events are grouped into five categories:
- AUTHN —
login.ok,login.failed,logout,session.expired - AUTHZ —
access.denied - CONTENT — page save, page delete
- ADMIN — group membership changes, profile saves, user lifecycle (lock/unlock), API key issuance, and all SCIM operations (
scim.user.create,scim.user.update,scim.group.create,scim.group.update,scim.group.delete) - READ —
page.read(conditional and policy-controlled — recorded only on pages where read auditing is enabled)
Each entry carries: sequence number, timestamps, category, event type, actor identity, actor type, target, outcome (SUCCESS, FAILURE, or DENIED), source IP, user agent, correlation ID, detail text, and the chain hashes.
How the hash chain works
Every row stores two hash fields computed by AuditChainHasher:
prev_hash— therow_hashof the immediately preceding row (or a genesis constant for row 1).row_hash— SHA-256 of the row's canonical fields concatenated withprev_hash.
Writes are serialized by a PostgreSQL advisory transaction lock (pg_advisory_xact_lock) to prevent concurrent inserts from breaking the chain order. The chain is monotonic: sequence gaps are impossible during normal operation.
Verify and export
Integrity verification
From the admin UI at Admin → Audit Log, click Verify integrity to call GET /admin/audit/verify. The endpoint walks the full chain and returns either:
{ "ok": true }
or:
{ "ok": false, "firstBrokenSeq": 42 }
The UI displays a green or red banner accordingly. The REST endpoint requires the Admin role, enforced by AdminAuthFilter.
Export for SIEM ingestion
GET /admin/audit/export streams a complete CSV file named audit-log.csv with columns: seq, created_at, event_time, category, event_type, actor, outcome, target, source_ip. No filter is applied — you get the full log.
For filtered queries, use GET /admin/audit with parameters for actor, category, event type, outcome, date range, and cursor-based pagination (beforeSeq) for large result sets up to 1000 entries per page.
Retention and purge
The audit_log table is partitioned by month in PostgreSQL. The retention script (bin/db/audit-retention.sh) runs on a configurable schedule and does two things:
- Pre-creates upcoming monthly partitions so the runtime never needs to create them on the hot path.
- Archives then drops partitions older than the retention window (
AUDIT_RETENTION_MONTHS, default 84 months / 7 years). Each over-age partition ispg_dumped to an archive directory and verified withpg_restore --listbefore the drop. A failed verification skips the drop entirely — no data is lost without a confirmed archive.
After a partition is dropped, the chain automatically re-anchors on the oldest surviving row. Purged history is only verifiable from the archive dump files, but it was archived before dropping, so the data exists — it just isn't in the live database.
Compliance posture
The combination of hash-chain integrity, explicit REVOKE UPDATE, DELETE at the database level, append-only writes, and configurable long-term retention is designed to satisfy the audit trail requirements in frameworks such as SOC 2 Type II, ISO 27001, and GDPR record-keeping obligations. Your security team can verify the chain at any time and export the full log for independent review.
Separation of privilege. The application role can only INSERT and SELECT audit rows. Creating and dropping monthly partitions requires the migrate role — a separate, privileged role used only by the retention script and schema migrations, not by the running application.
Frequently asked questions
What does "tamper-evident" mean for the Wikantik audit log?
Each audit row stores a SHA-256 hash of its own fields concatenated with the hash of the previous row — forming a linked chain. Modifying any row changes its hash, which invalidates every subsequent row's prev_hash. The verify endpoint walks the full chain and reports the exact sequence number where the chain breaks. Additionally, the database role used by the application is granted only INSERT and SELECT on audit_log — UPDATE and DELETE are explicitly revoked at the database level.
Can I export audit records for a SIEM?
Yes. GET /admin/audit/export streams a complete CSV file with columns: seq, created_at, event_time, category, event_type, actor, outcome, target, source_ip. The REST endpoint also supports filtered JSON queries with actor, category, eventType, outcome, date range, and cursor-based pagination for large result sets.
How long are audit records kept?
The default retention is 84 months (7 years), configurable via the AUDIT_RETENTION_MONTHS setting. The retention script archives each over-age monthly partition to a dump file and verifies the archive before dropping the partition — purged history is cold-stored, not destroyed. The chain automatically re-anchors on the oldest surviving row after a partition is dropped.