1. Introduction: Tầm quan trọng của bảo mật trong Automation
Automation workflows đang trở thành xương sống của hầu hết các hệ thống hiện đại. N8N, với khả năng tích hợp hàng trăm services và APIs, mang lại sức mạnh to lớn - nhưng cũng đi kèm với rủi ro bảo mật nghiêm trọng nếu không được quản lý đúng cách.
Một workflow N8N thông thường có thể chứa:
- API keys của 10+ external services
- Database credentials
- OAuth tokens với quyền truy cập rộng
- Webhook URLs chứa sensitive parameters
- Personal access tokens của GitHub, GitLab
- Cloud provider credentials (AWS, GCP, Azure)
Thực tế đáng lo ngại: Theo khảo sát bảo mật 2025, hơn 67% các tổ chức đã vô tình commit credentials vào Git repository ít nhất một lần. Với N8N workflows được lưu dưới dạng JSON, rủi ro này càng cao hơn.
Một câu hỏi quan trọng mà mọi Tech Lead cần trả lời: "Nếu N8N database của bạn bị leak hôm nay, bao nhiêu hệ thống khác sẽ bị compromise?"
Bài viết này sẽ hướng dẫn bạn cách xây dựng một security architecture vững chắc cho N8N, từ development đến production.
2. Các lỗi bảo mật phổ biến trong N8N
Trước khi đi vào solutions, hãy nhận diện các anti-patterns nguy hiểm:
2.1. Hardcode Credentials trong Workflows
❌ NGUY HIỂM:
{
"nodes": [
{
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://api.service.com/data",
"authentication": "none",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer sk-proj-abc123xyz789..."
}
]
}
}
}
]
}
Vấn đề:
- Credentials được lưu plain text trong database
- Xuất hiện trong workflow JSON khi export
- Có thể bị log trong debugging
- Không có rotation mechanism
2.2. Environment Variables trong Docker Compose
❌ NGUY HIỂM:
# docker-compose.yml
version: '3.8'
services:
n8n:
image: n8nio/n8n
environment:
- OPENAI_API_KEY=sk-proj-abc123...
- DATABASE_PASSWORD=supersecret123
- AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
Vấn đề:
- Secrets được commit vào Git
- Visible qua docker inspect
- Không có audit trail
- Khó rotate credentials
2.3. Webhook URLs chứa Authentication Parameters
❌ NGUY HIỂM:
// Webhook URL shared publicly https://n8n.company.com/webhook/data-sync?api_key=sk_live_abc123&user_token=usr_xyz789
Vấn đề:
- URLs được log ở nhiều layers (proxy, CDN, browser history)
- Có thể bị share qua Slack, email
- Không có expiration
- Bypass rate limiting
2.4. Insufficient Access Control
❌ NGUY HIỂM:
- Tất cả developers có quyền view/edit tất cả credentials
- Không có role-based access control
- Production và development dùng chung credentials
- Không có audit log cho credential access
3. Credentials Management - N8N Built-in Features
N8N cung cấp một số features bảo mật cơ bản, nhưng cần được configure đúng cách.
3.1. N8N Credentials System
N8N lưu credentials trong database với encryption at rest (nếu được enable):
Cấu hình encryption key:
# .env N8N_ENCRYPTION_KEY="your-super-secure-32-char-key-here-abc123xyz"
⚠️ CRITICAL: Encryption key này phải:
- Được generate bằng cryptographically secure random generator
- Không bao giờ commit vào Git
- Được backup riêng biệt với database
- Được rotate định kỳ (6-12 tháng)
Generate secure encryption key:
# Linux/macOS
openssl rand -base64 32
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
3.2. Credential Types và Best Practices
N8N hỗ trợ nhiều credential types:
// Example: Properly scoped OAuth2 credential
{
"name": "Google Sheets OAuth2",
"type": "googleSheetsOAuth2Api",
"data": {
"clientId": "{{GOOGLE_CLIENT_ID}}",
"clientSecret": "{{GOOGLE_CLIENT_SECRET}}",
"scope": "https://www.googleapis.com/auth/spreadsheets.readonly"
}
}
Best Practices:
- Least Privilege: Chỉ request scopes tối thiểu cần thiết
- Naming Convention: Sử dụng tên mô tả rõ ràng: Production_Stripe_ReadOnly, Dev_AWS_S3_Upload
- Separation: Tách credentials cho từng environment
- Documentation: Comment vào credential description về expiration date và owner
3.3. Credential Sharing và Access Control
N8N Enterprise features:
// Role-based credential access
{
"credentialId": "cred_abc123",
"name": "Production_Database",
"type": "postgres",
"data": {...},
"sharedWith": [
{
"userId": "user_xyz",
"role": "credential:read"
}
]
}
Self-hosted workaround (không có enterprise):
- Tạo separate N8N instances cho production vs development
- Sử dụng database-level permissions
- Implement audit logging qua database triggers
4. HashiCorp Vault Integration - Step by Step Setup
HashiCorp Vault là solution phổ biến nhất cho secrets management ở enterprise level.
4.1. Architecture Overview
N8N Workflow → HTTP Request Node → Vault API
↓
Retrieve Secret
↓
Use in subsequent nodes
4.2. Vault Setup
Docker Compose với Vault:
version: '3.8'
services:
vault:
image: hashicorp/vault:latest
container_name: vault
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: "dev-only-token"
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
cap_add:
- IPC_LOCK
volumes:
- vault-data:/vault/data
- vault-logs:/vault/logs
command: server -dev
n8n:
image: n8nio/n8n:latest
depends_on:
- vault
ports:
- "5678:5678"
environment:
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- VAULT_ADDR=http://vault:8200
- VAULT_TOKEN=${VAULT_ROOT_TOKEN}
volumes:
- n8n-data:/home/node/.n8n
volumes:
vault-data:
vault-logs:
n8n-data:
4.3. Store Secrets trong Vault
# Initialize Vault (production - chỉ làm 1 lần)
vault operator init
# Unseal Vault (required after every restart)
vault operator unseal <unseal-key-1>
vault operator unseal <unseal-key-2>
vault operator unseal <unseal-key-3>
# Login
vault login <root-token>
# Enable KV v2 secrets engine
vault secrets enable -path=n8n kv-v2
# Store secrets
vault kv put n8n/production/openai api_key="sk-proj-abc123..."
vault kv put n8n/production/stripe \
secret_key="sk_live_xyz789..." \
publishable_key="pk_live_abc123..." \
webhook_secret="whsec_def456..."
# Create policy for N8N
vault policy write n8n-production - <<EOF
path "n8n/data/production/*" {
capabilities = ["read", "list"]
}
EOF
# Create token with policy
vault token create -policy=n8n-production -ttl=720h
4.4. N8N Workflow để retrieve Vault Secrets
Reusable Workflow: Vault Secret Retrieval
// Node 1: HTTP Request - Get Secret from Vault
{
"parameters": {
"url": "={{$env.VAULT_ADDR}}/v1/n8n/data/production/{{$node[\"Config\"].json[\"secretPath\"]}}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"httpHeaderAuth": {
"name": "X-Vault-Token",
"value": "={{$env.VAULT_TOKEN}}"
},
"options": {
"response": {
"response": {
"fullResponse": false,
"responseFormat": "json"
}
}
}
},
"name": "Get Secret from Vault",
"type": "n8n-nodes-base.httpRequest"
}
// Node 2: Code - Extract Secret Value
{
"parameters": {
"jsCode": "// Extract secret from Vault response\nconst vaultResponse = $input.all()[0].json;\nconst secretData = vaultResponse.data.data;\n\n// Return specific secret value\nreturn {\n secret: secretData[$node[\"Config\"].json[\"secretKey\"]],\n metadata: {\n version: vaultResponse.data.metadata.version,\n created_time: vaultResponse.data.metadata.created_time\n }\n};"
},
"name": "Extract Secret",
"type": "n8n-nodes-base.code"
}
// Node 3: Use Secret in API Call
{
"parameters": {
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "none",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{$node[\"Extract Secret\"].json[\"secret\"]}}"
}
]
},
"body": {
"specifyBody": "json",
"jsonBody": "={\"model\": \"gpt-4\", \"messages\": {{$json.messages}}}"
}
},
"name": "OpenAI API Call",
"type": "n8n-nodes-base.httpRequest"
}
4.5. Production Vault Configuration
vault-config.hcl:
storage "postgresql" {
connection_url = "postgres://vault:vaultpass@postgres:5432/vault?sslmode=disable"
max_parallel = 128
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/vault/certs/vault.crt"
tls_key_file = "/vault/certs/vault.key"
tls_min_version = "tls12"
}
seal "awskms" {
region = "ap-southeast-1"
kms_key_id = "your-kms-key-id"
}
api_addr = "https://vault.company.com:8200"
cluster_addr = "https://vault-node1.internal:8201"
ui = true
telemetry {
prometheus_retention_time = "30s"
disable_hostname = false
}
4.6. Vault Secret Rotation
Automated rotation script:
// n8n-vault-rotation.js
const vault = require('node-vault')({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
async function rotateSecret(path, generateNewSecret) {
try {
// 1. Generate new secret
const newSecret = await generateNewSecret();
// 2. Store new version in Vault
await vault.write(`n8n/data/${path}`, {
data: newSecret
});
// 3. Update external service with new secret
// (service-specific implementation)
// 4. Wait for propagation (configurable)
await sleep(60000); // 1 minute
// 5. Verify new secret works
const verified = await verifySecret(newSecret);
if (!verified) {
// Rollback to previous version
const oldVersion = await vault.read(`n8n/data/${path}`, {
version: -1
});
await vault.write(`n8n/data/${path}`, {
data: oldVersion.data.data
});
throw new Error('New secret verification failed');
}
console.log(`✅ Secret rotated successfully: ${path}`);
} catch (error) {
console.error(`❌ Rotation failed for ${path}:`, error);
throw error;
}
}
// Schedule rotation
const CronJob = require('cron').CronJob;
new CronJob('0 0 1 * *', async () => {
await rotateSecret('production/api-key', generateApiKey);
}, null, true, 'Asia/Ho_Chi_Minh');
5. Environment Variables Best Practices
5.1. The .env Hierarchy
❌ KHÔNG BAO GIỜ làm:
# .env (committed to Git) DATABASE_PASSWORD=supersecret123 OPENAI_API_KEY=sk-proj-abc123...
✅ ĐÚNG CÁCH:
# .env.example (committed to Git - template only) DATABASE_PASSWORD= OPENAI_API_KEY= STRIPE_SECRET_KEY= VAULT_TOKEN= N8N_ENCRYPTION_KEY= # Instructions # Copy this to .env and fill in actual values # Never commit .env to Git
# .env (in .gitignore) DATABASE_PASSWORD=actual_secure_password_from_1password OPENAI_API_KEY=sk-proj-actual-key-from-vault STRIPE_SECRET_KEY=sk_live_actual_key VAULT_TOKEN=hvs.actual_vault_token N8N_ENCRYPTION_KEY=actual_32_char_encryption_key
5.2. Environment-Specific Configuration
Production-ready structure:
project/ ├── .env.example # Template ├── .env.local # Local development (gitignored) ├── .env.staging # Staging secrets (encrypted, not committed) ├── .env.production # Production secrets (stored in secrets manager) └── .gitignore # Must include .env*
.gitignore:
.env .env.* !.env.example
5.3. Docker Secrets (Production)
docker-compose.production.yml:
version: '3.8'
services:
n8n:
image: n8nio/n8n:latest
ports:
- "5678:5678"
secrets:
- n8n_encryption_key
- vault_token
- db_password
environment:
- N8N_ENCRYPTION_KEY_FILE=/run/secrets/n8n_encryption_key
- VAULT_TOKEN_FILE=/run/secrets/vault_token
- DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/db_password
volumes:
- n8n-data:/home/node/.n8n
secrets:
n8n_encryption_key:
external: true
vault_token:
external: true
db_password:
external: true
Create Docker secrets:
# From 1Password/LastPass echo "your-encryption-key" | docker secret create n8n_encryption_key - # From Vault vault kv get -field=token n8n/tokens/vault | docker secret create vault_token - # From AWS Secrets Manager aws secretsmanager get-secret-value \ --secret-id n8n/db/password \ --query SecretString \ --output text | docker secret create db_password -
5.4. Kubernetes Secrets (Enterprise)
n8n-secrets.yaml:
apiVersion: v1
kind: Secret
metadata:
name: n8n-secrets
namespace: production
type: Opaque
stringData:
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} # Injected by CI/CD
VAULT_TOKEN: ${VAULT_TOKEN}
DB_PASSWORD: ${DB_PASSWORD}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: n8n-config
namespace: production
data:
VAULT_ADDR: "https://vault.company.internal:8200"
N8N_HOST: "n8n.company.com"
N8N_PROTOCOL: "https"
N8N_PORT: "443"
n8n-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: n8n
namespace: production
spec:
replicas: 3
template:
spec:
containers:
- name: n8n
image: n8nio/n8n:latest
envFrom:
- configMapRef:
name: n8n-config
- secretRef:
name: n8n-secrets
volumeMounts:
- name: n8n-data
mountPath: /home/node/.n8n
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
6. AWS Secrets Manager / Other Vault Solutions
6.1. AWS Secrets Manager Integration
Setup AWS Secrets:
# Create secret
aws secretsmanager create-secret \
--name n8n/production/openai \
--description "OpenAI API Key for N8N Production" \
--secret-string '{"api_key":"sk-proj-abc123..."}'
# Create secret with rotation
aws secretsmanager create-secret \
--name n8n/production/database \
--secret-string '{"username":"n8n_user","password":"super_secure_pass"}' \
--tags Key=Environment,Value=Production Key=Application,Value=N8N
N8N Workflow - Retrieve from AWS Secrets Manager:
// Node: AWS Secrets Manager - Get Secret
{
"parameters": {
"authentication": "credentials",
"resource": "secret",
"operation": "get",
"secretId": "n8n/production/{{$json.secretName}}"
},
"name": "Get AWS Secret",
"type": "n8n-nodes-base.aws",
"credentials": {
"aws": "AWS_Production_ReadOnly"
}
}
// Node: Parse Secret
{
"parameters": {
"jsCode": "const secret = JSON.parse($input.first().json.SecretString);\nreturn { secret };"
},
"name": "Parse Secret",
"type": "n8n-nodes-base.code"
}
IAM Policy for N8N:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:ap-southeast-1:123456789:secret:n8n/production/*"
},
{
"Effect": "Allow",
"Action": "secretsmanager:ListSecrets",
"Resource": "*",
"Condition": {
"StringEquals": {
"secretsmanager:ResourceTag/Application": "N8N"
}
}
}
]
}
6.2. Google Cloud Secret Manager
N8N Workflow - GCP Secret Manager:
// HTTP Request Node - Get Secret
{
"parameters": {
"url": "https://secretmanager.googleapis.com/v1/projects/{{$env.GCP_PROJECT_ID}}/secrets/{{$json.secretName}}/versions/latest:access",
"authentication": "oAuth2",
"oAuth2": "Google_Cloud_OAuth2",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"name": "Get GCP Secret",
"type": "n8n-nodes-base.httpRequest"
}
// Code Node - Decode Base64
{
"parameters": {
"jsCode": "const base64Secret = $input.first().json.payload.data;\nconst secret = Buffer.from(base64Secret, 'base64').toString('utf-8');\nreturn { secret: JSON.parse(secret) };"
},
"name": "Decode Secret",
"type": "n8n-nodes-base.code"
}
6.3. Azure Key Vault
Setup:
# Create Key Vault az keyvault create \ --name n8n-production-vault \ --resource-group n8n-resources \ --location southeastasia # Add secret az keyvault secret set \ --vault-name n8n-production-vault \ --name openai-api-key \ --value "sk-proj-abc123..." # Grant N8N access az keyvault set-policy \ --name n8n-production-vault \ --object-id <n8n-managed-identity-id> \ --secret-permissions get list
6.4. Comparison Matrix
| Feature | HashiCorp Vault | AWS Secrets Manager | GCP Secret Manager | Azure Key Vault |
| Cost | Free (OSS) / Paid (Enterprise) | $0.40/secret/month | $0.06/10K accesses | $0.03/10K ops |
| Rotation | ✅ Built-in | ✅ Automated | ⚠️ Manual | ⚠️ Manual |
| Audit Logging | ✅ Excellent | ✅ CloudTrail | ✅ Cloud Logging | ✅ Monitor |
| Multi-cloud | ✅ Yes | ❌ AWS only | ❌ GCP only | ❌ Azure only |
| Learning Curve | High | Low | Low | Medium |
| HA Setup | Complex | Managed | Managed | Managed |
| Best For | Multi-cloud, Enterprise | AWS-heavy orgs | GCP-native | Azure-native |
Recommendation:
- Startup/SMB: AWS Secrets Manager (nếu đã dùng AWS) hoặc Vault dev mode
- Enterprise: HashiCorp Vault Enterprise hoặc cloud-native solution
- Multi-cloud: HashiCorp Vault OSS
7. Security Audit Checklist
7.1. Pre-Production Security Audit
Credentials & Secrets:
☐ All credentials stored in Vault/Secrets Manager ☐ No hardcoded secrets in workflows ☐ N8N_ENCRYPTION_KEY is 32+ characters, randomly generated ☐ Encryption key backed up securely (separate from DB backup) ☐ Environment variables loaded from secure source ☐ .env files in .gitignore ☐ No API keys in webhook URLs ☐ OAuth scopes follow least privilege ☐ Service accounts have minimal IAM permissions ☐ Credentials named with environment prefix (prod_, dev_)
Network Security:
☐ N8N UI behind authentication ☐ Webhook endpoints use authentication headers ☐ TLS/SSL enabled (HTTPS only) ☐ Firewall rules restrict N8N access ☐ Database not publicly accessible ☐ Vault/Secrets Manager accessible only from N8N network ☐ Rate limiting configured on webhooks ☐ CORS properly configured ☐ No admin panel exposed publicly
Database Security:
☐ Database encryption at rest enabled ☐ Database backups encrypted ☐ Backup retention policy defined (30-90 days) ☐ Database credentials rotated quarterly ☐ Connection pooling configured ☐ Query logging enabled (without logging secrets) ☐ Separate database user for N8N (not root) ☐ Database accessible only from N8N network
Access Control:
☐ Multi-factor authentication enabled ☐ Strong password policy enforced ☐ Role-based access control implemented ☐ Credentials shared only with authorized users ☐ Inactive users deprovisioned ☐ Audit logs enabled and monitored ☐ Session timeout configured (< 8 hours) ☐ API access tokens have expiration
Code & Workflow Security:
☐ Code nodes reviewed for injection vulnerabilities ☐ User input sanitized in HTTP Request nodes ☐ Error messages don't expose sensitive data ☐ Workflows exported without credentials ☐ Git repository for workflows doesn't contain secrets ☐ Dependency vulnerabilities scanned (npm audit) ☐ Docker images from official sources only ☐ Container scanning enabled
Monitoring & Logging:
☐ Failed login attempts monitored ☐ Unusual credential access alerts configured ☐ Workflow execution errors logged ☐ Performance metrics tracked ☐ Secrets access audited ☐ Log retention policy defined ☐ Logs don't contain sensitive data ☐ SIEM integration configured (if enterprise)
7.2. Automated Security Scanning
secrets-scan.sh:
#!/bin/bash
# Scan codebase for potential secrets
echo "🔍 Scanning for secrets in codebase..."
# Using gitleaks
docker run --rm -v $(pwd):/code zricethezav/gitleaks:latest \
detect --source /code --verbose --report-path /code/gitleaks-report.json
# Using trufflehog
docker run --rm -v $(pwd):/code trufflesecurity/trufflehog:latest \
filesystem /code --json --output /code/trufflehog-report.json
# Custom regex patterns
echo "Checking for common patterns..."
grep -r -E "(api_key|apikey|secret_key|password|token)\s*=\s*['\"][^'\"]{20,}" . \
--include="*.json" --include="*.js" --include="*.env*" \
--exclude-dir=node_modules
echo "✅ Scan complete. Review reports."
7.3. Penetration Testing Checklist
Testing scenarios:
- Credential Extraction:
- Attempt to extract credentials from DB backup
- Try to access Vault without proper token
- Test credential sharing bypass
- Injection Attacks:
- SQL injection in Code nodes
- Command injection in Execute Command nodes
- SSRF via HTTP Request nodes
- Access Control:
- Attempt privilege escalation
- Test credential access across users
- Verify workflow execution permissions
- Data Leakage:
- Check error messages for sensitive data
- Verify logs don't contain secrets
- Test workflow export for credential leakage
8. Production Deployment Security
8.1. Production-Grade Docker Compose
docker-compose.prod.yml:
version: '3.8'
services:
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678" # Only localhost
networks:
- n8n-internal
secrets:
- n8n_encryption_key
- db_password
- vault_token
environment:
# Security
- N8N_ENCRYPTION_KEY_FILE=/run/secrets/n8n_encryption_key
- N8N_USER_MANAGEMENT_DISABLED=false
- N8N_SECURE_COOKIE=true
- N8N_JWT_EXPIRE=24h
# Database
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n_user
- DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/db_password
# Vault
- VAULT_ADDR=https://vault.company.internal:8200
- VAULT_TOKEN_FILE=/run/secrets/vault_token
# Execution
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
# Logging
- N8N_LOG_LEVEL=info
- N8N_LOG_OUTPUT=file,console
- N8N_LOG_FILE_LOCATION=/home/node/.n8n/logs/
# Webhooks
- WEBHOOK_URL=https://n8n.company.com
- N8N_PROTOCOL=https
- N8N_HOST=n8n.company.com
volumes:
- n8n-data:/home/node/.n8n
- n8n-logs:/home/node/.n8n/logs
depends_on:
- postgres
- redis
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5678/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
postgres:
image: postgres:15-alpine
restart: unless-stopped
networks:
- n8n-internal
secrets:
- db_password
environment:
- POSTGRES_USER=n8n_user
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- POSTGRES_DB=n8n
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --lc-collate=C --lc-ctype=C
volumes:
- postgres-data:/var/lib/postgresql/data
- ./postgres-init:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n_user -d n8n"]
interval: 10s
timeout: 5s
retries: 5
command:
- "postgres"
- "-c"
- "max_connections=200"
- "-c"
- "shared_buffers=256MB"
- "-c"
- "effective_cache_size=1GB"
redis:
image: redis:7-alpine
restart: unless-stopped
networks:
- n8n-internal
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- nginx-logs:/var/log/nginx
networks:
- n8n-internal
depends_on:
- n8n
networks:
n8n-internal:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
volumes:
n8n-data:
n8n-logs:
postgres-data:
redis-data:
nginx-logs:
secrets:
n8n_encryption_key:
external: true
db_password:
external: true
vault_token:
external: true
8.2. Nginx Reverse Proxy với SSL
nginx.conf:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 2048;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 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;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=n8n_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Upstream
upstream n8n_backend {
server n8n:5678 max_fails=3 fail_timeout=30s;
}
# HTTP → HTTPS redirect
server {
listen 80;
server_name n8n.company.com;
return 301 https://$server_name$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name n8n.company.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Max upload size
client_max_body_size 50M;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# Security
limit_req zone=n8n_limit burst=20 nodelay;
limit_conn addr 10;
location / {
proxy_pass http://n8n_backend;
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;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
# WebSocket support
proxy_http_version 1.1;
proxy_buffering off;
}
# Healthcheck endpoint (no auth)
location /healthz {
proxy_pass http://n8n_backend/healthz;
access_log off;
}
# Block access to sensitive endpoints
location ~ ^/(rest/credentials|rest/users) {
deny all;
return 403;
}
}
}
8.3. Backup Strategy
backup.sh:
#!/bin/bash
set -e
BACKUP_DIR="/backups/n8n"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
# Backup database
echo "🔄 Backing up database..."
docker exec n8n-postgres pg_dump -U n8n_user n8n | \
gzip > "${BACKUP_DIR}/db_${TIMESTAMP}.sql.gz"
# Backup N8N data
echo "🔄 Backing up N8N data..."
docker run --rm \
-v n8n-data:/data \
-v ${BACKUP_DIR}:/backup \
alpine tar czf /backup/n8n_data_${TIMESTAMP}.tar.gz /data
# Encrypt backups
echo "🔐 Encrypting backups..."
gpg --encrypt --recipient ops@company.com \
"${BACKUP_DIR}/db_${TIMESTAMP}.sql.gz"
gpg --encrypt --recipient ops@company.com \
"${BACKUP_DIR}/n8n_data_${TIMESTAMP}.tar.gz"
# Remove unencrypted
rm "${BACKUP_DIR}/db_${TIMESTAMP}.sql.gz"
rm "${BACKUP_DIR}/n8n_data_${TIMESTAMP}.tar.gz"
# Upload to S3
echo "☁️ Uploading to S3..."
aws s3 cp "${BACKUP_DIR}/db_${TIMESTAMP}.sql.gz.gpg" \
s3://company-backups/n8n/ \
--storage-class STANDARD_IA \
--server-side-encryption AES256
aws s3 cp "${BACKUP_DIR}/n8n_data_${TIMESTAMP}.tar.gz.gpg" \
s3://company-backups/n8n/ \
--storage-class STANDARD_IA \
--server-side-encryption AES256
# Cleanup old local backups
echo "🧹 Cleaning up old backups..."
find ${BACKUP_DIR} -name "*.gpg" -mtime +${RETENTION_DAYS} -delete
echo "✅ Backup complete: ${TIMESTAMP}"
Cron job:
# Daily backup at 2 AM 0 2 * * * /opt/n8n/backup.sh >> /var/log/n8n-backup.log 2>&1 # Weekly full backup with verification 0 3 * * 0 /opt/n8n/backup-verify.sh >> /var/log/n8n-backup-verify.log 2>&1
8.4. Disaster Recovery Plan
Recovery steps:
#!/bin/bash
# restore.sh
BACKUP_FILE=$1
ENCRYPTION_KEY_FILE=$2
# 1. Stop N8N
docker-compose down
# 2. Download from S3
aws s3 cp s3://company-backups/n8n/${BACKUP_FILE} ./
# 3. Decrypt
gpg --decrypt ${BACKUP_FILE} > ${BACKUP_FILE%.gpg}
# 4. Restore database
gunzip < db_backup.sql.gz | \
docker exec -i n8n-postgres psql -U n8n_user n8n
# 5. Restore N8N data
docker run --rm \
-v n8n-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/n8n_data_backup.tar.gz -C /
# 6. Restore encryption key
docker secret create n8n_encryption_key ${ENCRYPTION_KEY_FILE}
# 7. Start N8N
docker-compose up -d
# 8. Verify
sleep 30
curl -f https://n8n.company.com/healthz || exit 1
echo "✅ Recovery complete"
9. Conclusion
Bảo mật trong N8N automation không phải là một tính năng optional - đó là yêu cầu bắt buộc cho bất kỳ production deployment nào. Qua bài viết này, chúng ta đã đi qua:
Key Takeaways
- Never Trust, Always Verify:
- Không hardcode credentials
- Không commit secrets vào Git
- Không share credentials qua insecure channels
- Luôn encrypt at rest và in transit
- Defense in Depth:
- Multi-layer security: Network → Application → Data
- Secrets management với Vault/AWS Secrets Manager
- Access control và audit logging
- Regular security audits và penetration testing
- Automation Security:
- Environment-specific credentials
- Automated secret rotation
- Encrypted backups
- Disaster recovery planning
- Production Readiness:
- TLS/SSL termination
- Rate limiting
- Monitoring và alerting
- Incident response procedures
Implementation Priority
Phase 1 (Week 1): Foundation
- ✅ Setup N8N_ENCRYPTION_KEY
- ✅ Configure .gitignore for .env files
- ✅ Migrate hardcoded credentials to N8N credential system
- ✅ Enable HTTPS
Phase 2 (Week 2-3): Secrets Management
- ✅ Deploy HashiCorp Vault hoặc setup AWS Secrets Manager
- ✅ Migrate all credentials to vault
- ✅ Implement credential rotation policy
- ✅ Setup backup encryption
Phase 3 (Week 4): Hardening
- ✅ Configure network isolation
- ✅ Implement rate limiting
- ✅ Setup monitoring và alerting
- ✅ Conduct security audit
Phase 4 (Ongoing): Maintenance
- ✅ Quarterly credential rotation
- ✅ Monthly security scans
- ✅ Regular backup testing
- ✅ Security training for team
Final Thoughts
Security là một journey, không phải destination. Threat landscape thay đổi hàng ngày, và automation workflows của bạn cần được cập nhật liên tục để đối phó với các threats mới.
Remember: Chi phí của một security breach (data loss, downtime, reputation damage, legal liability) luôn luôn cao hơn nhiều so với investment vào security infrastructure.
Bắt đầu với basics, implement incrementally, và build security culture trong team. Your future self sẽ cảm ơn bạn.