·QueueHub Team·21 min read

Securing Redis for BullMQ on Kubernetes: Network Isolation, Secrets Management & Security Observability

RedisKubernetesSecurityBullMQcert-managerTerraformPrometheusHashiCorp Vault

Running Redis for BullMQ in production is a lot like locking your front door but leaving the garage window open. You've probably set up TLS and passwords — that's the front door lock. But the infrastructure around your Redis, the way credentials flow through your system, and whether you can actually detect an attack in progress — that's the garage window.

BullMQ stores job payloads, results, and scheduling state in Redis. A compromised Redis instance means an attacker can read every job your system processes, inject poisoned payloads, exfiltrate sensitive data, or silently drop jobs. For platforms handling payments, PII, or internal business logic, this is a critical risk.

This guide assumes you've already covered the basics — TLS, Redis passwords, and ACLs (if you haven't, start with our first post on Redis security fundamentals). Here, we tackle four operational layers that most teams neglect: Kubernetes-native controls, secrets management at scale, security monitoring and forensics, and Infrastructure-as-Code compliance — with cloud-specific patterns for AWS, GCP, and Azure.


Section 1: Kubernetes-Native Redis Security

Kubernetes doesn't secure Redis by default. Every pod can reach every other pod. Your Redis port (6379) is exposed to the entire cluster unless you explicitly lock it down. Here's the Kubernetes-native approach to closing those gaps.

1.1 NetworkPolicies: Micro-Segmentation for Redis

A NetworkPolicy tells Kubernetes which pods can talk to which other pods and on which ports. Without it, any compromised container in your cluster can hit Redis directly.

The goal is straightforward: only BullMQ workers and your Queue Hub server should reach Redis. Nothing else. Here's a production-grade NetworkPolicy that enforces that:

# redis-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: redis-ingress-restrict
  namespace: queue-hub
spec:
  podSelector:
    matchLabels:
      app: redis
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/component: worker
          namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: queue-hub
        - podSelector:
            matchLabels:
              app.kubernetes.io/component: server
          namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: queue-hub
      ports:
        - protocol: TCP
          port: 6379
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - protocol: TCP
          port: 6379
---
# Deny all other ingress explicitly (defense in depth)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: redis-deny-all-other-ingress
  namespace: queue-hub
spec:
  podSelector:
    matchLabels:
      app: redis
  policyTypes:
    - Ingress
  ingress: []

Why two policies? The first allows specific traffic. The second acts as a catch-all — any pod not matching the worker or server labels gets denied at the network layer. This prevents a compromised CI runner or debug pod from reaching Redis even if someone forgets to label it correctly.

Testing it: Deploy a debug pod and try to connect:

kubectl run -it --rm debug --image=nicolaka/netshoot -- redis-cli -h redis-service

If the NetworkPolicy is working, this connection will hang and eventually time out.

Advanced: CiliumNetworkPolicy for L7-aware rules. If you're running Cilium as your CNI, you can block dangerous Redis commands at the network layer — FLUSHALL, CONFIG, SLAVEOF, DEBUG — before they ever reach the Redis process. This adds a defense-in-depth layer even for clients that have valid credentials.

1.2 cert-manager: Automated TLS Certificate Lifecycle

Manual TLS certificate management for Redis is a common source of production outages. A cert expires → BullMQ workers can't connect → queues back up → on-call gets paged at 3 AM. cert-manager eliminates this by handling issuance, renewal, and secret population automatically.

Architecture: cert-manager issues certificates from a self-signed cluster issuer → stores them as Kubernetes Secrets → Redis reads them from mounted volumes. On renewal, cert-manager updates the Secret atomically, and Redis 7.2+ can reload TLS config without restarting:

# redis-tls-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: redis-ca-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: redis-tls-cert
  namespace: queue-hub
spec:
  secretName: redis-tls
  duration: 2160h # 90 days
  renewBefore: 360h # 15 days before expiry
  commonName: redis.queue-hub.svc.cluster.local
  dnsNames:
    - redis
    - redis.queue-hub.svc.cluster.local
    - redis.queue-hub.svc
  issuerRef:
    name: redis-ca-issuer
    kind: ClusterIssuer

