Tools
Guides
On this page

August 18, 2026

How to install an SSH server on Windows: built-in OpenSSH for 11/10, manual setup for 8.1 and 7

Remote Desktop gives you a screen; SSH gives you a channel. That distinction is why an SSH server on a Windows box earns its keep: scp a build artifact onto a test machine without sharing a folder, tail a log on a headless CI runner from your laptop, script a deployment against twenty Windows hosts from one shell, or give a teammate access to a shared workstation without handing them an interactive desktop session. Since Windows 10 1809 (October 2018), Microsoft ships a real OpenSSH server as an optional feature — the same Win32-OpenSSH code base the PowerShell team maintains — which turned what used to be an afternoon of Cygwin archaeology into a two-command job.

The catch is that “Windows” spans several eras, and the install path is completely different on each side of the 1809 line. On Windows 11 and Windows 10 1809+, OpenSSH Server is a Features-on-Demand capability you enable from Settings or one PowerShell cmdlet. On Windows 10 before 1809, Windows 8.1, and Windows 7, nothing is built in, and you either install the Win32-OpenSSH release manually or use Cygwin. The configuration layer — sshd_config, key authentication, the infamous administrators_authorized_keys rule — is identical everywhere, and that is where most of the real troubleshooting lives.

This guide covers all of it: install per version, then the configuration and hardening that applies to every install, then a diagnostic playbook for the failures you will actually hit.

Windows 11 and Windows 10 (1809 and later): use the built-in feature#

If your machine reports 1809 or newer — check with winver — the built-in optional feature is the only path worth considering. It is serviced through Windows Update, its services integrate with the Service Control Manager like any other, and uninstalling it later is one command. Windows Server 2019 and later have the same capability, so everything in this section applies to server installs too.

See what is already installed#

The OpenSSH client is often already present; the server almost never is. Check both at once from an elevated PowerShell:

Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*'

You will see two lines, OpenSSH.Client~~~~0.0.1.0 and OpenSSH.Server~~~~0.0.1.0, each with a State of Installed or NotPresent. The client alone does not accept inbound connections — it is the ssh command you use to connect out. The server capability is what installs the sshd service.

Install from Settings (the GUI path)#

Open Settings → Apps → Optional features (on Windows 11 it is Apps → Optional features; on older Windows 10 builds look under Apps & features → Optional features), click Add a feature or View features, type “OpenSSH” in the search box, and select OpenSSH Server — not “OpenSSH Client”. Click Next → Install. The download takes a minute or two because capabilities are fetched from Windows Update; on a fully offline machine you would need a Features-on-Demand ISO as a source, which is the one scenario where the manual Win32-OpenSSH install (below) is simpler even on Windows 11.

Install from PowerShell (the repeatable path)#

The scripted equivalent, which is what you want for anything you will do more than once:

# Elevated PowerShell required
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

The four tildes are part of the capability name, not a typo — the string is OpenSSH.Server~~~~0.0.1.0 exactly. When it finishes, the output shows Online : True and RestartNeeded : False; no reboot is required.

Start the service and make it survive reboots#

Installing the capability registers the sshd service but leaves it stopped and set to manual start — a deliberate choice, since an SSH listener should be something you turned on, not something that happened to you. Two commands fix both:

Start-Service sshd
Set-Service -Name sshd -StartupType Automatic

On first start, sshd generates its host keys under C:\ProgramData\ssh\ (ssh_host_ed25519_key, ssh_host_rsa_key, and friends). This is also the moment the automatic firewall rule appears, which matters for the next step. If you plan to use passphrase-protected keys on this machine as a client, also enable the ssh-agent service — more on that in the hardening section.

Confirm the firewall rule#

A capable install creates an inbound rule named OpenSSH-Server-In-TCP allowing TCP 22 on all profiles. Verify it exists and is enabled:

Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP"

If it is missing — most commonly after a hardened baseline image or a group policy that prunes rules — recreate it:

