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)
This commit is contained in:
2026-08-22 00:34:50 +01:00
parent 1429eddd34
commit f9e8385d5a
19 changed files with 1047 additions and 472 deletions
+22 -20
View File
@@ -75,11 +75,12 @@ SQUARE_ALLOW_REAL_API=
# (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. Until it lands, the verification code is delivered
# via the server log (a [2FA]-prefixed line) when the operator opts into
# TWO_FACTOR_ALLOW_LOG_DELIVERY=true (see below); in enforced/production
# environments an operator must relay the logged code to the user out-of-band;
# the API never returns the code while enforcement is ON.
# 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
@@ -89,14 +90,6 @@ REQUIRE_2FA=true
# digest with a one-time warning. Generate with:
# openssl rand -base64 32
TWO_FACTOR_PEPPER=
# TWO_FACTOR_ALLOW_LOG_DELIVERY — defaults false. Production 2FA code issuance
# FAILS CLOSED without a delivery channel: there is no email/SMS transport yet,
# so the ONLY production channel is the operator's explicit opt-in to the
# insecure server-log delivery ([2FA] prefix — anyone with backend log access
# can defeat the account 2FA gate). MUST be set to true to deliver
# 2FA codes via the server log in production until email/SMS lands. Dev/test
# builds always deliver via the log and never consult this flag.
TWO_FACTOR_ALLOW_LOG_DELIVERY=false
# 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
@@ -138,14 +131,23 @@ GO_TESTING=
# 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. sabredav/server.php refuses to
# start when unset or set to a known weak/default value ('admin' etc.) — this
# server exposes customer PII vCards, so no public default credential is ever
# acceptable. The placeholder below satisfies compose validation only; it MUST
# be replaced before any deployment (compose.yml fails fast via
# ${DAV_ADMIN_PASSWORD:?} if it is ever unset). Generate a strong random value:
# 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
DAV_ADMIN_PASSWORD=changeme-admin-password
# 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