Playbook: Pinbox24 W3/W4 Credential Rotation
Last updated: 2026-07-06 Applies to: bms-1 — W3 (v32-prod, s3-v32-prod) and W4 (v42-prod, s3-v42-prod) Trigger: Credential exposure, 90-day rotation schedule, or security incident
1. Overview
After this CI/CD PR merges, credential rotation is fully automated:
Edit SOPS file → PR → merge to main
→ secrets-sync.yml fires automatically
→ deploys pinbox24-w{3,4}.env to bms-1
→ updates build-dir env files via Python
→ force-recreates containers
→ verifies no auth errors (W3 only)
Manual steps are only needed if CI/CD is unavailable or partially fails.
2. SOPS key mapping
W3 — secrets/pinbox24-w3.env.sops
| SOPS key | Env file | Env var | Container |
|---|---|---|---|
V32_MONGODB_URL | backend-environment.env | MONGODB_URL | v32-prod |
V32_PMONGODB_URL | backend-environment.env | PMONGODB_URL | v32-prod |
V32_DB_URI | s3-environment.env | DB_URI | s3-v32-prod |
V32_s3Bucket_api_accessKeyId | both env files | s3Bucket_api_accessKeyId | both |
V32_s3Bucket_api_secretAccessKey | both env files | s3Bucket_api_secretAccessKey | both |
W4 — secrets/pinbox24-w4.env.sops
All V42_ prefixed keys map to the same key name without the prefix (V42_NEW_MONGODB_URI → NEW_MONGODB_URI) in backend-environment.env. The Python deploy script strips the V42_ prefix and updates all matching keys.
Key credential keys:
| SOPS key | Env var | Notes |
|---|---|---|
V42_NEW_MONGODB_URI | NEW_MONGODB_URI | W4 main MongoDB |
V42_v3MongoUrl | v3MongoUrl | W3 DB backward-compat reads |
V42_RABBIT_MQ_URL | RABBIT_MQ_URL | RabbitMQ AMQP |
V42_REDIS_PASSWORD | REDIS_PASSWORD | redis-v42 (local) |
V42_JWT_TOKEN_SECRET | JWT_TOKEN_SECRET | JWT signing |
V42_CONTAINER_NAME | CONTAINER_NAME | Used by docker-compose variable substitution |
3. Automated rotation (standard flow)
Step 1 — Edit SOPS file
# Windows dev
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
# W3
sops secrets\pinbox24-w3.env.sops
# W4
sops secrets\pinbox24-w4.env.sopsEdit the values directly in the SOPS interactive editor.
Step 2 — Canary verify
sops --decrypt --input-type dotenv --output-type dotenv secrets\pinbox24-w3.env.sops | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }Step 3 — Commit and merge
git add secrets/pinbox24-w3.env.sops
git commit -m "chore: rotate W3 MongoDB credentials — w3_app"
# PR → merge to main → secrets-sync.yml fires automaticallyStep 4 — Monitor CI
- Watch
sync-pinbox24-w3/sync-pinbox24-w4jobs in GitHub Actions - “Deploy W3 containers” step verifies zero auth errors before completing
- Check
infra_operationsSupabase table for deploy log entry
4. Manual rotation (CI unavailable)
Use this if secrets-sync.yml is broken or unavailable.
W3 manual procedure
# 1. Decrypt W3 SOPS silently
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$env:V32_MONGODB_URL = (sops --decrypt --input-type dotenv --output-type dotenv secrets\pinbox24-w3.env.sops | Select-String "^V32_MONGODB_URL=").ToString().Split("=",2)[1]
$env:V32_PMONGODB_URL = (sops --decrypt --input-type dotenv --output-type dotenv secrets\pinbox24-w3.env.sops | Select-String "^V32_PMONGODB_URL=").ToString().Split("=",2)[1]
$env:V32_DB_URI = (sops --decrypt --input-type dotenv --output-type dotenv secrets\pinbox24-w3.env.sops | Select-String "^V32_DB_URI=").ToString().Split("=",2)[1]
# 2. Write update script to temp file (no secrets in script content)
# 3. SSH to bms-1 and run update script (see pinbox24-w3-operations.md §Correct procedure)
# 4. Force-recreate containers on bms-1Step 4 must RECREATE the containers, never
docker restartthem.docker restartreuses the environment baked into each container at creation time and does not re-read the on-disk env file — so a restarted container keeps the OLD credential even thoughbackend-environment.env/mailgun-environment.envon disk was just updated. Onlydocker-compose up -d --force-recreate(after the old container is stopped/removed) picks up the new value. Recreate every container that consumes the rotated MongoDB credential in the same operation:
- W3 (
w3_app):v32-prod- W4 (
w4_app):v42-prod,mailgun-v42-prod, ands3-v2-v42-prodSkipping this recreate is exactly what caused the 2026-07-10 silent mailgun outage (mailgun-mongodb-stale-credential-hang.md): the
w4_apppassword was corrected on disk butmailgun-v42-prodwas never recreated, so every inbound webhook hung on a dead MongoDB connection and ~450 emails produced zero records with no error. Confirm the recreate took with the sha256 env-hash comparison from that playbook (never prints the secret). The per-consumer steps are automated in w4-mongodb-credential-rotation.md / w3-mongodb-credential-rotation.md.
Reference: docs/playbooks/pinbox24-w3-operations.md §Correct procedure: update credentials
5. Security rules for credentials
MongoDB URIs must use app users, not admin
| Stack | MongoDB user | authSource |
|---|---|---|
| W3 (v32-prod, s3-v32-prod) | w3_app | w3_db |
| W4 (v42-prod, s3-v42-prod) | w4_app | w4_db |
Never use admin credentials in container env files.
Redis password (W4 only)
redis-v42 is a local bms-1 container. Password rotation requires:
- Update
V42_REDIS_PASSWORDinsecrets/pinbox24-w4.env.sops - Also update the
redis-v42container’s Redis password (viaredis-cli CONFIG SET requirepass <new>while live, or recreate with--requirepass) - Both must change atomically — v42-prod and redis-v42 must use the same password
MongoDB URI URL-encoding
MongoDB passwords with special characters must be URL-encoded in the connection URI:
from urllib.parse import quote
encoded_password = quote(raw_password, safe='')
uri = f"mongodb://w3_app:{encoded_password}@host/db?authSource=w3_db"The SOPS file stores the full pre-encoded URI — not the raw password.
6. Rotation schedule
| Credential | Rotation interval | Owner |
|---|---|---|
| MongoDB w3_app | 90 days | p24-infra sys-security |
| MongoDB w4_app | 90 days | p24-infra sys-security |
| Redis password (redis-v42) | 90 days | p24-infra sys-security |
| JWT_TOKEN_SECRET | 180 days or on exposure | p24-infra sys-security |
| Wasabi IAM keys | automated via wasabi-iam-rotator | wasabi-iam-rotator (Vercel) |
| PayU, Przelewy24, Twilio | on exposure only | manual (external providers) |
7. Incident response
If a credential is suspected exposed:
- Create GH issue immediately:
[Security] W3/W4 <key-name> — suspected exposure - Rotate the credential in the external system first (MongoDB admin console / provider dashboard)
- Edit SOPS file with new value
- Create PR — merge immediately (skip normal review queue if P1)
- Confirm CI deploys successfully (check “Deploy W3/W4 containers” step)
- Verify no service errors after deploy
- Update GH issue with resolution
PM2 log exposure: if /var/log/v32-prod/pm2/ or /var/log/v42-prod/pm2/ logs showed a MongoDB URI with a password, rotate that credential immediately following steps above. The log files will contain the old password in plain text — treat them as sensitive until log rotation clears them.
Reference: docs/playbooks/static-api-key-incident-rotation.md