Mount the certificate Secret into your Redis StatefulSet:

# StatefulSet fragment — TLS volume mounts
spec:
  template:
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          command:
            - redis-server
            - /etc/redis/redis.conf
          ports:
            - containerPort: 6380
              name: redis-tls
          volumeMounts:
            - name: redis-config
              mountPath: /etc/redis
            - name: redis-tls
              mountPath: /etc/redis/tls
              readOnly: true
            - name: redis-data
              mountPath: /data
      volumes:
        - name: redis-config
          configMap:
            name: redis-config
        - name: redis-tls
          secret:
            secretName: redis-tls

Redis config to enable TLS with the cert-manager certs:

# redis-tls.conf
tls-port 6380
port 0
tls-cert-file /etc/redis/tls/tls.crt
tls-key-file /etc/redis/tls/tls.key
tls-ca-cert-file /etc/redis/tls/ca.crt
tls-auth-clients no
tls-replication yes

Your BullMQ application connects using the CA-verified TLS certificate:

import { Queue, Worker, QueueEvents } from 'bullmq';
import { Redis } from 'ioredis';
import * as fs from 'node:fs';

const tlsCa = fs.readFileSync('/etc/queue-hub/redis-ca.crt', 'utf-8');

const connection = new Redis({
  host: 'redis.queue-hub.svc.cluster.local',
  port: 6380,
  tls: {
    ca: [tlsCa],
    rejectUnauthorized: true,
    servername: 'redis.queue-hub.svc.cluster.local',
  },
  maxRetriesPerRequest: null,
  enableReadyCheck: false,
});

const queue = new Queue('notifications', { connection });
const worker = new Worker('notifications', async (job) => {
  // process notification job
}, { connection });
const events = new QueueEvents('notifications', { connection });

// Graceful shutdown
async function shutdown() {
  await worker.close();
  await queue.close();
  await events.close();
  await connection.quit();
}
process.on('SIGTERM', shutdown);

Handling cert rotation: cert-manager updates the Secret automatically, but Redis needs a signal to reload. Options include:

  • Stakater Reloader: watches Secret changes and restarts the pod
  • Redis 7.2+ tls-reload: call redis-cli CONFIG SET tls-cert-file ... on cert rotation via a custom controller
  • Sidecar watcher: a lightweight sidecar that monitors file modification timestamps and triggers a Redis reload

1.3 StatefulSet with Encrypted Persistent Storage

Redis persistence files (RDB snapshots, AOF logs) contain job data — potentially including sensitive payloads. Encrypt them at rest.

Create an encrypted StorageClass and reference it in your StatefulSet's volumeClaimTemplates:

# storage-class-encrypted.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ssd-encrypted
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  encrypted: "true"
  kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/abc-123"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

Then reference it in the StatefulSet from Section 1.2:

volumeClaimTemplates:
  - metadata:
      name: redis-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: ssd-encrypted
      resources:
        requests:
          storage: 10Gi

1.4 Pod Security Standards for Redis Containers

The default Redis Docker image runs as root. This violates the Kubernetes Pod Security Standard (PSS) restricted profile. Running Redis as non-root reduces the blast radius if a container is compromised.

# In the StatefulSet template:
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 999
    runAsGroup: 999
    fsGroup: 999
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: redis
      securityContext:
        allowPrivilegeEscalation: false
        privileged: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
          add:
            - CHOWN       # needed for Redis to write to /data
            - SETUID      # needed for Redis background save
            - SETGID
      # ... rest of container spec

If readOnlyRootFilesystem is true, mount emptyDir volumes for /data and /tmp — Redis needs both for persistence and temporary files.


Section 2: Secrets Management for Redis Credentials

The most common pattern for Redis credentials in Kubernetes is a static Secret referenced as an environment variable. This is better than hardcoding passwords — but it's far from production-ready. Static Secrets are base64-encoded (not encrypted), never rotated, and visible in /proc/self/environ to any process in the container.

2.1 HashiCorp Vault Integration via External Secrets Operator

External Secrets Operator (ESO) bridges Kubernetes and external secret stores. Here's the pattern with HashiCorp Vault:

