Repository navigation
Require a password on production Redis #438
Description
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-documentsjob makes the worker write any content into any document. It skips the Read-only and Private checks. - A forged
genericemail job sends any mail through the docs.plus SMTP account. - A neighbour can inject sync updates and
doc:{id}:accessevents, 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.
- A forged
- 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 isredis-cli ping(:130). - Env schema has no password key:
apps/hocuspocus.server/src/config/env.schema.ts:61-69. Theredisblock inapps/hocuspocus.server/src/config/env.ts:45-55mirrors it. buildRedisConfiginapps/hocuspocus.server/src/lib/redis.ts:39-97sets no password. Every app client goes through it: the sync client, the subscriber, the publisher, and every BullMQ connection throughcreateRedisConnection(:261-279).RedisExtensionbuilds 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 withNOAUTH.redis-exporterconnects with no password:docker-compose.observability.yml:233-238.- Staging uses the same
docker-compose.prod.ymlwith/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.
- Add
REDIS_PASSWORD: z.string().optional()toenv.schema.ts, andpassword: env.REDIS_PASSWORDto theredisblock inenv.ts. - In
buildRedisConfig, setpassword: config.redis.password. This covers every client thatlib/redis.tsbuilds. - In
hocuspocus.config.ts, addpassword: config.redis.passwordto theRedisExtensionoptions. Update the comment above it. - Add an empty
REDIS_PASSWORD=line with a one-line comment to the Redis section of.env.example. - Add a strong
REDIS_PASSWORDto/opt/projects/prod.docs.plus/.envand/opt/projects/stage.docs.plus/.env. Production app services read it throughenv_file: .env.production, which the deploy copies from the host.env(.github/workflows/prod.docs.plus.yml:537). The staging deploy writes.env.staginginstead (.github/workflows/stage.docs.plus.yml:230). Confirm which file the staging containers load before Deploy 2.
Deploy 2: Redis requires it.
- In
docker-compose.prod.yml, add--requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD is required}to the Rediscommand. The:?form fails the deploy if the key is missing. - Add
REDISCLI_AUTH: ${REDIS_PASSWORD}under the Redis serviceenvironment. Then the healthcheck and the runbook'sredis-clicommands keep working unchanged. - In
docker-compose.observability.yml, addREDIS_PASSWORD: ${REDIS_PASSWORD}toredis-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. - 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.networklabel 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
commandline.
Acceptance criteria
- On staging and production,
docker run --rm --network docsplus-network redis:7-alpine redis-cli -h docsplus-redis pingreturnsNOAUTH. - WebSocket sync across replicas, the store, email and push queues all work on staging.
-
redis-cli CLIENT LIST | grep -c bzpopmininside the Redis container returns 6. That is three blocking clients (document, email, push) for each of the 2hocuspocus-workerreplicas. - The Redis container is
healthy, andredis-exporterreportsredis_up 1. - Local dev with no
REDIS_PASSWORDstill starts.
Verify
cd apps/hocuspocus.server && bun run typecheck && bun test.- Locally, run
make dev-local. Set a password withdocker exec docsy-redis-local redis-cli CONFIG SET requirepass <password>, and put the same value inREDIS_PASSWORDin.env.local. Restart the server, edit a document, and check that the save lands. Undo withCONFIG SET requirepass "". RedisExtensionruns only whenNODE_ENVisproduction, so only staging proves cross-replica sync. Open one document in two browsers on staging and check that edits appear in both.- After each deploy, run the acceptance checks on the host with
docker exec.
Related
- Run only reviewed main-branch workflows on the production runner #437 run only reviewed main-branch workflows on the production runner
Generated by Claude Code
Activity
[-][Security] Production Redis has no password on a network shared with public apps[/-][+]Require a password on production Redis[/+]docs(repo): record the 9 Oct security and email batch in the changelo…
HMarzban commentedon Oct 9, 2026
Built but held, not in production. The diff is saved as a patch (
Notes/security-416/s-redis-media-438-439.patch).REDIS_PASSWORDis optional and safe to ship; it is held together with #439, which shares the env files.