Skip to content

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:

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 key

Argon2 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 key

Scrypt parameters: Same as Argon2 equivalent

Migration path: Argon2 is the default; Scrypt keys are marked for re-encryption.

Configuration

Environment Variables

Terminal window
# 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=1048576

Configuration File

config/config.yml
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 logs

Encrypted Fields

The following fields are automatically encrypted in the database:

User Credentials

CollectionFieldReason
userspassword_hash (bcrypt, not encrypted)Password stored as bcrypt hash, not encrypted
userstotp_secret (if 2FA enabled)TOTP secret key

Note: Passwords use bcrypt hashing, not encryption. They are one-way hashes.

OAuth2 Tokens

CollectionFieldReason
db_linksaccess_tokenOAuth2 access token (can be used to call APIs)
db_linksrefresh_tokenOAuth2 refresh token (used to get new access token)
db_linkswebhook_signature_secretSecret for validating webhook signatures

API Keys & Credentials

CollectionFieldReason
component_configsconfig.api_keyAPI key for external service (OpenAI, AWS, etc.)
component_configsconfig.passwordDatabase password for SQL connections
component_configsconfig.client_secretOAuth2 client secret
component_configsconfig.access_key_idAWS access key
component_configsconfig.secret_access_keyAWS secret key

Connection Details

CollectionFieldReason
component_configsconfig.connection_stringDatabase URL with credentials
component_configsconfig.smtp_passwordSMTP server password
component_configsconfig.http_auth_passwordHTTP basic auth password

Key-Value Store

ScopeConditionEncryption
AnyKey prefix: secret:*Always encrypted
AnyValue type: string, length > UNIT_OUTPUT_UI_MAX_BYTESEncrypted
AnySensitive key patterns: password, token, key, secretEncrypted by heuristic

Audit Logs

FieldEncryption
before_state (sensitive fields)Redacted (hash or [REDACTED])
after_state (sensitive fields)Redacted (hash or [REDACTED])
request_bodySensitive fields redacted

Redaction mode:

# In config.yml
audit:
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/v1
kind: Ingress
metadata:
name: flow8
spec:
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: 4454

MongoDB connection (optional mTLS):

Terminal window
# If MongoDB is remote, use TLS
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/database?ssl=true&authSource=admin&tlsCAFile=/etc/ssl/certs/ca.pem

Key Generation

Generating ENC_KEY_SECRET

Generate a cryptographically random 256-character hex string:

Terminal window
# Using openssl
openssl rand -hex 128
# Using Python
python3 -c "import secrets; print(secrets.token_hex(128))"
# Using Go
go 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:

Terminal window
openssl rand -hex 32
# or
python3 -c "import secrets; print(secrets.token_hex(32))"

Storing Keys Securely

Development:

Terminal window
# .env file (gitignored)
ENC_KEY_SECRET=<your-256-char-hex>
ENV_KEY_SALT=<your-64-char-hex>

Production (Kubernetes):

apiVersion: v1
kind: Secret
metadata:
name: flow8-encryption
type: Opaque
stringData:
ENC_KEY_SECRET: "abcdef0123456789..."
ENV_KEY_SALT: "abcdef0123456789..."
apiVersion: v1
kind: Pod
metadata:
name: flow8
spec:
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_SALT

Production (AWS Secrets Manager):

Terminal window
# Store secrets
aws secretsmanager create-secret \
--name flow8/encryption \
--secret-string '{"ENC_KEY_SECRET":"...","ENV_KEY_SALT":"..."}'
# Retrieve at startup
aws secretsmanager get-secret-value \
--secret-id flow8/encryption

Key Rotation

Key rotation is necessary to limit exposure if a key is compromised.

Rotation Process

  1. Generate new ENC_KEY_SECRET and ENV_KEY_SALT
  2. 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>
  3. 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
    }
  4. Verify all fields re-encrypted (monitoring)
  5. Remove old key from environment
  6. 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)

Terminal window
# Backup to encrypted local storage
mongodump \
--uri="mongodb://user:password@localhost:27017/flow8?ssl=true" \
--out=/tmp/backup
# Encrypt backup directory with GPG
tar czf - /tmp/backup | gpg --symmetric --cipher-algo AES256 > backup.tar.gz.gpg

AWS S3 Backup

Terminal window
# S3 with server-side encryption
aws s3 cp backup.tar.gz s3://flow8-backups/ \
--sse=AES256 \
--metadata "source=flow8,date=$(date -u +%Y-%m-%d)"

Kubernetes Persistent Volume

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: flow8-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: encrypted # Ensure encrypted storage class

Testing Encryption

Unit Tests

Terminal window
cd pkg/crypto
go test -v ./... -run TestEncryption

Integration Test

Terminal window
# Create an encrypted field
curl -X POST http://localhost:4454/api/v1/links \
-H "Authorization: Bearer $TOKEN" \
-d '{
"integration": "slack",
"access_token": "xoxb-1234567890..."
}'
# Verify it's encrypted in MongoDB
mongo 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)