Skip to content

Default MasterKey enforcement to Strict so an empty key refuses startup#444

Merged
ppXD merged 2 commits into
mainfrom
fix/p5-master-key-strict-default
Jun 15, 2026
Merged

Default MasterKey enforcement to Strict so an empty key refuses startup#444
ppXD merged 2 commits into
mainfrom
fix/p5-master-key-strict-default

Conversation

@ppXD

@ppXD ppXD commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

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 to Strict — 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() passes Strict explicitly rather than changing the shared EnforcementModeReader.Read default, so the other hardening consumers (cert validation, sensitive-variable decrypt) keep their Warn default.
  • The committed appsettings.json MasterKey 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) and Constructor_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).

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 to Strict — a missing or weak key now refuses startup
rather than 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|off.

Scoped to MasterKey only — passes Strict explicitly from ReadEnforcementMode
rather than changing the shared EnforcementModeReader.Read default, so the
other hardening consumers (cert validation, sensitive-variable decrypt) keep
their Warn default.

The committed appsettings MasterKey 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.

Tests: invert the constructor backward-compat unit test (empty key now
throws) + add valid-key-boots and warn-opt-out companions (hermetic, env-var
controlled in the serialised collection); add an integration test that the
real DI container boots under the Strict default with the configured key and
round-trips a secret. Full unit suite (6037) + at-rest integration green.
@ppXD ppXD added this to the 1.9.2 milestone Jun 15, 2026
…rd, hermetic env, docs

Adversarial review surfaced five fix-optional items, all addressed:

- The integration test proved only "a configured key works" — a valid key
  passes ValidateMasterKey in every mode, so it would not catch the default
  regressing to Warn. Add a container-tier NEGATIVE test: override the
  container's SecuritySetting with an empty MasterKey and assert
  IVariableEncryptionService refuses to construct under the (unset) Strict
  default. Soften the positive test's name/message to claim only what it
  proves.
- The constructor tests mutate the process-global SQUID_MASTER_KEY_ENFORCEMENT
  env var. Two other classes that also construct the service
  (VariableEncryptionServiceB10Tests, DeploymentAccountCredentialsEncryptionTests)
  were outside the serialised collection — benign today (they use real keys)
  but a future flake trap. Move them into the serialised collection.
- "Refuses startup" was inaccurate: the encryption service is scoped, so the
  throw surfaces on first construction (first deploy / secret operation), not
  at process boot. Reword the code comments + appsettings to say so.
- Update the docs/HARDENING_TRACKING.md B.1 row, which still documented the
  old Warn default and claimed "NOT A BREAKING CHANGE".
- Reword the stale "Warn mode (default)" comments in the unit test that
  contradicted the now-Strict default.

Full unit suite (6037) + master-key integration (positive + negative) green.
@ppXD
ppXD merged commit bbf81b7 into main Jun 15, 2026
15 checks passed
@ppXD
ppXD deleted the fix/p5-master-key-strict-default branch June 15, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant