Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2f93b8b
ee/wcp: restore and update the browser-privilege design doc
BeryJu Aug 16, 2026
7b56f4b
ee/wcp: mint the sign-in browser's token from a dedicated account
BeryJu Aug 16, 2026
62cfc4c
ee/wcp: update BROWSER_PRIVILEGE.md for the account work just built
BeryJu Aug 16, 2026
4a61cc7
vpkg/windows: create the dedicated wcp browser service account
BeryJu Aug 16, 2026
0d1bd36
ee/wcp: fix a stale comment about which token the logon path uses
BeryJu Aug 16, 2026
ba9384d
ee/wcp: record the VM run of the secure-desktop checklist
BeryJu Aug 16, 2026
b609551
ee/wcp: drop stored-pw's password-reuse machinery from this branch
BeryJu Aug 16, 2026
499ccb7
Merge remote-tracking branch 'origin/main' into ee/wcp/browser-privilege
BeryJu Aug 16, 2026
f3aedcc
ee/wcp: fix a trait name gone stale after merging main
BeryJu Aug 16, 2026
9fd90e7
vpkg/windows: drop $ from the wcp service account's placeholder password
BeryJu Aug 16, 2026
6138e7a
ee/wcp: trim doc comments down to one point each
BeryJu Aug 16, 2026
31d6368
vpkg/windows: trim the service account comment to one paragraph
BeryJu Aug 16, 2026
a27f2f6
vpkg/windows: replace the placeholder password with a random string
BeryJu Aug 16, 2026
d7d1cee
Merge branch 'main' into ee/wcp/browser-privilege
BeryJu Aug 17, 2026
b47abe0
ee/wcp: fix two issues that survived the main merge
BeryJu Aug 17, 2026
fce12c7
Merge branch 'main' into ee/wcp/browser-privilege
BeryJu Aug 17, 2026
c612d9f
Merge branch 'main' into ee/wcp/browser-privilege
BeryJu Aug 17, 2026
bc5d521
try to fix
BeryJu Aug 17, 2026
49de0bc
ee/wcp: enable the privileges CreateProcessAsUserW and the S4U logon …
BeryJu Aug 17, 2026
1724d85
ee/wcp: log which privilege failed and who we're actually running as
BeryJu Aug 18, 2026
458134d
ee/wcp: replace S4U with LogonUserW + CreateRestrictedToken
BeryJu Aug 18, 2026
c23059c
ee/wcp: fix a comment left over from the S4U-to-LogonUserW switch
BeryJu Aug 18, 2026
274d58b
ee/wcp: switch to CreateProcessWithTokenW, named pipes for IPC
BeryJu Aug 18, 2026
31b8923
ee/wcp: fix named-pipe DACL for CPUS_CREDUI, detect an early child exit
BeryJu Aug 18, 2026
cd25b9e
try without root cache path
BeryJu Aug 19, 2026
d9a8658
ee/wcp: give each ak_cef.exe launch its own cache directory
BeryJu Aug 19, 2026
0d0aba3
ee/wcp: build a real environment block for the service account's token
BeryJu Aug 19, 2026
0172025
ee/wcp: use CEF's Alloy runtime instead of Chrome style for the sign-…
BeryJu Aug 19, 2026
1b8c2f8
ee/wcp: log ak_cef.exe's build hash on startup
BeryJu Aug 19, 2026
74fb7f5
ee/wcp: cleanup pass - drop stale S4U references, trim long comments
BeryJu Aug 19, 2026
b654fbf
ee/wcp: grant the service account write access to its own log directory
BeryJu Aug 19, 2026
e19fe56
ee/wcp: log the result of the exact CreateMutex call ProcessSingleton…
BeryJu Aug 19, 2026
c704057
Merge branch 'main' into ee/wcp/browser-privilege
BeryJu Aug 19, 2026
d63ca1a
ee/wcp: expand the CreateMutex diagnostic and log the token's own ide…
BeryJu Aug 19, 2026
81c3fca
Merge branch 'ee/wcp/browser-privilege' of https://github.com/goauthe…
BeryJu Aug 19, 2026
e92a4b4
ee/wcp: switch the service account from Batch to Service logon
BeryJu Aug 20, 2026
ef3dc8a
ee/wcp: grant the service account BaseNamedObjects create-object rights
BeryJu Aug 20, 2026
f7f866c
ee/wcp: drop diagnostic scaffolding, rewrite the design doc for final…
BeryJu Aug 20, 2026
c849961
ee/wcp: proxy ak-sysd calls through credprovider instead of granting …
BeryJu Aug 20, 2026
785f61d
ee/wcp: log the pipe DACL grant and the connecting token's own identity
BeryJu Aug 20, 2026
e8245bd
ee/wcp: grant the pipe SIDs object-specific rights instead of generic…
BeryJu Aug 20, 2026
928e4d0
ee/wcp: log the pipe's DACL as the kernel actually stored it
BeryJu Aug 20, 2026
5e02d7f
ee/wcp: grant the service account access to the named-pipe namespace …
BeryJu Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,16 @@ windows = { version = "0.62", features = [
"Win32_System_Com",
"Win32_System_LibraryLoader",
"Win32_System_Pipes",
"Win32_System_Environment",
"Win32_System_IO",
"Win32_Storage_FileSystem",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_UI_Shell",
"Win32_Security_Credentials",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_RemoteDesktop",
"Win32_System_StationsAndDesktops",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
Expand All @@ -99,6 +102,12 @@ windows = { version = "0.62", features = [
"Win32_NetworkManagement_NetManagement",
"Win32_System_SystemInformation",
"Win32_Storage_EnhancedStorage",
# `NtOpenDirectoryObject` — no Win32 wrapper exists for opening an
# arbitrary Object Manager directory (`BaseNamedObjects`) the way
# `OpenDesktopW` does for desktops.
"Wdk_Foundation",
"Wdk_Storage_FileSystem",
"Wdk_System_SystemServices",
# `ICredentialProviderUser::GetValue` returns a PROPVARIANT, so the e2e
# harness needs these to implement that interface for `SetUserArray`.
"Win32_System_Com_StructuredStorage",
Expand Down
157 changes: 157 additions & 0 deletions ee/wcp/BROWSER_PRIVILEGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Lowering the privilege of the sign-in browser

The problem in one line: `ak_cef.exe` renders untrusted remote web content on
the Windows logon screen, and on the path that matters most it runs as
`NT AUTHORITY\SYSTEM`.

## Status

Branch `ee/wcp/browser-privilege` builds Option B: a dedicated local service
account (`ak-wcp-browser`), created by the installer, that `ak_cef.exe` runs
as instead of SYSTEM. Option A (sandbox the CEF renderer/GPU processes
instead) needs real upstream-shaped work — see that section — so it's out of
scope here; the two are complementary, not exclusive.

Verified on a VM against the manual checklist in `e2e/README.md` on an
earlier version of the token/spawn path; needs a fresh run against the
current code.

### The design

- **Token**: `LogonUserW(LOGON32_LOGON_SERVICE)` with a stored password,
then `CreateRestrictedToken(DISABLE_MAX_PRIVILEGE)` to strip every
privilege — the same pattern Google Credential Provider for Windows
(GCPW) uses for its own LogonUI-hosted helper identity
(`CreateLogonToken`, `chrome/credential_provider/gaiacp/gcp_utils.cc`).
- **Spawn**: `CreateProcessWithTokenW`, brokered through the Secondary
Logon service — the only API of the three tried that actually works from
inside LogonUI (see "Roads not taken").
- **IPC**: named pipes, not anonymous-and-inherited —
`CreateProcessWithTokenW` has no handle-inheritance mechanism at all. A
random UUID name per launch; an SDDL DACL scopes access to SYSTEM plus
the service account's own SID.
- **Desktop**: `syscalls::ensure_desktop_access` grants the account's SID
`GENERIC_ALL` on `WinSta0` and its `Winlogon` desktop.
- **`BaseNamedObjects`**: `syscalls::ensure_base_named_objects_access`
grants the account's SID create-object rights on the session's
`BaseNamedObjects` directory via the native `NtOpenDirectoryObject` (no
Win32 wrapper exists for opening an arbitrary Object Manager directory) —
GCPW hits the identical requirement and answers it the identical way
(`AllowLogonSIDOnLocalBasedNamedObjects`, `os_process_manager.cc`).
- **Hardening**: `syscalls::deny_interactive_and_network_logon` denies the
account interactive/network/RDP logon rights — it must not be usable to
sign in at the screen it serves.
- **Cache/profile**: each launch gets its own unique subdirectory under
`wcp-cache` (`cef-host::browser_state_dir`), not a shared fixed path —
removed after a successful run, left behind after a failed one for
inspection.
- **CEF runtime style**: `RuntimeStyle::ALLOY`, not the Chrome-style
default — this window needs no Chrome UI (tabs, extensions, profile
manager).
- **`ak-sysd`**: `ak_cef.exe` no longer talks to it at all — the service
account has no access to its pipe, and granting that would widen a pipe
every other platform/consumer shares for the sake of one caller.
`credprovider` fetches the sign-in URL and header token before spawning
(passed on the command line) and validates the redirect's token once
`ak_cef.exe` reports it over the result pipe (`ipc::auth_result_for`) —
`ak_cef.exe`'s own job shrinks to "open this URL, inject this header,
report what came back."

Previously, a fresh logon ran the browser as SYSTEM outright (no
interactive-user token existed yet to duplicate) and unlock used the
locked-out user's own token, which then failed to spawn — no access to the
secure desktop. Both scenarios now go through the same service-account token
unconditionally; `CPUS_CREDUI` (the debug-only scenario) is unchanged, and
still falls back to launching in the caller's own session.

### Roads not taken

Each of these looked like the obvious approach and turned out not to work,
confirmed against real installs rather than documentation alone:

- **S4U (`LsaLogonUser`/`MSV1_0_S4U_LOGON`)** avoids a stored password
entirely, but needs `SE_TCB_NAME` — which LogonUI's token does not hold
*at all* (not merely disabled; `PsExec -s whoami /priv` shows SYSTEM
holding it fine, so LogonUI's own hosting process token is evidently not
that same generic SYSTEM token). This is by design: the
credential-provider model since Vista deliberately keeps LogonUI more
sandboxed than `winlogon.exe`/LSASS specifically so third-party code
loaded into it doesn't get TCB-level trust. GCPW has never used S4U for
the identical reason.
- **`CreateProcessAsUserW`** needs the caller to hold
`SE_ASSIGNPRIMARYTOKEN_NAME`/`SE_INCREASE_QUOTA_NAME` — also absent from
LogonUI's token, not disabled. `CreateProcessWithTokenW` sidesteps this:
brokered through the Secondary Logon service, it only needs the caller to
hold `SE_IMPERSONATE_NAME`, which is present.
- **`LOGON32_LOGON_BATCH`** (what GCPW itself uses for its own helper
identity) cannot create *any* named synchronization object
(mutex/event/semaphore) in the session's object namespace — confirmed via
`CreateMutex` failing with `ERROR_ACCESS_DENIED` even for random,
non-reserved names under both `Local\` and `Global\`, while the same
token creates named pipes, files and registry keys with no issue. Fatal
to Chromium's own `ProcessSingleton`, which runs unconditionally during
`CefInitialize`. GCPW never hits this itself because its sign-in UI
helper isn't a Chromium browser process at all — it's `rundll32.exe`
reloading their own DLL through an entrypoint (`ForkGaiaLogonStub`) — but
their code still explicitly grants `BaseNamedObjects` access to the logon
SID, which is what led to `ensure_base_named_objects_access` above.
`LOGON32_LOGON_SERVICE` is the fix: batch logons carry the
`NT AUTHORITY\BATCH` well-known SID rather than `INTERACTIVE`, and
hardened `BaseNamedObjects` ACLs commonly key on logon-type SID.
- **CEF's Chrome-vs-Alloy runtime style** looked like it might also explain
the `ProcessSingleton` failure (Chrome style pulls in that whole
subsystem), but doesn't: the choice only selects a *style* layered on top
of an always-Chrome *bootstrap* (confirmed via CEF's own architecture
docs and the "Delete Alloy bootstrap" change in M128) —
`ProcessSingleton` runs during `CefInitialize` regardless of
`runtime_style`. Kept Alloy anyway, since it's still the right choice for
a single-purpose window with no Chrome UI.

## Option A — enable the CEF sandbox

Not started. Targets the actual risk — hostile input being parsed — rather
than the privilege of the process that hosts the parser, and needs no new
account, installer change, or desktop ACL grant. Complementary to Option B,
not a substitute for it.

Turns out to be more than "link a lib and pass a pointer": the `cef` crate
in use here (151.4.0) has no Windows sandbox support at the Rust level —
`cef::sandbox::Sandbox` is macOS-only, and `cef-dll-sys`'s `build.rs` never
builds or links `cef_sandbox.lib` despite the `sandbox` feature being on by
default. Doing this properly needs a small C++ shim exposing a C ABI around
CEF's `CefScopedSandboxInfo`, plus a `cef-dll-sys` `build.rs` patch —
upstream-shaped work, independent of this branch.

## Decisions — resolved for this branch

1. Integrated auth (Kerberos/SPNEGO)? Moot — this account uses a real
password-based logon, not S4U, so nothing about this design rules it out
if it's ever needed.
2. Granting a non-SYSTEM account access to `WinSta0\Winlogon`? Yes, scoped
to this one account only — a deliberate, documented expansion, not a
free improvement.
3. Does the browser need to be non-SYSTEM at all once renderers are
sandboxed (Option A)? Not settled; revisit if/when Option A is built.

## Known gaps

- **`GENERIC_ALL` on the desktop/`BaseNamedObjects` grants** is broader
than strictly needed. Narrowing to the specific rights CEF actually uses
is still open.
- **Profile paths beyond `root_cache_path`** (temp, fonts, crashpad) are
not yet addressed; not the source of any observed failure so far, but
"hasn't failed yet" isn't "handled."
- **Deployment friction** — GPO blocking local account creation, endpoint
monitoring flagging a new local account, no local accounts on a domain
controller — isn't addressable from this codebase.
- **The `ak-sysd` start call is now serialized before the spawn** rather than
overlapping `ak_cef.exe`'s own startup — simpler, and the one thing
`credprovider` cannot get wrong is which process the `ak-sysd` pipe trusts,
but it does mean the window appears after both round trips added together
rather than the slower of the two.
- **The account's own password validation is a no-op**:
`RealSyscalls::validate` uses `LOGON32_LOGON_NETWORK`, but the account is
denied network logon by design (`deny_interactive_and_network_logon`), so
it always reports "inconclusive" and the stored password is used without
ever being re-verified. Harmless today (falls through to "use it
anyway"), but worth a real fix.
3 changes: 1 addition & 2 deletions ee/wcp/cef-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,5 @@ ak-platform = { path = "../../../ak-platform" }
log = { workspace = true }
sentry = { workspace = true }
windows = { workspace = true }
eyre = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
cef = { version = "151.4.0", features = ["build-util"] }
Loading
Loading