Skip to content

Require a password on production Redis #438

Description

@HMarzban

Summary

Production Redis runs with no password on docsplus-network. The review counted 23 containers on that network. They include internet-facing third-party apps (Grafana, GlitchTip, Uptime Kuma) and the host app tiny.docs.plus. If any of them is compromised, it can read and write Redis, which holds the BullMQ queues and the Yjs sync pub/sub.

  • Severity: Medium. It needs a neighbour compromise first, but the impact after that is high. A compromised neighbour can do this:
    • A forged store-documents job makes the worker write any content into any document. It skips the Read-only and Private checks.
    • A forged generic email job sends any mail through the docs.plus SMTP account.
    • A neighbour can inject sync updates and doc:{id}:access events, and wipe rate-limit keys.
    • Redis 7 defaults block the old file-write route to a shell. So the risk is data and queue integrity, not container takeover.
  • Area: docker-compose.prod.yml, docker-compose.observability.yml, hocuspocus Redis config
  • Source: security review of 2026-10-06, finding M6

Where

  • Redis service, no --requirepass: docker-compose.prod.yml:104-134. Its healthcheck is redis-cli ping (:130).
  • Env schema has no password key: apps/hocuspocus.server/src/config/env.schema.ts:61-69. The redis block in apps/hocuspocus.server/src/config/env.ts:45-55 mirrors it.
  • buildRedisConfig in apps/hocuspocus.server/src/lib/redis.ts:39-97 sets no password. Every app client goes through it: the sync client, the subscriber, the publisher, and every BullMQ connection through createRedisConnection (:261-279).
  • RedisExtension builds its own ioredis options with no password: apps/hocuspocus.server/src/config/hocuspocus.config.ts:125-142. The comment there warns that a password set only here breaks every queue with NOAUTH.
  • redis-exporter connects with no password: docker-compose.observability.yml:233-238.
  • Staging uses the same docker-compose.prod.yml with /opt/projects/stage.docs.plus/.env (.github/workflows/stage.docs.plus.yml:46-48).

Fix plan

Ship in two deploys. Old replicas send no password. If requirepass ships with the client change, sync and queues fail until every replica has rolled.

Deploy 1: clients send the password. ioredis only logs a warning when Redis has no password set. So this deploy is safe on its own.

  1. Add REDIS_PASSWORD: z.string().optional() to env.schema.ts, and password: env.REDIS_PASSWORD to the redis block in env.ts.
  2. In buildRedisConfig, set password: config.redis.password. This covers every client that lib/redis.ts builds.
  3. In hocuspocus.config.ts, add password: config.redis.password to the RedisExtension options. Update the comment above it.
  4. Add an empty REDIS_PASSWORD= line with a one-line comment to the Redis section of .env.example.
  5. Add a strong REDIS_PASSWORD to /opt/projects/prod.docs.plus/.env and /opt/projects/stage.docs.plus/.env. Production app services read it through env_file: .env.production, which the deploy copies from the host .env (.github/workflows/prod.docs.plus.yml:537). The staging deploy writes .env.staging instead (.github/workflows/stage.docs.plus.yml:230). Confirm which file the staging containers load before Deploy 2.

Deploy 2: Redis requires it.

  1. In docker-compose.prod.yml, add --requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD is required} to the Redis command. The :? form fails the deploy if the key is missing.
  2. Add REDISCLI_AUTH: ${REDIS_PASSWORD} under the Redis service environment. Then the healthcheck and the runbook's redis-cli commands keep working unchanged.
  3. In docker-compose.observability.yml, add REDIS_PASSWORD: ${REDIS_PASSWORD} to redis-exporter. Add the key to /opt/projects/prod.docs.plus/.env.observability, and to the required-key loop in .github/workflows/observability.docs.plus.yml:63.
  4. Roll to staging first, then production.

Leave dev and local compose files alone. The key is optional, so they keep running with no password.

Out of scope

  • Moving Grafana, GlitchTip and Uptime Kuma to their own network. A password closes the open write. A network split also needs Traefik on both networks and a traefik.docker.network label on each moved container (scripts/traefik/traefik.yml:82). File it separately if wanted.
  • A Redis ACL user per client, and moving the password out of the command line.

Acceptance criteria

  • On staging and production, docker run --rm --network docsplus-network redis:7-alpine redis-cli -h docsplus-redis ping returns NOAUTH.
  • WebSocket sync across replicas, the store, email and push queues all work on staging.
  • redis-cli CLIENT LIST | grep -c bzpopmin inside the Redis container returns 6. That is three blocking clients (document, email, push) for each of the 2 hocuspocus-worker replicas.
  • The Redis container is healthy, and redis-exporter reports redis_up 1.
  • Local dev with no REDIS_PASSWORD still starts.

Verify

  1. cd apps/hocuspocus.server && bun run typecheck && bun test.
  2. Locally, run make dev-local. Set a password with docker exec docsy-redis-local redis-cli CONFIG SET requirepass <password>, and put the same value in REDIS_PASSWORD in .env.local. Restart the server, edit a document, and check that the save lands. Undo with CONFIG SET requirepass "".
  3. RedisExtension runs only when NODE_ENV is production, so only staging proves cross-replica sync. Open one document in two browsers on staging and check that edits appear in both.
  4. After each deploy, run the acceptance checks on the host with docker exec.

Related


Generated by Claude Code

Activity

  1. added theissue type on Oct 6, 2026
  2. changed the title [-][Security] Production Redis has no password on a network shared with public apps[/-] [+]Require a password on production Redis[/+] on Oct 6, 2026
  3. added
    SecuritySecurity, access control, and data exposure
    and removed
    bugSomething isn't working
    on Oct 6, 2026
  4. HMarzban commented on Oct 9, 2026

    @HMarzban
    CollaboratorAuthor

    Built but held, not in production. The diff is saved as a patch (Notes/security-416/s-redis-media-438-439.patch). REDIS_PASSWORD is optional and safe to ship; it is held together with #439, which shares the env files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DevOpsSecuritySecurity, access control, and data exposure

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions