Skip to main content

Overview

Duckling provides multiple layers of security including authentication, rate limiting, session management, and encryption. This guide covers all security features and best practices.

Authentication

Duckling supports three authentication methods:

1. API Key Authentication

For programmatic access (scripts, applications, integrations).
DUCKLING_API_KEY
string
required
API key for Bearer token authenticationGenerate a secure key:
openssl rand -hex 32
Usage:
curl -H "Authorization: Bearer ${DUCKLING_API_KEY}" \
  http://localhost:3001/api/tables
Security considerations:
  • Store in environment variables, not in code
  • Rotate regularly (recommended: every 90 days)
  • Use separate keys for different environments
  • Never commit to version control

2. Session-Based Authentication

For web dashboard (cookie-based login).
ADMIN_USERNAME
string
required
Admin username for dashboard login
ADMIN_USERNAME=admin
Change from default value before deploying to production.
ADMIN_PASSWORD
string
required
Admin password for dashboard login
ADMIN_PASSWORD=your-secure-password-here
Password requirements:
  • Minimum 16 characters recommended
  • Mix of uppercase, lowercase, numbers, symbols
  • Not based on dictionary words
  • Unique to this service
SESSION_SECRET
string
required
Secret key for session cookie encryption
SESSION_SECRET=$(openssl rand -hex 32)
CRITICAL: Must be a random 64-character hex string. Never use the same secret across environments.

3. JWT Authentication

For stateless API access (mobile apps, SPAs).
JWT_SECRET
string
JWT signing secretFalls back to SESSION_SECRET if not set.
JWT_SECRET=$(openssl rand -hex 32)
JWT_EXPIRES_IN
string
default:"1h"
JWT token expiration
JWT_EXPIRES_IN=1h    # 1 hour
JWT_EXPIRES_IN=7d    # 7 days
JWT_EXPIRES_IN=30m   # 30 minutes
Shorter expiration is more secure but requires more frequent re-authentication.

Login Endpoint

# Web dashboard login
curl -X POST http://localhost:3001/api/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin",
    "password": "your-password"
  }'

# Returns JWT token
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Rate Limiting

Duckling includes sophisticated rate limiting to protect against abuse and ensure fair resource allocation.

Enable Rate Limiting

RATE_LIMIT_ENABLED
boolean
default:"true"
Enable rate limiting
RATE_LIMIT_ENABLED=true
Disable only for testing. Always enable in production.
RATE_LIMIT_MODE
enum
default:"enforce"
Rate limit enforcement mode
  • enforce - Block requests exceeding limits (recommended)
  • shadow - Log violations but allow requests (testing only)
RATE_LIMIT_MODE=enforce

Rate Limit Categories