Architecture: Vault stores Redis credentials → ESO's ClusterSecretStore connects to Vault → ExternalSecret resource syncs to a native K8s Secret → your BullMQ pod mounts it.

# cluster-secret-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: "https://vault.queue-hub.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "queue-hub-role"
          serviceAccountRef:
            name: external-secrets-sa
---
# external-secret-redis.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: redis-credentials
  namespace: queue-hub
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: redis-credentials
    creationPolicy: Owner
    deletionPolicy: Retain
  data:
    - secretKey: redis-host
      remoteRef:
        key: "secret/data/queue-hub/redis/production"
        property: "host"
    - secretKey: redis-port
      remoteRef:
        key: "secret/data/queue-hub/redis/production"
        property: "port"
    - secretKey: redis-password
      remoteRef:
        key: "secret/data/queue-hub/redis/production"
        property: "password"
    - secretKey: redis-username
      remoteRef:
        key: "secret/data/queue-hub/redis/production"
        property: "username"
    - secretKey: redis-tls-ca
      remoteRef:
        key: "secret/data/queue-hub/redis/production"
        property: "tls_ca"

Vault policy to restrict access:

path "secret/data/queue-hub/redis/*" {
  capabilities = ["read", "list"]
}
path "secret/metadata/queue-hub/redis/*" {
  capabilities = ["list"]
}

2.2 AWS Secrets Manager (without Vault in the Stack)

Teams without Vault can use AWS Secrets Manager directly with ESO's AWS provider. This integrates with IAM roles for service accounts (IRSA) and supports automatic password rotation via Lambda:

# cluster-secret-store-aws.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: aws-secretsmanager
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa
---
# external-secret-aws-redis.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: redis-credentials
  namespace: queue-hub
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: aws-secretsmanager
    kind: ClusterSecretStore
  target:
    name: redis-credentials
    creationPolicy: Owner
    deletionPolicy: Retain
  data:
    - secretKey: redis-url
      remoteRef:
        key: queue-hub/redis/production
        property: "REDIS_URL"

2.3 Environment Variables vs Mounted Volumes

When consuming secrets in your BullMQ application, prefer volume mounts over environment variables. Environment variables leak via /proc/self/environ, appear in crash reports, and can be accidentally logged. Volume mounts restrict file access to processes with filesystem access and support permission modes:

# deployment-bullmq-server.yaml (partial)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: queue-hub-server
  namespace: queue-hub
spec:
  template:
    spec:
      containers:
        - name: server
          image: queue-hub/server:latest
          env:
            - name: REDIS_HOST
              valueFrom:
                secretKeyRef:
                  name: redis-credentials
                  key: redis-host
            - name: REDIS_PORT
              valueFrom:
                secretKeyRef:
                  name: redis-credentials
                  key: redis-port
          volumeMounts:
            - name: redis-credentials
              mountPath: /etc/queue-hub/secrets/redis
              readOnly: true
      volumes:
        - name: redis-credentials
          secret:
            secretName: redis-credentials
            defaultMode: 0400

And the TypeScript side — reading credentials from mounted files:

import { Redis } from 'ioredis';
import { Queue, Worker, QueueEvents } from 'bullmq';
import * as fs from 'node:fs';

interface RedisCredentials {
  host: string;
  port: number;
  password: string;
  username?: string;
  tlsCa?: string;
}

function loadRedisCredentials(secretsPath: string): RedisCredentials {
  const host = fs.readFileSync(`${secretsPath}/redis-host`, 'utf-8').trim();
  const port = parseInt(fs.readFileSync(`${secretsPath}/redis-port`, 'utf-8').trim(), 10);
  const password = fs.readFileSync(`${secretsPath}/redis-password`, 'utf-8').trim();

  let tlsCa: string | undefined;
  try {
    tlsCa = fs.readFileSync(`${secretsPath}/redis-tls-ca`, 'utf-8').trim();
  } catch {
    // TLS CA is optional
  }

  return { host, port, password, tlsCa };
}

function createBullMQConnection(creds: RedisCredentials): Redis {
  const options: import('ioredis').RedisOptions = {
    host: creds.host,
    port: creds.port,
    password: creds.password,
    maxRetriesPerRequest: null,
    enableReadyCheck: false,
  };

  if (creds.tlsCa) {
    options.tls = {
      ca: [creds.tlsCa],
      rejectUnauthorized: true,
      servername: creds.host,
    };
  }

  if (creds.username) {
    options.username = creds.username;
  }

  return new Redis(options);
}

