Skip to content

feat(wsl): SC-036b Negotiate Phase 1 — bootstrap krb5 + px-proxy#73

Open
xxthunder wants to merge 7 commits into
developfrom
feature/sc-036b-negotiate-bootstrap
Open

feat(wsl): SC-036b Negotiate Phase 1 — bootstrap krb5 + px-proxy#73
xxthunder wants to merge 7 commits into
developfrom
feature/sc-036b-negotiate-bootstrap

Conversation

@xxthunder

@xxthunder xxthunder commented May 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Phase 1 of the Negotiate (Kerberos) proxy workflow: installs krb5-user and px-proxy via a temporary Basic-auth proxy without ever exporting credentials to any process environment or placing them on the cmdline.
  • Bootstrap creds are piped to setup-proxy-negotiate.sh over stdin (consumed by read -r BOOTSTRAP_PROXY_URL); the script refuses to read from a TTY so a missing pipe fails fast. Basic-auth URL lives only in root-owned /etc/apt/apt.conf.d/99proxy and a temp ~/.config/pip/pip.conf removed by an EXIT trap.
  • Mode marker is written as negotiate-bootstrap early so SC-036e teardown dispatches correctly even on partial failure.
  • Adds an optional -StdinInput parameter to Invoke-WslDistroScript plus an Invoke-WslExeWithStdin helper (Invoke-Expression path can't pipe stdin). Removes the now-dead exit-10 handler in Install-WslProxy.

Backlog: SC-036b (parent: SC-036)

UAT Procedure

Run on a corporate WSL distro with this branch checked out:

  • Happy path: wsl-manager setup-proxyA (or M) → confirm/enter URL → auth N → enter bootstrap username/password. Expect: apt-get install completes, pipx install px-proxy completes, no errors.
  • End state (after script returns 0):
    • cat /etc/wsl-manager/proxy-mode prints negotiate-bootstrap
    • ls -l ~/.local/bin/px exists and is executable
    • command -v kinit resolves to /usr/bin/kinit
    • cat /etc/apt/apt.conf.d/99proxy shows the bootstrap URL with creds
    • ls ~/.config/pip/pip.conf returns "No such file or directory" (trap-deleted)
  • No env leakage: open a new shell, run env | grep -i proxy. Expect: nothing related to the bootstrap creds (Phase 4 will set the localhost URL later — for now the env should be unchanged from before setup).
  • Idempotent re-run: run setup again, enter the same creds. Expect: pipx install is skipped with "px-proxy already installed (pipx list); skipping pipx install"; apt operations are no-ops; script still exits 0.
  • Cred typo: re-run, enter wrong password. Expect: apt-get update fails with 407 Proxy Authentication Required; script exits 2; mode marker still reads negotiate-bootstrap so SC-036e teardown can later clean up.
  • Empty username at prompt: re-run, press Enter at the username prompt. Expect: PS1 throws "Bootstrap credentials are required for Negotiate mode setup."; no script invocation.
  • Direct shell invocation guard: from inside the distro, run bash lib/wsl/scripts/setup-proxy-negotiate.sh --proxy-url=http://x:1 --no-proxy=y --username=$USER (no stdin pipe). Expect: error "Bootstrap proxy URL must be piped on stdin. Refusing to read from a terminal." and exit 4.
  • Regression — Basic mode: full SC-007 happy path (A/MB → creds). Expect: unchanged behavior; /etc/wsl-manager/proxy-mode reads basic.

🤖 Generated with Claude Code

@codecov

codecov Bot commented May 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.92%. Comparing base (bf22bdc) to head (aaf79a3).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop      #73      +/-   ##
===========================================
+ Coverage    86.00%   86.92%   +0.92%     
===========================================
  Files           22       22              
  Lines         1908     2035     +127     
===========================================
+ Hits          1641     1769     +128     
+ Misses         267      266       -1     
Flag Coverage Δ
PS7 86.92% <100.00%> (+0.92%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@xxthunder xxthunder force-pushed the feature/sc-036b-negotiate-bootstrap branch from 55d50bb to f4c067b Compare May 5, 2026 15:44
@xxthunder xxthunder force-pushed the feature/sc-036b-negotiate-bootstrap branch 2 times, most recently from 35f5edf to 259acdc Compare May 29, 2026 14:57
xxthunder and others added 3 commits May 31, 2026 13:56
…36b)

Phase 1 of the Negotiate (Kerberos) proxy workflow. Installs krb5-user
and px-proxy through a temporary Basic-auth proxy without ever exporting
credentials to any process environment or placing them on the cmdline.

Bootstrap creds are piped to setup-proxy-negotiate.sh via stdin (one
line, consumed by `read -r BOOTSTRAP_PROXY_URL`); the script refuses to
read from a TTY so a missing pipe fails fast. The Basic-auth URL is
written only to root-owned /etc/apt/apt.conf.d/99proxy and a temp
~/.config/pip/pip.conf that is removed by an EXIT trap regardless of
outcome. The mode marker is written as `negotiate-bootstrap` early so
SC-036e teardown dispatches correctly even on partial failure.

Adds an optional -StdinInput parameter to Invoke-WslDistroScript with a
small Invoke-WslExeWithStdin helper, since Invoke-CommandLine's
Invoke-Expression path cannot pipe stdin. Removes the now-dead exit-10
handler in Install-WslProxy left over from the SC-036a stub.

UAT hardening:
- Strip any legacy SC-007 Basic-mode proxy block from ~/.profile and
  ~/.bashrc at bootstrap start, so a distro migrating from Basic mode
  stops leaking embedded creds into env during Phases 1-3 (UAT step 3).
- Treat a 407 from `apt-get update` as a hard failure: apt exits 0 on
  failed fetches, so a wrong bootstrap password would otherwise produce
  a false success. Capture the output and exit 2 on "Proxy
  Authentication Required" (UAT step 5).
- Make the Install-WslProxy success banner mode-aware: Negotiate now
  reports the installed tooling, apt bootstrap config, mode marker, and
  removed legacy block instead of the Basic-mode .profile/Docker/Podman
  list.

Tests: direct unit coverage for Get-NegotiateBootstrapCredential
(URL-encoding + empty-username branch) and Invoke-WslExeWithStdin (the
native wsl.exe call), which were previously only exercised via mocks;
plus source-text assertions for the legacy-block strip and the 407
guard. All 8 UAT steps pass; story closed (Done).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on the SC-036b bootstrap path:

- Force C locale on `apt-get update` so a localized "Proxy Authentication
  Required" line can't slip past the 407 grep and mask a bad-password run
  as a false success (the exact failure SC-036b exists to catch).
- chmod 600 /etc/apt/apt.conf.d/99proxy: tee left it world-readable (644)
  while holding Basic-auth creds; lock it to root-only like the temp pip.conf.
- Percent-encode the bootstrap username, not just the password, so domain/UPN
  logins ('DOMAIN\user', 'user@corp.com') produce a valid URL userinfo segment.
- Stream the script's stdout via Out-Host instead of Out-Null so the user sees
  apt/pipx progress live during the multi-minute install.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…handling (SC-036b)

Cleanup follow-ups on the SC-036b bootstrap path:

- Extract Get-ProxyCredentialPrefix in setProxy.ps1 and route both
  Get-ProxyCredentialsFromUser (Basic) and Get-NegotiateBootstrapCredential
  (Negotiate) through it, removing the near-duplicate SecureString->encode
  logic. Basic mode now also percent-encodes the username (latent bug fixed
  for free); both prompt strings are preserved.
- Broaden the apt-get update failure check: a 407 isn't the only false-success
  path — an unreachable/refused proxy also leaves apt at exit 0, so also fail
  on "Failed to fetch" / connection errors.
- case 2 in Install-WslProxy now emits a Negotiate-specific message
  (credentials/proxy/install failure) instead of the Basic "file system
  permissions" text.
- Collapse the two near-identical Invoke-WslDistroScript calls into one
  splatted call; -StdinInput is added only on the Negotiate path.
- Factor the legacy .profile/.bashrc block removal into a parameterized
  remove_managed_block helper instead of repeating the grep+sed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@xxthunder xxthunder force-pushed the feature/sc-036b-negotiate-bootstrap branch from a5772e8 to 282e0a3 Compare May 31, 2026 11:56
xxthunder and others added 4 commits June 1, 2026 19:54
…036c)

