Securing Redis for BullMQ on Kubernetes: Network Isolation, Secrets Management & Security Observability
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: callredis-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,SHUTDOWNcommands - 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 forrediss://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
Best BullMQ Dashboard Alternatives in 2026: A Comprehensive Comparison
Comparing every BullMQ UI option side by side: Bull Board, Arena, Taskforce, QueueHub, and raw redis-cli. Feature matrices, pricing, pros and cons, and recommendations for every team size.
QueueHub vs pg-boss: Redis vs PostgreSQL as a Job Queue Backend
pg-boss uses PostgreSQL as a Node.js job queue, while BullMQ (monitored by QueueHub) uses Redis. We compare the two approaches across throughput, persistence, transactional queues, deployment, and when transactional enqueueing matters.
QueueHub vs Temporal: Job Queues vs Workflow Orchestration
Temporal is a workflow orchestration platform — a different category from Redis-backed job queues. We compare Temporal.io against BullMQ + QueueHub, covering complexity, use cases, durability, observability, and when to choose each.