// Application startup
const creds = loadRedisCredentials('/etc/queue-hub/secrets/redis');
const connection = createBullMQConnection(creds);

export const taskQueue = new Queue('tasks', { connection });
export const taskWorker = new Worker('tasks', async (job) => {
  console.log(`Processing job ${job.id}: ${job.name}`);
  // process the job
}, { connection });

Section 3: Redis Security Monitoring and Forensics

Even with perfect TLS, ACLs, and NetworkPolicies, you still need to detect active attacks. Redis has built-in tools for this — you just have to use them.

3.1 ACL LOG Monitoring — Detecting Attack Patterns

Redis ACL LOG records every authentication failure and command authorization denial. Attack patterns it reveals:

  • Brute-force password guessing: repeated AUTH failures from the same source
  • Privilege escalation attempts: unauthorized CONFIG, DEBUG, SHUTDOWN commands
  • Key access violations: attempts to read/write keys outside the user's key pattern

Here's a polling script that detects these patterns and exports Prometheus-style metrics:

import { Redis } from 'ioredis';

interface AclLogEntry {
  count: number;
  reason: string;
  context: string;
  object: string;
  username: string;
  ageSeconds: number;
  clientInfo: string;
}

async function pollAclLog(redis: Redis, minCount: number = 5): Promise<AclLogEntry[]> {
  const raw = await redis.call('ACL', 'LOG', '0') as unknown[][];

  const entries: AclLogEntry[] = raw.map((entry: any[]) => ({
    count: Number(entry[1]),
    reason: String(entry[3]),
    context: String(entry[5]),
    object: String(entry[7]),
    username: String(entry[9]),
    ageSeconds: Number(entry[11]),
    clientInfo: String(entry[13]),
  }));

  return entries.filter((e) => e.count >= minCount);
}

async function checkForAttacks(entries: AclLogEntry[]): Promise<string[]> {
  const alerts: string[] = [];
  const authFailures = entries.filter((e) => e.reason === 'auth' && e.context === 'AUTH');
  const commandViolations = entries.filter(
    (e) =>
      e.reason === 'command' &&
      ['CONFIG', 'DEBUG', 'SHUTDOWN', 'FLUSHALL', 'SLAVEOF', 'ROLE'].includes(e.context),
  );

  if (authFailures.length > 20) {
    alerts.push(
      `BRUTE_FORCE: ${authFailures.length} AUTH failures in recent ACL LOG. ` +
        `Users targeted: ${[...new Set(authFailures.map((e) => e.username))].join(', ')}`
    );
  }

  if (commandViolations.length > 0) {
    alerts.push(
      `PRIVILEGE_ESCALATION: Blocked dangerous commands — ` +
        `${commandViolations.map((e) => `${e.context} (user: ${e.username})`).join(', ')}`
    );
  }

  return alerts;
}

export { pollAclLog, checkForAttacks };

3.2 Slowlog Analysis for Security Anomalies

Redis SLOWLOG isn't just for performance tuning. Anomalous slow commands can indicate an attacker probing your key space or exfiltrating data. Configure it in redis.conf:

slowlog-log-slower-than 10000   # 10ms threshold
slowlog-max-len 128

A security-focused slowlog scanner:

import { Redis } from 'ioredis';

interface SlowlogEntry {
  id: number;
  timestamp: number;
  durationMicros: number;
  command: string[];
  clientAddress: string;
  clientName: string;
}

async function scanSlowlog(redis: Redis, threshold: number = 100): Promise<SlowlogEntry[]> {
  const raw = await redis.call('SLOWLOG', 'GET', threshold) as unknown[][];

  return raw.map((entry: any[]) => ({
    id: Number(entry[0]),
    timestamp: Number(entry[1]),
    durationMicros: Number(entry[2]),
    command: (entry[3] as string[]).slice(0, 3),
    clientAddress: String(entry[4] || 'unknown'),
    clientName: String(entry[5] || ''),
  }));
}

