Files
popertots f9e8385d5a fix: auth/2FA security — stdout-log code delivery is dev/test-only, production fails closed until email/SMS; verification-code hashing, lockout recovery, sabredav fail-closed
- TWO_FACTOR_ALLOW_LOG_DELIVERY production opt-in REMOVED: plaintext codes are written to the stdout log ([2FA]/[VERIFY]) only in dev/test builds as a local DEV ONLY feature while email/SMS delivery (P6) is implemented. Production builds have no delivery channel and code issuance fails closed (503) under any configuration — no silent log-based code leak
- verification/2FA codes hashed at rest (HMAC-SHA256 via TWO_FACTOR_PEPPER, CHAR(64)); [VERIFY] dev log relay; per-user brute-force budget; password_reset purpose clears lockout for self-service recovery; dummy-bcrypt on login no-user path kills timing oracle
- sabredav weak-password list + entropy gate; .env.example ships fail-closed DAV_ADMIN_PASSWORD
- delete-account re-auth (current_password + fresh 2FA code when enforced)
- prod-tag suite (run-prod-tag-tests.sh) compiles and runs the production 2FA issuance gate: production ALWAYS reports no delivery channel and refuses issuance after the pepper check
- startup_checks_test SNAPSHOT_ENC_KEY values built at runtime so gitleaks sees no secret-shaped literals
- env-docs parity updated (flag removed, 38 vars)
2026-08-22 00:34:50 +01:00

182 lines
9.0 KiB
Bash

