← Back to Installation

System Architecture

Three Docker containers. One exposed port. Here's exactly what's running inside your server after docker compose up.

architecture — docker network
┌──────────────────────────────────────────────────────────────┐
│ Your Browser / API Clients │
└───────────────────────────┬──────────────────────────────────┘
│ HTTPS (you provision TLS upstream)
│ Port 80 — only host-exposed port
┌───────────────────────────▼──────────────────────────────────┐
tampertrail-client (Nginx)
│ │
│ + Serves React dashboard (Single Page App) │
│ + Rate limits: login 5/min · log 100/min · API 200/min │
│ + Security headers: CSP · HSTS · X-Frame-Options · nosniff │
│ + Reverse proxies /v1/* → tampertrail-server:8000 │
│ + Blocks /docs /redoc /openapi.json from public access │
└───────────────────────────┬──────────────────────────────────┘
│ Internal Docker bridge (port 8000)
┌───────────────────────────▼──────────────────────────────────┐
tampertrail-server (FastAPI + Python 3.12)
│ │
│ + WAL ingestion: writes to queue.wal before DB commit │
│ + SHA-256 hash chaining on every write │
│ + Fernet AES-128-CBC metadata encryption (in memory) │
│ + JWT cookies (HTTPOnly + SameSite=Lax) │
│ + Argon2id password + API key hashing │
│ + Multi-tenant RLS enforcement (app layer) │
│ + Async micro-batch writer + chain verification engine │
│ + Single Uvicorn worker (intentional — no state split) │
└───────────────────────────┬──────────────────────────────────┘
│ Internal Docker bridge (port 5432)
┌───────────────────────────▼──────────────────────────────────┐
tampertrail-db (PostgreSQL 16)
│ │
│ + audit_logs: monthly range-partitioned table │
│ + metadata: BYTEA — Fernet ciphertext, never plaintext │
│ + tags: JSONB with GIN index for fast filtering │
│ + Row-Level Security on all sensitive tables (FORCE RLS) │
│ + chain_checkpoints: monthly tamper-evident snapshots │
│ + Advisory locks for single-writer hash chain integrity │
└──────────────────────────────────────────────────────────────┘

Docker Volumes

🗄️postgres_data

PostgreSQL data directory. Your encrypted audit logs live here. Persists across container restarts and updates.

⚙️server_data

App config (config.json), WAL crash-recovery files (queue.wal). Contains your Fernet encryption key — back this up.

🔑secrets

db_password file. Shared between tampertrail-server and tampertrail-db. Never exposed outside Docker network.

Key Design Decisions

Single Uvicorn worker

Intentional. Hash chain integrity requires a strict sequential write order. Multiple workers would need distributed locking — adding latency with no throughput benefit, since the bottleneck is disk I/O, not CPU.

WAL before database

Every incoming log is written to queue.wal on disk before PostgreSQL receives it. If the process crashes mid-batch, the WAL is replayed on restart. Zero data loss by design — same pattern PostgreSQL itself uses.

Encryption in application memory

The metadata field is Fernet-encrypted before the database driver is ever called. PostgreSQL never sees the plaintext — even with direct database read access, an attacker sees only binary ciphertext.

Nginx as the only exposed container

FastAPI and PostgreSQL bind exclusively to the internal Docker bridge network. They are unreachable from outside the host machine without explicitly publishing their ports — which TamperTrail's compose file does not do.

PostgreSQL Row-Level Security

FORCE ROW LEVEL SECURITY is enabled on audit_logs and chain_checkpoints. Even if the application-level tenant_id filter has a bug, the database returns zero rows instead of leaking cross-tenant data.

Read Full CTO WhitepaperView on GitHub← Back to Installation