Security hardening: red-team remediation + CCP/WIP updates

## Security (red-team audit 2026-04-12)

Public data exposure (P0):
- Public map converted to server-side heatmap, 2-decimal (~1.1km) bucketing,
  no addresses/support-levels/sign-info returned
- Petition signers endpoint strips displayName/signerComment/geoCity/geoCountry
- Petition public-stats drops recentSigners entirely
- Response wall strips userComment + submittedByName
- Campaign createdByUserEmail + moderation fields gated to SUPER_ADMIN

Access control (P1):
- Campaign findById/update/delete/email-stats enforce owner === req.user.id
  (SUPER_ADMIN bypasses), return 404 to avoid enumeration
- GPS tracking session route restricted to session owner or SUPER_ADMIN
- Canvass volunteer stats restricted to self or SUPER_ADMIN
- People household endpoints restricted to INFLUENCE + MAP roles (was ADMIN*)
- CCP upgrade.service.ts + certificate.service.ts gate user-controlled
  shell inputs (branch, path, slug, SAN hostname) behind regex validators

Token security (P2):
- Query-param JWT auth replaced with HMAC-signed short-lived URLs
  (utils/signed-url.ts + /api/media/sign endpoint); legacy ?token= removed
  from media streaming, photos, chat-notifications, and social SSE
- GITEA_SSO_SECRET + SERVICE_PASSWORD_SALT now REQUIRED (min 32 chars);
  JWT_ACCESS_SECRET fallback removed — BREAKING for existing deployments
- Refresh tokens bound to device fingerprint (UA + /24 IP) via `df` JWT
  claim; mismatch revokes all user sessions
- Refresh expiry reduced 7d → 24h
- Refresh/logout via request body removed — httpOnly cookie only
- Password-reset + verification-resend rate limits now keyed on (IP, email)
  composite to prevent both IP rotation and email enumeration

Defense-in-depth (P3):
- DOMPurify sanitization applied to GrapesJS landing page HTML/CSS
- /api/health?detailed=true disk-space leak removed
- Password-reset/verification token log lines no longer include userId

## Deployment

- docker-compose.yml + docker-compose.prod.yml: media-api now receives
  GITEA_SSO_SECRET + SERVICE_PASSWORD_SALT; empty fallbacks removed
- CCP templates/env.hbs adds both new secrets; refresh expiry → 24h
- CCP secret-generator.ts generates giteaSsoSecret + servicePasswordSalt
- leaflet.heat added to admin/package.json for heatmap rendering

## Operator action required on existing installs

Run `./config.sh` once (idempotent — only fills empty values) or manually
add GITEA_SSO_SECRET + SERVICE_PASSWORD_SALT to .env via
`openssl rand -hex 32`. Startup fails with a clear Zod error otherwise.

See SECURITY_REDTEAM_2026-04-12.md for full audit and verification matrix.

## Other

Includes in-flight CCP work: instance schema tweaks, agent server updates,
health service, tunnel service, DEV_WORKFLOW doc updates, and new migration
dropping composeProject uniqueness.

Bunker Admin
This commit is contained in:
2026-04-12 15:17:00 -06:00
parent 26ec925d9b
commit e55bc07eb6
59 changed files with 1387 additions and 510 deletions

View File

@@ -46,20 +46,27 @@ JWT_ACCESS_SECRET=GENERATE_WITH_openssl_rand_hex_32
JWT_REFRESH_SECRET=GENERATE_WITH_openssl_rand_hex_32
JWT_INVITE_SECRET=GENERATE_WITH_openssl_rand_hex_32
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d
# Reduced from 7d → 24h on 2026-04-12 (P2-3 hardening). Combined with
# device-fingerprint binding in the JWT payload, this tightens the
# exploitation window for stolen refresh tokens.
JWT_REFRESH_EXPIRY=24h
# Encryption key for DB-stored secrets (SMTP password, etc.)
# REQUIRED in production — must NOT reuse JWT_ACCESS_SECRET
# Generate with: openssl rand -hex 32
ENCRYPTION_KEY=GENERATE_WITH_openssl_rand_hex_32
# Gitea SSO cookie signing secret (separate from JWT — falls back to JWT_ACCESS_SECRET if empty)
# Generate with: openssl rand -hex 32
GITEA_SSO_SECRET=
# Salt for deriving deterministic service passwords (Gitea, Rocket.Chat)
# Falls back to JWT_ACCESS_SECRET if empty — set a dedicated value to isolate secret rotation
# Generate with: openssl rand -hex 32
SERVICE_PASSWORD_SALT=
# BREAKING CHANGE (2026-04-12): both GITEA_SSO_SECRET and SERVICE_PASSWORD_SALT
# are now REQUIRED (min 32 chars). The previous fallback to JWT_ACCESS_SECRET
# has been removed — a JWT leak must not compromise SSO cookies or service
# account passwords. Both values must be distinct from each other and from
# all JWT_* secrets. Generate with: openssl rand -hex 32
# Gitea SSO cookie signing secret (required, ≥32 chars, distinct from JWT secrets)
GITEA_SSO_SECRET=GENERATE_WITH_openssl_rand_hex_32
# Salt for deriving deterministic service passwords (Gitea, Rocket.Chat).
# Required, ≥32 chars, distinct from all other secrets.
SERVICE_PASSWORD_SALT=GENERATE_WITH_openssl_rand_hex_32
# --- Initial Super Admin User (auto-created during database seeding) ---
# These credentials are used to create the initial super admin account