function detectSuspiciousSlowlog(entries: SlowlogEntry[]): SlowlogEntry[] {
  const dangerousCommands = new Set([
    'KEYS', 'FLUSHALL', 'FLUSHDB', 'CONFIG', 'DEBUG',
    'EVAL', 'EVALSHA', 'SCRIPT', 'SLAVEOF', 'REPLICAOF',
    'MIGRATE', 'RESTORE', 'SORT', 'BGREWRITEAOF', 'BGSAVE',
  ]);

  return entries.filter((entry) => {
    const cmd = entry.command[0]?.toUpperCase();
    if (!cmd) return false;

    // Flag dangerous commands regardless of duration
    if (dangerousCommands.has(cmd)) return true;

    // Flag KEYS/SCAN with large result sets
    if ((cmd === 'KEYS' || cmd === 'SCAN') && entry.durationMicros > 50_000) return true;

    // Flag suspiciously slow bulk reads
    if ((cmd === 'MGET' || cmd === 'GET') && entry.durationMicros > 200_000) return true;

    return false;
  });
}

export { scanSlowlog, detectSuspiciousSlowlog };

3.3 Prometheus Exporter + Grafana Alerting

The redis_exporter (oliver006/redis_exporter) exposes ACL LOG counts, slowlog, and command stats as Prometheus metrics. Add these security-specific PrometheusRules:

# redis-security-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: redis-security-alerts
  namespace: queue-hub
spec:
  groups:
    - name: redis-security
      interval: 30s
      rules:
        - alert: RedisACLBruteForce
          expr: |
            rate(redis_acl_log_denied_commands_total{reason="auth"}[5m]) > 1
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: "Redis ACL brute-force attack detected on {{ $labels.instance }}"
            description: >
              More than 1 auth failure per second over 5 minutes on {{ $labels.instance }}.

        - alert: RedisDangerousCommandExecuted
          expr: |
            sum by (instance, cmd) (
              rate(redis_command_calls_total{cmd=~"FLUSHALL|FLUSHDB|DEBUG|CONFIG|SLAVEOF|SHUTDOWN"}[5m])
            ) > 0
          for: 1m
          labels:
            severity: critical
          annotations:
            summary: "Dangerous Redis command executed on {{ $labels.instance }}"
            description: "{{ $labels.cmd }} was executed on {{ $labels.instance }}."

        - alert: RedisKeyEvictionWarning
          expr: |
            rate(redis_evicted_keys_total[5m]) > 100
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Redis key eviction rate high on {{ $labels.instance }}"
            description: >
              Evicting {{ $value | humanize }} keys/sec. This may cause
              job data loss in BullMQ queues.

        - alert: RedisConnectionSaturation
          expr: |
            redis_connected_clients / redis_config_maxclients > 0.8
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Redis connection saturation on {{ $labels.instance }}"
            description: >
              {{ $value | humanizePercentage }} of max connections used.
              BullMQ workers may fail to establish new connections.

3.4 Redis Security Audit CLI

A portable CLI that any DevOps engineer can run against a Redis instance. It checks protected-mode, authentication, TLS, dangerous command renaming, slowlog configuration, and idle connection timeout:

#!/usr/bin/env tsx
import { Redis } from 'ioredis';

interface AuditCheck {
  name: string;
  status: 'PASS' | 'WARN' | 'FAIL';
  message: string;
  remediation?: string;
}

