Skip to content

Security: mevijays/cached

docs/SECURITY.md

Security model and audit findings

This document tracks the threat model, the audit done as part of Phase 3, and the fixes that landed. New code paths must consider these items.


Threat model

cached is exposed to clients on an unauthenticated TCP port until they AUTH. The threats we defend against, in order of likelihood:

  1. Memory-exhaustion DoS — a single malicious client (or a compromised internal service) trying to OOM the pod or burn through CPU.
  2. AUTH brute force — credential-stuffing against a known endpoint.
  3. Slow-loris / connection flooding — opening many half-dead connections to exhaust the MaxClients cap.
  4. Privilege escalation via ACL gaps — bypassing per-user command allow-lists.
  5. Replication abuse — submitting forged Raft frames (mitigated by the private Raft network port, NetworkPolicy-restricted).
  6. Container escape — escalation out of the pod. Mitigated by distroless + nonroot + dropped capabilities.

Out of scope (defer to higher layers): nation-state attackers on the control plane; physical access to nodes; supply-chain compromise of go.mod dependencies (we run Trivy in CI but don't pin transitives).


Findings and fixes (Phase 3)

F-1: Bulk allocation DoS (HIGH, fixed)

Issue: RESP allowed $524288000\r\n (500 MB bulk), and readBulk did make([]byte, n+2) based on the client-supplied length. A single attacker could request many large bulks in parallel and OOM the pod.

Fix: MaxBulkSize is now a runtime-configurable value defaulting to 64 MiB (was 512 MiB). Exposed via CACHE_MAX_BULK_SIZE. The check n > MaxBulkSize runs before any allocation. (internal/resp/reader.go)

F-2: RESP array recursion DoS (MEDIUM, fixed)

Issue: readArray called ReadValue recursively without a depth limit. A maliciously crafted nested array could blow the Go stack (default 1MB, growable but bounded).

Fix: Added MaxArrayDepth = 32 enforced in the reader. Configurable via CACHE_MAX_ARRAY_DEPTH. Anything deeper returns ErrTooDeep. (internal/resp/reader.go)

F-3: AUTH brute force (HIGH, fixed)

Issue: No rate limiting on failed AUTH attempts. An attacker could try thousands of passwords per second.

Fix: Per-IP sliding-window limiter in internal/auth/ratelimit.go. Defaults to 30 failed attempts per minute per source IP. Exceeding the limit returns WRONGPASS plus a 1-second sleep before close. Configurable via CACHE_AUTH_RATELIMIT_PER_MIN.

F-4: MULTI queue exhaustion (MEDIUM, fixed)

Issue: A client could send MULTI followed by unlimited queued commands without ever calling EXEC/DISCARD. TxnQueued would grow unbounded.

Fix: Cap of 10,000 commands or 50 MiB of queued bytes per connection's MULTI buffer. Exceeding either limit marks the transaction Aborted and the next EXEC returns EXECABORT. (internal/command/txn_cmds.go)

F-5: Protected mode (MEDIUM, added)

Issue: Running with auth.enabled=false on a port reachable from the network is dangerous. Operators sometimes do this in dev and forget to lock it down in prod.

Fix: New --protected-mode flag (default ON). When AUTH is disabled, only connections from loopback are accepted; anything else receives an error and is closed. Matches Redis's behavior since 3.2. Set CACHE_PROTECTED_MODE=false to disable (e.g. when running behind a strict NetworkPolicy). (internal/server/conn.go)

F-6: Connection-level read deadline before AUTH (LOW, fixed)

Issue: A pre-AUTH client could hold open a connection (taking a MaxClients slot) and send bytes very slowly to keep the read alive.

Fix: First-frame read uses a 10-second deadline (separate from IdleTimeout). After AUTH, the longer idle timeout applies. Slow-loris mitigation.

F-7: KEYS as a foot-gun (LOW, documented)

Issue: KEYS * walks every shard's map under lock. A single client calling it on a 10M-key cache stalls the entire pod for seconds.

Fix: Logged at WARN level whenever called. Not removed (it's useful for ops). The docs now warn against using it on hot paths.

F-8: ACL not Raft-replicated (KNOWN, documented)

Issue: ACL SETUSER mutates only the local replica. Different replicas can end up with different ACLs.

Status: Documented. Replicating ACLs through Raft is Phase 4. Operators should configure ACLs identically at startup via a config file (also Phase 4) or run ACL SETUSER against each pod.


Hardened defaults applied by the Helm chart

These defaults are set in helm/cached/values.yaml:

  • AUTH enabled with auto-generated 32-char password.
  • containerSecurityContext.readOnlyRootFilesystem: true.
  • containerSecurityContext.capabilities.drop: ["ALL"].
  • containerSecurityContext.allowPrivilegeEscalation: false.
  • podSecurityContext.runAsNonRoot: true.
  • podSecurityContext.seccompProfile.type: RuntimeDefault.
  • automountServiceAccountToken: false on the dedicated SA.
  • NetworkPolicy restricting ingress to same-namespace + labeled namespaces.
  • protectedMode: true (refuses non-loopback when AUTH is off).
  • maxBulkSize: 64 MiB, authRateLimitPerMin: 30.

Reporting

If you find a security issue, please do not open a public GitHub issue. Instead, email the maintainer or use GitHub's "Report a vulnerability" button on the repository.

There aren't any published security advisories