New-NetFirewallRule -Name sshd -DisplayName "OpenSSH Server (sshd)" `
  -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

Two things worth knowing about this layer. First, if you later change the SSH port in sshd_config, this rule does not follow you; a new port needs its own rule or an edit of LocalPort. Second, the Windows firewall is only one of possibly several: corporate endpoints often run a host security agent with its own policy, and “the rule exists but the agent blocks it” is a real failure mode that produces the same connection-refused symptom as no rule at all.

First connection#

On the server, collect two facts: the exact username via whoami (this prints COMPUTERNAME\alice, and the part after the backslash is what you type at the client — Windows account names with spaces are legal and a reliable source of first-connection confusion), and the IPv4 address via ipconfig. Then from another machine:

ssh [email protected]

You will be prompted for alice’s Windows password and asked to confirm the host key fingerprint. Do not blindly type yes: compare the offered fingerprint against the server’s own record with

# Run on the server
ssh-keygen -lf C:\ProgramData\ssh\ssh_host_ed25519_key.pub

If they match, accept. You should land in a cmd.exe prompt that says Microsoft Windows [Version ...] — that is the default shell, and changing it is one of the first things most people do (covered below). Try one command to prove the session really is the remote machine:

whoami
hostname

One account-model note before moving on: if the account signs in with a Microsoft account rather than a local one, the SSH password is the Microsoft account password, and the username is still the local profile name from whoami. This combination surprises people constantly. For machines you will administer over SSH, a dedicated local account is the simpler substrate.

Windows 10 before 1809, Windows 8.1, and Windows 7#

These systems predate the capability. You have two realistic options, and the right choice depends mostly on whether the machine already has a POSIX-ish environment on it.

Option A: Win32-OpenSSH from the GitHub release#

This is the same project Microsoft later folded into Windows, packaged as a zip you install yourself. It works on Windows 7 SP1 through everything current — though note that recent builds target modern Windows, and for a Windows 7 box you want an older release from the same page; the old tags remain downloadable precisely for this.

The procedure, every step from an elevated PowerShell:

  1. Download the OpenSSH-Win64.zip (or Win32 for 32-bit systems) from the Win32-OpenSSH GitHub releases page, and unblock it before extraction — files downloaded from the internet carry a Mark-of-the-Web zone identifier that makes PowerShell refuse the scripts inside:

    Unblock-File .\OpenSSH-Win64.zip
    Expand-Archive .\OpenSSH-Win64.zip -DestinationPath "$env:ProgramFiles"

    The scripts expect to live in C:\Program Files\OpenSSH, so if the zip extracted to C:\Program Files\OpenSSH-Win64, rename that folder to C:\Program Files\OpenSSH.

  2. Install and register the services:

    powershell.exe -ExecutionPolicy Bypass -File "C:\Program Files\OpenSSH\install-sshd.ps1"

    This creates the sshd and ssh-agent services, generates host keys under C:\ProgramData\ssh\, and writes a default sshd_config there. If it reports a missing DLL on a fresh Windows 7 install, the Visual C++ runtime is the usual suspect — install the current x86/x64 redistributable and rerun.

  3. Open the firewall:

    New-NetFirewallRule -Name sshd -DisplayName "OpenSSH Server (sshd)" `
      -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

    On Windows 7, New-NetFirewallRule does not exist — use netsh instead:

    netsh advfirewall firewall add rule name="OpenSSH Server (sshd)" dir=in action=allow protocol=TCP localport=22
  4. Start the service and set it to start at boot:

    Set-Service sshd -StartupType Automatic
    Start-Service sshd

To remove it later, Stop-Service sshd and run uninstall-sshd.ps1 from the same folder — it deregisters the services cleanly, which is a large part of why this option beats hand-rolling a service.

Option B: Cygwin or MSYS2 OpenSSH#

If the machine already runs Cygwin — common for legacy build boxes — installing OpenSSH inside the existing environment keeps one toolchain instead of two. Cygwin’s package manager offers openssh, and its ssh-host-config script does the Windows-service plumbing for you, using Cygwin’s service runner. The trade-offs are structural: Cygwin sshd authenticates against its emulated POSIX account database mapped onto Windows accounts, path translation (/cygdrive/c versus C:\) leaks into every session, and you now own the maintenance of the Cygwin layer itself. MSYS2 can also run sshd via pacman -S openssh, but it has no service framework comparable to ssh-host-config, so you end up hand-wiring a scheduled task or a wrapper — workable, rarely worth it unless MSYS2 is already the center of that machine’s universe.

For a box whose job is “be an SSH-reachable Windows machine,” Option A is the honest default; Option B is for machines that already live inside one of those environments.

Configuration and hardening, common to every version#

Everything below edits C:\ProgramData\ssh\sshd_config — the same file in the same place whether you installed via capability or via the GitHub zip. After each change, restart the service: Restart-Service sshd. Keep a second session open while you test the first change; the classic self-inflicted outage is editing the config from inside the only SSH session and restarting sshd with a syntax error.

The sshd_config essentials#

The defaults are conservative and sane. The three lines most installs eventually touch:

Port 22
PasswordAuthentication yes
PubkeyAuthentication yes
  • Port: changing it is security-through-obscurity with a small real benefit — it removes you from the path of indiscriminate scanners, which measurably reduces log noise on internet-facing hosts. If you change it, you must also fix the firewall rule from earlier; this two-step is the number-one cause of “I changed the port and now I can’t connect.”
  • PasswordAuthentication no: the single most valuable hardening line, but only after key login is proven working. Disable it while a working password session could still save you, not before. Note that even locked out over SSH, a machine with local console or RDP access is recoverable — the lockout is annoying, not fatal, unless the host is remote and headless.
  • PubkeyAuthentication yes is on by default; you rarely need to touch it, but knowing where it lives matters when debugging.

