ARCHITECTURE.md - Full component map with ASCII diagram - Complete session lifecycle (init + heartbeat + failure path) - Cryptographic protocol spec (hash chain formula, canonical JSON) - Stack machine instruction set table with stack effects - Behavioral validation thresholds - SQLite schema, threat model summary, module reference DEPLOYMENT.md - Build instructions (WASM + server + convenience script) - native binary, systemd (with hardened sandbox notes), Docker - nginx, Nginx Proxy Manager, and HAProxy reverse proxy configs - Integration options (sidecar vs proxy-only) - Full configuration table with all constants - Observability (RUST_LOG levels), health check, security checklist API.md - Full /init and /hb request/response schemas with field tables - Canonical signing payload specification - Complete validation rules table (all 13 rejection conditions) - Hash chain byte-level specification - WASM exported function reference THREAT_MODEL.md - Four attacker profiles (script kiddie → sophisticated adversary) - Eight attack vectors with mitigations (replay, forgery, hijack, DoS…) - Explicit out-of-scope limitations - Operational security notes (CORS, TLS, log level, SQLite)
347 lines
8.4 KiB
Markdown
347 lines
8.4 KiB
Markdown
# ChronoSeal — Deployment Guide
|
||
|
||
## Prerequisites
|
||
|
||
| Tool | Minimum version | Purpose |
|
||
|---|---|---|
|
||
| Rust | 1.87 stable | Server + WASM compilation |
|
||
| wasm-pack | 0.13 | WASM build and packaging |
|
||
| Docker + Compose | 24 / 2.x | Container deployment |
|
||
| nginx / NPM / HAProxy | any | TLS termination, reverse proxy |
|
||
|
||
Install Rust: https://rustup.rs
|
||
Install wasm-pack: `cargo install wasm-pack`
|
||
|
||
---
|
||
|
||
## Build
|
||
|
||
### 1. Build the WASM module
|
||
|
||
```bash
|
||
wasm-pack build wasm --target web --release
|
||
mv wasm/pkg frontend/pkg
|
||
```
|
||
|
||
This produces `frontend/pkg/antibot_wasm.js` and `frontend/pkg/antibot_wasm_bg.wasm`,
|
||
which are loaded by `frontend/main.js` at runtime.
|
||
|
||
### 2. Build the server
|
||
|
||
```bash
|
||
cargo build -p server --release
|
||
```
|
||
|
||
Binary output: `target/release/server`
|
||
|
||
### 3. Build both (convenience script)
|
||
|
||
```bash
|
||
bash scripts/build.sh
|
||
```
|
||
|
||
---
|
||
|
||
## Running
|
||
|
||
### Development
|
||
|
||
```bash
|
||
bash scripts/dev.sh
|
||
```
|
||
|
||
Runs the server with `cargo run --release`. The server serves the `frontend/`
|
||
directory statically at `/` via tower-http `ServeDir`.
|
||
|
||
Open `http://localhost:3000` in a browser. Open DevTools console — heartbeats
|
||
should appear every 12–25 seconds. No visible UI is rendered; the protection
|
||
is entirely silent.
|
||
|
||
### Production (native binary)
|
||
|
||
```bash
|
||
cargo build -p server --release
|
||
sudo cp target/release/server /usr/local/bin/chronoseal
|
||
```
|
||
|
||
Set environment variables before running:
|
||
|
||
```bash
|
||
export RUST_LOG=info # or warn for quieter output
|
||
chronoseal
|
||
```
|
||
|
||
The server binds to `0.0.0.0:3000` by default. Place behind a reverse proxy
|
||
for TLS — do not expose port 3000 directly.
|
||
|
||
---
|
||
|
||
## systemd
|
||
|
||
### Service file
|
||
|
||
The provided `chronoseal.service` includes hardened systemd sandboxing:
|
||
|
||
```
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ProtectKernelTunables=true
|
||
ProtectKernelModules=true
|
||
ProtectControlGroups=true
|
||
MemoryDenyWriteExecute=true
|
||
RestrictRealtime=true
|
||
RestrictSUIDSGID=true
|
||
LockPersonality=true
|
||
SystemCallArchitectures=native
|
||
```
|
||
|
||
### Install
|
||
|
||
```bash
|
||
# Create a dedicated system user
|
||
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chronoseal
|
||
|
||
# Install binary and frontend
|
||
sudo cp target/release/server /usr/local/bin/chronoseal
|
||
sudo mkdir -p /opt/chronoseal/frontend
|
||
sudo cp -r frontend/ /opt/chronoseal/frontend/
|
||
sudo chown -R chronoseal:chronoseal /opt/chronoseal
|
||
|
||
# Install and enable service
|
||
sudo cp chronoseal.service /etc/systemd/system/
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now chronoseal
|
||
```
|
||
|
||
### Verify
|
||
|
||
```bash
|
||
sudo systemctl status chronoseal
|
||
journalctl -u chronoseal -f
|
||
```
|
||
|
||
---
|
||
|
||
## Docker
|
||
|
||
### Build and run
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
### docker-compose.yml overview
|
||
|
||
```yaml
|
||
services:
|
||
chronoseal:
|
||
build: .
|
||
restart: unless-stopped
|
||
ports:
|
||
- "3000:3000"
|
||
environment:
|
||
RUST_LOG: info
|
||
tmpfs:
|
||
- /tmp
|
||
```
|
||
|
||
The `tmpfs` mount ensures the in-memory SQLite database is never written to
|
||
disk, even if Docker's storage driver were to flush the container filesystem.
|
||
|
||
### Dockerfile stages
|
||
|
||
The Dockerfile uses a two-stage build:
|
||
|
||
1. `rust:1.87-bookworm` — compiles the server binary
|
||
2. `debian:bookworm-slim` — minimal runtime image with only `ca-certificates`
|
||
|
||
The WASM module and frontend must be built separately (wasm-pack requires a
|
||
browser toolchain not present in the server image) and mounted or copied into
|
||
the container at `/opt/chronoseal/frontend/`.
|
||
|
||
```bash
|
||
# Build WASM first
|
||
wasm-pack build wasm --target web --release
|
||
mv wasm/pkg frontend/pkg
|
||
|
||
# Then build and run the container
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Or mount the pre-built frontend as a volume:
|
||
|
||
```yaml
|
||
volumes:
|
||
- ./frontend:/opt/chronoseal/frontend:ro
|
||
```
|
||
|
||
---
|
||
|
||
## Reverse Proxy
|
||
|
||
ChronoSeal must be served over HTTPS. The heartbeat payload contains a
|
||
timestamp; if traffic is observable in plaintext, timing attacks become
|
||
easier. TLS 1.3 is strongly recommended.
|
||
|
||
### nginx
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name your.domain.com;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
|
||
ssl_protocols TLSv1.3;
|
||
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||
|
||
# Tight timeouts — heartbeat interval is 12–25s
|
||
proxy_read_timeout 35s;
|
||
proxy_send_timeout 10s;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
}
|
||
|
||
server {
|
||
listen 80;
|
||
server_name your.domain.com;
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
```
|
||
|
||
### Nginx Proxy Manager
|
||
|
||
1. Add a new Proxy Host pointing to `http://chronoseal:3000`
|
||
2. Enable SSL, Request Let's Encrypt certificate
|
||
3. Enable HTTP/2, Force SSL
|
||
4. Under Advanced, add:
|
||
```
|
||
proxy_read_timeout 35s;
|
||
proxy_send_timeout 10s;
|
||
```
|
||
|
||
### HAProxy
|
||
|
||
```haproxy
|
||
frontend https_front
|
||
bind *:443 ssl crt /etc/haproxy/certs/your.domain.pem alpn h2,http/1.1
|
||
default_backend chronoseal_back
|
||
|
||
backend chronoseal_back
|
||
server chronoseal 127.0.0.1:3000 check
|
||
timeout connect 5s
|
||
timeout server 35s
|
||
```
|
||
|
||
---
|
||
|
||
## Integration into an Existing Site
|
||
|
||
ChronoSeal is designed to run as a sidecar — its `/init` and `/hb` endpoints
|
||
can be proxied from any existing web server. The frontend assets (`pkg/`) need
|
||
to be served from the same origin as the protected page (or CORS must be
|
||
configured).
|
||
|
||
### Option A — Serve everything from ChronoSeal
|
||
|
||
ChronoSeal serves `frontend/` statically. Put your protected HTML inside
|
||
`frontend/` and let ChronoSeal serve it directly.
|
||
|
||
### Option B — Proxy only the API endpoints
|
||
|
||
Keep your existing server. Proxy `/init` and `/hb` to ChronoSeal, and serve
|
||
the WASM and JS assets from your CDN or existing static file server.
|
||
|
||
```nginx
|
||
# On your existing server:
|
||
location ~ ^/(init|hb)$ {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
}
|
||
```
|
||
|
||
Add to your protected pages:
|
||
|
||
```html
|
||
<script type="module" src="/pkg/antibot_wasm.js"></script>
|
||
<script type="module" src="/main.js"></script>
|
||
```
|
||
|
||
---
|
||
|
||
## Configuration
|
||
|
||
All parameters are in `shared/src/constants.rs`. Recompile after changes.
|
||
|
||
| Constant | Default | Notes |
|
||
|---|---|---|
|
||
| `SESSION_ID_LEN` | 32 bytes | 256-bit entropy — do not reduce |
|
||
| `SALT_LEN` | 16 bytes | Per-heartbeat salt |
|
||
| `HEARTBEAT_MIN_INTERVAL_MS` | 12 000 ms | Increase to reduce server load |
|
||
| `HEARTBEAT_MAX_INTERVAL_MS` | 25 000 ms | Jitter upper bound |
|
||
| `EXPIRATION_MINUTES` | 30 min | Session TTL after last heartbeat |
|
||
| `RATE_LIMIT_COUNT` | 5 | Max heartbeats per window per session |
|
||
| `RATE_LIMIT_WINDOW_SECS` | 10 s | Rate limit window |
|
||
| `MAX_TIMESTAMP_DRIFT_MS` | 30 000 ms | Anti-replay window; account for NTP skew |
|
||
| `MIN_MOUSE_TOTAL_DIST` | 10.0 px | Lower for low-activity pages |
|
||
| `MAX_MOUSE_AVG_SPEED` | 2.0 px/ms | Raise if legitimate users are rejected |
|
||
| `MIN_PAUSE_COUNT` | 1 | Minimum natural pause events |
|
||
|
||
---
|
||
|
||
## Observability
|
||
|
||
ChronoSeal uses `tracing` with `tracing-subscriber`. Log levels:
|
||
|
||
| Level | Events |
|
||
|---|---|
|
||
| `INFO` | Server start, request method + path + status |
|
||
| `WARN` | Heartbeat validation failures (with session ID and reason) |
|
||
| `DEBUG` | Rate limit hits |
|
||
|
||
```bash
|
||
RUST_LOG=info chronoseal # production
|
||
RUST_LOG=debug chronoseal # development
|
||
RUST_LOG=warn chronoseal # minimal output
|
||
```
|
||
|
||
Log format is plain text to stdout. Pipe to `journald`, `fluentd`, or any
|
||
log aggregator via stdout capture.
|
||
|
||
---
|
||
|
||
## Health Check
|
||
|
||
The server has no dedicated `/health` endpoint. Use a TCP check on port 3000,
|
||
or a lightweight HTTP check on `GET /` (which serves `index.html`).
|
||
|
||
```bash
|
||
# Docker health check (add to docker-compose.yml if needed)
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-sf", "http://localhost:3000/"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 3
|
||
```
|
||
|
||
---
|
||
|
||
## Security Checklist
|
||
|
||
- [ ] TLS 1.3 enabled, TLS 1.0/1.1 disabled
|
||
- [ ] HTTP/2 enabled
|
||
- [ ] Port 3000 not exposed to the public internet (only via reverse proxy)
|
||
- [ ] `RUST_LOG=warn` or `info` in production (not `debug` — session IDs appear in logs)
|
||
- [ ] systemd service running as `chronoseal` user with hardened sandbox
|
||
- [ ] `MemoryDenyWriteExecute=true` in service file (prevents JIT in process)
|
||
- [ ] CORS `CorsLayer::permissive()` replaced with origin-restricted policy for production
|
||
- [ ] Frontend assets served over the same HTTPS origin as protected pages
|