async function auditRedisSecurity(connection: Redis): Promise<AuditCheck[]> {
  const checks: AuditCheck[] = [];
  const config = await connection.config('GET', [
    'protected-mode', 'requirepass', 'port', 'tls-port',
    'tls-cert-file', 'tls-auth-clients', 'slowlog-log-slower-than',
    'maxclients', 'timeout',
  ]);

  // 1. Protected mode
  if (config[1] === 'yes') {
    checks.push({ name: 'protected-mode', status: 'PASS', message: 'Protected mode is enabled' });
  } else {
    checks.push({
      name: 'protected-mode', status: 'FAIL',
      message: 'Protected mode is disabled — Redis is exposed without auth',
      remediation: 'Set protected-mode yes in redis.conf or configure requirepass + bind',
    });
  }

  // 2. Authentication
  const hasPassword = config[3]?.length > 0;
  if (hasPassword) {
    checks.push({ name: 'authentication', status: 'PASS', message: 'requirepass is set' });
  } else {
    checks.push({
      name: 'authentication', status: 'FAIL',
      message: 'No password configured — anonymous access allowed',
      remediation: 'Set requirepass or configure ACL users',
    });
  }

  // 3. TLS
  const tlsPort = config[7];
  const tlsCert = config[9];
  if (tlsPort && tlsPort !== '' && tlsPort !== '0' && tlsCert) {
    checks.push({
      name: 'tls', status: 'PASS',
      message: `TLS enabled on port ${tlsPort} with certificate at ${tlsCert}`,
    });
  } else {
    checks.push({
      name: 'tls', status: 'WARN',
      message: 'TLS not configured — traffic is unencrypted',
      remediation: 'Use cert-manager or manual TLS certs',
    });
  }

  // 4. Slowlog configured for security
  const slowlogThreshold = parseInt(config[13], 10);
  if (slowlogThreshold <= 10000) {
    checks.push({ name: 'slowlog', status: 'PASS', message: `Slowlog threshold: ${slowlogThreshold}μs` });
  } else {
    checks.push({
      name: 'slowlog', status: 'WARN',
      message: `Slowlog threshold is ${slowlogThreshold}μs — too high for security monitoring`,
      remediation: 'Set slowlog-log-slower-than 10000 (10ms)',
    });
  }

  // 5. Idle connection timeout
  const timeout = parseInt(config[17], 10);
  if (timeout > 0 && timeout <= 300) {
    checks.push({ name: 'timeout', status: 'PASS', message: `Idle timeout: ${timeout}s` });
  } else {
    checks.push({
      name: 'timeout', status: 'WARN',
      message: `Idle timeout is ${timeout || 'disabled'} — connections not cleaned up`,
      remediation: 'Set timeout 300 to close idle connections after 5 minutes',
    });
  }

  return checks;
}

async function main() {
  const url = process.env.REDIS_URL;
  if (!url) {
    console.error('Usage: REDIS_URL=redis://... tsx redis-audit.ts');
    process.exit(1);
  }

  const connection = new Redis(url, { maxRetriesPerRequest: null, enableReadyCheck: false });
  const checks = await auditRedisSecurity(connection);

  console.log('\n=== Redis Security Audit ===\n');
  for (const check of checks) {
    const icon = check.status === 'PASS' ? '✓' : check.status === 'WARN' ? '⚠' : '✗';
    console.log(` ${icon} [${check.status}] ${check.name}`);
    console.log(`   ${check.message}`);
    if (check.remediation) console.log(`   Fix: ${check.remediation}`);
    console.log();
  }

  const passCount = checks.filter((c) => c.status === 'PASS').length;
  console.log(`Summary: ${passCount}/${checks.length} checks passed`);
  await connection.quit();
}

main().catch(console.error);

Section 4: Infrastructure-as-Code Security

Manual Redis configuration is fragile and unverifiable. The only way to enforce security across multiple environments is to codify it.

4.1 Terraform Module for Secure ElastiCache

Here's a Terraform module that provisions an ElastiCache (Valkey/Redis) cluster with encryption, security groups, and IAM auth — every option locked to its most secure value:

# modules/secure-redis-eks/main.tf
resource "aws_elasticache_replication_group" "bullmq" {
  replication_group_id          = "bullmq-redis-${var.environment}"
  description                   = "BullMQ Redis cluster - ${var.environment}"
  node_type                     = var.node_type
  num_cache_clusters            = var.num_replicas + 1
  port                          = 6379
  parameter_group_name          = "default.redis7.cluster.on"

  # Security: encryption at rest and in transit
  at_rest_encryption_enabled    = true
  transit_encryption_enabled    = true
  auth_token                    = random_password.redis_auth.result

  # Network: VPC only
  subnet_group_name             = aws_elasticache_subnet_group.private.name
  security_group_ids            = [aws_security_group.redis.id]

  # Maintenance
  auto_minor_version_upgrade    = true
  maintenance_window            = "sun:05:00-sun:06:00"

  tags = var.tags
}