Key generation, on the client side#

On any Windows 10 1809+ or Windows 11 machine the OpenSSH client is built in, ssh-keygen included — no PuTTYgen needed. Generate an Ed25519 key, the modern default choice (small keys, fast signatures, no meaningful length debates):

ssh-keygen -t ed25519 -C "alice@laptop"

Accept the default location (C:\Users\alice\.ssh\id_ed25519), and give it a passphrase — a key without one is a bearer token that rides around in backups and sync folders. The result is a private key id_ed25519 that never leaves the client, and a public key id_ed25519.pub whose single line you will install on servers. On the client, enable ssh-agent once so the passphrase is asked per boot rather than per connection:

Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
ssh-add

The administrators_authorized_keys rule — read this before key auth#

Here is the gotcha that consumes more troubleshooting hours than every other Windows SSH topic combined. On Linux, your public key goes in ~/.ssh/authorized_keys, full stop. On Windows there is a fork:

  • If the account is a standard user, the key goes in C:\Users\<user>\.ssh\authorized_keys, as you would expect.
  • If the account is a member of the Administrators group, sshd ignores that file and reads C:\ProgramData\ssh\administrators_authorized_keys instead.

The default sshd_config ends with a block that implements exactly this:

Match Group administrators
       AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys

Why the split exists is worth understanding, because it explains the ACL chore that follows. Keys that grant administrative logins are treated as machine-wide configuration rather than per-user data: C:\ProgramData\ssh is writable only by Administrators and SYSTEM, so a process running as a lower-privilege account cannot plant a key there and quietly enroll itself for admin-level SSH. Per-user profile directories, by contrast, follow the user’s own ACLs. On POSIX this is policed by file ownership checks that have exact equivalents; on Windows, mapping “this file belongs to this user” onto the ACL model for elevated accounts is where the incompatibilities lived, and centralizing admin keys sidesteps the whole question. You can delete that Match block to force per-user files everywhere, but the saner path is to work with the design.

So: for an admin account, put the public key into the machine file, pasting the single line from id_ed25519.pub:

notepad C:\ProgramData\ssh\administrators_authorized_keys

Then — the step everyone misses — fix the ACL. The file inherits ProgramData’s permissive inheritance, and sshd refuses keys from files that groups other than Administrators/SYSTEM can write. The refusal is silent from the client’s point of view: your key just does not work, and you fall back to password if it is still enabled. The canonical fix:

icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /inheritance:r
icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /grant "SYSTEM:(F)"
icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /grant "BUILTIN\Administrators:(F)"

/inheritance:r removes inherited ACEs entirely — the r means remove, not replace — leaving a file accessible to exactly SYSTEM and the built-in Administrators group. Verify:

icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys"

The output should list only NT AUTHORITY\SYSTEM and BUILTIN\Administrators, both (F). Anything else listed, and key auth for admin accounts will keep silently failing.

Standard users get the same treatment in miniature: C:\Users\<user>\.ssh\authorized_keys also fails if the ACL is too loose, which it often is after a copy from another machine. Tighten it the same way:

icacls.exe "$env:USERPROFILE\.ssh\authorized_keys" /inheritance:r /grant "${env:USERNAME}:(F)" /grant "SYSTEM:(F)"

Two smaller traps in the same neighborhood. First, the file must be plain text — old Notepad’s default “Unicode” encoding (UTF-16) makes the key unreadable; save as plain text or UTF-8 without BOM, or better, write the file with PowerShell’s Set-Content/Add-Content, which does the right thing. Second, there is no ssh-copy-id on Windows, and piping your public key through ssh user@host "cat >> ..." behaves differently depending on the server’s default shell — cmd.exe has no cat. Pasting into Notepad is unglamorous and always works; unglamorous and always works is the correct trade for a one-time setup step.

Make PowerShell the default shell#

By default an SSH session drops you into cmd.exe, which is a working shell from 1988. Point sshd at PowerShell instead — this is a registry setting, not an sshd_config one, which surprises people the first time:

if (-not (Test-Path "HKLM:\SOFTWARE\OpenSSH")) {
    New-Item -Path "HKLM:\SOFTWARE\OpenSSH" -Force
}

# PowerShell 7+, if installed
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
  -Value "C:\Program Files\PowerShell\7\pwsh.exe" -PropertyType String -Force

# ...or built-in Windows PowerShell 5.1
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
  -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force

New sessions pick it up immediately. This one change transforms the day-to-day experience — object pipelines over SSH for remote administration instead of parsing cmd text output — and it is also what tools like VS Code’s Remote-SSH extension expect when the target is Windows.

