Security Guide
Secure your TopGun deployment with TLS encryption for all network communications.
This guide covers:
- 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
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
import { ServerCoordinator } from '@topgunbuild/server';
const server = new ServerCoordinator({
port: 443,
clusterPort: 9443,
nodeId: 'prod-node-1',
// Client-facing TLS (HTTPS + WSS)
tls: {
enabled: true,
certPath: '/etc/topgun/tls/server.crt',
keyPath: '/etc/topgun/tls/server.key',
minVersion: 'TLSv1.3'
},
// Cluster mTLS
clusterTls: {
enabled: true,
certPath: '/etc/topgun/tls/cluster.crt',
keyPath: '/etc/topgun/tls/cluster.key',
caCertPath: '/etc/topgun/tls/ca.crt',
requireClientCert: true
}
}); 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
| 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: [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 |