Security Guide
Secure your TopGun deployment with TLS encryption for all network communications.
This guide covers:
- Security model and threat boundary
- TLS/WSS configuration for client connections
- mTLS (mutual TLS) for cluster communication
- Environment variables reference
- Certificate generation and management
- Client-side encryption for data at rest
Server Deployment Model
topgun-server is a library crate. There is no standalone production binary today. The only binary included in the repository is test-server — intended for development and integration testing only.
For production, embed the server programmatically in your Rust application:
use topgun_server::{ServerBuilder, NetworkConfig};
let server = ServerBuilder::new()
.network(NetworkConfig { port: 8090, ..Default::default() })
.build()
.await?;
server.start().await?;
A standalone topgun-server production binary with full env-var configuration (including TLS) is planned but does not yet exist.
Security Model
Trust Boundary
Clients are untrusted. The server is authoritative. While TopGun’s “Client as Replica” architecture gives clients local CRDT replicas for zero-latency reads and writes, this does not mean clients have unrestricted access. The server enforces all security policies.
Security Pipeline
Every write passes through the security pipeline before reaching CRDT merge:
Client write --> Auth check --> Map ACL check --> HLC sanitization --> CRDT merge --> Persist
| Layer | What it does |
|---|---|
| JWT Authentication | Clients must authenticate with a JWT containing a standard sub claim. No operations are permitted before authentication. |
| Map-level ACL | Per-connection, per-map read/write permissions. Simple allow/deny rules control which maps a client can access. |
| HLC Sanitization | The server replaces client-provided HLC timestamps with server-generated ones, preventing future-timestamp attacks that would “win” all LWW conflicts. |
| Value Size Limits | The server enforces maximum value size per write to prevent abuse. |
| RBAC | Role-based access control allows defining roles with specific permissions. Roles are assigned to connections and checked against map ACLs. |
Trusted-Origin Bypass
Operations arriving from trusted internal origins skip the WriteValidator security checks entirely. This is by design for internal server-to-server operations (replication, backup restore, cluster forwarding) that must be applied without re-validation.
| Caller Origin | Passes Through Security Pipeline? |
|---|---|
Client | Yes — all checks apply |
Forwarded | No — cluster-forwarded writes, bypass WriteValidator |
Backup | No — backup restore operations, bypass WriteValidator |
Wan | No — WAN replication, bypass WriteValidator |
System | No — internal system operations, bypass WriteValidator |
External client connections always carry CallerOrigin::Client and pass through all security checks. Trusted origins are only set by the server itself for internal operations.
Transport Security
- TLS encrypts all client-to-server traffic (HTTPS + WSS)
- mTLS (mutual TLS) for cluster communication is planned for v3.0; cluster traffic is currently plaintext TCP
- See sections below for configuration details
Server API Reference
For the full Rust server embed API includingNetworkConfig and TlsConfig, see the Server API reference.Production Requirement
TLS/WSS Configuration
When TLS is enabled, TopGun automatically:
- Creates an HTTPS server instead of HTTP
- Upgrades WebSocket connections to WSS
- Logs a warning if TLS is disabled in production
topgun-server production binary and TLS environment variables shown below are planned for a future release. Currently, use cargo run —bin test-server for testing, or embed the server programmatically with TlsConfig.# PLANNED — these TLS environment variables do not exist yet.
# A standalone topgun-server production binary with env-var TLS config is planned.
# For TLS today, configure it programmatically via TlsConfig when embedding the server.
#
# PORT=443 \
# DATABASE_URL=postgres://user:pass@localhost/topgun \
# TOPGUN_TLS_ENABLED=true \
# TOPGUN_TLS_CERT_PATH=/etc/topgun/tls/server.crt \
# TOPGUN_TLS_KEY_PATH=/etc/topgun/tls/server.key \
# TOPGUN_TLS_MIN_VERSION=TLSv1.3 \
# TOPGUN_CLUSTER_TLS_ENABLED=true \ # Planned — v3.0
# TOPGUN_CLUSTER_TLS_CERT_PATH=/etc/topgun/tls/cluster.crt \
# TOPGUN_CLUSTER_TLS_KEY_PATH=/etc/topgun/tls/cluster.key \
# TOPGUN_CLUSTER_TLS_CA_PATH=/etc/topgun/tls/ca.crt \
# TOPGUN_CLUSTER_MTLS=true \
# topgun-server # <-- planned binary, not yet available Client Connection
Clients automatically use secure connections when connecting to a wss:// URL:
import { TopGunClient } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';
// Production: Use wss:// (WebSocket Secure)
const client = new TopGunClient({
serverUrl: 'wss://topgun.example.com',
storage: new IDBAdapter()
});
// Set the JWT after construction — the constructor has no token field
client.setAuthToken('your-jwt-token');
// Development only: ws:// (unencrypted)
const devClient = new TopGunClient({
serverUrl: 'ws://localhost:8080',
storage: new IDBAdapter()
}); TLS via Reverse Proxy (Recommended)
Often Already Configured
Many deployment platforms and reverse proxies automatically provide TLS termination:
| Platform / Proxy | TLS Handling | TopGun Server Config |
|---|---|---|
| Dokploy | Traefik terminates TLS | Use ws:// internally, clients connect via wss:// |
| Vercel / Netlify | Automatic HTTPS | Not applicable (edge functions) |
| Railway / Render | Automatic TLS | Use ws:// on server port |
| Kubernetes Ingress | TLS at ingress | Configure TLS on Ingress, not TopGun |
| Cloudflare | Edge TLS | Use ws:// to origin |
| Traefik / nginx / Caddy | Proxy terminates TLS | Use ws:// backend |
How it works:
Client (wss://) → Reverse Proxy (TLS termination) → TopGun Server (ws://)
- Client connects to
wss://your-domain.com - Reverse proxy handles TLS/SSL certificates
- Proxy forwards to TopGun via unencrypted
ws://localhost:8080 - All external traffic is encrypted; internal traffic stays on localhost
When to configure TLS directly in TopGun:
- Direct server exposure without reverse proxy
- End-to-end encryption requirements
- mTLS for cluster communication
mTLS for Cluster Communication
Cluster communication currently uses plaintext TCP. ClusterConfig has no TLS fields. mTLS for cluster nodes is on the roadmap for v3.0. See TODO-164 for progress.
With mTLS enabled:
- Each node presents its certificate when connecting to peers
- Nodes verify peer certificates against the CA
- Unauthorized nodes are rejected
- Encrypted inter-node traffic
- Verified node identity
- Protection against rogue nodes
- Zero-trust network security
- Plaintext inter-node traffic
- Any node can join cluster
- Vulnerable to MITM attacks
- Not suitable for production
Server Environment Variables
test-server binary and programmatic embedding.| Variable | Required | Default | Description |
|---|---|---|---|
PORT | No | 8090 | HTTP/WebSocket listen port |
DATABASE_URL | Yes | — | PostgreSQL connection string |
JWT_SECRET | Yes | — | HMAC secret or RSA public key (PEM) for JWT verification |
TOPGUN_ADMIN_PASSWORD | Yes (for admin) | — | Password for admin API login |
TOPGUN_ADMIN_USERNAME | No | admin | Username for admin API login |
TOPGUN_ADMIN_DIR | No | — | Path to admin SPA static files |
TOPGUN_LOG_FORMAT | No | human-readable | Set to json for structured JSON logging |
TLS Environment Variables (Planned)
The TLS environment variables below are planned for a future production binary. They do not exist in the current server. Currently, TLS is configured programmatically via the TlsConfig struct when embedding the server in a Rust application. See the Server API reference for the programmatic API.
| Variable | Type | Default | Description |
|---|---|---|---|
TOPGUN_TLS_ENABLED | boolean | false | Planned — Enable TLS for client connections |
TOPGUN_TLS_CERT_PATH | string | — | Planned — Path to certificate (PEM) |
TOPGUN_TLS_KEY_PATH | string | — | Planned — Path to private key (PEM) |
TOPGUN_TLS_CA_PATH | string | — | Planned — Path to CA certificate |
TOPGUN_TLS_MIN_VERSION | enum | TLSv1.2 | Planned — Minimum TLS version |
TOPGUN_TLS_PASSPHRASE | string | — | Planned — Key passphrase (if encrypted) |
TOPGUN_CLUSTER_TLS_ENABLED | boolean | false | Planned — Enable TLS for cluster |
TOPGUN_CLUSTER_TLS_CERT_PATH | string | — | Planned — Cluster certificate path |
TOPGUN_CLUSTER_TLS_KEY_PATH | string | — | Planned — Cluster key path |
TOPGUN_CLUSTER_TLS_CA_PATH | string | — | Planned — CA for peer verification |
TOPGUN_CLUSTER_MTLS | boolean | false | Planned — Require client certificate (mTLS) |
TOPGUN_CLUSTER_TLS_REJECT_UNAUTHORIZED | boolean | true | Planned — Verify peer certificates |
Example: Planned Production Environment (TLS vars not yet implemented)
# Working today:
PORT=8090
DATABASE_URL=postgres://user:pass@localhost/topgun
JWT_SECRET=your-secret-key
TOPGUN_ADMIN_PASSWORD=your-admin-password
# Planned (not yet implemented — use TlsConfig struct for programmatic TLS):
# TOPGUN_PORT=443
# TOPGUN_TLS_ENABLED=true
# TOPGUN_TLS_CERT_PATH=/etc/topgun/tls/server.crt
# TOPGUN_TLS_KEY_PATH=/etc/topgun/tls/server.key
# TOPGUN_TLS_MIN_VERSION=TLSv1.3
# Planned — cluster mTLS (v3.0):
# TOPGUN_CLUSTER_PORT=9443
# TOPGUN_CLUSTER_TLS_ENABLED=true
# TOPGUN_CLUSTER_TLS_CERT_PATH=/etc/topgun/tls/cluster.crt
# TOPGUN_CLUSTER_TLS_KEY_PATH=/etc/topgun/tls/cluster.key
# TOPGUN_CLUSTER_TLS_CA_PATH=/etc/topgun/tls/ca.crt
# TOPGUN_CLUSTER_MTLS=true Certificate Generation
Production Certificates
For production, use certificates from a trusted CA like Let’s Encrypt or your organization’s internal CA. Self-signed certificates are suitable only for development and testing.Development Certificates Script
#!/bin/bash
# Generate self-signed certificates for testing
# For production, use Let's Encrypt or your organization's CA
# Create output directory
mkdir -p certs && cd certs
# 1. Generate CA (Certificate Authority)
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 365 -key ca.key \
-out ca.crt -subj "/CN=TopGun CA"
# 2. Generate Server Certificate
openssl genrsa -out server.key 2048
openssl req -new -key server.key \
-out server.csr -subj "/CN=topgun.example.com"
# Create SAN (Subject Alternative Name) config
cat > san.cnf << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = topgun.example.com
DNS.2 = *.topgun.example.com
DNS.3 = localhost
IP.1 = 127.0.0.1
EOF
# Sign with CA
openssl x509 -req -days 365 -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -extfile san.cnf -extensions v3_req
# 3. Generate Cluster Node Certificates (for mTLS)
for i in 1 2 3; do
openssl genrsa -out node${i}.key 2048
openssl req -new -key node${i}.key \
-out node${i}.csr -subj "/CN=topgun-node-${i}"
openssl x509 -req -days 365 -in node${i}.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out node${i}.crt
done
echo "Certificates generated in ./certs/" Run the script:
chmod +x scripts/generate-certs.sh
./scripts/generate-certs.sh Kubernetes cert-manager Integration
For Kubernetes deployments, use cert-manager to automatically provision and renew certificates:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: topgun-tls
namespace: topgun
spec:
secretName: topgun-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- topgun.example.com
- "*.topgun-cluster.svc.cluster.local" Installation
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
# Apply configuration
kubectl apply -f k8s/cert-manager.yaml Troubleshooting
Useful OpenSSL Commands
# Verify certificate
openssl x509 -in server.crt -text -noout
# Check expiry date
openssl x509 -enddate -noout -in server.crt
# Verify key matches certificate
openssl x509 -noout -modulus -in server.crt | openssl md5
openssl rsa -noout -modulus -in server.key | openssl md5
# Both should output the same hash
# Test TLS connection
openssl s_client -connect localhost:443 -tls1_3
# View full certificate chain
openssl s_client -showcerts -connect topgun.example.com:443 Common Errors
| Error | Cause | Solution |
|---|---|---|
UNABLE_TO_VERIFY_LEAF_SIGNATURE | Missing CA certificate | Add caCertPath configuration |
CERT_HAS_EXPIRED | Certificate expired | Renew the certificate |
DEPTH_ZERO_SELF_SIGNED_CERT | Self-signed cert in production | Use CA-signed certificate |
ERR_TLS_CERT_ALTNAME_INVALID | Hostname mismatch | Add correct SAN to certificate |
ENOENT on cert path | File not found | Check path and file permissions |
Security Best Practices
Do
- Use TLS 1.3 when possible
- Enable mTLS for cluster
- Rotate certificates regularly
- Store keys in secrets/vault
- Monitor certificate expiry
Don’t
- Disable TLS in production
- Use self-signed certs in prod
- Commit certificates to git
- Use weak cipher suites
- Ignore certificate warnings
Recommended Cipher Suites
For maximum security, TopGun recommends using modern cipher suites:
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256 For TLS 1.2 compatibility:
ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-RSA-AES256-GCM-SHA384
ECDHE-ECDSA-CHACHA20-POLY1305
ECDHE-RSA-CHACHA20-POLY1305 Client-Side Encryption (Data at Rest)
While TLS protects data in transit, client-side encryption protects data at rest on user devices. The EncryptedStorageAdapter wraps any storage adapter (like IndexedDB) with AES-256-GCM encryption using the Web Crypto API.
What It Protects
Protected Against
- Browser DevTools inspection
- Malicious browser extensions
- Physical device access (disk dumps)
- IndexedDB data export
Not Protected Against
- XSS attacks (if attacker has JS execution)
- Compromised application code
- Key theft from memory
Basic Usage
import { TopGunClient, EncryptedStorageAdapter } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';
// Get encryption key (see key management strategies below)
const encryptionKey = await getEncryptionKey(); // or getDeviceKey()
// Create base adapter and wrap with encryption
const baseAdapter = new IDBAdapter();
const encryptedAdapter = new EncryptedStorageAdapter(baseAdapter, encryptionKey);
const client = new TopGunClient({
serverUrl: 'wss://topgun.example.com',
storage: encryptedAdapter
}); Key Management Strategies
Choose a key management strategy based on your security requirements:
Password-Derived Key (PBKDF2)
Best for user-authenticated encryption where the key is derived from a user’s password:
async function getEncryptionKey(password: string): Promise<CryptoKey> {
const encoder = new TextEncoder();
// Get or create salt
let saltString = localStorage.getItem('encryption_salt');
let salt: Uint8Array;
if (saltString) {
salt = Uint8Array.from(atob(saltString), c => c.charCodeAt(0));
} else {
salt = crypto.getRandomValues(new Uint8Array(16));
localStorage.setItem('encryption_salt', btoa(String.fromCharCode(...salt)));
}
// Derive key from password
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
} Device-Bound Random Key
Best for protecting data from disk dumps without requiring a user password. Data is tied to the device:
async function getDeviceKey(): Promise<CryptoKey> {
const storedKey = localStorage.getItem('topgun_device_key');
if (storedKey) {
// Import existing key
const keyData = Uint8Array.from(atob(storedKey), c => c.charCodeAt(0));
return crypto.subtle.importKey(
'raw',
keyData,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Generate new key
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true, // extractable
['encrypt', 'decrypt']
);
// Store for future use
const exported = await crypto.subtle.exportKey('raw', key);
localStorage.setItem(
'topgun_device_key',
btoa(String.fromCharCode(...new Uint8Array(exported)))
);
return key;
} Key Management Warning
When to Use Client-Side Encryption
| Use Case | Recommendation |
|---|---|
| Medical or financial data | Required - regulatory compliance |
| Personal user data | Recommended - privacy protection |
| Multi-tenant applications | Recommended - tenant isolation |
| Gaming or non-sensitive apps | Optional - based on threat model |