Audit replication
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 pathfastpace 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 replicatorsassets/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
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
'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:
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
DeleteObjectfor 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 withchattr +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.