Skip to content

Instantly share code, notes, and snippets.

@YoraiLevi
Created July 1, 2026 04:39
Show Gist options
  • Select an option

  • Save YoraiLevi/995c7558842d263585a0a6aa3bff5a8a to your computer and use it in GitHub Desktop.

Select an option

Save YoraiLevi/995c7558842d263585a0a6aa3bff5a8a to your computer and use it in GitHub Desktop.
Getting a credential-bearing session on Windows over SSH: logon types, why cmdkey/DPAPI/mapped drives fail under OpenSSH, and every fix (sshd-as-user, Cygwin passwd -R, Bitvise, scheduled tasks, Ansible become) with trade-offs to choose by your constraints.

Getting a credential-bearing session on Windows over SSH — why cmdkey, DPAPI and mapped drives fail under OpenSSH, the logon-type model that explains it, and every way to fix it (sshd-as-user, Cygwin passwd -R, Bitvise, scheduled tasks, Ansible become), with the trade-offs to choose by your own constraints.

By: YoraiLevi | Date: 2026-07-01


You SSH into a Windows box, and things that work fine at the desktop quietly fail. cmdkey /add … returns "Credentials cannot be saved from this logon session." Drive letters you mapped at the console (W:, Z:) don't exist in your SSH shell. net use \\server\share can't reach the share, or maps it and then loses it on reboot. Anything that reads a DPAPI-protected secret (Credential Manager, some Git/AWS credential helpers, saved Wi-Fi/VPN creds) comes up empty.

These look like five unrelated bugs. They are one thing seen through five APIs. Understand Windows logon session types and the whole landscape collapses into a single decision: how do I get my SSH shell into a logon session that carries credentials? This guide builds that model, shows exactly why the built-in OpenSSH can't give you one, and then walks every practical fix — with the mechanism, the setup, and the honest cost of each — so you can pick by your constraints, not mine.

A caveat threaded through the whole guide: every fix below ultimately hands a password to LogonUser (or reuses an existing token). None of them conjure credentials from nothing. What differs is where the password lives, when the logon happens, and what it costs you operationally. Hold that; the "unifying model" section makes it precise.


The foundational model: logon sessions and their types

When any principal authenticates to Windows, the OS creates a logon session — a kernel object identified by a LUID — and stamps it with a logon type recording how you logged in. The type is not cosmetic. Windows attaches (or withholds) capabilities based on it. The Microsoft "logon types" reference (SECURITY_LOGON_TYPE) enumerates them; these are the ones that matter here:

Type Name How you get it Reusable creds in the LSA session?
2 Interactive Sit at the console; RDP; LogonUser(…INTERACTIVE) Yes
3 Network Reach a service over the network (SMB, WinRM, SSH pubkey via S4U) No
4 Batch Scheduled Task with stored creds Yes
5 Service A service the SCM started as an account Yes
8 NetworkCleartext Network logon that was handed a password (SSH password auth, IIS Basic) No (network-class)
9 NewCredentials runas /netonly, LOGON32_LOGON_NEW_CREDENTIALS local token only; supplied creds used for network

Two capabilities hang off this type, and they are the entire problem:

Gate A — a "credential set." Each logon session may or may not have an associated store where saved credentials (what cmdkey/CredWrite write) can live. Interactive (2), Batch (4), and Service (5) have one. Network (3) and NetworkCleartext (8) do not. CredWrite in a network logon fails with ERROR_NO_SUCH_LOGON_SESSION — surfaced by cmdkey as "Credentials cannot be saved from this logon session." This is documented behavior on CredWriteW: "Network logon sessions do not have an associated credential set."

Gate B — an unlocked DPAPI master key. DPAPI (the encryption behind Credential Manager, and much else) derives its per-user master key from the user's password. A logon that was given a password (Interactive, NetworkCleartext, Batch, Service-as-user) can unlock it. A logon with no password — which is what public-key SSH produces, via the S4U protocol — cannot: there is no password to derive the key from, so DPAPI stays locked even if a credential set existed.

So the question "will cmdkey/DPAPI/net use work in this shell?" reduces to: is my logon session a non-network type (gate A) that was established with a password (gate B)?

Drive letters are the same story from a third angle: a mapped drive is an object in its logon session's DOS-device namespace (\Sessions\<id>\DosDevices\). Letters mapped in the console session live under a different LUID and are simply not present in the SSH session — and vice-versa. (This is also why EnableLinkedConnections does not help here: it bridges the UAC elevated/non-elevated split within one desktop session, not the gap between the SSH logon and the desktop logon.)