resource "random_password" "redis_auth" {
  length  = 32
  special = false
}

resource "aws_security_group" "redis" {
  name        = "bullmq-redis-${var.environment}"
  description = "Security group for BullMQ Redis"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 6379
    to_port         = 6379
    protocol        = "tcp"
    security_groups = [var.worker_sg_id, var.server_sg_id]
    description     = "BullMQ workers and QueueHub server only"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

4.2 OPA/conftest Compliance Policies

Validate your redis.conf and infrastructure code against security policies using Open Policy Agent via conftest. Example policy — reject configurations without TLS or with weak passwords:

# policy/redis-security.rego
package main

deny[msg] {
  input.port == 6379
  input["tls-port"] == ""
  input["tls-cert-file"] == ""
  msg = "Plain Redis port 6379 is exposed without TLS — bind to 127.0.0.1 or disable plain port"
}

deny[msg] {
  input["requirepass"] == ""
  not input["aclfile"]
  msg = "No password or ACL file configured. Set requirepass or aclfile"
}

deny[msg] {
  input["rename-command"] == ""
  msg = "Dangerous commands not renamed. Recommended: FLUSHALL, FLUSHDB, CONFIG, DEBUG"
}

warn[msg] {
  input["protected-mode"] == "no"
  msg = "Protected mode is disabled. Enable it unless Redis is firewalled"
}

Run the policy in your CI pipeline:

# .github/workflows/redis-security.yaml
name: Redis Security Compliance
on: [pull_request]
jobs:
  conftest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install conftest
        run: |
          wget https://github.com/open-policy-agent/conftest/releases/download/v0.45.0/conftest_0.45.0_Linux_x86_64.tar.gz
          tar xzf conftest_0.45.0_Linux_x86_64.tar.gz
          sudo mv conftest /usr/local/bin/
      - name: Validate redis.conf
        run: |
          conftest test deploy/redis/redis.conf \
            --policy policy/redis-security.rego \
            --all-namespaces
      - name: Validate Terraform
        run: |
          conftest test modules/secure-redis-eks/main.tf \
            --policy policy/terraform-elasticache.rego \
            --all-namespaces

4.3 CI/CD Security Scanning with pre-commit

Add security scanning as a pre-commit hook so issues are caught before they reach PR review:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/bridgecrewio/checkov
    rev: 3.2.0
    hooks:
      - id: checkov
        args:
          - --directory=deploy/redis
          - --directory=modules/secure-redis-eks

  - repo: https://github.com/open-policy-agent/conftest
    rev: v0.45.0
    hooks:
      - id: conftest
        args:
          - test
          - deploy/redis/redis.conf
          - --policy=policy/redis-security.rego

  - repo: local
    hooks:
      - id: redis-audit-check
        name: Redis Security Audit (via CLI)
        entry: bash -c 'REDIS_URL="${REDIS_URL_DEV}" tsx scripts/redis-audit.ts || echo "Audit skipped"'
        language: system
        pass_filenames: false

Section 5: Cloud-Specific Redis Security Patterns

Each cloud provider has unique Redis security features and pitfalls. Here's what to know for AWS, GCP, and Azure.

5.1 AWS ElastiCache

ElastiCache offers three key security controls for BullMQ:

  • Security groups: the primary network control — attach only to BullMQ worker/server security groups
  • In-transit encryption (transit_encryption_enabled = true): mandatory for rediss:// connections
  • IAM auth (Valkey 7.2+): authenticate via IAM credentials instead of a shared password

BullMQ connection with ElastiCache IAM auth:

import { Redis } from 'ioredis';
import { Queue, Worker } from 'bullmq';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';

async function createElastiCacheConnection() {
  const credProvider = fromNodeProviderChain();
  const credentials = await credProvider();

  // IAM user must have elasticache:Connect permission
  const token = `${credentials.accessKeyId}:${credentials.secretAccessKey}`;
  const connection = new Redis({
    host: 'bullmq-redis-prod.xxxxxx.cache.amazonaws.com',
    port: 6379,
    tls: {
      rejectUnauthorized: true,
    },
    password: token,
    maxRetriesPerRequest: null,
  });

  return connection;
}

// Usage with BullMQ
async function main() {
  const connection = await createElastiCacheConnection();

  const queue = new Queue('notifications', { connection });
  const worker = new Worker('notifications', async (job) => {
    return { processed: true };
  }, { connection });

  process.on('SIGTERM', async () => {
    await worker.close();
    await queue.close();
    await connection.quit();
  });
}

The required IAM policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "elasticache:Connect",
      "Resource": "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:bullmq-redis-prod"
    }
  ]
}

