---
sensitivity: public
schema_version: 1
last_validated: 2026-04-29
status: shipped
---

# Audit log replication

> F3.11 — Replicate the F0.2 hash-chained audit log to immutable storage.
> Closes the residual Tampering risk in `THREAT-MODEL.md`: a host-compromised
> attacker today can rewrite the audit chain plus the matching signatures
> (private key on the same machine). With a copy in immutable storage, the
> attacker would also need to rewrite the storage tier's record.

## What ships

- `fastpace replicate to-file --dest <path>` — copy to a local / SMB / NFS path
- `fastpace replicate to-http --url <url> [--headers '{...}']` — POST to a
  configured ingest URL (use behind your SIEM)
- `fastpace replicate status` — show the manifest (offset, lines, history)
- `src/audit-replication.js` — the replicator contract + bundled file +
  HTTP replicators
- `assets/docs/audit-replication.md` — this file

## What does NOT ship

Cloud-specific replicators (S3, GCS, Azure Blob) are intentionally not bundled.
Each cloud has a distinct SDK surface, auth model, and credential conventions.
fastpace stays zero-dep so we provide the contract and let the org wire the
SDK. The contract is small enough that an S3 replicator is ~30 lines.

## The contract

```javascript
const replicator = {
  name: 'my-s3-replicator',
  config: { bucket: 'audit-logs', region: 'us-east-1' },
  async replicate(record) {
    // record: { cwd, full_path, basename, size, sha256, body_buffer }
    // Return: { ok: bool, location?: string, error?: string }
  },
  // Optional: enables fastpace replicate verify
  async read(historyEntry) {
    // historyEntry comes from the replication manifest's history[] array
    // and includes .location, .sha256, .ts, .lines
    // Return: Buffer with the original bytes that were replicated
  },
};
```

## Example: AWS S3 replicator

```javascript
'use strict';
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });

module.exports = function s3Replicator({ bucket, prefix = 'fastpace/audit/' }) {
  return {
    name: 's3',
    config: { bucket, prefix },
    async replicate(record) {
      const Key = prefix + record.basename;
      try {
        await s3.send(new PutObjectCommand({
          Bucket: bucket,
          Key,
          Body: record.body_buffer,
          ContentType: 'application/x-ndjson',
          // S3 Object Lock + WORM bucket settings recommended at the
          // bucket level so an attacker with valid creds can't delete.
          Metadata: { 'fastpace-sha256': record.sha256, 'fastpace-size': String(record.size) },
        }));
        return { ok: true, location: `s3://${bucket}/${Key}` };
      } catch (e) {
        return { ok: false, error: String(e.message) };
      }
    },
    async read(historyEntry) {
      const Key = (historyEntry.location || '').split(`s3://${bucket}/`)[1];
      const r = await s3.send(new GetObjectCommand({ Bucket: bucket, Key }));
      const chunks = [];
      for await (const c of r.Body) chunks.push(c);
      return Buffer.concat(chunks);
    },
  };
};
```

To use:

```javascript
const repl = require('@fastpace-ai/fp/src/audit-replication');
const s3Replicator = require('./my-s3-replicator')({ bucket: 'acme-fastpace-audit' });
await repl.replicate(process.cwd(), s3Replicator);
```

## Hardening the bucket

Replication moves the integrity tier from the developer machine to the
storage tier. Get value out of that move:

- **S3:** turn on Object Lock + a WORM retention policy. The bucket should
  refuse `DeleteObject` for the retention window.
- **GCS:** Bucket Lock + retention policy.
- **Azure Blob:** immutability policy with time-based retention.
- **Self-hosted:** WORM-mode object storage (MinIO with `mc retention`),
  or filesystem mounted with `chattr +a` (append-only).

The replication manifest at `fastpace/replication/manifest.json` records
each chunk's sha256. `fastpace replicate verify` (when the replicator
provides `read()`) reads the chunks back and compares against the live
audit log up to the last-replicated offset; drift is a tampering signal.

## Schedule

`fastpace replicate to-file/to-http` is idempotent — re-running picks up
from `last_offset`. Wire it on a cron / systemd timer / CI cadence:

```
*/5 * * * *  cd /path/to/repo && /usr/local/bin/fastpace replicate to-http \
   --url https://siem.example.com/ingest/fastpace \
   --headers '{"Authorization":"Bearer XXXX"}' \
   >> /var/log/fastpace-replication.log 2>&1
```

Or have the F3.10 webhook event bus fire `audit.broken` to your incident
response channel if `fastpace verify` ever fails — that's the canary that
something tried to rewrite history.

## Privacy

The audit log carries `sha256` digests of prompts/responses, never the
plaintext. Replicating the log to storage doesn't leak content; it only
extends the integrity perimeter.