Why the built-in OpenSSH gives you a dead session

Microsoft's Win32-OpenSSH hardcodes the logon it creates, in contrib/win32/win32compat/win32_usertoken_utils.c:

  • Public-key authLsaLogonUser(… Network …) via the S4U ("Service-for-User") protocol. Network, type 3. No password was involved, so both gates are shut: no credential set, DPAPI locked. This is the default and the one most people hit.
  • Password authLogonUserExExW(… LOGON32_LOGON_NETWORK_CLEARTEXT …). NetworkCleartext, type 8. A password was supplied (gate B could open), but it is still a network-class logon, so gate A stays shutcmdkey fails anyway.

There is no sshd_config directive and no registry value to change the logon type; it's a compile-time constant. A pull request that tried to switch password auth to Interactive (openssh-portable #387) was closed unmerged — deliberately, because Interactive + UAC token-splitting widens a privilege-escalation surface. And crucially: switching to password auth does not fix cmdkey — verified in Win32-OpenSSH #2273 (2024). Microsoft's own key-management docs state the position plainly: a key-based session "isn't capable of outbound authentication as the user… This behavior is by design."

So on stock OpenSSH, over SSH, you cannot save a credential, cannot see console drive maps, and cannot read DPAPI secrets. That is not a misconfiguration; it is the design. Every fix below replaces the logon with a better one.


The one primitive under every fix

Strip the packaging off all of them and each does the same thing:

Present the account's password (or reuse an existing non-network token), call LogonUser/LsaLogonUser/the SCM/the Task engine, and receive a non-Network token with gates A and B open.

That's it. The differences that actually matter when you choose:

  • Which token type you end up with (Interactive 2 / Service 5 / Batch 4 — all work; the nuances are elevation and cross-session visibility).
  • Where the password lives (off-box in a vault, on-box in the registry/LSA, in memory per-call, or nowhere for the token-reuse cases).
  • When the logon happens (once and persistent for the whole SSH session, or freshly per task).
  • Whether it works with key-only auth (all of them do, because the credential logon is separate from how the SSH connection authenticated).
  • What it costs (single-user lock, a second daemon, a standing on-box secret, a control node).

The rest of this guide is those variations.


Route 1 — Run sshd as the user (Service logon, type 5)

What it is. Reconfigure the existing Microsoft OpenSSH service to log on as a specific user account instead of LocalSystem. No new software.

How it works. On a key-auth connection where the service is not LocalSystem, sshd's get_user_token() compares the connecting user's SID to the service account's SID; on a match it returns OpenProcessToken() of the service process — the Service (type 5) token the SCM created when it started sshd. Type 5 has a credential set, and because the SCM authenticated the service with the account's real password at start, DPAPI is unlocked. Both gates open. The password never crosses the SSH connection.

Setup (elevated).

  1. Grant the service account SeTcbPrivilege ("Act as part of the operating system") and SeAssignPrimaryTokenPrivilege ("Replace a process level token") — local admins lack these by default; without SeTcbPrivilege the service won't start (Error 1297). (secpol.msc → User Rights Assignment.)
  2. Point the service's Log On at the account via services.msc (its GUI auto-grants "Log on as a service") → This account.\user + password. (CLI: sc.exe config sshd obj= ".\user" password= "…", note the mandatory spaces, then grant the logon-as-service right yourself.)
  3. Host-key ACLs: if the account is a local admin, the existing Administrators:(F) ACE already covers C:\ProgramData\ssh\ssh_host_*_key — no change. Don't alter ownership (StrictModes needs SYSTEM/Administrators).
  4. Full Stop-Service sshd; Start-Service sshd so the SCM re-reads the account and loads the profile.
  5. Verify: from a key session, cmdkey /add … succeeds; Security-log Event 4624 shows Logon Type 5.

What it costs.

  • Single-user lockonly that one account can SSH into the box afterward; any other principal's key connection gets a NULL token → "Permission denied." It's in the source, not configurable.
  • Drives mapped in the SSH (service) session are invisible to the desktop session and vice-versa.
  • The account's password sits in LSA secrets (_SC_sshd), readable by SYSTEM/admins.
  • A password change on the account means updating the service credential (sc.exe config … again), like any service account.
  • If password auth is ever enabled and used, that login reverts to NetworkCleartext (type 8) and breaks again — so this pairs naturally with key-only auth.

Choose it when: one owner per box, key-only auth, you want to keep the built-in OpenSSH and add no software, and you're fine with the single-user restriction.


Route 2 — Cygwin OpenSSH with passwd -R (Interactive logon, type 2 — OSS, free for commercial use)

What it is. Genuine OpenSSH (the same openssh-portable BSD codebase) compiled for the Cygwin runtime, which adds the Windows layer that can call LogonUser with a stored password. License: OpenSSH (BSD) + Cygwin library (LGPLv3 + Linking Exception) → free for commercial use, unlike Bitvise.

How it works. After a public-key login, Cygwin's seteuid() walks a token-building chain (winsup/cygwin/sec/auth.cc):

  • lsaprivkeyauth() — looks up a password stored earlier by passwd -R in LSA private registry data (keyed to the user's SID) and, if found, calls LogonUserW(user, domain, password, LOGON32_LOGON_INTERACTIVE, …) → a genuine Interactive (type 2) token: DPAPI unlocked, cmdkey/net use /persistent:yes work.
  • s4uauth() fallback (no stored password) → LsaLogonUser S4U → Network (type 3), i.e. the same dead session as the built-in.

passwd -R is the switch: store the password once, and every subsequent key login mints a type-2 session. The stored password is used only to construct the token — the SSH connection still authenticates by key.

Setup. Install Cygwin with openssh + cygrunsrv; ssh-host-config -y (defaults to a privileged service account with SeTcbPrivilege); put keys in the user's Cygwin ~/.ssh/authorized_keys with Unix perms (StrictModes); run passwd -R (enter the account's real Windows password); start the cygsshd service. Free port 22 (stop the built-in) or run Cygwin on another port.

What it costs.

  • Password stored in the registry (LSA private data) — a standing on-box secret, same shape as Bitvise's cache.
  • Re-run passwd -R after every Windows password change — a stale entry silently falls back to S4U (type 3): the session looks fine but DPAPI/cmdkey quietly break.
  • Needs a real local password — PIN-only / pure-Microsoft-account logins have nothing for LogonUser.
  • A full Cygwin environment to install and maintain; a Cygwin core-DLL upgrade kills the running service (restart it).

Choose it when: you need a true Interactive session for a live, interactive SSH shell, you want open-source and free-for-commercial, and the extra environment is acceptable.


Route 3 — Bitvise SSH Server (Interactive logon, type 2 — proprietary, free for personal use only)

What it is. A Windows-only SSH server (a different daemon, not OpenSSH) with a per-account logon-type selector. Closed source; free Personal Edition for individual non-commercial use; $99.95/installation for commercial.

How it works. Populate the account's Windows password into Bitvise's password cache, set the account's Logon type = Interactive, and Bitvise calls LogonUser(…INTERACTIVE) for each key session → type 2, gates open. It also has a "Windows session cache" that reuses one Windows session across reconnects — its built-in answer to the network-share reliability problem.

What it costs. A proprietary second daemon (replaces OpenSSH on port 22 — a mixed-server fleet if other boxes stay on OpenSSH); the Windows password stored in Bitvise's registry cache (AES-256); key-only with no cached password silently downgrades to Network (type 3) — you must cache the password for the Interactive path. Free only for personal use — a company/employer install needs the paid license.

Choose it when: you want a supported, GUI-configured product with a genuine Interactive selector and multi-user support, and either the use is personal or you'll pay the per-install license.


Route 4 — Scheduled Task with stored credentials (Batch logon, type 4)

What it is. Don't change the SSH server at all. Register a one-shot scheduled task, running as the target account with its stored password, to execute the credential-writing command; a task runs under a Batch (type 4) logon, which has a credential set and (a password was supplied) an unlocked DPAPI.

How it works. schtasks /create /ru <user> /rp <password> /sc once /tr 'cmd /c cmdkey /add:…'schtasks /run → poll → schtasks /delete (it stores a credential; always clean up). Or the at-logon self-deleting variant, which needs no password at registration but only fires at the next interactive login.

What it costs. It's a job launcher, not a shell — stdin/stdout don't pipe back, so it's for "seed a credential / run a command and exit," not for interactive work. Needs the account password at registration (except the at-logon variant). Session-scoped effects (a mapped drive) don't persist to your SSH session — but a cmdkey write does persist (see the persistence note below).

Choose it when: you only need to seed a credential or run a fixed command, and you don't want to touch the SSH server or add software.


Route 5 — On-demand elevation: Ansible become, or a LogonUser wrapper (Interactive, type 2, per task)

What it is. Instead of changing the session, upgrade the token for a single command. This is what Ansible's Windows become: yes does, and you can reproduce its core with one built-in cmdlet.

How it works. A wrapper performs a second, independent local logon, orthogonal to how the SSH/WinRM connection authenticated: it calls LogonUser(… INTERACTIVE …) with a supplied password and logon_flags=with_profile (loads the profile so DPAPI unlocks), then CreateProcessWithTokenW/CreateProcessAsUser to run the task under that fresh type-2 token. The connection's Network token is irrelevant; the two coexist. Ansible automates exactly this in the Ansible.Become C# assembly (default logon_type=interactive, logon_flags=with_profile), taking the password from Ansible Vault.

The minimal standalone equivalent (no Ansible), run from inside the SSH session:

$cred = [PSCredential]::new($env:USERNAME,(ConvertTo-SecureString 'WindowsPassword' -AsPlainText -Force))
Start-Process powershell.exe -Credential $cred -LoadUserProfile -Wait -NoNewWindow `
  -ArgumentList '-Command','cmdkey /add:server /user:u /pass:p; net use Z: \\server\share /persistent:yes'

Start-Process -Credential internally calls CreateProcessWithLogonWLogonUser(INTERACTIVE); the child gets a type-2 token with DPAPI unlocked.

What it costs. The password must be presented per call (in memory, or vaulted off-box for Ansible). Requires the seclogon service, and (for Windows runas-style elevation) the connecting user to be a local admin. Double-hop caveat: an NTLM-derived Interactive token has no Kerberos ticket, so \\server\share inside the elevated process can fail unless you use logon_type=new_credentials (type 9). One flagged uncertainty: CreateProcessWithLogonW from a headless, session-0 SSH shell may have window-station quirks on some builds — test before you depend on it.

Choose it when: you want per-task elevation without reconfiguring the server, or you're already doing config management — Ansible keeps the password off-box, vault-encrypted, decrypted per run, the lowest standing on-box exposure of the automatable options. (become_user: SYSTEM needs no password — but SYSTEM can't unlock your user's DPAPI, so it's for machine-level ops, not "act as me.")


Route 0 — Don't fight the logon type: UNC + inline credentials

What it is. The zero-setup escape hatch. Stop trying to save credentials or use drive letters over SSH; address shares directly and supply the credential for this session only.

How it works. net use \\server\share /user:u <pass> establishes an authenticated SMB session in the current (network) logon — a Network logon can open a new SMB session when given explicit credentials; it just can't reuse stored ones. Then use \\server\share\… UNC paths directly (or an ephemeral net use Z: … letter that dies with the session).

What it costs. Credentials passed per invocation (visible in history/process args); no persistent drive letters; no Credential Manager entry. But nothing to install, nothing to reconfigure, works today over key-only SSH.

Choose it when: you just need to move files or run a command over SSH and don't actually need persistent creds or DPAPI. Often the right answer — the community's most common conclusion is "don't fight it."


The unifying model — choose by your constraints

Every route is the same primitive with a different password location and lifetime:

Route → token type Password lives Scope Standing on-box secret OSS / free-commercial Multi-user
sshd-as-user Service (5) SCM credential store (on-box) whole SSH session Yes Built-in (MIT) / yes No (single-user lock)
Cygwin passwd -R Interactive (2) LSA registry (on-box) whole SSH session Yes Yes (BSD+LGPL) Yes
Bitvise (Interactive) Interactive (2) Bitvise cache (on-box) whole SSH session Yes No / personal-only Yes
Scheduled task Batch (4) Task Scheduler LSA (on-box) task runtime Yes Built-in / yes Yes
Ansible become Interactive (2) Vault, off-box per task None Yes (GPL/BSD) Yes
Start-Process -Credential Interactive (2) in memory, per call subprocess None Built-in / yes Yes
UNC + inline creds (stays Network) in command, per call per command None Built-in / yes Yes
become_user: SYSTEM Service (reuse) — (token reuse) per task None Built-in / yes n/a (not your creds)

Decision shortcuts:

  • "I just need to move files / run a command." → Route 0 (UNC + inline). Don't build anything.
  • "One owner per box, keep OpenSSH, add nothing." → Route 1 (sshd-as-user). Accept the single-user lock.
  • "Live interactive shell with DPAPI, open-source, free even commercially." → Route 2 (Cygwin passwd -R).
  • "Supported product, multi-user, personal use or willing to pay." → Route 3 (Bitvise).
  • "Seed a credential / run a fixed command; touch nothing." → Route 4 (scheduled task).
  • "Config management, or keep the secret off-box." → Route 5 (Ansible become, vaulted password) or a Start-Process -Credential wrapper.

Where the password lives — the honest threat model

Read past the mechanics: on-box storage means a standing secret. Cygwin's passwd -R blob, Bitvise's cache, the sshd-as-user and scheduled-task LSA secrets are all decryptable by SYSTEM/local-admin on the running machine (extractable with SharpDPAPI / mimikatz-class tooling). On a single-user desktop where you're already the admin, that adds essentially no new exposure — you could already read your own secrets. On a shared or higher-value box it is a real widening: another admin, or an attacker who reaches admin/SYSTEM, gets that password. The routes that keep no standing on-box secret are the per-call ones (Ansible Vault off-box, Start-Process -Credential in memory, UNC inline) — at the cost of presenting the password each time.

And the boundary none of these cross: once your shell holds a credential-bearing token, any code running as you can use it. These mechanisms defend against "a different user, or someone who copied the files" — not against malware running in your own session.


How to validate which logon type you actually got

Don't trust that a fix worked — confirm the logon type and the two gates:

  • Logon type: the definitive check is the Security event log — Get-WinEvent -FilterHashtable @{LogName='Security';Id=4624} -MaxEvents 5 and read the Logon Type field (2 = Interactive, 3 = Network, 4 = Batch, 5 = Service). Type 3 means you're still in the dead session.
  • Gate A (credential set): cmdkey /add:__test /user:x /pass:y — success means a credential set exists; "Credentials cannot be saved from this logon session" means you're still network-class.
  • Gate B (DPAPI): write then read back a cmdkey entry, or a [System.Security.Cryptography.ProtectedData]::Protect/Unprotect round-trip under CurrentUser scope — a failure like "Key not valid for use in specified state" means DPAPI didn't unlock (no password / profile not loaded).
  • Drive persistence: map with /persistent:yes, then check from the next console/RDP login, not from SSH — persistent maps land in HKCU\Network and reappear in the interactive session, not the SSH one.
  • cmdkey persistence gotcha: a cmdkey written under a credential-bearing token as your own user lands in your DPAPI-encrypted Credential Manager and persists (and is shared with your interactive sessions). Written under a different user, it does not carry over. Seeding is a one-shot that survives; only the drive-letter visibility is session-scoped.

Sources

  • Win32-OpenSSH #996 (root cause: cmdkey in a network logon; jborean93's logon-type explanation), #2273 (password auth also fails), #1950 (sshd-as-user makes cmdkey work), #139/#1734 (mapped drives invisible over SSH), #1824 (sshd-as-user privileges / Error 1297), openssh-portable PR #387 (Interactive-for-password, closed by-design).
  • Win32-OpenSSH source contrib/win32/win32compat/win32_usertoken_utils.c (hardcoded Network/NetworkCleartext logon calls) and spawn-ext.c (token selection).
  • Microsoft Learn: CredWriteW ("Network logon sessions do not have an associated credential set"), LogonUserExExW, logon-types reference, OpenSSH server configuration, Service User Accounts (SCM loads the profile), CreateProcessWithTokenW.
  • Cygwin: passwd(1) man page and the ntsec docs (passwd -RLogonUser(INTERACTIVE) after pubkey auth); winsup/cygwin/sec/auth.cc; cygwin.com/licensing.html.
  • Bitvise: SSH Server logon-type guide, version history (S4U → Network downgrade), pricing.
  • Ansible: Ansible.Become.cs, become_wrapper.ps1 (defaults Interactive + WithProfile), "Understanding privilege escalation: become", "Making the double hop on Windows" (jborean93); forum confirmation that become needs the password.
  • Independent: Sebastian Solnica, "How Ansible impersonates users on Windows"; bloggingforlogging.com on logon-session drive scoping.

This guide is a synthesis of primary sources (the OpenSSH/Cygwin/Ansible source, Microsoft API docs, and the Win32-OpenSSH issue tracker) assembled to be reused. Verify the specific command/version behavior on your own build before depending on it — Windows security surfaces shift between releases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment