Security

Security Best Practices for Workflow Automation [2025]

House of Loops TeamOctober 21, 202514 min read
Security Best Practices for Workflow Automation [2025]

Security Best Practices for Workflow Automation

Automation workflows often have access to your most sensitive systems and data. A single misconfigured workflow can expose customer data, compromise API credentials, or create backdoors into your infrastructure. This comprehensive guide covers everything you need to secure your automation platform from development through production.

Understanding the Threat Landscape

Before implementing security measures, understand what you're protecting against:

Common Attack Vectors

  1. Credential theft: Exposed API keys, database passwords, OAuth tokens
  2. Code injection: SQL injection, command injection, SSRF attacks
  3. Data exfiltration: Unauthorized data access and export
  4. Privilege escalation: Gaining admin access through workflow manipulation
  5. Supply chain attacks: Compromised dependencies and integrations
  6. Denial of service: Resource exhaustion through malicious workflows
  7. Man-in-the-middle: Intercepting data in transit
  8. Insider threats: Malicious or negligent internal users

Threat Modeling Framework

┌─────────────────────────────────────────────────────────────┐
│                      Threat Model                            │
│                                                              │
│  ┌────────────────┐     ┌──────────────┐   ┌─────────────┐ │
│  │  Entry Points  │────▶│    Assets    │◀──│  Attackers  │ │
│  │                │     │              │   │             │ │
│  │ • Webhooks     │     │ • Credentials│   │ • External  │ │
│  │ • APIs         │     │ • Data       │   │ • Internal  │ │
│  │ • UI           │     │ • Workflows  │   │ • Automated │ │
│  │ • Integrations │     │ • Systems    │   │             │ │
│  └────────────────┘     └──────────────┘   └─────────────┘ │
│         │                      │                    │       │
│         └──────────────────────┼────────────────────┘       │
│                                │                            │
│                        ┌───────▼────────┐                   │
│                        │  Threats &     │                   │
│                        │  Mitigations   │                   │
│                        └────────────────┘                   │
└─────────────────────────────────────────────────────────────┘

Layer 1: Authentication and Authorization

Multi-Factor Authentication

Always require MFA for platform access:

// n8n webhook for MFA verification
{
  "name": "MFA Verification",
  "nodes": [
    {
      "name": "Verify TOTP",
      "type": "n8n-nodes-base.function",
      "parameters": {
        "functionCode": `
const speakeasy = require('speakeasy');

const userSecret = $node["Get User MFA Secret"].json.mfa_secret;
const providedToken = $json.body.mfa_token;

const verified = speakeasy.totp.verify({
  secret: userSecret,
  encoding: 'base32',
  token: providedToken,
  window: 2 // Allow 2 time steps (60 seconds) of drift
});

if (!verified) {
  throw new Error('Invalid MFA token');
}

// Generate session token
const sessionToken = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 3600000); // 1 hour

return {
  session_token: sessionToken,
  expires_at: expiresAt,
  user_id: $json.body.user_id
};
`
      }
    }
  ]
}

Role-Based Access Control (RBAC)

Implement fine-grained permissions:

-- RBAC schema
CREATE TABLE roles (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) UNIQUE NOT NULL,
  description TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE permissions (
  id SERIAL PRIMARY KEY,
  resource VARCHAR(100) NOT NULL, -- workflow, credential, execution
  action VARCHAR(50) NOT NULL,    -- create, read, update, delete, execute
  description TEXT,
  UNIQUE(resource, action)
);

CREATE TABLE role_permissions (
  role_id INTEGER REFERENCES roles(id),
  permission_id INTEGER REFERENCES permissions(id),
  PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
  user_id VARCHAR(255),
  role_id INTEGER REFERENCES roles(id),
  organization_id INTEGER,
  granted_at TIMESTAMP DEFAULT NOW(),
  granted_by VARCHAR(255),
  PRIMARY KEY (user_id, role_id, organization_id)
);

-- Pre-defined roles
INSERT INTO roles (name, description) VALUES
  ('admin', 'Full access to all resources'),
  ('developer', 'Create and manage workflows'),
  ('operator', 'Execute workflows and view results'),
  ('viewer', 'Read-only access');

-- Permission examples
INSERT INTO permissions (resource, action) VALUES
  ('workflow', 'create'),
  ('workflow', 'read'),
  ('workflow', 'update'),
  ('workflow', 'delete'),
  ('workflow', 'execute'),
  ('credential', 'create'),
  ('credential', 'read'),
  ('credential', 'update'),
  ('credential', 'delete'),
  ('execution', 'read'),
  ('execution', 'retry');

Permission Checking Function

// Check user permissions before workflow execution
const checkPermission = async (userId, resource, action) => {
  const result = await db.query(
    `
    SELECT EXISTS (
      SELECT 1
      FROM user_roles ur
      JOIN role_permissions rp ON ur.role_id = rp.role_id
      JOIN permissions p ON rp.permission_id = p.id
      WHERE ur.user_id = $1
        AND p.resource = $2
        AND p.action = $3
        AND ur.organization_id = current_setting('app.current_organization')::int
    ) AS has_permission
  `,
    [userId, resource, action]
  );

  if (!result.rows[0].has_permission) {
    throw new Error(`Permission denied: ${action} on ${resource}`);
  }

  return true;
};

// Use in workflow
const executeWorkflow = async (userId, workflowId) => {
  await checkPermission(userId, 'workflow', 'execute');

  // Log access attempt
  await auditLog({
    user_id: userId,
    action: 'workflow.execute',
    resource_id: workflowId,
    timestamp: new Date(),
    ip_address: request.ip,
  });

  // Execute workflow
  return await runWorkflow(workflowId);
};

Layer 2: Secrets Management

Never Hard-Code Credentials

Bad example:

// ❌ NEVER DO THIS
const apiKey = 'sk-1234567890abcdefg';
const dbPassword = 'MyP@ssw0rd123';

Good example:

// ✅ Use environment variables and secret managers
const apiKey = process.env.API_KEY;
const dbPassword = await getSecret('database/password');

HashiCorp Vault Integration

// Secure credential management with Vault
const Vault = require('node-vault');

class SecretManager {
  constructor() {
    this.vault = Vault({
      apiVersion: 'v1',
      endpoint: process.env.VAULT_ADDR,
      token: process.env.VAULT_TOKEN,
    });
  }

  async getSecret(path) {
    try {
      const result = await this.vault.read(`secret/data/${path}`);
      return result.data.data;
    } catch (error) {
      console.error(`Failed to retrieve secret: ${path}`, error);
      throw new Error('Secret retrieval failed');
    }
  }

  async createSecret(path, data, metadata = {}) {
    // Validate secret before storing
    this.validateSecret(data);

    return await this.vault.write(`secret/data/${path}`, {
      data: data,
      metadata: {
        created_by: metadata.user_id,
        created_at: new Date().toISOString(),
        rotation_period: metadata.rotation_period || 90,
      },
    });
  }

  async rotateSecret(path) {
    const secret = await this.getSecret(path);

    // Generate new secret (implementation depends on secret type)
    const newSecret = await this.generateNewSecret(secret);

    // Store new version
    await this.createSecret(path, newSecret);

    // Update dependent systems
    await this.updateDependentSystems(path, newSecret);

    return newSecret;
  }

  validateSecret(data) {
    // Check for common weak patterns
    const weakPatterns = [/password123/i, /admin/i, /12345/, /qwerty/i];

    const values = Object.values(data).join(' ');

    for (const pattern of weakPatterns) {
      if (pattern.test(values)) {
        throw new Error('Weak secret detected');
      }
    }

    // Check minimum complexity
    if (values.length < 20) {
      throw new Error('Secret too short');
    }
  }

  async listSecretsNeedingRotation() {
    const secrets = await this.vault.list('secret/metadata');
    const needRotation = [];

    for (const secretPath of secrets.data.keys) {
      const metadata = await this.vault.read(`secret/metadata/${secretPath}`);
      const createdAt = new Date(metadata.data.created_time);
      const daysSinceCreation = (Date.now() - createdAt) / (1000 * 60 * 60 * 24);
      const rotationPeriod = metadata.data.custom_metadata?.rotation_period || 90;

      if (daysSinceCreation > rotationPeriod) {
        needRotation.push({
          path: secretPath,
          age_days: Math.floor(daysSinceCreation),
          rotation_period: rotationPeriod,
        });
      }
    }

    return needRotation;
  }
}

// Use in n8n workflow
const secretManager = new SecretManager();
const apiCredentials = await secretManager.getSecret('integrations/stripe/api_key');

Credential Encryption at Rest

// Encrypt credentials before storing in database
const crypto = require('crypto');

class CredentialEncryption {
  constructor(masterKey) {
    this.algorithm = 'aes-256-gcm';
    this.masterKey = Buffer.from(masterKey, 'hex');
  }

  encrypt(data) {
    // Generate unique IV for each encryption
    const iv = crypto.randomBytes(16);

    // Create cipher
    const cipher = crypto.createCipheriv(this.algorithm, this.masterKey, iv);

    // Encrypt data
    const encrypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);

    // Get auth tag
    const authTag = cipher.getAuthTag();

    // Return IV + authTag + encrypted data
    return {
      encrypted: Buffer.concat([iv, authTag, encrypted]).toString('base64'),
      algorithm: this.algorithm,
    };
  }

  decrypt(encryptedData) {
    const buffer = Buffer.from(encryptedData.encrypted, 'base64');

    // Extract components
    const iv = buffer.slice(0, 16);
    const authTag = buffer.slice(16, 32);
    const encrypted = buffer.slice(32);

    // Create decipher
    const decipher = crypto.createDecipheriv(this.algorithm, this.masterKey, iv);
    decipher.setAuthTag(authTag);

    // Decrypt
    const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);

    return JSON.parse(decrypted.toString('utf8'));
  }

  rotateEncryption(oldMasterKey, newMasterKey) {
    // Re-encrypt all credentials with new master key
    const oldCrypto = new CredentialEncryption(oldMasterKey);
    const newCrypto = new CredentialEncryption(newMasterKey);

    // Get all encrypted credentials from database
    const credentials = db.query('SELECT id, encrypted_data FROM credentials');

    for (const cred of credentials) {
      const decrypted = oldCrypto.decrypt(cred.encrypted_data);
      const reEncrypted = newCrypto.encrypt(decrypted);

      db.query('UPDATE credentials SET encrypted_data = $1 WHERE id = $2', [reEncrypted, cred.id]);
    }
  }
}

Layer 3: Input Validation and Sanitization

Prevent Injection Attacks

// SQL Injection Prevention
class SecureDatabase {
  async query(sql, params = []) {
    // ALWAYS use parameterized queries
    return await this.pool.query(sql, params);
  }

  // ❌ NEVER DO THIS
  async unsafeQuery(userId) {
    return await this.pool.query(`SELECT * FROM users WHERE id = ${userId}`);
  }

  // ✅ DO THIS
  async safeQuery(userId) {
    return await this.pool.query('SELECT * FROM users WHERE id = $1', [userId]);
  }

  // Validate and sanitize input
  validateInput(input, schema) {
    const Joi = require('joi');
    const { error, value } = schema.validate(input);

    if (error) {
      throw new Error(`Invalid input: ${error.message}`);
    }

    return value;
  }
}

// Input validation schemas
const schemas = {
  webhook: Joi.object({
    user_id: Joi.string().uuid().required(),
    action: Joi.string().valid('create', 'update', 'delete').required(),
    data: Joi.object().max(100).required(), // Limit object size
  }),

  email: Joi.object({
    to: Joi.string().email().required(),
    subject: Joi.string().max(200).required(),
    body: Joi.string().max(10000).required(),
  }),
};

Command Injection Prevention

// Safely execute external commands
const { execFile } = require('child_process');
const { promisify } = require('util');
const execFileAsync = promisify(execFile);

// ❌ NEVER DO THIS
const unsafeCommand = async userInput => {
  const { exec } = require('child_process');
  return exec(`convert ${userInput} output.pdf`); // Command injection risk!
};

// ✅ DO THIS
const safeCommand = async userInput => {
  // Whitelist allowed commands
  const allowedCommands = ['convert', 'imagemagick'];

  // Validate input
  const sanitizedInput = path.basename(userInput); // Remove path traversal

  // Use execFile with separate arguments (not shell)
  return await execFileAsync('convert', [sanitizedInput, 'output.pdf'], {
    timeout: 30000,
    maxBuffer: 10 * 1024 * 1024, // 10MB
  });
};

SSRF Prevention

// Prevent Server-Side Request Forgery
const validUrl = require('valid-url');
const ipRangeCheck = require('ip-range-check');

const isUrlSafe = (url) => {
  // Validate URL format
  if (!validUrl.isUri(url)) {
    throw new Error('Invalid URL format');
  }

  const parsedUrl = new URL(url);

  // Block internal IP ranges
  const blockedRanges = [
    '10.0.0.0/8',
    '172.16.0.0/12',
    '192.168.0.0/16',
    '127.0.0.0/8',
    '169.254.0.0/16',
    'fc00::/7',
    'fe80::/10',
    '::1/128'
  ];

  // Resolve hostname to IP
  const dns = require('dns').promises;
  const addresses = await dns.resolve4(parsedUrl.hostname);

  for (const ip of addresses) {
    if (ipRangeCheck(ip, blockedRanges)) {
      throw new Error('Access to internal resources blocked');
    }
  }

  // Whitelist allowed domains
  const allowedDomains = [
    'api.stripe.com',
    'api.github.com',
    'hooks.slack.com'
  ];

  if (!allowedDomains.some(domain => parsedUrl.hostname.endsWith(domain))) {
    throw new Error('Domain not in allowlist');
  }

  return true;
};

// Use in HTTP Request node
const safeHttpRequest = async (url, options) => {
  await isUrlSafe(url);

  return await fetch(url, {
    ...options,
    timeout: 10000, // 10 second timeout
    size: 10485760, // 10MB max response size
    redirect: 'manual' // Don't follow redirects automatically
  });
};

Layer 4: Network Security

TLS/SSL Configuration

# nginx.conf - Secure TLS configuration
server {
  listen 443 ssl http2;
  server_name automation.company.com;

  # SSL certificates
  ssl_certificate /etc/ssl/certs/automation.crt;
  ssl_certificate_key /etc/ssl/private/automation.key;

  # Modern TLS configuration
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
  ssl_prefer_server_ciphers on;

  # HSTS
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

  # OCSP stapling
  ssl_stapling on;
  ssl_stapling_verify on;

  # 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 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

  # Rate limiting
  limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
  limit_req zone=api burst=20 nodelay;

  location / {
    proxy_pass http://n8n-cluster;
    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;
  }
}

# Redirect HTTP to HTTPS
server {
  listen 80;
  server_name automation.company.com;
  return 301 https://$server_name$request_uri;
}

Network Segmentation

# Kubernetes NetworkPolicy for n8n isolation
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: n8n-network-policy
  namespace: n8n
spec:
  podSelector:
    matchLabels:
      app: n8n
  policyTypes:
    - Ingress
    - Egress

  # Ingress rules
  ingress:
    # Allow from load balancer
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
      ports:
        - protocol: TCP
          port: 5678

    # Allow from monitoring
    - from:
        - namespaceSelector:
            matchLabels:
              name: monitoring
      ports:
        - protocol: TCP
          port: 9090

  # Egress rules
  egress:
    # Allow to PostgreSQL
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

    # Allow to Redis
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - protocol: TCP
          port: 6379

    # Allow DNS
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53

    # Allow HTTPS to external APIs
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: TCP
          port: 443

Layer 5: Data Protection

Data Classification

// Classify data and apply appropriate protections
const DataClassification = {
  PUBLIC: 'public',
  INTERNAL: 'internal',
  CONFIDENTIAL: 'confidential',
  RESTRICTED: 'restricted',
};

const dataHandlers = {
  [DataClassification.PUBLIC]: {
    encrypt: false,
    log: true,
    retention_days: 365,
  },
  [DataClassification.INTERNAL]: {
    encrypt: true,
    log: true,
    retention_days: 90,
    access_control: 'authenticated',
  },
  [DataClassification.CONFIDENTIAL]: {
    encrypt: true,
    log: true,
    retention_days: 30,
    access_control: 'role_based',
    audit: true,
  },
  [DataClassification.RESTRICTED]: {
    encrypt: true,
    log: false, // Don't log content
    retention_days: 7,
    access_control: 'explicit_grant',
    audit: true,
    mfa_required: true,
    masking: true,
  },
};

// Automatic data classification
const classifyData = data => {
  const patterns = {
    [DataClassification.RESTRICTED]: [
      /\b\d{3}-\d{2}-\d{4}\b/, // SSN
      /\b\d{16}\b/, // Credit card
      /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, // Email (PII)
    ],
    [DataClassification.CONFIDENTIAL]: [/password/i, /secret/i, /api[_-]?key/i, /token/i],
  };

  const dataString = JSON.stringify(data);

  for (const [classification, regexList] of Object.entries(patterns)) {
    for (const regex of regexList) {
      if (regex.test(dataString)) {
        return classification;
      }
    }
  }

  return DataClassification.INTERNAL; // Default
};

// Apply data handling policies
const handleData = async (data, classification) => {
  const policy = dataHandlers[classification];

  let processedData = data;

  // Encryption
  if (policy.encrypt) {
    processedData = await encrypt(processedData);
  }

  // Masking for restricted data
  if (policy.masking) {
    processedData = maskSensitiveFields(processedData);
  }

  // Audit logging
  if (policy.audit) {
    await auditLog({
      action: 'data_access',
      classification: classification,
      user_id: getCurrentUser(),
      timestamp: new Date(),
    });
  }

  // Set retention policy
  await setRetention(processedData, policy.retention_days);

  return processedData;
};

// Mask sensitive data
const maskSensitiveFields = data => {
  const masked = { ...data };

  // Credit cards: show last 4 digits
  if (masked.credit_card) {
    masked.credit_card = masked.credit_card.replace(/\d(?=\d{4})/g, '*');
  }

  // SSN: show last 4 digits
  if (masked.ssn) {
    masked.ssn = masked.ssn.replace(/\d(?=\d{4})/g, '*');
  }

  // Email: show first letter and domain
  if (masked.email) {
    masked.email = masked.email.replace(/^(.)(.*)(@.*)$/, '$1***$3');
  }

  return masked;
};

Data Loss Prevention (DLP)

// DLP rules engine
const DLPRules = {
  rules: [
    {
      name: 'Block SSN in external webhooks',
      pattern: /\b\d{3}-\d{2}-\d{4}\b/,
      action: 'block',
      scope: 'external_webhooks',
    },
    {
      name: 'Alert on credit card transmission',
      pattern: /\b\d{16}\b/,
      action: 'alert',
      scope: 'all',
    },
    {
      name: 'Encrypt health data',
      pattern: /patient|medical|diagnosis/i,
      action: 'encrypt',
      scope: 'all',
    },
  ],

  async check(data, scope) {
    const dataString = JSON.stringify(data);
    const violations = [];

    for (const rule of this.rules) {
      if (rule.scope !== 'all' && rule.scope !== scope) {
        continue;
      }

      if (rule.pattern.test(dataString)) {
        violations.push(rule);

        switch (rule.action) {
          case 'block':
            throw new Error(`DLP violation: ${rule.name}`);

          case 'alert':
            await this.sendAlert(rule, data);
            break;

          case 'encrypt':
            data = await this.enforceEncryption(data);
            break;
        }
      }
    }

    return {
      data,
      violations,
    };
  },

  async sendAlert(rule, data) {
    await sendToSecurityTeam({
      severity: 'high',
      rule: rule.name,
      data_summary: this.createSummary(data),
      timestamp: new Date(),
    });
  },

  createSummary(data) {
    // Create non-sensitive summary for alerting
    return {
      size: JSON.stringify(data).length,
      fields: Object.keys(data),
      classification: classifyData(data),
    };
  },
};

Layer 6: Monitoring and Incident Response

Security Event Monitoring

// Security event detection
const SecurityMonitor = {
  events: [],

  async track(event) {
    this.events.push({
      ...event,
      timestamp: new Date(),
    });

    // Check for suspicious patterns
    await this.analyzeThreat(event);
  },

  async analyzeThreat(event) {
    // Multiple failed authentication attempts
    if (event.type === 'auth_failure') {
      const recentFailures = this.events.filter(
        e =>
          e.type === 'auth_failure' &&
          e.user_id === event.user_id &&
          Date.now() - e.timestamp < 300000 // Last 5 minutes
      );

      if (recentFailures.length >= 5) {
        await this.respondToThreat({
          threat: 'brute_force_attack',
          user_id: event.user_id,
          action: 'lock_account',
        });
      }
    }

    // Unusual data access patterns
    if (event.type === 'data_access') {
      const recentAccess = this.events.filter(
        e =>
          e.type === 'data_access' &&
          e.user_id === event.user_id &&
          Date.now() - e.timestamp < 3600000 // Last hour
      );

      if (recentAccess.length > 100) {
        await this.respondToThreat({
          threat: 'data_exfiltration_attempt',
          user_id: event.user_id,
          action: 'alert_security_team',
        });
      }
    }

    // Privilege escalation attempts
    if (event.type === 'permission_denied' && event.resource === 'admin') {
      await this.respondToThreat({
        threat: 'privilege_escalation_attempt',
        user_id: event.user_id,
        action: 'alert_security_team',
      });
    }
  },

  async respondToThreat(threat) {
    // Log to SIEM
    await this.logToSIEM(threat);

    // Execute automated response
    switch (threat.action) {
      case 'lock_account':
        await lockUserAccount(threat.user_id);
        break;

      case 'alert_security_team':
        await sendSecurityAlert(threat);
        break;

      case 'block_ip':
        await blockIPAddress(threat.ip_address);
        break;
    }

    // Create incident ticket
    await createIncident(threat);
  },

  async logToSIEM(event) {
    // Send to Splunk, ELK, or other SIEM
    await fetch(process.env.SIEM_ENDPOINT, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...event,
        source: 'n8n-automation',
        timestamp: new Date().toISOString(),
      }),
    });
  },
};

Incident Response Playbook

# incident-response.yml
incident_types:
  credential_leak:
    severity: critical
    steps:
      - action: rotate_all_credentials
        timeout: 15m
      - action: revoke_active_sessions
        timeout: 5m
      - action: notify_security_team
        immediate: true
      - action: audit_recent_access
        window: 24h
      - action: notify_affected_users
        sla: 2h

  data_breach:
    severity: critical
    steps:
      - action: isolate_affected_systems
        timeout: 5m
      - action: preserve_evidence
        immediate: true
      - action: notify_legal_team
        immediate: true
      - action: begin_forensics
        sla: 1h
      - action: notify_authorities
        sla: 72h # GDPR requirement

  unauthorized_access:
    severity: high
    steps:
      - action: lock_account
        immediate: true
      - action: review_access_logs
        sla: 1h
      - action: check_lateral_movement
        sla: 2h
      - action: reset_credentials
        sla: 4h

  dos_attack:
    severity: medium
    steps:
      - action: enable_rate_limiting
        immediate: true
      - action: activate_waf_rules
        timeout: 5m
      - action: scale_infrastructure
        timeout: 10m
      - action: identify_attack_source
        sla: 30m

Compliance Frameworks

GDPR Compliance

// GDPR data subject rights implementation
const GDPRCompliance = {
  // Right to access
  async exportUserData(userId) {
    const userData = {
      personal_info: await db.query('SELECT * FROM users WHERE id = $1', [userId]),
      workflows: await db.query('SELECT * FROM workflows WHERE user_id = $1', [userId]),
      executions: await db.query('SELECT * FROM executions WHERE user_id = $1', [userId]),
      audit_log: await db.query('SELECT * FROM audit_log WHERE user_id = $1', [userId]),
    };

    return {
      format: 'json',
      data: userData,
      generated_at: new Date().toISOString(),
    };
  },

  // Right to erasure
  async deleteUserData(userId) {
    await db.query('BEGIN');

    try {
      // Anonymize instead of delete for audit trail
      await db.query(
        `
        UPDATE users
        SET email = 'deleted-' || id || '@anonymized.local',
            name = 'Deleted User',
            deleted_at = NOW()
        WHERE id = $1
      `,
        [userId]
      );

      // Delete personal data from workflows
      await db.query('DELETE FROM workflows WHERE user_id = $1', [userId]);

      // Anonymize execution data
      await db.query(
        `
        UPDATE executions
        SET user_id = NULL,
            data = NULL
        WHERE user_id = $1
      `,
        [userId]
      );

      await db.query('COMMIT');

      // Log deletion for compliance
      await auditLog({
        action: 'gdpr_data_deletion',
        user_id: userId,
        timestamp: new Date(),
      });
    } catch (error) {
      await db.query('ROLLBACK');
      throw error;
    }
  },

  // Right to rectification
  async updateUserData(userId, updates) {
    await db.query(
      `
      UPDATE users
      SET ${Object.keys(updates)
        .map((k, i) => `${k} = $${i + 2}`)
        .join(', ')}
      WHERE id = $1
    `,
      [userId, ...Object.values(updates)]
    );

    await auditLog({
      action: 'gdpr_data_rectification',
      user_id: userId,
      changes: Object.keys(updates),
      timestamp: new Date(),
    });
  },

  // Data breach notification
  async notifyDataBreach(incident) {
    const affectedUsers = await this.getAffectedUsers(incident);

    // Must notify within 72 hours of discovery
    const deadline = new Date(incident.discovered_at);
    deadline.setHours(deadline.getHours() + 72);

    // Notify supervisory authority
    await this.notifySupervisoryAuthority({
      incident: incident,
      affected_count: affectedUsers.length,
      deadline: deadline,
    });

    // Notify affected individuals
    for (const user of affectedUsers) {
      await this.notifyUser(user, {
        incident_type: incident.type,
        data_affected: incident.data_types,
        actions_taken: incident.mitigation_steps,
        contact: 'privacy@company.com',
      });
    }
  },
};

