DocsGuidesSecurity

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

Understanding TopGun’s trust boundaries and security pipeline.

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
LayerWhat it does
JWT AuthenticationClients must authenticate with a JWT containing a standard sub claim. No operations are permitted before authentication.
Map-level ACLPer-connection, per-map read/write permissions. Simple allow/deny rules control which maps a client can access.
HLC SanitizationThe server replaces client-provided HLC timestamps with server-generated ones, preventing future-timestamp attacks that would “win” all LWW conflicts.
Value Size LimitsThe server enforces maximum value size per write to prevent abuse.
RBACRole-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 OriginPasses Through Security Pipeline?
ClientYes — all checks apply
ForwardedNo — cluster-forwarded writes, bypass WriteValidator
BackupNo — backup restore operations, bypass WriteValidator
WanNo — WAN replication, bypass WriteValidator
SystemNo — 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 including NetworkConfig and TlsConfig, see the Server API reference.

Production Requirement

Always enable TLS in production environments. Without TLS, all data including authentication tokens is transmitted in plaintext and can be intercepted by attackers.

TLS/WSS Configuration

Enable HTTPS and secure WebSocket (WSS) for client connections.

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
The 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.
Server Configuration (planned)
# 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:

Client
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()
});

Often Already Configured

If you’re using a deployment platform or reverse proxy, TLS is likely already handled for you. In this case, you don’t need to configure certificates in TopGun server.

Many deployment platforms and reverse proxies automatically provide TLS termination:

Platform / ProxyTLS HandlingTopGun Server Config
DokployTraefik terminates TLSUse ws:// internally, clients connect via wss://
Vercel / NetlifyAutomatic HTTPSNot applicable (edge functions)
Railway / RenderAutomatic TLSUse ws:// on server port
Kubernetes IngressTLS at ingressConfigure TLS on Ingress, not TopGun
CloudflareEdge TLSUse ws:// to origin
Traefik / nginx / CaddyProxy terminates TLSUse ws:// backend

How it works:

Client (wss://) → Reverse Proxy (TLS termination) → TopGun Server (ws://)
  1. Client connects to wss://your-domain.com
  2. Reverse proxy handles TLS/SSL certificates
  3. Proxy forwards to TopGun via unencrypted ws://localhost:8080
  4. 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

Planned for v3.0

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.

Mutual TLS (mTLS) ensures that only authorized nodes can join the cluster.

With mTLS enabled:

  • Each node presents its certificate when connecting to peers
  • Nodes verify peer certificates against the CA
  • Unauthorized nodes are rejected
With mTLS
  • Encrypted inter-node traffic
  • Verified node identity
  • Protection against rogue nodes
  • Zero-trust network security
Without mTLS
  • Plaintext inter-node traffic
  • Any node can join cluster
  • Vulnerable to MITM attacks
  • Not suitable for production

Server Environment Variables

Environment variables that are implemented and work today in the test-server binary and programmatic embedding.
VariableRequiredDefaultDescription
PORTNo8090HTTP/WebSocket listen port
DATABASE_URLYesPostgreSQL connection string
JWT_SECRETYesHMAC secret or RSA public key (PEM) for JWT verification
TOPGUN_ADMIN_PASSWORDYes (for admin)Password for admin API login
TOPGUN_ADMIN_USERNAMENoadminUsername for admin API login
TOPGUN_ADMIN_DIRNoPath to admin SPA static files
TOPGUN_LOG_FORMATNohuman-readableSet to json for structured JSON logging

TLS Environment Variables (Planned)

Planned TLS configuration via environment variables for a future production binary.
Planned — not yet implemented

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.

VariableTypeDefaultDescription
TOPGUN_TLS_ENABLEDbooleanfalsePlanned — Enable TLS for client connections
TOPGUN_TLS_CERT_PATHstringPlanned — Path to certificate (PEM)
TOPGUN_TLS_KEY_PATHstringPlanned — Path to private key (PEM)
TOPGUN_TLS_CA_PATHstringPlanned — Path to CA certificate
TOPGUN_TLS_MIN_VERSIONenumTLSv1.2Planned — Minimum TLS version
TOPGUN_TLS_PASSPHRASEstringPlanned — Key passphrase (if encrypted)
TOPGUN_CLUSTER_TLS_ENABLEDbooleanfalsePlanned — Enable TLS for cluster
TOPGUN_CLUSTER_TLS_CERT_PATHstringPlanned — Cluster certificate path
TOPGUN_CLUSTER_TLS_KEY_PATHstringPlanned — Cluster key path
TOPGUN_CLUSTER_TLS_CA_PATHstringPlanned — CA for peer verification
TOPGUN_CLUSTER_MTLSbooleanfalsePlanned — Require client certificate (mTLS)
TOPGUN_CLUSTER_TLS_REJECT_UNAUTHORIZEDbooleantruePlanned — Verify peer certificates

Example: Planned Production Environment (TLS vars not yet implemented)

.env.production (planned — 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

Generate certificates for development and testing.

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

scripts/generate-certs.sh
#!/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:

terminal
chmod +x scripts/generate-certs.sh
./scripts/generate-certs.sh

Kubernetes cert-manager Integration

Automate certificate management with cert-manager.

For Kubernetes deployments, use cert-manager to automatically provision and renew certificates:

k8s/cert-manager.yaml
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

terminal
# 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

Common TLS issues and solutions.

Useful OpenSSL Commands

terminal
# 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

ErrorCauseSolution
UNABLE_TO_VERIFY_LEAF_SIGNATUREMissing CA certificateAdd caCertPath configuration
CERT_HAS_EXPIREDCertificate expiredRenew the certificate
DEPTH_ZERO_SELF_SIGNED_CERTSelf-signed cert in productionUse CA-signed certificate
ERR_TLS_CERT_ALTNAME_INVALIDHostname mismatchAdd correct SAN to certificate
ENOENT on cert pathFile not foundCheck 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

For maximum security, TopGun recommends using modern cipher suites:

Recommended TLS 1.3 Ciphers
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256

For TLS 1.2 compatibility:

Recommended TLS 1.2 Ciphers
ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-RSA-AES256-GCM-SHA384
ECDHE-ECDSA-CHACHA20-POLY1305
ECDHE-RSA-CHACHA20-POLY1305

Client-Side Encryption (Data at Rest)

Encrypt local data stored in IndexedDB or other client-side storage.

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

Encrypted Storage
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:

Password-Derived Key
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:

Device-Bound Key
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

If the encryption key is lost, all encrypted data becomes irrecoverable. Consider implementing key backup strategies for password-derived keys, or accept data loss on device change for device-bound keys.

When to Use Client-Side Encryption

Use CaseRecommendation
Medical or financial dataRequired - regulatory compliance
Personal user dataRecommended - privacy protection
Multi-tenant applicationsRecommended - tenant isolation
Gaming or non-sensitive appsOptional - based on threat model

API Reference

For detailed API documentation, see the EncryptedStorageAdapter section in the Adapter API reference.