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
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. |
Transport Security
- TLS encrypts all client-to-server traffic (HTTPS + WSS)
- mTLS (mutual TLS) secures cluster-to-cluster communication, ensuring only authorized nodes can join
- 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.# Start the TopGun server with TLS enabled
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 \
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 Client Connection
Clients automatically use secure connections when connecting to a wss:// URL:
import { TopGunClient } from '@topgunbuild/client';
// Production: Use wss:// (WebSocket Secure)
const client = new TopGunClient({
serverUrl: 'wss://topgun.example.com',
token: 'your-jwt-token'
});
// Development only: ws:// (unencrypted)
const devClient = new TopGunClient({
serverUrl: 'ws://localhost:8080'
}); 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
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
Environment Variables Reference
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 | Enable TLS for client connections |
TOPGUN_TLS_CERT_PATH | string | - | Path to certificate (PEM) |
TOPGUN_TLS_KEY_PATH | string | - | Path to private key (PEM) |
TOPGUN_TLS_CA_PATH | string | - | Path to CA certificate |
TOPGUN_TLS_MIN_VERSION | enum | TLSv1.2 | Minimum TLS version (TLSv1.2 or TLSv1.3) |
TOPGUN_TLS_PASSPHRASE | string | - | Key passphrase (if encrypted) |
TOPGUN_CLUSTER_TLS_ENABLED | boolean | false | Enable TLS for cluster |
TOPGUN_CLUSTER_TLS_CERT_PATH | string | - | Cluster certificate path |
TOPGUN_CLUSTER_TLS_KEY_PATH | string | - | Cluster key path |
TOPGUN_CLUSTER_TLS_CA_PATH | string | - | CA for peer verification |
TOPGUN_CLUSTER_MTLS | boolean | false | Require client certificate (mTLS) |
TOPGUN_CLUSTER_TLS_REJECT_UNAUTHORIZED | boolean | true | Verify peer certificates |
Example: Production Environment
# Client 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
# Cluster mTLS
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: admin@example.com
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 |