Skip to content

fix(infra): fail closed when the design-time migration connection is unset or insecure (#318) - #329

Merged
mforce merged 3 commits into
mainfrom
fix/318-designtime-conn-failclosed
Aug 1, 2026
Merged

mforce merged 3 commits into
mainfrom
fix/318-designtime-conn-failclosed

Conversation

@mforce

@mforce mforce commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes #318.

Problem

AppDbContextDesignTimeFactory — what dotnet ef migrations add / database update use to build a DbContext — fell back to a predictable Host=localhost;…;Username=postgres;Password=postgres connection whenever CLUCKWORK_MIGRATIONS_CONNECTION was unset, and always skipped the Production TLS floor. That let an operator typo silently target the wrong database, and normalized a publicly-known credential.

This is deliberately sequenced before the pre-launch schema batch (#283, #270, #307) so the design-time path is fail-closed before more migrations get generated through it.

Fix

Scoped to the design-time factory only — the migrate CLI verb runs against the built host's own configuration and is untouched.

Tests

15 tests in AppDbContextDesignTimeFactoryTests, covering both sides of every boundary: unset/blank rejected; the error leaks no credentials; no fallback target remains; plaintext rejected without the opt-in; plaintext accepted with it on loopback; the opt-in still rejecting a non-loopback host; weak remote TLS rejected via both key-value and URI form; VerifyFull/VerifyCA accepted.

Mutation-checked with three independent mutants:

  1. Reintroduced the postgres/postgres fallback → 3 tests RED.
  2. Deleted the TLS-floor enforcement pass → 4 tests RED.
  3. Deleted the loopback-host restriction → 1 test RED, confirming the opt-in boundary is tested on both sides.

Worth recording: mutant 1 initially left one test green — it had been passing for the wrong reason (the TLS floor happened to reject the fallback host independently). That test was strengthened to pin the "no default target" message so it detects the defect on its own. A test that passes with the bug present is a broken test.

Verification

  • dotnet build Cluckwork.sln — 0 warnings, 0 errors
  • dotnet test Cluckwork.sln — 979/979 passing (283 Domain + 114 Application + 582 Integration)

Docs

AGENTS.md gains a bullet beside the existing #260/#261/#262 hardening entries, and deploy/README.md a section documenting the variable and the escape hatch. Commands use placeholders only (<host>, <user>, <password>) — no credentials, no provider names, no concrete deploy values.

AppDbContextDesignTimeFactory used to fall back to a predictable
Host=localhost;...;Username=postgres;Password=postgres connection when
CLUCKWORK_MIGRATIONS_CONNECTION was unset, and always skipped the
Production TLS floor. An unset/blank env var now throws immediately
naming the variable, and every target is held to the same allow-list
TLS floor a Production boot enforces (#261/#262), reused via
PostgresConnectionString.NormalizeAndValidate. Plaintext is permitted
only via CLUCKWORK_MIGRATIONS_ALLOW_INSECURE_LOOPBACK=true, and only
when the connection targets a loopback host.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7296d458a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

onWarning: warnings.Add);
foreach (var warning in warnings)
{
Console.Error.WriteLine($"warning: {warning}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report the design-time escape hatch in plaintext warnings

When a plaintext loopback connection succeeds, the reused validator produces a warning saying that access was permitted via Database:AllowInsecureConnection, and this loop prints it unchanged. The design-time factory never reads that setting—the relevant acknowledgement is CLUCKWORK_MIGRATIONS_ALLOW_INSECURE_LOOPBACK—so the security warning directs operators to an ineffective control. Emit a design-time-specific warning naming the actual environment variable.

AGENTS.md reference: AGENTS.md:L51-L51

Useful? React with 👍 / 👎.

mforce added 2 commits July 31, 2026 20:33
…warn branch

Review of #329 noted the test class carries no [Collection], so it is its own
implicit collection and still runs in wall-clock parallel with every other
collection in the assembly — while CLUCKWORK_MIGRATIONS_* are process-global.
Nothing else reads those names today, so the isolation was accidental, and a
future test touching either would have flaked silently. A dedicated
DisableParallelization collection makes it structural.

Also adds the missing sslmode=Require case. The warn tier had no coverage at
all: deleting the warning loop left every test green. The new test pins both
sides — that Require is accepted, and that the warning is actually emitted
(verified by deleting the loop and watching it go red).
…at works

Review of #329: the TLS floor is reused from the boot path, so its warning
text names Database:AllowInsecureConnection. Design-time tooling never reads
that setting — the acknowledgement here is CLUCKWORK_MIGRATIONS_ALLOW_INSECURE_LOOPBACK
— so an operator following the warning would have been sent to a control that
does nothing for `dotnet ef`. Rewrite the borrowed name on the way out.

Test asserts the emitted warning names the env var and not the boot setting;
verified by printing the warning verbatim again and watching it go red.
@mforce

mforce commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Correct, and it's the cost of reusing the boot-path validator — the substance transferred but the remediation pointer didn't.

The borrowed name is now rewritten on the way out, so the warning names CLUCKWORK_MIGRATIONS_ALLOW_INSECURE_LOOPBACK rather than Database:AllowInsecureConnection. New test asserts the emitted stderr contains the env var and not the boot setting; mutation-checked by printing the warning verbatim again and watching it go red.

Full suite 981/981, 0 warnings.

@mforce
mforce merged commit 3e021ea into main Aug 1, 2026
8 checks passed
@mforce
mforce deleted the fix/318-designtime-conn-failclosed branch August 1, 2026 04:00
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.

Fail closed when the design-time migration connection is unset or insecure

1 participant