Every security incident investigation starts with the same question: what happened? The answer lives in the logs. If the logs are intact, you can reconstruct the sequence of events, identify the point of compromise, determine the scope of impact, and build a timeline for legal, regulatory, and operational purposes. If the logs have been tampered with, you have nothing. You are guessing.
This makes log integrity a foundational security property. Not a nice-to-have. Not a compliance checkbox. A primitive -- in the same sense that cryptographers use the word. It is a building block upon which other guarantees depend. Authentication logs that can be silently modified provide no assurance that the authentication system is working. Access control logs that can be deleted by the people they audit provide no accountability. Financial transaction logs that can be altered after the fact provide no basis for dispute resolution.
The property we want is called append-only: new entries can be added to the log, but existing entries cannot be modified or deleted. This sounds simple. Implementing it in a way that holds up against a motivated adversary -- including an adversary with root access to the logging system itself -- requires careful engineering.
This article covers the theory and practice of append-only audit logging. We will start with why it matters, move through the formal properties, examine several implementation approaches at different levels of the stack, and end with the operational realities that determine whether your immutable log is actually immutable or just inconvenient to modify.
Why Audit Logs Matter
Audit logs serve four distinct purposes, each with different requirements. Understanding which purposes your system needs to satisfy determines how strong your append-only guarantee needs to be.
Forensics. After a security incident, the logs are the primary evidence source. They tell you what the attacker did, when, from where, and -- if you are lucky -- how they got in. Forensic use requires that the logs be complete (no gaps in coverage), accurate (timestamps and event data reflect reality), and unmodified (the attacker could not alter them to cover their tracks). The append-only property directly addresses the third requirement.
Compliance. Regulatory frameworks -- SOX, HIPAA, PCI DSS, GDPR -- require that certain events be logged and that those logs be retained for specified periods. Some frameworks explicitly require log immutability. PCI DSS Requirement 10.3, for instance, requires that audit trails cannot be altered. SOX Section 802 makes it a federal crime to alter or destroy audit records related to financial reporting. Compliance use requires demonstrable integrity: you must be able to prove that the logs have not been modified, not merely assert it.
Accountability. In multi-user systems, audit logs create a record of who did what. This is the basis for holding people accountable for their actions -- and equally, for exonerating people who did not do what they are accused of. Accountability requires that users cannot modify their own audit trail. If a database administrator can delete the log entries that record their access to sensitive tables, the audit log provides no accountability for database administrators.
Dispute resolution. When two parties disagree about what happened -- a financial transaction, an SLA violation, a data access event -- the audit log is the arbiter. Both parties must trust that the log has not been altered in favor of the other. This is the strongest integrity requirement, because it must hold against both parties, either of whom may have had the ability to modify the log.
Append-Only as a Formal Property
Let us be precise about what append-only means. A log L is append-only if the following properties hold:
Insertion. A new entry e can be added to L, producing L' = L || e (the concatenation of the existing log with the new entry). This operation always succeeds if the system is operational.
Immutability. For any entry e that exists in L at position i, there is no operation that produces L' where L'[i] differs from L[i]. Existing entries cannot be modified.
Non-deletion. For any entry e that exists in L, there is no operation that produces L' where e does not exist in L'. Existing entries cannot be removed. The length of L' is always greater than or equal to the length of L.
These three properties together mean that the log only grows. It never shrinks. It never changes. The only thing you can do to it is add to the end.
Note what this definition does not include. It does not guarantee completeness -- an attacker who can prevent events from being written to the log in the first place is not violating the append-only property. It does not guarantee availability -- the log can be destroyed entirely (which is different from selectively modifying it). And it does not guarantee confidentiality -- anyone who can read the log can see all entries.
Implementation: Cryptographic Chaining
The most widely applicable approach to append-only logging is cryptographic chaining -- also called a hash chain. The idea is simple: each log entry includes the hash of the previous entry, creating a chain where any modification to a past entry would change its hash, which would break the chain at the next link.
Hash Chain Construction
Given a hash function H (SHA-256 is the standard choice), each log entry is constructed as:
entry[0] = { data: event_data, prev_hash: GENESIS, hash: H(event_data || GENESIS) }
entry[n] = { data: event_data, prev_hash: entry[n-1].hash, hash: H(event_data || entry[n-1].hash) }
GENESIS is a well-known initial value (often all zeros, or a hash of the system's public key and creation timestamp).
To verify the chain, you recompute the hash of each entry using the stated data and previous hash, then confirm it matches the stored hash. If any entry has been modified -- even a single bit -- its hash will not match, and every subsequent entry's previous-hash reference will also fail to validate.
This gives you tamper evidence, not tamper prevention. The hash chain does not stop an attacker from modifying the log. It makes modification detectable, provided someone checks the chain. An attacker who modifies entry 47 and then recomputes the hashes for entries 48 through the end of the log can produce a valid-looking chain -- but only if they do it before anyone has recorded an independent copy of the original hashes.
Strengthening the Chain: External Witnesses
The hash chain's weakness is that an attacker with write access to the entire log can recompute the chain from any point of modification forward. The defense is to periodically publish a checkpoint -- the hash of the most recent entry -- to a location the attacker does not control.
This external witness can be another server operated by a different team, a third-party timestamping service, a public transparency log, or even a printed record. The checkpoint does not need to contain the log data itself. It only needs to contain the hash of the latest entry and the entry number. If the attacker modifies the log and recomputes the chain, the new hashes will not match the checkpoints that were already published.
The granularity of checkpointing determines your detection window. If you checkpoint every hour, an attacker who modifies the log and recomputes the chain can do so undetected only if they control both the log and the checkpoint destination, or if they complete the attack between checkpoints and you do not verify until the next cycle. More frequent checkpoints narrow this window. Publishing to multiple independent witnesses narrows it further.
Merkle Trees for Efficient Verification
A hash chain requires O(n) work to verify the entire log -- you must check every entry in sequence. For large logs, this is slow. Merkle trees provide O(log n) verification of any individual entry and efficient proof that an entry exists (or does not exist) in the log.
A Merkle tree is a binary tree where each leaf node is the hash of a log entry and each internal node is the hash of its two children. The root hash represents the entire log. To prove that a specific entry exists in the log, you provide the entry, its position, and the sibling hashes along the path from the leaf to the root. The verifier recomputes the root hash from these values and compares it to the known root.
This structure is used in Certificate Transparency (RFC 6962), where every TLS certificate issued by a public CA is logged in a Merkle-tree-based append-only log. Monitors and auditors can efficiently verify that specific certificates are present (or absent) in the log without downloading the entire thing.
For audit logging, Merkle trees are most useful when you need to provide proofs to third parties -- for instance, proving to a regulator that a specific event was logged at a specific time, without revealing the rest of the log. The tree structure makes this possible with a proof that is logarithmic in the size of the log rather than linear.
Forward-Secure Logging
Standard hash chains protect against post-hoc modification of log data. They do not protect against an attacker who compromises the logging system and obtains the current signing key. With that key, the attacker can forge new entries that appear legitimate.
Forward-secure logging addresses this by using key evolution: the signing key changes over time, and old keys are securely deleted. Compromising the current key allows forging future entries but not past ones, because the keys that signed past entries no longer exist.
The construction works as follows. Start with a master key K0. Derive K1 = H(K0), then delete K0. Derive K2 = H(K1), then delete K1. Entry n is authenticated using Kn. An attacker who compromises the system at time t obtains Kt but cannot compute K0 through K(t-1), because the hash function is one-way.
This is the approach described by Bellare and Yee in their forward-secure logging scheme and implemented in systems like Ma and Tsudik's forward-secure sequential aggregate (FssAgg) authentication. The practical implication is significant: even if your logging server is fully compromised today, the attacker cannot forge entries that appear to have been created yesterday.
The cost is operational complexity. Key evolution must happen automatically and the old keys must be securely erased -- not just deleted from the filesystem, but overwritten so they cannot be recovered from disk. This is straightforward on modern hardware with full-disk encryption and TRIM support, but it requires deliberate implementation.
Practical Architecture
Theory is necessary but not sufficient. The implementation architecture determines whether your append-only guarantees survive contact with real operational environments.
The Three-Stage Pipeline
A production-grade append-only logging system has three stages: local buffering, authenticated transport, and immutable storage.
Local buffer. Applications write log entries to a local buffer -- a file, a Unix domain socket, a shared memory segment. The buffer exists because the application should not block on log delivery. If the network to the log store is down, the application continues operating and the buffer holds entries until connectivity returns. The buffer itself is not append-only. It is a staging area.
Authenticated transport. Entries move from the local buffer to the log store over an authenticated, encrypted channel. TLS with mutual authentication is the minimum. The transport must guarantee ordering -- entries must arrive at the store in the same order they were generated. If you are using TCP (which provides ordering), this is automatic. If you are using UDP-based protocols for performance, you need sequence numbers and reordering at the receiver.
Immutable store. The log store is where the append-only property is enforced. This can be a database with appropriate access controls, a write-once filesystem, a cloud storage service with object lock, or a custom application that implements hash chaining. The store must refuse modification and deletion requests. The store's access credentials must be limited to the append operation -- the principle of least privilege applied to logging.
PostgreSQL Append-Only Patterns
If your application already uses PostgreSQL, you can implement append-only audit logging without introducing a new storage system. The approach uses PostgreSQL's permission model and trigger system to enforce the append-only property at the database level.
Schema and Permissions
Create a dedicated audit schema with a table that stores log entries and a role that has only INSERT permission:
CREATE SCHEMA audit;
CREATE TABLE audit.events (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
actor TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
detail JSONB,
prev_hash BYTEA,
entry_hash BYTEA NOT NULL
);
-- The application role can only INSERT
CREATE ROLE audit_writer;
GRANT USAGE ON SCHEMA audit TO audit_writer;
GRANT INSERT ON audit.events TO audit_writer;
GRANT USAGE ON SEQUENCE audit.events_id_seq TO audit_writer;
-- Explicitly deny UPDATE and DELETE
REVOKE UPDATE, DELETE ON audit.events FROM audit_writer;
REVOKE UPDATE, DELETE ON audit.events FROM PUBLIC;
This gives you permission-based append-only: the application, connecting as audit_writer, can add entries but cannot modify or remove them. The limitation is that the PostgreSQL superuser (postgres) can bypass these restrictions. If your threat model includes a compromised DBA, you need additional controls.
Trigger-Based Protection
Add a trigger that prevents UPDATE and DELETE operations regardless of the caller's permissions:
CREATE OR REPLACE FUNCTION audit.deny_modification()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Audit log entries cannot be modified or deleted';
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER no_update_delete
BEFORE UPDATE OR DELETE ON audit.events
FOR EACH ROW
EXECUTE FUNCTION audit.deny_modification();
This adds a second layer of defense. Even if someone grants UPDATE or DELETE privileges to a role, the trigger blocks the operation. A superuser can still disable the trigger, but now they must take two deliberate actions (disable the trigger, then modify data), which creates a higher bar and more opportunity for detection.
Replication to a Read-Only Replica
For stronger guarantees, replicate the audit table to a separate PostgreSQL instance that the application team does not control. Use logical replication to stream only the audit schema to a replica managed by a different team -- security, compliance, or an external auditor.
The replica serves as an independent copy. If someone modifies the primary audit log, the replica retains the original entries. Periodic comparison between the primary and replica detects tampering. This is the database equivalent of the external witness pattern described earlier.
Filesystem Approaches
Operating systems provide several mechanisms for enforcing append-only at the filesystem level. These are useful as defense-in-depth layers, though they should not be your only protection.
The Append-Only File Attribute
On Linux, the chattr command can set the append-only attribute on a file:
# Set append-only: file can be opened for append but not for
# overwrite, truncation, or deletion
chattr +a /var/log/audit/events.log
# Verify the attribute
lsattr /var/log/audit/events.log
# Output: -----a------------ /var/log/audit/events.log
With this attribute set, the file can be opened with O_APPEND but not O_TRUNC. It cannot be deleted, renamed, or hard-linked. Writes must go to the end of the file.
The limitation is that root can remove the attribute with chattr -a. On systems where the threat model includes root compromise, you can use Linux Security Modules (SELinux, AppArmor) to restrict which processes can modify file attributes, or use the immutable attribute (chattr +i) on the directory containing the log files to prevent deletion.
WORM Storage
Write Once Read Many (WORM) storage is the physical-layer version of append-only. Traditional WORM storage used optical media -- once a sector was written, the physical substrate prevented overwrite. Modern WORM implementations use firmware-enforced write protection on disk arrays or tape.
WORM is the strongest append-only guarantee available because it operates below the operating system. Root access does not help. Firmware exploits are the only attack vector, and those require physical access to the storage controller. For regulated industries where log immutability must be demonstrable to auditors and courts, WORM storage remains the gold standard.
The tradeoff is flexibility. WORM storage cannot be reused. Retention policies must be set at write time, and extending or shortening retention requires administrative action on the storage array. This makes WORM appropriate for long-term archival of audit logs but impractical for hot-path logging where entries are being written continuously.
Cloud Patterns
The major cloud providers offer native object-lock mechanisms that enforce append-only (or more precisely, write-once-read-many) at the storage service level.
S3 Object Lock
Amazon S3 Object Lock provides two modes. Governance mode allows users with specific IAM permissions to override the lock -- useful for development and testing but not a real security control. Compliance mode prevents anyone, including the root account, from deleting or overwriting the object until the retention period expires. The retention period is set per-object or via a bucket-level default.
For audit logging, the pattern is: write log entries as individual objects (or batched into time-windowed objects) to a bucket with compliance-mode Object Lock and a retention period matching your regulatory requirement. Once written, the objects cannot be modified or deleted by anyone, including AWS support, until the retention period expires.
The operational implication is that mistakes are permanent for the duration of the retention period. If you accidentally write sensitive data to the audit log (a password in a debug entry, for instance), you cannot delete it. This is the correct behavior for an audit log and the wrong behavior for a debug log. Be careful about what you log.
Azure Immutable Blob Storage
Azure provides equivalent functionality through immutable blob storage policies. Time-based retention policies prevent modification or deletion for a specified period. Legal hold policies prevent modification or deletion indefinitely, until the hold is explicitly removed -- useful for litigation hold requirements where you do not know in advance when the hold will end.
Both S3 Object Lock and Azure immutable blobs operate at the storage service level, below the operating system of the application writing the logs. An attacker who compromises the application server cannot modify objects that are already locked, because the lock is enforced by the cloud provider's storage infrastructure.
Log Integrity Verification
An append-only log that is never verified is an append-only log that you hope is intact. Verification must be a scheduled, automated process with alerting on failure.
Chain Verification
For hash-chained logs, verification means recomputing the hash of each entry and confirming it matches the stored hash and the previous-hash reference of the next entry. This is a sequential scan -- you must visit every entry in order. For a log with N entries, verification is O(N).
Full verification should run at least daily. For high-value logs, run it continuously as a background process that trails the write head by a configurable delay. When verification fails, the alert must go to a different channel than the one the log system uses -- if the attacker compromised the logging system, they may have also compromised its alerting.
Out-of-Band Checkpoints
Periodically compute the hash of the current log head and publish it to an independent system. The checkpoint record should include: the entry sequence number, the entry hash, a timestamp, and a signature from the logging system's key.
Checkpoint verification then becomes: fetch the checkpoint from the independent system, fetch the corresponding entry from the log, recompute the hash, and compare. If they match, the log has not been tampered with up to that checkpoint. If they do not match, either the log or the checkpoint has been modified -- and since the checkpoint is stored in a system the log operator does not control, the log is the suspect.
The checkpoint interval determines your worst-case detection latency. A checkpoint every 5 minutes means tampering can go undetected for at most 5 minutes. A checkpoint every hour means up to an hour. Choose the interval based on the value of the data being logged and the cost of verification.
What Append-Only Does Not Solve
It is important to be precise about the limits of append-only logging, because overstating the guarantee is a form of security theater in its own right.
It Does Not Prevent Log Suppression
An attacker who can kill the logging process or sever the network connection between the application and the log store can prevent new entries from being written. The existing log remains intact -- no entries are modified or deleted -- but the gap in coverage means the attacker's actions during the suppression window are unrecorded.
Defense against suppression requires a separate mechanism: a heartbeat or watchdog that detects when the logging pipeline has stopped producing entries. If the log store has not received an entry for longer than the expected interval, that silence is itself an alert. This is sometimes called a "canary" pattern -- the absence of expected entries is evidence of a problem.
It Does Not Guarantee Completeness
Even if the logging process is running, an attacker who controls the application can prevent specific events from being logged. If the application decides what to log, and the attacker controls the application, they can log everything except their own actions.
Defense against selective omission requires logging at a layer the attacker does not control. Network-level logging (NetFlow, packet capture) captures events regardless of what the application chooses to log. Kernel-level audit frameworks (Linux auditd, Windows Security Event Log) capture system calls regardless of application behavior. The gap between what the application logs and what the infrastructure logs is evidence of selective omission.
It Does Not Protect Confidentiality
Audit logs frequently contain sensitive information: usernames, IP addresses, resource identifiers, timestamps of activity. An append-only log that is readable by anyone provides an accountability guarantee at the cost of a privacy one. Access control on the read path is as important as integrity on the write path.
Some systems address this with encrypted audit logs, where entries are encrypted with a key held by an authorized auditor. The logging system can append entries but cannot read them. Only the auditor, with the decryption key, can access the log contents. This pattern is useful in multi-tenant environments where the system operator should not be able to read tenants' audit trails.
Operational Considerations
Storage Growth
An append-only log grows without bound. You cannot delete old entries because that violates the append-only property. You cannot compact or vacuum the log because that modifies existing data. The log only grows.
For systems with high event volume, storage costs can become significant. The practical response is tiered storage: hot storage (fast, expensive) for recent entries, warm storage (slower, cheaper) for older entries, cold storage (slowest, cheapest) for archival entries. The append-only property must hold at every tier. Moving an entry from hot to cold storage is a copy-then-verify operation, not a move. The hot copy is retained until the cold copy is verified.
Retention policies must be defined before the system goes into production, because changing them retroactively is difficult when you cannot delete entries. If your policy says "retain for 7 years," you need 7 years of storage capacity. If you are using cloud object lock with compliance mode, the retention period is immutable once set -- you literally cannot shorten it.
Legal Hold
Legal hold is the requirement to preserve all records potentially relevant to litigation, regardless of your normal retention policy. When a legal hold is issued, you must stop all deletion of affected records -- which, for an append-only log, means nothing changes operationally. The log is already immutable.
This is one of the underappreciated benefits of append-only logging. When legal asks "can you guarantee that none of the records relevant to this case have been deleted," the answer is yes, by construction. The system does not permit deletion. This simplifies litigation readiness and reduces the risk of spoliation claims.
Mistakes Are Permanent
If you accidentally log a password, a social security number, or other sensitive data to an append-only log, you cannot remove it. The entry is there for the duration of the retention period. This creates tension with data minimization requirements under regulations like GDPR, which mandate that personal data should not be retained longer than necessary.
The resolution is to control what enters the log, not to rely on the ability to clean it up afterward. Log sanitization must happen before the entry reaches the immutable store. Strip sensitive fields, tokenize identifiers where possible, and review your logging configuration regularly to ensure that debug-level output containing raw request bodies is not being sent to the audit pipeline.
Putting It Together
A practical append-only audit logging system combines multiple layers of the approaches described above. No single mechanism provides a complete solution. The combination provides defense in depth.
At the application layer: hash-chain each entry, including the hash of the previous entry in the new entry. This provides tamper evidence even if higher layers fail.
At the database layer: use a dedicated role with INSERT-only permissions and triggers that block UPDATE and DELETE. Replicate to an independent instance managed by a separate team.
At the storage layer: use append-only file attributes or cloud object lock to prevent modification below the database. This catches attacks that bypass the database entirely (direct file manipulation, storage-level exploits).
At the verification layer: run continuous chain verification with alerting to an independent channel. Publish periodic checkpoints to an external witness. Compare the primary log against replicas on a regular schedule.
Each layer addresses a different threat. The application-layer hash chain catches modification of individual entries. The database permissions prevent the application from modifying its own audit trail. The storage-layer lock prevents the DBA from modifying the underlying files. The external checkpoints catch tampering that repairs the hash chain.
No single layer is sufficient. An attacker who compromises only the application is stopped by the database permissions. An attacker who compromises the database is stopped by the storage-layer lock. An attacker who compromises the storage is caught by the external checkpoints. An attacker who compromises everything -- the application, the database, the storage, and the external witness -- has a problem that append-only logging alone cannot solve. At that point, you are dealing with a total infrastructure compromise, and your audit logs are one of many things that are lost.
The goal is not to build an unbreakable system. The goal is to make tampering expensive, detectable, and attributable. An attacker who must compromise four independent systems to modify an audit log will, in most cases, either be detected during the attempt or decide the effort is not worth the reward. That is the practical value of append-only logging: it raises the cost of deception to the point where honest operation is the path of least resistance.
Start with the hash chain. It requires no infrastructure changes and provides immediate tamper evidence. Add database permissions and triggers -- an afternoon of work if you are already on PostgreSQL. Set up replication to an independent instance when you can. Publish checkpoints to a system you do not control. Each layer you add narrows the set of attackers who can tamper with your logs without being caught.
The alternative -- mutable logs that an attacker can quietly edit -- is not a logging system. It is a suggestion box.