5.2 GCP Memorystore

Memorystore for Redis requires VPC peering (or the newer Private Service Connect). Key security settings:

# GCP Memorystore with TLS and password from Secret Manager
resource "google_redis_instance" "bullmq" {
  name           = "bullmq-redis-${var.environment}"
  memory_size_gb = var.memory_size_gb
  region         = var.region
  redis_version  = "REDIS_7_0"
  tier           = var.tier

  # Security settings
  auth_enabled            = true
  auth_string             = random_password.redis_master.result
  transit_encryption_mode = "SERVER_AUTHENTICATION"
  connect_mode            = "PRIVATE_SERVICE_ACCESS"
  reserved_ip_range       = "10.0.0.0/29"
}

resource "random_password" "redis_master" {
  length  = 32
  special = false
}

BullMQ connection to Memorystore (GCP uses self-signed internal certs):

import { Redis } from 'ioredis';
import { Queue, Worker } from 'bullmq';

const connection = new Redis({
  host: '10.0.0.3',                         // Private IP from reserved range
  port: 6379,
  password: process.env.REDIS_PASSWORD,
  tls: {
    rejectUnauthorized: false,              // Memorystore self-signed internal certs
  },
  maxRetriesPerRequest: null,
  enableReadyCheck: false,
});

const queue = new Queue('tasks', { connection });
const worker = new Worker('tasks', async (job) => {
  // job processing
}, { connection });

5.3 Azure Cache for Redis

Azure Cache requires private endpoints (Private Link) for production use. TLS 1.2 is enforced by default:

import { Redis } from 'ioredis';
import { Queue, Worker } from 'bullmq';

// Azure Cache with private endpoint + access key auth
const connection = new Redis({
  host: 'bullmq-redis-prod.redis.cache.windows.net',
  port: 6380,
  password: process.env.AZURE_REDIS_ACCESS_KEY,
  tls: {
    rejectUnauthorized: true,
  },
  maxRetriesPerRequest: null,
});

const queue = new Queue('jobs', { connection });
const worker = new Worker('jobs', async (job) => {
  // process job
}, { connection });

Conclusion: The Layered Security Model

No single control makes Redis secure. The patterns in this guide work together as layers of defense:

Infrastructure layer:

  • NetworkPolicy restricts Redis to BullMQ pods only
  • Redis runs as non-root with Pod Security Standards
  • TLS certificates managed by cert-manager with auto-renewal
  • Persistent storage encrypted at rest

Secrets layer:

  • Redis credentials sourced from Vault, AWS Secrets Manager, or ESO
  • Mounted as volumes (not environment variables)
  • Rotated on a schedule

Observability layer:

  • ACL LOG polled for brute-force and privilege escalation attempts
  • SLOWLOG monitored for anomalous commands (KEYS, FLUSHALL, CONFIG)
  • Prometheus alerts configured for eviction rates, connection saturation, and dangerous commands
  • Audit CLI for ad-hoc compliance checks

Compliance layer:

  • redis.conf validated by conftest in CI/CD
  • Terraform modules enforce encryption, security groups, and IAM auth
  • pre-commit hooks catch misconfigurations before PR review

Start with the audit CLI. Run REDIS_URL=redis://your-instance:6379 tsx redis-audit.ts against your current production Redis. You'll likely find at least one gap you didn't know existed. Fix that, add a Prometheus alert, and work your way up the stack.

If you're already managing BullMQ queues in production, Queue Hub surfaces security metrics directly — ACL violations, connection anomalies, and TLS status — alongside your queue monitoring. It's the observability layer for everything covered in this guide, integrated into a single dashboard.

Try Queue Hub free at queuehub.tech and start securing your Redis infrastructure today.

Related Articles