Bỏ qua để đến Nội dung

Bảo mật trong Automation N8N: Cách quản lý Credentials và biến môi trường an toàn

Hướng dẫn chi tiết về bảo mật N8N automation: quản lý credentials với HashiCorp Vault, AWS Secrets Manager, environment variables best practices, và production security checklist cho Tech Lead.

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:

  1. Least Privilege: Chỉ request scopes tối thiểu cần thiết
  2. Naming Convention: Sử dụng tên mô tả rõ ràng: Production_Stripe_ReadOnly, Dev_AWS_S3_Upload
  3. Separation: Tách credentials cho từng environment
  4. 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

FeatureHashiCorp VaultAWS Secrets ManagerGCP Secret ManagerAzure Key Vault
CostFree (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 CurveHighLowLowMedium
HA SetupComplexManagedManagedManaged
Best ForMulti-cloud, EnterpriseAWS-heavy orgsGCP-nativeAzure-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:

  1. Credential Extraction:
    • Attempt to extract credentials from DB backup
    • Try to access Vault without proper token
    • Test credential sharing bypass
  2. Injection Attacks:
    • SQL injection in Code nodes
    • Command injection in Execute Command nodes
    • SSRF via HTTP Request nodes
  3. Access Control:
    • Attempt privilege escalation
    • Test credential access across users
    • Verify workflow execution permissions
  4. 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

  1. 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
  2. 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
  3. Automation Security:
    • Environment-specific credentials
    • Automated secret rotation
    • Encrypted backups
    • Disaster recovery planning
  4. 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.

Additional Resources