Encryption
Encryption Overview
flow8 implements field-level encryption using NaCl SecretBox with Argon2/Scrypt key derivation. This ensures that sensitive data is encrypted at rest in the database, decrypted only when needed in memory, and never exposed in logs or audit trails.
Cryptographic Algorithm
NaCl SecretBox (XSalsa20-Poly1305)
flow8 uses the golang.org/x/crypto/nacl/secretbox package, which implements:
- Symmetric cipher: XSalsa20 (256-bit key, 192-bit nonce)
- Message authentication: Poly1305 (AEAD encryption)
- Constant-time verification: Protection against timing attacks
- Nonce format: Random 24 bytes per encryption
Why SecretBox?
- Battle-tested cryptographic library (libsodium port)
- AEAD (authenticated encryption): Prevents tampering
- Constant-time operations: Immune to timing attacks
- Simple API: Reduces implementation errors
Encryption Process
Input: plaintext string ↓Generate random nonce (24 bytes) ↓Derive encryption key from ENC_KEY_SECRET + ENV_KEY_SALT ↓secretbox.Seal(plaintext, &nonce, &key) ↓Output: nonce (24 bytes) + ciphertext (plaintext + 16-byte tag)Example encrypted field in MongoDB:
{ "_id": "link_123", "user_id": "user_456", "integration": "slack", "access_token": { "encrypted": true, "nonce": "a1b2c3d4e5f6g7h8i9j0k1l2m3", // 24 bytes, base64 "ciphertext": "m9n8o7p6q5r4s3t2u1v0w9x8y7z6..." // Variable length }}Key Derivation
Two key derivation options are supported:
Option 1: Argon2 (Recommended)
Input: ENC_KEY_SECRET (256-char hex) ENV_KEY_SALT (64-char hex) ↓argon2id.Key([]byte(ENC_KEY_SECRET), []byte(ENV_KEY_SALT), 32768, 8, 1, 32) ↓Output: 32-byte encryption keyArgon2 parameters:
- N = 32768 (memory cost, ~32 MB)
- R = 8 (rounds, CPU cost)
- P = 1 (parallelism)
- Key length = 32 bytes (256 bits)
- Hash type = Argon2id (hybrid resistant to GPU/side-channel attacks)
Derivation time: ~500-1000ms on modern CPU (prevents brute force)
Option 2: Scrypt (Legacy, for backwards compatibility)
Input: ENC_KEY_SECRET (256-char hex) ENV_KEY_SALT (64-char hex) ↓scrypt.Key([]byte(ENC_KEY_SECRET), []byte(ENV_KEY_SALT), 32768, 8, 1, 32) ↓Output: 32-byte encryption keyScrypt parameters: Same as Argon2 equivalent
Migration path: Argon2 is the default; Scrypt keys are marked for re-encryption.
Configuration
Environment Variables
# Required: 256-character hex string (88 bytes)ENC_KEY_SECRET=<generate with: openssl rand -hex 128>
# Required: 64-character hex string (32 bytes)ENV_KEY_SALT=<generate with: openssl rand -hex 32>
# Optional: Key derivation algorithm (default: "argon2id")ENC_KEY_ALGORITHM=argon2id # or "scrypt"
# Optional: Max encrypted field size (default: 1MB)ENC_FIELD_MAX_BYTES=1048576
# Optional: Audit trail field size limit (default: 4KB)AUDIT_LOG_FIELD_MAX_BYTES=4096
# Optional: Unit output size limit (default: 1MB)UNIT_OUTPUT_UI_MAX_BYTES=1048576Configuration File
encryption: key_algorithm: "argon2id" # or "scrypt" key_derivation_n: 32768 # Memory cost key_derivation_r: 8 # CPU rounds key_derivation_p: 1 # Parallelism field_max_bytes: 1048576 # 1 MB audit_field_max_bytes: 4096 # 4 KB redact_mode: "hash" # How to redact sensitive fields in logsEncrypted Fields
The following fields are automatically encrypted in the database:
User Credentials
| Collection | Field | Reason |
|---|---|---|
users | password_hash (bcrypt, not encrypted) | Password stored as bcrypt hash, not encrypted |
users | totp_secret (if 2FA enabled) | TOTP secret key |
Note: Passwords use bcrypt hashing, not encryption. They are one-way hashes.
OAuth2 Tokens
| Collection | Field | Reason |
|---|---|---|
db_links | access_token | OAuth2 access token (can be used to call APIs) |
db_links | refresh_token | OAuth2 refresh token (used to get new access token) |
db_links | webhook_signature_secret | Secret for validating webhook signatures |
API Keys & Credentials
| Collection | Field | Reason |
|---|---|---|
component_configs | config.api_key | API key for external service (OpenAI, AWS, etc.) |
component_configs | config.password | Database password for SQL connections |
component_configs | config.client_secret | OAuth2 client secret |
component_configs | config.access_key_id | AWS access key |
component_configs | config.secret_access_key | AWS secret key |
Connection Details
| Collection | Field | Reason |
|---|---|---|
component_configs | config.connection_string | Database URL with credentials |
component_configs | config.smtp_password | SMTP server password |
component_configs | config.http_auth_password | HTTP basic auth password |
Key-Value Store
| Scope | Condition | Encryption |
|---|---|---|
| Any | Key prefix: secret:* | Always encrypted |
| Any | Value type: string, length > UNIT_OUTPUT_UI_MAX_BYTES | Encrypted |
| Any | Sensitive key patterns: password, token, key, secret | Encrypted by heuristic |
Audit Logs
| Field | Encryption |
|---|---|
before_state (sensitive fields) | Redacted (hash or [REDACTED]) |
after_state (sensitive fields) | Redacted (hash or [REDACTED]) |
request_body | Sensitive fields redacted |
Redaction mode:
# In config.ymlaudit: redact_mode: "hash" # "hash" (show hash of value) or "redact" (show [REDACTED]) redact_patterns: - "password" - "api_key" - "token" - "secret"Encryption in Transit
flow8 does not handle TLS directly. Instead, deploy behind a reverse proxy that terminates TLS:
Client │ HTTPS/TLS 1.3 ▼Reverse Proxy (nginx, Caddy, AWS ALB) │ HTTP (internal network only) ▼flow8 (port 4454, unencrypted internally)Reverse proxy configuration (nginx example):
upstream flow8 { server localhost:4454;}
server { listen 443 ssl http2; server_name app.flow8.io;
ssl_certificate /etc/ssl/certs/flow8.crt; ssl_certificate_key /etc/ssl/private/flow8.key; ssl_protocols TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;
location / { proxy_pass http://flow8; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto https; }}
server { listen 80; server_name app.flow8.io; return 301 https://$server_name$request_uri;}Kubernetes Ingress example:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: flow8spec: tls: - hosts: - app.flow8.io secretName: flow8-tls-cert rules: - host: app.flow8.io http: paths: - path: / pathType: Prefix backend: service: name: flow8 port: number: 4454MongoDB connection (optional mTLS):
# If MongoDB is remote, use TLSMONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/database?ssl=true&authSource=admin&tlsCAFile=/etc/ssl/certs/ca.pemKey Generation
Generating ENC_KEY_SECRET
Generate a cryptographically random 256-character hex string:
# Using opensslopenssl rand -hex 128
# Using Pythonpython3 -c "import secrets; print(secrets.token_hex(128))"
# Using Gogo run -c "package main; import (\"crypto/rand\"; \"encoding/hex\"; \"fmt\"; \"io\"); func main() { b := make([]byte, 128); io.ReadFull(rand.Reader, b); fmt.Println(hex.EncodeToString(b)) }"Generating ENV_KEY_SALT
Generate a cryptographically random 64-character hex string:
openssl rand -hex 32# orpython3 -c "import secrets; print(secrets.token_hex(32))"Storing Keys Securely
Development:
# .env file (gitignored)ENC_KEY_SECRET=<your-256-char-hex>ENV_KEY_SALT=<your-64-char-hex>Production (Kubernetes):
apiVersion: v1kind: Secretmetadata: name: flow8-encryptiontype: OpaquestringData: ENC_KEY_SECRET: "abcdef0123456789..." ENV_KEY_SALT: "abcdef0123456789..."apiVersion: v1kind: Podmetadata: name: flow8spec: containers: - name: flow8 env: - name: ENC_KEY_SECRET valueFrom: secretKeyRef: name: flow8-encryption key: ENC_KEY_SECRET - name: ENV_KEY_SALT valueFrom: secretKeyRef: name: flow8-encryption key: ENV_KEY_SALTProduction (AWS Secrets Manager):
# Store secretsaws secretsmanager create-secret \ --name flow8/encryption \ --secret-string '{"ENC_KEY_SECRET":"...","ENV_KEY_SALT":"..."}'
# Retrieve at startupaws secretsmanager get-secret-value \ --secret-id flow8/encryptionKey Rotation
Key rotation is necessary to limit exposure if a key is compromised.
Rotation Process
- Generate new
ENC_KEY_SECRETandENV_KEY_SALT - Deploy application with both old and new keys in environment:
Terminal window ENC_KEY_SECRET=<new_key>ENC_KEY_SECRET_OLD=<old_key>ENV_KEY_SALT=<new_salt>ENV_KEY_SALT_OLD=<old_salt> - Run re-encryption job:
pkg/service/key_rotation_service.go func (s *KeyRotationService) ReencryptAll(ctx context.Context, companyID string) error {// Iterate all encrypted fields// Decrypt with old key// Encrypt with new key// Update MongoDB} - Verify all fields re-encrypted (monitoring)
- Remove old key from environment
- Redeploy
Estimated time: ~30 minutes for 100,000 encrypted fields
Decryption with Old Key
If decryption with new key fails, fall back to old key:
func (s *EncryptionService) Decrypt(encrypted []byte) ([]byte, error) { // Try current key first if plaintext, err := decryptWithKey(encrypted, s.currentKey); err == nil { return plaintext, nil }
// Fall back to old key if available if plaintext, err := decryptWithKey(encrypted, s.oldKey); err == nil { // Log warning: old key was used, should trigger re-encryption s.logger.Warn("decrypted with old key", zap.String("encrypted_id", "...")) return plaintext, nil }
return nil, fmt.Errorf("decryption failed with all keys")}Encryption in Backups
Ensure backups are encrypted:
MongoDB Backup (mongodump)
# Backup to encrypted local storagemongodump \ --uri="mongodb://user:password@localhost:27017/flow8?ssl=true" \ --out=/tmp/backup
# Encrypt backup directory with GPGtar czf - /tmp/backup | gpg --symmetric --cipher-algo AES256 > backup.tar.gz.gpgAWS S3 Backup
# S3 with server-side encryptionaws s3 cp backup.tar.gz s3://flow8-backups/ \ --sse=AES256 \ --metadata "source=flow8,date=$(date -u +%Y-%m-%d)"Kubernetes Persistent Volume
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: flow8-dataspec: accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: encrypted # Ensure encrypted storage classTesting Encryption
Unit Tests
cd pkg/cryptogo test -v ./... -run TestEncryptionIntegration Test
# Create an encrypted fieldcurl -X POST http://localhost:4454/api/v1/links \ -H "Authorization: Bearer $TOKEN" \ -d '{ "integration": "slack", "access_token": "xoxb-1234567890..." }'
# Verify it's encrypted in MongoDBmongo flow8> db.db_links.findOne({integration: "slack"}){ "_id": ObjectId(...), "access_token": { "encrypted": true, "nonce": "...", "ciphertext": "..." }}Compliance Notes
- GDPR: Encryption at rest meets data minimization and protection requirements
- HIPAA: Encryption at rest + field-level encryption + audit logging support BAA requirements
- PCI DSS: Encryption at rest recommended for cardholder data (not recommended for payment processing)
- SOC 2: Encryption controls support C1 requirements (security) and C7 (system monitoring)