Different endpoint categories have different limits:
RATE_LIMIT_READ_MAX
number
default:"120"
Read endpoint limit (requests/minute)Applies to: GET /tables, GET /status, GET /health
RATE_LIMIT_READ_MAX=120
RATE_LIMIT_QUERY_MAX
number
default:"80"
Query endpoint limit (requests/minute)Applies to: POST /query
RATE_LIMIT_QUERY_MAX=80
RATE_LIMIT_WRITE_MAX
number
default:"10"
Write endpoint limit (requests/minute)Applies to: POST /sync/*, POST /automation/*
RATE_LIMIT_WRITE_MAX=10
RATE_LIMIT_AUTH_MAX
number
default:"10"
Authentication endpoint limit (requests/minute)Applies to: POST /login
RATE_LIMIT_AUTH_MAX=10
Low limit prevents brute-force attacks.

Request Costs

Different operations have different “costs” for rate limiting:
RATE_LIMIT_COST_QUERY
number
default:"1"
Cost per query request
RATE_LIMIT_COST_QUERY=1
RATE_LIMIT_COST_WRITE
number
default:"3"
Cost per write requestWrite operations (sync, backup) are more expensive.
RATE_LIMIT_COST_WRITE=3

Tiered Rate Limits

Authenticated users get higher limits:
TierMultiplierApplies To
Anonymous1xUnauthenticated requests
JWT2xJWT token authentication
API Key5xAPI key authentication
# Adjust tier multipliers
RATE_LIMIT_JWT_MULTIPLIER=2
RATE_LIMIT_APIKEY_MULTIPLIER=5
Example:
  • Anonymous: 80 queries/min
  • JWT: 160 queries/min (80 × 2)
  • API Key: 400 queries/min (80 × 5)

Concurrency Limits

Prevent resource exhaustion from parallel queries:
RATE_LIMIT_JWT_QUERY_MAX_IN_FLIGHT
number
default:"6"
Max concurrent queries (JWT)
RATE_LIMIT_JWT_QUERY_MAX_IN_FLIGHT=6
RATE_LIMIT_APIKEY_QUERY_MAX_IN_FLIGHT
number
default:"12"
Max concurrent queries (API key)
RATE_LIMIT_APIKEY_QUERY_MAX_IN_FLIGHT=12
RATE_LIMIT_QUERY_INFLIGHT_TTL_MS
number
default:"300000"
In-flight tracking TTL (ms)Time to keep query tracking data (5 minutes).
RATE_LIMIT_QUERY_INFLIGHT_TTL_MS=300000

Rate Limit Scope

RATE_LIMIT_USE_SESSION_SCOPE
boolean
default:"false"
Use session-based rate limiting
  • true - Limit per session ID
  • false - Limit per IP address (default)
RATE_LIMIT_USE_SESSION_SCOPE=false
RATE_LIMIT_INCLUDE_DB_SCOPE
boolean
default:"true"
Include database in rate limit scope
  • true - Separate limits per database (recommended)
  • false - Global limits across all databases
RATE_LIMIT_INCLUDE_DB_SCOPE=true

MySQL Protocol Authentication

Duckling exposes a MySQL-compatible wire protocol on port 3307.
MYSQL_PROTOCOL_USER
string
default:"duckling"
MySQL protocol username
MYSQL_PROTOCOL_USER=duckling
MYSQL_PROTOCOL_PASSWORD
string
MySQL protocol passwordFalls back to DUCKLING_API_KEY if not set.
MYSQL_PROTOCOL_PASSWORD=your-password-here
Connect with MySQL client:
mysql -h localhost -P 3307 -u duckling -p
# Enter password: <MYSQL_PROTOCOL_PASSWORD or DUCKLING_API_KEY>

CORS Configuration

ENABLE_CORS
boolean
default:"true"
Enable Cross-Origin Resource Sharing
ENABLE_CORS=true
CORS is required for browser-based applications. Disable only if all clients are server-side.

Encryption

Session cookies are encrypted using SESSION_SECRET.
# Generate secure session secret
SESSION_SECRET=$(openssl rand -hex 32)

S3 Backup Encryption

S3 backups support multiple encryption modes:
ModeSecurityUse Case
noneNo encryptionDevelopment only
sse-s3AWS-managed keysGood for most cases
sse-kmsAWS KMS + audit trailCompliance requirements
client-aes256Client-side (best)Recommended for production
Client-side encryption (recommended):
# Generate 32-byte encryption key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Configure via API:
curl -X PUT http://localhost:3001/api/databases/production/s3 \
  -H "Authorization: Bearer ${DUCKLING_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "bucket": "my-backups",
    "region": "us-east-1",
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "encryption": "client-aes256",
    "encryptionKey": "a3f1c2d4e5b6a7f8..."
  }'
Benefits of client-side encryption:
  • Encryption key never leaves your server
  • Protects against compromised AWS credentials
  • Protects against bucket misconfiguration
  • HMAC integrity verification on restore

Security Validation

Duckling validates security configuration on startup.

Check for Security Issues

# View startup logs
docker logs duckling | grep -i "security"

# Check via API
curl http://localhost:3001/health
Critical issues that prevent startup:
  • JWT_SECRET using default value
  • ADMIN_USERNAME or ADMIN_PASSWORD empty
  • SESSION_SECRET not set

Network Security

Firewall Rules

Minimum required ports:
# Allow HTTPS only (recommended)
allow 443/tcp from anywhere

# Or HTTP (requires reverse proxy)
allow 80/tcp from anywhere

# MySQL protocol (optional)
allow 3307/tcp from trusted IPs only

# Deny all other inbound traffic
deny from anywhere

Reverse Proxy with SSL/TLS

Nginx with Let’s Encrypt:
server {
  listen 443 ssl http2;
  server_name duckling.example.com;

  ssl_certificate /etc/letsencrypt/live/duckling.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/duckling.example.com/privkey.pem;
  
  # Strong SSL configuration
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
  ssl_prefer_server_ciphers off;
  
  # HSTS (force HTTPS)
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  
  # Security headers
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header X-XSS-Protection "1; mode=block" always;
  
  location / {
    proxy_pass http://localhost:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

VPN/Private Network

For maximum security, deploy Duckling in a private network:
# AWS VPC
# Only allow access from VPN or bastion host

# Docker network
docker network create --internal duckling-private
docker run --network duckling-private duckling:latest

MySQL Source Security

Read-Only User

Create a dedicated read-only MySQL user for Duckling:
CREATE USER 'duckling'@'%' IDENTIFIED BY 'secure-password';
GRANT SELECT, SHOW VIEW ON database_name.* TO 'duckling'@'%';
FLUSH PRIVILEGES;

Connection Encryption

Use SSL/TLS for MySQL connections:
MYSQL_CONNECTION_STRING=mysql://user:pass@host:3306/db?ssl={"rejectUnauthorized":true}

IP Allowlisting

Restrict MySQL access to Duckling server IP:
CREATE USER 'duckling'@'<duckling-server-ip>' IDENTIFIED BY 'secure-password';
GRANT SELECT ON database_name.* TO 'duckling'@'<duckling-server-ip>';

Audit Logging

Enable Detailed Logging

LOG_LEVEL=info  # or 'debug' for more detail

Log Rotation

# Docker log rotation
docker run -d \
  --log-driver=json-file \
  --log-opt max-size=100m \
  --log-opt max-file=10 \
  duckling:latest

Centralized Logging

# Send logs to syslog
docker run -d \
  --log-driver=syslog \
  --log-opt syslog-address=udp://logserver:514 \
  duckling:latest

Sentry Error Tracking

SENTRY_DSN=https://[email protected]/xxx
SENTRY_RELEASE=[email protected]

Security Checklist

Authentication configured
  • DUCKLING_API_KEY set to random 64-char hex
  • ADMIN_PASSWORD changed from default (16+ characters)
  • SESSION_SECRET set to random 64-char hex
  • JWT_SECRET set (or using SESSION_SECRET)
  • Default credentials never used in production
Rate limiting enabled
  • RATE_LIMIT_ENABLED=true
  • RATE_LIMIT_MODE=enforce
  • Limits tuned for expected traffic
  • Concurrency limits set
Network security
  • Reverse proxy with SSL/TLS
  • Firewall rules configured
  • Unnecessary ports closed
  • HTTPS enforced (no HTTP)
MySQL source security
  • Read-only MySQL user created
  • MySQL connection uses SSL/TLS
  • IP allowlisting configured
  • Strong MySQL password
Data encryption
  • S3 backups use client-side encryption
  • Encryption keys stored securely
  • Session cookies encrypted
  • HTTPS for all web traffic
Monitoring & logging
  • Log level set appropriately
  • Log rotation configured
  • Centralized logging (optional)
  • Error tracking (Sentry) configured
Access control
  • Principle of least privilege
  • Separate credentials per environment
  • Regular credential rotation
  • API keys stored in secrets manager

Credential Rotation

API Key Rotation

# 1. Generate new key
NEW_API_KEY=$(openssl rand -hex 32)

# 2. Update .env
DUCKLING_API_KEY=${NEW_API_KEY}

# 3. Restart container
docker restart duckling

# 4. Update all clients to use new key

Session Secret Rotation

# 1. Generate new secret
NEW_SESSION_SECRET=$(openssl rand -hex 32)

# 2. Update .env
SESSION_SECRET=${NEW_SESSION_SECRET}

# 3. Restart container (invalidates all sessions)
docker restart duckling

# 4. Users must re-login

S3 Encryption Key Rotation

Critical: Rotating S3 encryption keys requires re-encrypting all backups. Plan carefully.
# 1. Generate new encryption key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# 2. Update S3 config via API
curl -X PUT http://localhost:3001/api/databases/production/s3 \
  -H "Authorization: Bearer ${DUCKLING_API_KEY}" \
  -d '{"encryptionKey": "new-key-here"}'

# 3. Trigger new backup (old backups remain with old key)
curl -X POST 'http://localhost:3001/api/backups/s3?db=production' \
  -H "Authorization: Bearer ${DUCKLING_API_KEY}"

# 4. Keep old key until old backups expire

Incident Response

Suspected API Key Compromise

  1. Immediately rotate DUCKLING_API_KEY
  2. Review access logs for unauthorized access
  3. Check for data exfiltration
  4. Update all legitimate clients
  5. Consider enabling IP allowlisting

Suspected Admin Credential Compromise

  1. Immediately change ADMIN_PASSWORD
  2. Rotate SESSION_SECRET (invalidates all sessions)
  3. Review audit logs for unauthorized actions
  4. Check for configuration changes
  5. Verify database integrity

Suspected Database Compromise

  1. Immediately stop sync to prevent further damage
  2. Restore from known-good backup
  3. Review MySQL source for tampering
  4. Verify DuckDB file integrity
  5. Re-sync after validation

Compliance Considerations

GDPR

  • Encrypt data at rest (S3 client-side encryption)
  • Encrypt data in transit (SSL/TLS)
  • Access logging and audit trails
  • Right to deletion (backup cleanup)

HIPAA

  • Use sse-kms or client-aes256 for S3 backups
  • Enable audit logging
  • Restrict access to PHI
  • Encrypt all network traffic

SOC 2

  • Enable Sentry error tracking
  • Centralized logging
  • Access control (API keys, rate limiting)
  • Regular credential rotation
  • Backup encryption

Next Steps

Production Guide

Production deployment best practices

Configuration

Full environment variable reference

Docker Deployment

Docker deployment guide

Build docs developers (and LLMs) love