SOC 2 Controls

// SOC 2 control implementation
const SOC2Controls = {
  // CC6.1 - Logical Access Controls
  async enforceAccessControls() {
    // Implement least privilege
    await this.reviewUserPermissions();

    // Enforce MFA
    const usersWithoutMFA = await db.query(`
      SELECT user_id FROM users
      WHERE mfa_enabled = false
        AND last_login > NOW() - INTERVAL '30 days'
    `);

    for (const user of usersWithoutMFA.rows) {
      await this.enforceMFA(user.user_id);
    }
  },

  // CC7.2 - System Monitoring
  async monitorSystems() {
    // Monitor for security events
    const suspiciousActivities = await SecurityMonitor.getRecentThreats();

    // Check system health
    const healthMetrics = await this.collectHealthMetrics();

    // Generate compliance report
    return {
      security_events: suspiciousActivities.length,
      system_uptime: healthMetrics.uptime,
      failed_logins: suspiciousActivities.filter(a => a.type === 'auth_failure').length,
      timestamp: new Date(),
    };
  },

  // CC8.1 - Change Management
  async trackChanges(change) {
    await db.query(
      `
      INSERT INTO change_log (
        change_type,
        resource_id,
        old_value,
        new_value,
        changed_by,
        approved_by,
        timestamp
      ) VALUES ($1, $2, $3, $4, $5, $6, NOW())
    `,
      [
        change.type,
        change.resource_id,
        JSON.stringify(change.old_value),
        JSON.stringify(change.new_value),
        change.changed_by,
        change.approved_by,
      ]
    );
  },
};

Security Testing

Automated Security Scanning

# .github/workflows/security-scan.yml
name: Security Scan

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * *' # Daily

jobs:
  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run Snyk dependency scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

  secret-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/owasp-top-ten

  container-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build image
        run: docker build -t n8n-app .

      - name: Run Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: n8n-app
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

Penetration Testing Checklist

  • Authentication bypass attempts
  • Authorization testing (vertical and horizontal privilege escalation)
  • Input validation testing (SQL injection, XSS, command injection)
  • Session management testing
  • Cryptography testing
  • Business logic testing
  • API security testing
  • Configuration testing
  • Error handling testing
  • Denial of service testing

Security Checklist for Production

Pre-Deployment

  • All secrets stored in secret manager (Vault/AWS Secrets Manager)
  • Database connections use encrypted connections (SSL/TLS)
  • All API endpoints require authentication
  • Rate limiting configured on all public endpoints
  • Input validation implemented on all user inputs
  • RBAC properly configured
  • Audit logging enabled for all sensitive operations
  • Security headers configured (CSP, HSTS, etc.)
  • Network policies restrict unnecessary communication
  • Backup and disaster recovery tested

Post-Deployment

  • Security monitoring and alerting active
  • Regular security scans scheduled
  • Incident response plan documented and tested
  • Security training completed for all team members
  • Regular access reviews scheduled
  • Vulnerability management process in place
  • Compliance requirements met and documented
  • Third-party security audit completed

Join the Community

Security is an ongoing journey, not a destination. The House of Loops community includes security professionals, compliance experts, and automation engineers who share knowledge about securing automation workflows.

Join us to:

  • Get security reviews of your workflows
  • Access security-hardened templates
  • Participate in security workshops
  • Stay updated on emerging threats
  • Connect with security-focused developers

Join House of Loops Today and build secure automation systems with confidence.


Have security questions? Our community has experts ready to help secure your automation platform!

H

House of Loops Team

House of Loops is a technology-focused community for learning and implementing advanced automation workflows using n8n, Strapi, AI/LLM, and DevSecOps tools.

Join Our Community

Join 1,000+ automation enthusiasts