Troubleshooting: the failures you will actually meet#

”Connection refused” or timeouts#

Work outward from the service:

Get-Service sshd                                # must be Running
Get-NetTCPConnection -LocalPort 22 -State Listen  # something must be listening

If nothing listens, the service is stopped or the config failed to parse — check the event log (below). If it listens, test locally on the server: ssh localhost. That succeeds while remote connections fail? The problem is between the machines: firewall rule missing or disabled, a host-security agent, a VPN split-tunnel route, or simply the wrong IP (machines with multiple NICs — Wi-Fi plus Ethernet — answer on one of them, and ipconfig shows both; make sure you are targeting the one on your network). Only after the local test should you suspect the network itself.

Key authentication silently failing#

The server accepted your password but ignored your key. In rough order of likelihood: your account is in the Administrators group and the key is in the wrong file (the Match Group administrators fork above — check with whoami /groups); the file is in the right place but the ACL is too permissive (run the icacls commands, verify the output shows only the allowed principals); the public key line got wrapped or re-encoded by an editor; or you are being prompted for the key’s passphrase and reading it as a password prompt. sshd does not tell the client any of this by design — authentication failures are deliberately uninformative on the wire — which is why the next tool exists.

sshd -d: live logs, one connection at a time#

The service writes to the event log, but the highest-signal diagnostic is running the daemon in the foreground in debug mode, where it narrates every authentication decision:

Stop-Service sshd
& "C:\Windows\System32\OpenSSH\sshd.exe" -d

(For the manual GitHub install the path is C:\Program Files\OpenSSH\sshd.exe.) Now connect from your client and watch: you will see the offered key evaluated, and if the ACL is wrong, a line like Authentication refused: bad ownership or modes for file ... — the exact file, the exact reason. -d serves exactly one connection, then exits; add a second d (-dd) for more verbosity, and -p 2222 to listen on an alternate port while you test config changes without touching the production listener. When you are done, Ctrl+C and Start-Service sshd.

The event log#

For everything that happened while the service ran normally: Event Viewer (eventvwr.msc) → Applications and Services Logs → OpenSSH → Operational. Service startup failures — a bad sshd_config line is the classic — appear here with the line number, which is more than the silent non-starting service will ever tell you.

FAQ#

Does sshd inside WSL conflict with the Windows sshd?#

Depends on the WSL version, because they network differently. WSL2 runs a real Linux kernel in a lightweight VM with its own NAT’d network stack — its port 22 and Windows’ port 22 are different interfaces, and both can listen simultaneously without complaint; reaching the WSL one from outside requires port forwarding. WSL1 shares the Windows network stack directly, so two sshd processes binding the same port on the same stack genuinely collide. In practice, if your goal is “SSH into this machine and get work done,” the Windows sshd is usually sufficient — and if what you actually want is for SSH sessions to land in Linux, point DefaultShell at C:\Windows\System32\wsl.exe rather than running a second daemon.

Can I listen on port 22 and a custom port at the same time?#

Yes, and it is the right way to migrate. sshd_config accepts multiple Port lines and listens on all of them:

Port 22
Port 2222

Add the firewall rule for the new port before restarting, restart, verify from the client on both, and only then remove port 22 if that is the plan. The same trick eases firewall cutovers: never move a working listener and its network path in one step.

Does this work for passwordless Git and VS Code Remote SSH?#

Both, with one clarification each. The OpenSSH client side is universal: your id_ed25519 works identically against GitHub, GitLab, or any Git host, and ssh -T git@... style tests behave exactly as on Linux — nothing about Windows changes the protocol. VS Code’s Remote-SSH extension connecting into this Windows box works well and is a genuinely pleasant way to develop on a remote Windows machine; set DefaultShell to PowerShell first, because the extension drives the target through shell commands and expects a capable one. The clarification: making this Windows box itself serve Git repositories over SSH is a different project — stock sshd gives you shell and SFTP, not git-receive-pack plumbing. Use the Windows box as your SSH endpoint freely; stand up a real Git server (Gitea, Forgejo, GitLab) if repositories are the goal.

Closing thoughts#

The version split is the map: 1809 and later is a two-command capability install, older systems take the Win32-OpenSSH zip or ride an existing Cygwin, and everything after installation converges on the same C:\ProgramData\ssh directory and the same rules. Get three things right and the rest is details — start the service and set it automatic, verify the firewall rule actually matches the port you listen on, and internalize the administrators_authorized_keys fork before you configure your first admin key, because its failure mode is silence. Add PasswordAuthentication no once key login is proven, set DefaultShell to PowerShell so sessions are worth having, and keep sshd -d in mind as the tool that turns invisible authentication failures into readable text. A hardened Windows SSH endpoint is a quiet, boring piece of infrastructure — which is exactly the compliment infrastructure deserves.

← All guides