Skip to content

fix(proxy): remove obsolete Phase AI CA roots on Windows with certutil - #324

Merged
nimish-ks merged 7 commits into
feat--agentsfrom
fix--windows-ca-rotation
Oct 26, 2026
Merged

nimish-ks merged 7 commits into
feat--agentsfrom
fix--windows-ca-rotation

Conversation

@rohan-chaturvedi

Copy link
Copy Markdown
Member

Problem

On Windows, phase ai enable fails at the trust step whenever an older Phase AI CA is still trusted in the current-user Root store. That happens every time the CA is replaced after it was trusted: renewal inside the 14-day window, a lost private key, or, as hit in testing, a later phase ai enable that generated a new CA.

Error: trust Phase AI CA for current user: inspect and rotate Phase AI CA in Windows current-user Root store: exit status 1: Remove-Item : The operation is on user root store and UI is not allowed.

trustWindows runs the rotation script as powershell.exe -NoLogo -NoProfile -NonInteractive, and the script deleted obsolete roots with Remove-Item on the Cert: drive. Windows PowerShell's certificate provider only deletes from CurrentUser\Root when it can show the confirmation dialog, and it decides it can't whenever -NonInteractive is on the process command line (IsUIAllowed in CertificateProvider.cs). So the removal fails on every Windows machine, whatever the session type.

Hit on Windows 11 with this PR's CI build (2.3.4-next). With the obsolete root still in the store, Remove-Item -WhatIf on it fails with the same error under -NonInteractive and succeeds without it.

CI doesn't reach this path: the Windows smoke test runs with --trust-ca=false because the consent dialog would block a headless runner, and the recorded-runner unit test only checks the script text. Linux and macOS remove obsolete roots with certutil -D and security delete-certificate, which aren't affected.

Fix

  • The PowerShell step is now read-only. It still prints PHASE_CA_CURRENT for the already-trusted fast path, and otherwise prints PHASE_CA_OBSOLETE <SHA-1 thumbprint> for each older Phase-owned root, using the same ownership filter as before.
  • trustWindows removes each obsolete root with certutil -user -delstore Root <thumbprint>, then adds the new CA with certutil -user -addstore -f Root as before. certutil shows Windows' confirmation dialog for each removal; -NonInteractive only affects PowerShell.
  • Thumbprints must be exactly 40 hex characters. -delstore also matches store indexes and common names (0 is the first root in the store), so anything else is rejected before anything is deleted.
  • If a removal fails or is declined, enable stops before adding the new CA, keeping the remove-then-add order used on Linux and macOS.
  • The Windows trust notice now mentions the extra confirmation for each older Phase AI CA being replaced.

Testing

  • go vet ./... and go test ./pkg/proxy/ pass locally on Windows (Go 1.27.3).
  • New unit tests: obsolete roots are removed through certutil, in order, before -addstore; a malformed thumbprint is rejected with nothing deleted; a failed removal stops before the new CA is added. The existing Windows test now also asserts that the inspection script contains no Remove-Item.
  • Not yet run end to end against a real Root store. To check on Windows with an older Phase AI CA trusted: phase ai enable --trust-ca should show one removal confirmation and one add confirmation, then report the CA as trusted.

phase ai enable failed on Windows whenever an older Phase AI CA was still
trusted in the current-user Root store:

    inspect and rotate Phase AI CA in Windows current-user Root store:
    exit status 1: Remove-Item : The operation is on user root store and
    UI is not allowed.

The rotation script deleted obsolete roots with Remove-Item on the Cert:
drive, but trustWindows runs PowerShell with -NonInteractive, and Windows
PowerShell's certificate provider refuses every removal from
CurrentUser\Root when -NonInteractive is on its command line.

The PowerShell step is now read-only: it prints PHASE_CA_OBSOLETE with
the SHA-1 thumbprint of each older Phase-owned root, and trustWindows
removes each one with certutil -user -delstore before adding the new CA
with -addstore, as before. Thumbprints must be 40 hex characters, since
-delstore also matches store indexes and common names. The Windows trust
notice now mentions the confirmation shown for each removed root.
A background job accepts the Root store confirmations certutil shows,
since nobody is at the runner's desktop. Remove once it has passed.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Deploying phase-cli-install-script with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5efa78f
Status: ✅  Deploy successful!
Preview URL: https://57dbbedd.phase-cli-install-script.pages.dev
Branch Preview URL: https://fix--windows-ca-rotation.phase-cli-install-script.pages.dev

View logs

@nimish-ks nimish-ks self-assigned this Oct 26, 2026
… step

Launched from PowerShell 7, Windows PowerShell inherits that shell's
PSModulePath, cannot auto-load the certificate provider, and the trust
step fails because the Cert: drive does not exist.
It proved the delete-and-add rotation on a real user Root store once, but
the confirmation behaviour on hosted Windows runners is not deterministic,
so it cannot stay as a gate.

@nimish-ks nimish-ks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified: unit tests and vets on both platforms, and a temporary Windows runner job proved the delete-and-add rotation against a real current-user Root store (initial trust, forced CA replacement leaving only the new root, then already-trusted). Also fixed on this branch: Windows PowerShell now gets its own module path, so the trust step works when enable is launched from PowerShell 7.

@nimish-ks
nimish-ks merged commit aaae4de into feat--agents Oct 26, 2026
13 checks passed
@nimish-ks
nimish-ks deleted the fix--windows-ca-rotation branch October 26, 2026 08:04
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.

2 participants