Implements the Negotiate workflow's Phases 2-3 in setup-proxy-negotiate.sh
(--activate) and Install-WslProxy: write /etc/krb5.conf and credential-less
~/.config/px/px.ini, run kinit for the corporate principal, then start px
transiently to verify the full chain (Kerberos -> SPNEGO -> upstream -> TLS)
end-to-end with curl, and stop it again. Marker stays negotiate-bootstrap;
.profile/apt/Docker/Podman are untouched (that switch is SC-036d).

Realm/KDC/principal and the proxy username are pre-filled from the Windows
domain session (USERDNSDOMAIN/LOGONSERVER/USERNAME) as Enter-to-accept
defaults, via Get-WindowsKerberosDefault and a new -DefaultUser on the shared
credential helpers.

Key decisions, all surfaced during UAT on a corporate distro:
- Interactive kinit needs a real pty. Run the activate step via a separate
  Invoke-WslExeInteractive using Start-Process -NoNewWindow -Wait -PassThru;
  the call operator (with Out-Host, or captured) corrupts the password read
  or swallows output and denies the pty.
- Verify with curl through px, not `px --test`: px's test client ignores the
  system CA bundle and false-fails HTTPS on a TLS-inspecting proxy. The
  diagnostic distinguishes 407 (auth) from a TLS cert error (corporate CA).
- px is verify-only here and stopped before returning; a lingering daemon
  keeps the WSL session alive and hangs the one-shot wsl.exe call. Persistent
  px (shell auto-start) is SC-036d.

Marks SC-036c Done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setProxy.ps1's -askForCreds path called the shared
Get-ProxyCredentialsFromUser helper without -DefaultUser, so its prompt
showed no "[user]" default — unlike wsl-manager, which already passes
$env:USERNAME. Pass the same default so both tools offer an
Enter-to-accept username and present a consistent UI.

Add a unit test asserting Initialize-ProxyConfiguration -AskForCreds
forwards $env:USERNAME as -DefaultUser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…036b)

The proxy-setup mode prompt aborted with "Invalid choice" when the user
pressed Enter, and the auth-method prompt had a hidden Basic default —
neither followed the [Y/n] convention of Get-UserConfirmation.

Add a reusable Get-UserChoice helper (single-choice sibling of
Get-UserConfirmation): Enter returns the default, the default's letter is
capitalized in the hint ([A]uto / [m]anual / [r]emove), invalid input
re-prompts up to a cap, and CI/test environments short-circuit without
prompting. Route both the mode and auth prompts in Install-WslProxy
through it, so Enter selects Auto / Basic instead of erroring.

proxy.Tests.ps1 forces interactive mode (Test-RunningInCIorTestEnvironment
-> false) so the existing Read-Host mocks keep driving the prompts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e (SC-036b)

Rework the proxy auth-method prompt into three first-class options — Anonymous (default), Basic, Negotiate — replacing the two-step "Basic/Negotiate then provide-credentials?" flow. Anonymous is the explicit no-credentials choice; choosing Basic is itself the opt-in to credentials. The prompt explanation now lives in docs/wsl-manager.md.

Eliminate the double password entry in Negotiate mode: the activate step recovers the bootstrap password from the root-owned apt proxy config Phase 1 wrote and reuses it for a non-interactive kinit run under setsid (which drops the controlling terminal so kinit's prompter reads the piped password from stdin instead of /dev/tty). Falls back to an interactive kinit prompt if the password can't be recovered or the KDC rejects it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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