Audit Logging
Recording who did what and when, the foundation of forensics and compliance.
Overview
Audit logging creates an immutable, tamper-evident record of security-relevant events: authentication attempts, authorisation decisions, data access, configuration changes, and administrative actions. Audit logs are evidence for incident response, forensic investigation, compliance (SOC 2, PCI DSS, HIPAA), and anomaly detection. They must capture who did what, when, from where, and to what resource.
Origin
Audit logging requirements were formalised in the Trusted Computer System Evaluation Criteria (TCSEC, Orange Book, DoD 1985). PCI DSS (2004) mandated logging for cardholder data environments. HIPAA Technical Safeguard 164.312(b) requires audit controls. SOC 2 Trust Service Criteria CC7.2 requires security event logging. The ELK Stack (Elasticsearch, Logstash, Kibana, 2010-2013) made centralised log analysis accessible.
Examples
Structured audit logging middleware in TypeScript
import { Request, Response, NextFunction } from 'express';
import logger from './logger'; // pino or winston
interface AuditEvent {
timestamp: string;
event: string;
userId: string | null;
ip: string;
method: string;
path: string;
statusCode: number;
durationMs: number;
requestId: string;
userAgent: string;
}
export function auditLogger(req: Request, res: Response, next: NextFunction): void {
const start = Date.now();
const requestId = req.headers['x-request-id'] as string ?? crypto.randomUUID();
res.setHeader('X-Request-ID', requestId);
res.on('finish', () => {
const event: AuditEvent = {
timestamp: new Date().toISOString(),
event: 'http.request',
userId: (req as any).user?.sub ?? null,
ip: req.ip ?? 'unknown',
method: req.method,
path: req.path,
statusCode: res.statusCode,
durationMs: Date.now() - start,
requestId,
userAgent: req.headers['user-agent'] ?? '',
};
if (res.statusCode >= 400) {
logger.warn(event, 'HTTP error');
} else {
logger.info(event, 'HTTP request');
}
});
next();
}Structured JSON logs (pino outputs NDJSON) are machine-parseable by Elasticsearch, Datadog, and CloudWatch Logs Insights. The requestId correlates all log entries for a single request across multiple services. Never log request bodies in plain text; they may contain passwords or PII.
Domain audit log in Ruby with an append-only table
class AuditLog < ApplicationRecord
# Append-only: no update or destroy allowed
before_update { raise ActiveRecord::ReadOnlyRecord }
before_destroy { raise ActiveRecord::ReadOnlyRecord }
belongs_to :user, optional: true
# db/migrate: audit_logs table
# t.string :event_type, null: false
# t.string :actor_type, null: false # "User", "System", "ApiKey"
# t.bigint :actor_id
# t.string :resource_type
# t.bigint :resource_id
# t.jsonb :metadata, default: {}
# t.string :ip_address
# t.datetime :occurred_at, null: false
# t.index [:actor_id, :occurred_at]
# t.index [:resource_type, :resource_id]
end
class AuditService
def self.log(event_type:, actor:, resource: nil, metadata: {}, request: nil)
AuditLog.create!(
event_type: event_type,
actor_type: actor.class.name,
actor_id: actor.try(:id),
resource_type: resource&.class&.name,
resource_id: resource&.id,
metadata: metadata,
ip_address: request&.remote_ip,
occurred_at: Time.current
)
end
end
AuditService.log(
event_type: 'order.cancelled',
actor: current_user,
resource: order,
metadata: { reason: 'Customer request', refund_amount_cents: 4999 },
request: request
)The before_update/before_destroy callbacks make audit records immutable at the application layer. For stronger guarantees, revoke UPDATE and DELETE from the application database user on the audit_logs table at the PostgreSQL layer.
Use Cases
- 01PCI DSS Requirement 10 mandates audit trails for all access to cardholder data with 12-month retention (3 months immediately available)
- 02Incident response: audit logs provide the timeline to answer "what did the attacker do after gaining access?"
- 03Anomaly detection: baseline normal behaviour patterns and alert when audit events deviate (login at unusual hour, bulk data export)
- 04Compliance evidence for SOC 2 and ISO 27001 audits demonstrating that access controls are enforced and monitored
When Not to Use
- //Do not log sensitive data (passwords, full credit card numbers, SSNs, unmasked API keys) even in audit logs; audit logs are high-value targets for attackers and are often shared broadly
- //Do not write audit logs to the same database as application data where a compromised application can delete evidence; write to a separate store or append-only sink
- //Do not rely solely on application-level audit logs; database audit plugins (pgaudit for PostgreSQL) and cloud audit trails (AWS CloudTrail) are harder to bypass than application code
Technical Notes
- pgaudit (PostgreSQL Audit Extension) logs DDL, DML, and role grants at the database level; it cannot be bypassed by application-level exploits that have database access
- AWS CloudTrail records all API calls to AWS services including IAM changes, S3 access, EC2 actions; enabling CloudTrail with a dedicated S3 bucket with MFA delete is a foundational security control
- Log forwarding to a SIEM (Splunk, Elastic Security, Microsoft Sentinel) enables correlation across sources and machine learning-based anomaly detection; shipping logs to an immutable S3 bucket with Object Lock provides tamper-evident storage
- OWASP Logging Cheat Sheet recommends logging authentication events, authorisation failures, input validation failures, application errors, and high-value transactions with consistent format including timestamp in UTC, severity, event type, user, IP, and resource
More in Safety