# Database Credentials for all services
# These are used by the 'postgres' service to initialize the database
# These are used by 'backend' (Go) and 'sabredav' (PHP)
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_DB=mydb
# and to connect to the 'postgres' service on the Docker network
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
# JWT_SECRET_KEY — REQUIRED, FAIL-CLOSED. The backend refuses to start with an
# empty, weak (<32 chars), or known-placeholder value, because a shared/public
# signing key lets anyone forge an admin JWT. Generate a strong random key:
# openssl rand -hex 32
JWT_SECRET_KEY=
# S3/R2 Configuration (for image storage)
# Dev: Uses local Rustfs container (see compose.yml)
# Prod: Use Cloudflare R2 credentials
S3_ENDPOINT=http://localhost:9000
# Public URL the BROWSER fetches images from. This is HOST-SPECIFIC: it must be
# the machine's current LAN IP (DHCP changes it). Run `hostname -I` to check.
# A stale IP makes images fail to load even though the objects exist in Rustfs.
S3_PUBLIC_URL=http://192.168.0.45:9000
S3_ACCESS_KEY=rustfsadmin
S3_SECRET_KEY=rustfsadmin
S3_BUCKET=crussell
S3_PROFILE_PICS_BUCKET=crussell-profile-pics
AWS_REGION=eu-west-2
# Set DAV_SKIP_INIT=1 to skip CardDAV server initialization (e.g., in CI/test environments).
DAV_SKIP_INIT=1
# Prod only: Cloudflare R2 (overrides S3_* vars in non-dev builds).
# Local dev uses the S3_* vars above (from .env). Not needed for local builds.
R2_ENDPOINT=
# Required for production object storage — the prod S3 client (backend/internal/s3/s3.go,
# via getEnv) reads all four below; not needed for local dev builds.
R2_ACCESS_KEY=
R2_SECRET_KEY=
R2_BUCKET=crussell
R2_PUBLIC_URL=
# Square Payment Gateway
SQUARE_ACCESS_TOKEN=
SQUARE_LOCATION_ID=
SQUARE_TERMINAL_DEVICE_ID=
SQUARE_ENVIRONMENT=mock
# SQUARE_ALLOW_REAL_API — dev-build safety valve. In a `//go:build dev` build the
# backend HARD-FAILS (refuses to construct the client) when SQUARE_ENVIRONMENT
# is 'production', because a leftover/typo'd production env + real key in a dev
# shell would create real charges. Set SQUARE_ALLOW_REAL_API=1 ONLY to
# deliberately route a dev build to the real production API. Never set in a
# deployed production build.
SQUARE_ALLOW_REAL_API=
# 2FA — homegrown two-factor authentication for ADMIN/ACCOUNT VERIFICATION ONLY
# (setup, disable, delete-account re-auth). It is NEVER a card-charge
# authorisation. Saved-card online payments are authorised EXCLUSIVELY by
# Square PSD2 SCA (3-D Secure / buyer verification), wired for both new-card
# and saved-card charges. PSR 2017 reg 100 makes SCA mandatory and non-waivable
# for customer-initiated stored-credential charges, so a saved-card charge
# carrying no Square verification token is REFUSED 402 verification_required
# (the payment does not go through; the customer can try again later, or at
# the till they can be invited to pay online later instead) — the homegrown
# 2FA fallback was removed entirely and no TWO_FACTOR_FALLBACK switch exists.
# Enforcement is FAIL-CLOSED:
# ON unless REQUIRE_2FA explicitly disables it (false/0/off/no, case-insensitive)
# OR SQUARE_ENVIRONMENT explicitly equals one of mock/dev/development/test.
# Empty or unknown SQUARE_ENVIRONMENT values are treated as production-enforced
# (a mistyped env var can never silently disarm the gate; the backend logs a
# startup warning in that case). Set REQUIRE_2FA=false only in controlled
# environments. The dev mock simulates SCA
# (SimulateSavedCardVerificationRequired + cnon:sca-... tokenize-results), so
# development has full parity with the SCA-only production posture.
# Code delivery: the intended channel is email/SMS (the method chosen at
# setup) — NOT wired yet (P6). Until it lands, verification/2FA codes are
# delivered to the LOCAL DEV stdout log (a [2FA]-prefixed line) in dev/test
# builds ONLY — stdout-log delivery is a dev-only convenience, never a
# production channel. Production builds have no delivery channel and code
# issuance FAILS CLOSED (503) until email/SMS is implemented; the API never
# returns the code while enforcement is ON.
REQUIRE_2FA=true
# TWO_FACTOR_PEPPER — server-side pepper for HMAC-hashing 2FA codes. REQUIRED
# in production builds: code issuance FAILS CLOSED when it is unset (an
# unsalted SHA-256 digest in the 1M code space would be offline-brute-forceable
# from a log/DB leak), mirroring JWT_SECRET_KEY's fail-fast stance. Optional
# only in dev/test builds, where an unset pepper falls back to the legacy
# digest with a one-time warning. Generate with:
# openssl rand -base64 32
TWO_FACTOR_PEPPER=
# SNAPSHOT_ENC_KEY — base64-encoded 32-byte AES-256 key for encrypting stored
# square_request_snapshot rows (buyer PII: email + ccof card tokens) at rest in
# non-mock (production/sandbox) deployments. If unset/invalid, snapshots fall
# back to PLAINTEXT with a one-time CRITICAL log warning (money-safety first:
# the replayable snapshot must not be lost). Generate with:
# openssl rand -base64 32
SNAPSHOT_ENC_KEY=
# Webhook config MUST exactly match the Square Dashboard webhook subscription
# (URL + signature key). If SQUARE_WEBHOOK_NOTIFICATION_URL is left unset it
# defaults to http://localhost:8080/webhooks/square, which is fail-closed (503
# without the signing key, 403 on missing/bad signature). Leave both empty if
# you do not use webhooks.
SQUARE_WEBHOOK_SIGNATURE_KEY=
SQUARE_WEBHOOK_NOTIFICATION_URL=
# Frontend (public — safe for the browser). Square Web Payments SDK:
# VITE_SQUARE_APPLICATION_ID — client-side application ID (sandbox IDs start with "sandbox-")
# VITE_SQUARE_LOCATION_ID — Square location ID
# VITE_SQUARE_ENVIRONMENT — 'mock' | 'sandbox' | 'production'. Local dev: 'mock' renders the
# frontend's built-in mock card form (tokens only; pairs with
# SQUARE_ENVIRONMENT=mock above). NEVER set 'mock' in production.
VITE_SQUARE_APPLICATION_ID=
VITE_SQUARE_LOCATION_ID=
VITE_SQUARE_ENVIRONMENT=mock
# Test Database (separate from main DB)
# Used by testutils/testdb for running tests without corrupting dev data
TEST_DB_HOST=localhost
TEST_DB_DSN=
# Dev/CI mode — set to "true" to enable mock services
# Disables zxcvbn password checks, skips artificial Square mock delays,
# skips DAV sync, and relaxes production guardrails
GO_TESTING=
# CardDAV (SabreDAV) — profile photo sync
# DAV_BASE_URL is retained for reference only: the Go backend no longer reads it
# — updateCardDAV writes through the dav.Service directly (Postgres-backed
# CardDAV store), it never calls this URL. Only the sabredav PHP container needs
# the server-side credential below.
DAV_BASE_URL=http://localhost:8080
# DAV_ADMIN_PASSWORD — REQUIRED, FAIL-CLOSED. This CardDAV/CalDAV server
# exposes customer PII vCards, so no public/default credential is ever
# acceptable. Leave it EMPTY here (as below): compose.yml fails fast via
# ${DAV_ADMIN_PASSWORD:?} when it is unset or empty, so a copy-paste
# `cp .env.example .env` deployment is caught BEFORE anything boots. As a
# second layer, sabredav/server.php refuses to start when the value is a known
# weak/default placeholder ('changeme-admin-password', 'changeme', 'secret',
# 'test', ...) or has <16 characters / <8 distinct characters (mirrors
# isWeakJWTSecret in backend/main.go). Generate a strong random value before
# any deployment:
# openssl rand -hex 32
# Production email/SMS delivery of verification codes is NOT wired yet (P6);
# code delivery happens only in dev/test builds via the LOCAL DEV stdout log
# ([2FA]/[VERIFY] prefixes). Production builds have no delivery channel, so
# 2FA/verification-code issuance FAILS CLOSED (503) until email/SMS delivery
# is implemented — see the 2FA section of README.md.
DAV_ADMIN_PASSWORD=
# Logging
# Set to "true" to disable ANSI color escape sequences in log output
NO_COLOR=
# Frontend
VITE_BACKEND_URL=http://localhost:8080
# Backend CORS allowlist — comma-separated list of allowed frontend origins
# (read by the backend CORS middleware, see backend/main.go). Falls back to
# http://localhost:5173 when unset.
FRONTEND_ORIGIN=http://localhost:5173
# TRUST_PROXY_HEADERS — defaults false. Set to true ONLY when a trusted proxy
# (nginx and/or the Cloudflare edge) sits between clients and this backend and
# overwrites X-Real-IP / CF-Connecting-IP with the real client IP. When true,
# the per-IP rate limiter keys requests on those proxy-set headers and main.go
# registers chi's ClientIPFromHeader("X-Real-IP") middleware. MUST be true
# behind nginx/Cloudflare, or every request keyed by IP collapses onto the
# proxy's IP — one client exhausting the limit throttles everyone, and per-IP
# limiter protection is effectively bypassed. MUST stay false when the backend
# is origin-exposed: a client talking directly to the backend could otherwise
# rotate X-Real-IP/CF-Connecting-IP to bypass per-IP rate limiting.
TRUST_PROXY_HEADERS=false
# Local S3 (Rustfs) — requires GO_TESTING=1 or dev build tag
# These are dev-only overrides used by the dev S3 implementation
RUSTFS_ENDPOINT=http://rustfs:9000
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
RUSTFS_BUCKET=crussell