S
SovereignShield
Back to Blog & Guides/HIPAA Audit Logging for SaaS: Technical Implementation of §164.312(b)
#HIPAA#Audit Logging#Compliance#SaaS#Security

HIPAA Audit Logging for SaaS: Technical Implementation of §164.312(b)

Build tamper-evident, immutable audit log systems for Protected Health Information (PHI) access tracking in multi-tenant SaaS environments.

SovereignShield Security Team

Technical Overview of §164.312(b) Audit Controls

Under the HIPAA Security Rule (45 CFR § 164.312(b)), covered entities and business associates must:

“Implement hardware, software, and/or procedural mechanisms that record and examine activity in information systems that contain or use electronic protected health information (ePHI).”

To pass a HIPAA audit, log entries must be immutable, tamper-evident, and retain records for a minimum of 6 years (per §164.316(b)(2)(i)).


1. Required Audit Event Schema

Every read, write, export, or deletion of ePHI must capture the 6 mandatory audit fields:

  1. Timestamp: ISO 8601 UTC string with millisecond precision.
  2. Actor Identifier: User ID, service account, or role triggering the request.
  3. Action Performed: READ, CREATE, UPDATE, DELETE, EXPORT, or AUTH_FAILURE.
  4. Target Resource: Unique ID and entity type of the ePHI (e.g., patient_record:10492).
  5. Tenant / Organization ID: Multi-tenant isolation boundary.
  6. Origin & Network Context: IP address, user agent, and request ID.

TypeScript Schema & Cryptographic Chaining

import { createHash } from 'crypto';

export interface AuditLogEntry {
  id: string;
  tenantId: string;
  actorId: string;
  actorRole: string;
  action: 'PHI_READ' | 'PHI_CREATE' | 'PHI_UPDATE' | 'PHI_DELETE' | 'PHI_EXPORT' | 'AUTH_FAILURE';
  resourceType: string;
  resourceId: string;
  timestamp: string;
  ipAddress: string;
  userAgent: string;
  previousHash: string; // Enables tamper-evident hash chaining
  entryHash: string;    // SHA-256 (ID + Timestamp + Actor + Action + Target + PrevHash)
}

export class ImmutableAuditLogger {
  private lastHash: string = 'GENESIS_BLOCK_HASH_00000000000000000000000000000000';

  /**
   * Generates a tamper-evident audit record with SHA-256 chain verification
   */
  public createAuditEntry(
    params: Omit<AuditLogEntry, 'id' | 'timestamp' | 'previousHash' | 'entryHash'>
  ): AuditLogEntry {
    const id = crypto.randomUUID();
    const timestamp = new Date().toISOString();
    const previousHash = this.lastHash;

    const rawPayload = `${id}|${params.tenantId}|${params.actorId}|${params.action}|${params.resourceType}:${params.resourceId}|${timestamp}|${previousHash}`;
    const entryHash = createHash('sha256').update(rawPayload).digest('hex');

    const entry: AuditLogEntry = {
      ...params,
      id,
      timestamp,
      previousHash,
      entryHash,
    };

    // Update chain state
    this.lastHash = entryHash;
    return entry;
  }
}

2. Immutable Storage Architecture

Audit logs must never be stored in the same primary database table as standard operational data. If an attacker gains SQL write access, they could alter audit records.

+------------------+      +-------------------+      +-------------------------+
| App Service      | ---> | Audit Middleware  | ---> | Write-Once S3/WORM Bucket|
| (API / GraphQL)  |      | (Hash Chain)      |      | (Object Lock Enforced)  |
+------------------+      +-------------------+      +-------------------------+

AWS S3 Object Lock Policy (WORM - Write Once, Read Many)

Enforce AWS S3 Object Lock in Compliance Mode for 6 years to prevent log deletion or modification—even by root AWS accounts:

{
  "ObjectLockConfiguration": {
    "ObjectLockEnabled": "Enabled",
    "Rule": {
      "DefaultRetention": {
        "Mode": "COMPLIANCE",
        "Years": 6
      }
    }
  }
}

3. Automated Chain Integrity Verification Routine

To verify during an auditor inspection that no records have been altered or deleted:

export function verifyAuditChain(logs: AuditLogEntry[]): { valid: boolean; brokenAt?: string } {
  let expectedPrevHash = 'GENESIS_BLOCK_HASH_00000000000000000000000000000000';

  for (const log of logs) {
    if (log.previousHash !== expectedPrevHash) {
      return { valid: false, brokenAt: log.id };
    }

    const rawPayload = `${log.id}|${log.tenantId}|${log.actorId}|${log.action}|${log.resourceType}:${log.resourceId}|${log.timestamp}|${log.previousHash}`;
    const computedHash = createHash('sha256').update(rawPayload).digest('hex');

    if (computedHash !== log.entryHash) {
      return { valid: false, brokenAt: log.id };
    }

    expectedPrevHash = log.entryHash;
  }

  return { valid: true };
}

Checklist for HIPAA Audit Log Readiness