Default MasterKey enforcement to Strict so an empty key refuses startup
Summary
- The MasterKey enforcement default was
Warn: an empty / too-short / all-zero key logged a warning and proceeded with a recoverable 0-byte-derived key, so at-rest encryption was theater by default on a misconfigured deploy. Flip the default toStrict— a missing or weak key now refuses startup instead of silently encrypting under a recoverable key. -
Non-breaking for real deployments: they always configure a real master key, so the Strict default never fires for them; it only catches a genuine misconfiguration (key never set). Operators who deliberately run without at-rest protection opt out with
SQUID_MASTER_KEY_ENFORCEMENT=warn(allow + log) or=off(silent). -
Scoped to MasterKey only:
ReadEnforcementMode()passesStrictexplicitly rather than changing the sharedEnforcementModeReader.Readdefault, so the other hardening consumers (cert validation, sensitive-variable decrypt) keep theirWarndefault. - The committed
appsettings.jsonMasterKey stays intentionally empty so a real deploy must supply its own. The E2E test config's previously all-zero key is replaced with a real random key so the E2E container still boots under Strict.
This completes the deferred dependency that made the P2 at-rest encryption cluster (#437/#438/#439) genuinely protective rather than cosmetic-by-default.
Behaviour change & opt-out
| MasterKey | Before (Warn default) | After (Strict default) |
|---|---|---|
| real 32-byte random | boots | boots |
| empty / missing | boots + warns (0-byte key) | refuses startup |
| too short / all-zero | boots + warns | refuses startup |
Opt-out preserved: SQUID_MASTER_KEY_ENFORCEMENT=warn or =off restores the prior lenient behaviour. The other hardening env vars are unaffected.
Test plan
-
Unit — invert the constructor backward-compat test (empty key now throws under the Strict default) + add Constructor_ValidMasterKey_DoesNotThrow(happy path boots) andConstructor_EmptyMasterKey_WarnOptOut_DoesNotThrow(opt-out). Hermetic: env var saved/cleared/restored, class moved into the serialised collection so the process-wide mutation can't race. -
Existing ValidateMasterKey(..., Strict)matrix already pins empty/short/all-zero → throw. -
Integration (real Postgres + real DI) — new IntegrationMasterKeyStrictDefault: the container boots under the Strict default with the configured real key and round-trips a secret through the V2 envelope. -
Full unit suite green (6037); at-rest + master-key integration green (13/13).