Skip to content

Repair mkcert CA trust on macOS instead of trusting presence alone - #1237

Merged
geodro merged 5 commits into
lerd-env:mainfrom
retr0ripper:fix/mkcert-ca-trust-drift
Jul 31, 2026
Merged

Repair mkcert CA trust on macOS instead of trusting presence alone#1237
geodro merged 5 commits into
lerd-env:mainfrom
retr0ripper:fix/mkcert-ca-trust-drift

Conversation

@retr0ripper

Copy link
Copy Markdown
Contributor

platformTrustCheck now parses security trust-settings-export -d, which security itself keys by each certificate's SHA-1 fingerprint and which only carries a trustSettings array for a certificate actually configured as trusted, the distinction find-certificate can't make. The old presence-only check is kept under platformPresenceCheck for the one thing it's still useful for, telling CAPresentButUntrusted apart from a CA that was never installed at all.

Since mkcert's own self-check has the same blind spot, ensureMkcertCA now repairs trust directly with the same security add-trusted-cert command mkcert itself uses, before mkcert gets a chance to wrongly decide there's nothing to do.

Refs #1236

CATrusted() only checked whether mkcert's root CA was present in the
keychain, via security find-certificate, never whether it actually carried
trust settings. A certificate can sit in the keychain with its trust
settings cleared independent of the certificate item itself, most commonly
after a macOS update, and find-certificate reports that identically to a
genuinely trusted CA. lerd would then treat a reinstall as a no-op and skip
the sudo announcement, leaving the CA silently untrusted and every site's
HTTPS certificate invalid in the browser, even though podman and the vhost
itself looked completely healthy.

platformTrustCheck now parses security trust-settings-export -d, which
security itself keys by each certificate's SHA-1 fingerprint and which only
carries a trustSettings array for a certificate actually configured as
trusted, the distinction find-certificate can't make. The old presence-only
check is kept under platformPresenceCheck for the one thing it's still
useful for, telling CAPresentButUntrusted apart from a CA that was never
installed at all.

mkcert's own "already installed" self-check can't be trusted to notice and
repair this on its own: it verifies the self-signed root against itself,
which succeeds cryptographically regardless of its actual trust settings,
the same false positive this fix exists to avoid. So ensureMkcertCA repairs
trust directly with the same security add-trusted-cert command mkcert
itself uses, before mkcert gets a chance to wrongly decide there's nothing
to do.
@retr0ripper
retr0ripper marked this pull request as ready for review July 30, 2026 09:12
@retr0ripper
retr0ripper requested a review from a team as a code owner July 30, 2026 09:12
@geodro

geodro commented Jul 30, 2026

Copy link
Copy Markdown
Member

Hey @retr0ripper some CI issues

CI failed on Build & Test (Linux) with TempDir cleanup errors in both
internal/podman and internal/xdebugops: unlinkat on a containers/storage
overlay diff directory, permission denied. Both packages' setupConfigHome
tried to block real podman by emptying PATH, but DetectHostGatewayIP's
last-resort fallback, parseHostGatewayFromProbe, runs podman run --rm
--network lerd docker.io/library/alpine getent hosts ... whenever every
cheaper probe comes up empty, which is always true in these tests since
lerd-nginx never actually runs. PodmanBin() also tries hardcoded absolute
paths (homebrew's install locations) before giving up, and those bypass
PATH entirely since a path containing a slash skips exec.Command's
LookPath, so whichever of those exists on the machine running the test
lets a real podman run and pull the alpine image, leaving overlay layers
that a plain os.RemoveAll can't remove afterward.

internal/podman's ini_race_test.go is in-package, so its setupConfigHome
now stubs the package's own execCommand seam directly, the same one every
other test in the package already uses, guaranteeing no real binary is
ever reached regardless of what PodmanBin() resolves to.
internal/xdebugops can't reach that unexported seam from outside the
package, so its setupConfigHome instead pre-creates the shared hosts file
with the gateway entry already in place, giving ensureFPMHostsFile its
already-done fast path so it returns before ever calling WriteContainerHosts.
@retr0ripper retr0ripper reopened this Jul 30, 2026
@retr0ripper

Copy link
Copy Markdown
Contributor Author

Hi @geodro ,
Dug into the CI failure, turns out it had nothing to do with the mkcert changes. The Linux job was failing on TempDir cleanup in internal/podman and internal/xdebugops, both tripping over the same container storage overlay layer. DetectHostGatewayIP has a last resort fallback that runs a real podman run of the alpine image when every cheaper probe comes up empty, which is always true in these tests since lerd nginx never actually runs. Both test files tried to block real podman by emptying PATH, but PodmanBin also tries a couple of hardcoded absolute paths as a fallback, and those skip PATH lookup entirely, so whichever of those exists on the machine running the test let a real podman through and left behind overlay layers that a plain RemoveAll can't clean up. Fixed the podman package test to stub its own exec seam directly, and the xdebugops one to pre seed the hosts file so it never reaches that fallback at all. Pushed the fix and reopened this.

geodro added 2 commits July 31, 2026 11:36
The trust check treated an entry in the admin trust-settings export as trusted only when it carried an explicit trustSettings array. Per SecTrustSettings an absent or empty array means trust as a root, which is how macOS records everything security add-trusted-cert installs, so two genuinely trusted roots on the machine this was measured on, a Laravel Valet CA and a Blizzard local cert, read as untrusted while security verify-cert reports both as verifying successfully and security dump-trust-settings counts both among the trusted certs. Only mkcert writes an explicit array, through the trust-settings-import step it runs after add-trusted-cert.

That mattered most for the repair itself. RepairSystemTrust writes the entry through add-trusted-cert alone, which leaves no explicit array, so a repaired CA would have kept reading as untrusted and every later lerd install would have announced and re-run the repair, prompting for authorization each time.

Presence of the entry is the trust decision now, and only an entry whose explicit results all refuse counts as untrusted. A certificate with no entry at all is still the drifted state the repair exists for, so a macOS update that drops the trust settings while leaving the certificate in the keychain is caught exactly as before.

An export that could not be read reports nothing rather than reporting untrusted, so a security that fails cannot drive a privileged repair on its own. A failed repair is named instead of swallowed, since macOS draws the authorization dialog itself and a run with no window server behind it fails there.
@geodro
geodro merged commit 87339f4 into lerd-env:main Jul 31, 2026
3 checks passed
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.

mkcert CA reported trusted on macOS by presence alone, not actual trust settings

2 participants