## 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
352 lines
9.4 KiB
Handlebars
352 lines
9.4 KiB
Handlebars
# ============================================================
|
|
# Changemaker Lite — Instance: {{name}}
|
|
# Generated by CCP on {{now}}
|
|
# ============================================================
|
|
|
|
# Core
|
|
NODE_ENV=production
|
|
DOMAIN={{domain}}
|
|
USER_ID=1000
|
|
GROUP_ID=1000
|
|
DOCKER_GROUP_ID=984
|
|
|
|
# V2 PostgreSQL
|
|
V2_POSTGRES_USER=changemaker
|
|
V2_POSTGRES_PASSWORD={{secrets.postgresPassword}}
|
|
V2_POSTGRES_DB=changemaker_v2
|
|
V2_POSTGRES_PORT={{ports.postgres}}
|
|
DATABASE_URL=postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2
|
|
|
|
# Redis
|
|
REDIS_PASSWORD={{secrets.redisPassword}}
|
|
REDIS_URL=redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379
|
|
|
|
# JWT Auth
|
|
JWT_ACCESS_SECRET={{secrets.jwtAccessSecret}}
|
|
JWT_REFRESH_SECRET={{secrets.jwtRefreshSecret}}
|
|
JWT_INVITE_SECRET={{secrets.jwtInviteSecret}}
|
|
JWT_ACCESS_EXPIRY=15m
|
|
# Reduced 2026-04-12 from 7d → 24h (P2-3). Combined with device-fingerprint
|
|
# binding in the refresh JWT payload, this tightens the exploitation window
|
|
# for stolen refresh tokens.
|
|
JWT_REFRESH_EXPIRY=24h
|
|
|
|
# Gitea SSO cookie signing + service password salt — REQUIRED 2026-04-12 (P2-2).
|
|
# Distinct from JWT secrets; empty values will now fail Zod validation on boot.
|
|
GITEA_SSO_SECRET={{secrets.giteaSsoSecret}}
|
|
SERVICE_PASSWORD_SALT={{secrets.servicePasswordSalt}}
|
|
|
|
# Encryption
|
|
ENCRYPTION_KEY={{secrets.encryptionKey}}
|
|
|
|
# Initial Admin
|
|
INITIAL_ADMIN_EMAIL={{secrets.adminEmail}}
|
|
INITIAL_ADMIN_PASSWORD={{secrets.initialAdminPassword}}
|
|
|
|
# API
|
|
API_PORT=4000
|
|
PORT=4000
|
|
API_URL=https://api.{{domain}}
|
|
CORS_ORIGINS=https://app.{{domain}},http://localhost:{{ports.admin}},http://localhost
|
|
ADMIN_URL=https://app.{{domain}}
|
|
|
|
# Admin GUI
|
|
ADMIN_PORT=3000
|
|
|
|
# Nginx
|
|
NGINX_HTTP_PORT={{ports.nginx}}
|
|
NGINX_HTTPS_PORT=443
|
|
|
|
# SMTP / Email
|
|
{{#if emailTestMode}}
|
|
SMTP_HOST={{containerPrefix}}-mailhog
|
|
SMTP_PORT=1025
|
|
SMTP_USER=
|
|
SMTP_PASS=
|
|
EMAIL_TEST_MODE=true
|
|
{{else}}
|
|
SMTP_HOST={{smtpHost}}
|
|
SMTP_PORT={{smtpPort}}
|
|
SMTP_USER={{smtpUser}}
|
|
SMTP_PASS=
|
|
EMAIL_TEST_MODE=false
|
|
{{/if}}
|
|
SMTP_FROM={{smtpFrom}}
|
|
SMTP_FROM_NAME={{name}}
|
|
TEST_EMAIL_RECIPIENT={{secrets.adminEmail}}
|
|
|
|
# NocoDB
|
|
NOCODB_V2_PORT=8080
|
|
NOCODB_URL=http://{{containerPrefix}}-nocodb:8080
|
|
NC_ADMIN_EMAIL={{secrets.adminEmail}}
|
|
NC_ADMIN_PASSWORD={{secrets.nocodbAdminPassword}}
|
|
|
|
# Listmonk
|
|
{{#if enableListmonk}}
|
|
LISTMONK_SYNC_ENABLED=true
|
|
LISTMONK_URL=http://{{containerPrefix}}-listmonk:9000
|
|
{{else}}
|
|
LISTMONK_SYNC_ENABLED=false
|
|
LISTMONK_URL=
|
|
{{/if}}
|
|
LISTMONK_PORT=9000
|
|
LISTMONK_DB_USER=listmonk
|
|
LISTMONK_DB_PASSWORD={{secrets.listmonkAdminPassword}}
|
|
LISTMONK_DB_NAME=listmonk
|
|
LISTMONK_WEB_ADMIN_USER=admin
|
|
LISTMONK_WEB_ADMIN_PASSWORD={{secrets.listmonkAdminPassword}}
|
|
LISTMONK_API_USER=v2-api
|
|
LISTMONK_API_TOKEN={{secrets.listmonkApiToken}}
|
|
LISTMONK_ADMIN_USER=v2-api
|
|
LISTMONK_ADMIN_PASSWORD={{secrets.listmonkApiToken}}
|
|
LISTMONK_PROXY_PORT=9002
|
|
LISTMONK_WEBHOOK_SECRET=
|
|
LISTMONK_DB_PORT=5434
|
|
LISTMONK_SMTP_HOST={{containerPrefix}}-mailhog
|
|
LISTMONK_SMTP_PORT=1025
|
|
LISTMONK_SMTP_USER=
|
|
LISTMONK_SMTP_PASSWORD=
|
|
LISTMONK_SMTP_TLS_TYPE=none
|
|
LISTMONK_SMTP_FROM={{name}} <noreply@{{domain}}>
|
|
|
|
# Media
|
|
{{#if enableMedia}}
|
|
ENABLE_MEDIA_FEATURES=true
|
|
MEDIA_API_PUBLIC_URL=https://media.{{domain}}
|
|
{{else}}
|
|
ENABLE_MEDIA_FEATURES=false
|
|
MEDIA_API_PUBLIC_URL=
|
|
{{/if}}
|
|
MEDIA_API_PORT=4100
|
|
MEDIA_ROOT=/media/local
|
|
MEDIA_UPLOADS=/media/uploads
|
|
MAX_UPLOAD_SIZE_GB=10
|
|
PUBLIC_MEDIA_PORT=3100
|
|
VIDEO_PLAYER_DEBUG=false
|
|
VIDEO_ANALYTICS_RETENTION_DAYS=90
|
|
VIDEO_ANALYTICS_IP_HASHING_ENABLED=true
|
|
VIDEO_SCHEDULE_DEFAULT_TIMEZONE=UTC
|
|
VIDEO_SCHEDULE_NOTIFICATION_ENABLED=true
|
|
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24
|
|
|
|
# NAR Data
|
|
NAR_DATA_DIR=/data
|
|
|
|
# Platform Service URLs (used for health checks)
|
|
MINI_QR_URL=http://{{containerPrefix}}-mini-qr:8080
|
|
EXCALIDRAW_URL=http://{{containerPrefix}}-excalidraw:80
|
|
EXCALIDRAW_WS_URL=wss://draw.{{domain}}
|
|
HOMEPAGE_URL=http://{{containerPrefix}}-homepage:3000
|
|
VAULTWARDEN_URL=http://{{containerPrefix}}-vaultwarden:80
|
|
VAULTWARDEN_ADMIN_TOKEN={{secrets.vaultwardenAdminToken}}
|
|
VAULTWARDEN_DOMAIN=https://vault.{{domain}}
|
|
VAULTWARDEN_SIGNUPS_ALLOWED=false
|
|
VAULTWARDEN_WEBSOCKET_ENABLED=true
|
|
VAULTWARDEN_SMTP_SECURITY=off
|
|
|
|
# Geocoding
|
|
MAPBOX_API_KEY=
|
|
GOOGLE_MAPS_API_KEY=
|
|
GOOGLE_MAPS_ENABLED=false
|
|
GEOCODING_RATE_LIMIT_MS=1100
|
|
GEOCODING_CACHE_ENABLED=true
|
|
GEOCODING_CACHE_TTL_HOURS=24
|
|
GEOCODING_PARALLEL_ENABLED=true
|
|
GEOCODING_BATCH_SIZE=10
|
|
BULK_GEOCODE_ENABLED=true
|
|
BULK_GEOCODE_MAX_BATCH=5000
|
|
|
|
# Represent API
|
|
REPRESENT_API_URL=https://represent.opennorth.ca
|
|
|
|
# Overpass / Area Import
|
|
OVERPASS_API_URL=https://overpass-api.de/api/interpreter
|
|
OVERPASS_MIN_DELAY_MS=30000
|
|
AREA_IMPORT_MAX_GRID_POINTS=500
|
|
|
|
# Pangolin Tunnel
|
|
PANGOLIN_API_URL=
|
|
PANGOLIN_API_KEY=
|
|
PANGOLIN_ORG_ID=
|
|
PANGOLIN_SITE_ID=
|
|
{{#if enablePangolin}}
|
|
PANGOLIN_ENDPOINT={{pangolin.endpoint}}
|
|
PANGOLIN_NEWT_ID={{pangolin.newtId}}
|
|
PANGOLIN_NEWT_SECRET={{pangolin.newtSecret}}
|
|
{{else}}
|
|
PANGOLIN_ENDPOINT=
|
|
PANGOLIN_NEWT_ID=
|
|
PANGOLIN_NEWT_SECRET=
|
|
{{/if}}
|
|
|
|
# Gancio
|
|
{{#if enableGancio}}
|
|
GANCIO_SYNC_ENABLED=true
|
|
GANCIO_URL=http://{{containerPrefix}}-gancio:13120
|
|
{{else}}
|
|
GANCIO_SYNC_ENABLED=false
|
|
GANCIO_URL=
|
|
{{/if}}
|
|
GANCIO_BASE_URL=https://events.{{domain}}
|
|
GANCIO_ADMIN_USER=admin
|
|
GANCIO_ADMIN_PASSWORD={{secrets.gancioAdminPassword}}
|
|
GANCIO_PORT=8092
|
|
|
|
# Chat (Rocket.Chat)
|
|
{{#if enableChat}}
|
|
ENABLE_CHAT=true
|
|
ROCKETCHAT_URL=http://{{containerPrefix}}-rocketchat:3000
|
|
ROCKETCHAT_ADMIN_USER=rcadmin
|
|
ROCKETCHAT_ADMIN_PASSWORD={{secrets.rocketchatAdminPassword}}
|
|
MONGO_ROOT_USER=rocketchat
|
|
MONGO_ROOT_PASSWORD={{secrets.mongoRootPassword}}
|
|
{{else}}
|
|
ENABLE_CHAT=false
|
|
ROCKETCHAT_URL=
|
|
ROCKETCHAT_ADMIN_USER=
|
|
ROCKETCHAT_ADMIN_PASSWORD=
|
|
MONGO_ROOT_USER=
|
|
MONGO_ROOT_PASSWORD=
|
|
{{/if}}
|
|
|
|
# Jitsi Meet (Video Conferencing)
|
|
ENABLE_MEET={{#if enableMeet}}true{{else}}false{{/if}}
|
|
{{#if enableMeet}}
|
|
JITSI_APP_ID=changemaker
|
|
JITSI_APP_SECRET={{secrets.jitsiAppSecret}}
|
|
JITSI_JICOFO_AUTH_PASSWORD={{secrets.jitsiJicofoAuthPassword}}
|
|
JITSI_JVB_AUTH_PASSWORD={{secrets.jitsiJvbAuthPassword}}
|
|
JITSI_URL=http://{{containerPrefix}}-jitsi-web:80
|
|
JVB_ADVERTISE_IP={{jvbAdvertiseIp}}
|
|
JVB_PORT=10000
|
|
{{else}}
|
|
JITSI_APP_ID=
|
|
JITSI_APP_SECRET=
|
|
JITSI_JICOFO_AUTH_PASSWORD=
|
|
JITSI_JVB_AUTH_PASSWORD=
|
|
JITSI_URL=
|
|
JVB_ADVERTISE_IP=
|
|
JVB_PORT=10000
|
|
{{/if}}
|
|
|
|
# SMS Campaigns
|
|
ENABLE_SMS={{#if enableSms}}true{{else}}false{{/if}}
|
|
TERMUX_API_URL=
|
|
TERMUX_API_KEY=
|
|
SMS_DELAY_BETWEEN_MS=3000
|
|
SMS_MAX_RETRIES=3
|
|
SMS_RESPONSE_SYNC_INTERVAL_MS=30000
|
|
SMS_DEVICE_MONITOR_INTERVAL_MS=30000
|
|
|
|
# Social Connections
|
|
ENABLE_SOCIAL={{#if enableSocial}}true{{else}}false{{/if}}
|
|
|
|
# People CRM
|
|
ENABLE_PEOPLE={{#if enablePeople}}true{{else}}false{{/if}}
|
|
|
|
# Analytics & GeoIP
|
|
ENABLE_ANALYTICS={{#if enableAnalytics}}true{{else}}false{{/if}}
|
|
MAXMIND_ACCOUNT_ID=
|
|
MAXMIND_LICENSE_KEY=
|
|
|
|
# Monitoring
|
|
GRAFANA_ADMIN_PASSWORD={{secrets.grafanaAdminPassword}}
|
|
GRAFANA_ROOT_URL=https://grafana.{{domain}}
|
|
PROMETHEUS_PORT=9090
|
|
GRAFANA_PORT=3000
|
|
CADVISOR_PORT=8086
|
|
NODE_EXPORTER_PORT=9100
|
|
REDIS_EXPORTER_PORT=9121
|
|
ALERTMANAGER_PORT=9093
|
|
ALERTMANAGER_EMBED_PORT={{math ports.embed "+" 16}}
|
|
GOTIFY_PORT=8889
|
|
GOTIFY_ADMIN_USER=admin
|
|
GOTIFY_ADMIN_PASSWORD=admin
|
|
|
|
# MkDocs
|
|
MKDOCS_PORT={{math ports.embed "+" 8}}
|
|
MKDOCS_SITE_SERVER_PORT={{math ports.embed "+" 14}}
|
|
MKDOCS_PREVIEW_URL=http://{{containerPrefix}}-mkdocs:8000
|
|
MKDOCS_DOCS_PATH=/mkdocs/docs
|
|
CODE_SERVER_PORT={{math ports.embed "+" 7}}
|
|
CODE_SERVER_URL=http://{{containerPrefix}}-code-server:8443
|
|
BASE_DOMAIN=https://{{domain}}
|
|
|
|
# Gitea
|
|
GITEA_URL=http://{{containerPrefix}}-gitea:3000
|
|
GITEA_SSH_PORT=2222
|
|
GITEA_DB_TYPE=postgres
|
|
GITEA_DB_HOST={{containerPrefix}}-postgres:5432
|
|
GITEA_DB_NAME=gitea
|
|
GITEA_DB_USER=changemaker
|
|
GITEA_DB_PASSWD={{secrets.postgresPassword}}
|
|
GITEA_ROOT_URL=https://git.{{domain}}
|
|
GITEA_DOMAIN=git.{{domain}}
|
|
GITEA_COMMENTS_ENABLED=false
|
|
GITEA_API_TOKEN=
|
|
GITEA_COMMENTS_REPO_OWNER=
|
|
GITEA_COMMENTS_REPO_NAME=docs-comments
|
|
GITEA_OAUTH_CLIENT_ID=
|
|
GITEA_OAUTH_CLIENT_SECRET=
|
|
|
|
# n8n
|
|
N8N_HOST=n8n.{{domain}}
|
|
N8N_URL=http://{{containerPrefix}}-n8n:5678
|
|
N8N_ENCRYPTION_KEY={{secrets.n8nEncryptionKey}}
|
|
N8N_USER_EMAIL={{secrets.adminEmail}}
|
|
N8N_USER_PASSWORD={{secrets.nocodbAdminPassword}}
|
|
GENERIC_TIMEZONE=UTC
|
|
|
|
# MailHog
|
|
MAILHOG_URL=http://{{containerPrefix}}-mailhog:8025
|
|
MAILHOG_SMTP_PORT=1025
|
|
MAILHOG_WEB_PORT=8025
|
|
|
|
# Homepage
|
|
HOMEPAGE_PORT=3010
|
|
HOMEPAGE_VAR_BASE_URL=http://localhost
|
|
|
|
# Dev Tools
|
|
{{#if enableDevTools}}
|
|
ENABLE_DEV_TOOLS=true
|
|
{{else}}
|
|
ENABLE_DEV_TOOLS=false
|
|
{{/if}}
|
|
|
|
# Payments
|
|
{{#if enablePayments}}
|
|
ENABLE_PAYMENTS=true
|
|
{{else}}
|
|
ENABLE_PAYMENTS=false
|
|
{{/if}}
|
|
|
|
# Vite (admin build)
|
|
VITE_API_URL=http://{{containerPrefix}}-api:4000
|
|
VITE_MKDOCS_URL=http://{{containerPrefix}}-mkdocs:8000
|
|
{{#if enableMedia}}
|
|
VITE_MEDIA_API_URL=http://{{containerPrefix}}-media-api:4100
|
|
{{/if}}
|
|
|
|
# Bunker Ops (Fleet Management)
|
|
INSTANCE_LABEL={{slug}}
|
|
BUNKER_OPS_ENABLED=false
|
|
BUNKER_OPS_REMOTE_WRITE_URL=
|
|
|
|
# Embed proxy ports (nginx proxy for iframe embedding in admin GUI)
|
|
NOCODB_EMBED_PORT={{math ports.embed "+" 0}}
|
|
N8N_EMBED_PORT={{math ports.embed "+" 1}}
|
|
GITEA_EMBED_PORT={{math ports.embed "+" 2}}
|
|
MAILHOG_EMBED_PORT={{math ports.embed "+" 3}}
|
|
MINI_QR_EMBED_PORT={{math ports.embed "+" 4}}
|
|
EXCALIDRAW_EMBED_PORT={{math ports.embed "+" 5}}
|
|
HOMEPAGE_EMBED_PORT={{math ports.embed "+" 6}}
|
|
CODE_SERVER_EMBED_PORT={{math ports.embed "+" 7}}
|
|
MKDOCS_EMBED_PORT={{math ports.embed "+" 8}}
|
|
VAULTWARDEN_EMBED_PORT={{math ports.embed "+" 9}}
|
|
ROCKETCHAT_EMBED_PORT={{math ports.embed "+" 10}}
|
|
GANCIO_EMBED_PORT={{math ports.embed "+" 11}}
|
|
GRAFANA_EMBED_PORT={{math ports.embed "+" 12}}
|
|
LISTMONK_EMBED_PORT={{math ports.embed "+" 13}}
|
|
MKDOCS_SITE_EMBED_PORT={{math ports.embed "+" 14}}
|
|
JITSI_EMBED_PORT={{math ports.embed "+" 15}}
|