cheat sheet
ssh
Connect to remote hosts, transfer files, and forward ports over an encrypted channel using the OpenSSH client built into Windows 10 and later.
ssh — Secure Shell Client for Windows
What it is
ssh (Secure Shell) is a cryptographic protocol and its command-line client for establishing encrypted, authenticated connections to remote machines. Microsoft bundles OpenSSH — the reference implementation maintained by the OpenBSD project — as a built-in optional feature in Windows 10 (1803+) and as a default install in Windows 11 and Windows Server 2019+. It replaces older Windows-specific tools like PuTTY, Telnet, and rsh, and is now the standard way to administer Linux servers, cloud VMs, and network devices directly from CMD or PowerShell. For GUI file transfer, WinSCP wraps the same protocol.
Availability
OpenSSH Client ships as a Windows optional feature and is pre-installed on most modern builds. Verify or install from an elevated PowerShell prompt.
Get-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
Output:
Name : OpenSSH.Client~~~~0.0.1.0
State : Installed
Install if the state shows NotPresent:
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
Output:
Path :
Online : True
RestartNeeded : False
Confirm the installed version:
ssh -V
Output:
OpenSSH_for_Windows_9.5p1, LibreSSL 3.8.2
Syntax
The base invocation takes an optional user, the hostname or IP, and an optional command to run non-interactively.
ssh [options] [user@]host [command]
Output: (none — opens an interactive shell, or executes the command and exits)
Essential options
| Option | Meaning |
|---|---|
-p <port> | Connect on a non-default port (default 22) |
-i <keyfile> | Private key file to use for authentication |
-l <user> | Remote username (alternative to user@host) |
-L <lport>:<host>:<rport> | Local port forward |
-R <rport>:<host>:<lport> | Remote port forward |
-D <port> | Dynamic SOCKS5 proxy on the given local port |
-N | No remote command — forward ports only |
-T | Disable pseudo-TTY allocation |
-J <jump_host> | Route through a jump / bastion host |
-A | Forward the local SSH agent to the remote host |
-v | Verbose — shows authentication handshake (stack up to -vvv) |
-o <Key=Value> | Pass any ssh_config option inline |
Connecting to a remote host
On first connection ssh prints the remote host's fingerprint and asks you to confirm. Once accepted, the fingerprint is stored in %USERPROFILE%\.ssh\known_hosts and future connections proceed without the prompt.
ssh alicedev@myhost
Output:
The authenticity of host 'myhost (192.168.1.50)' can't be established.
ED25519 key fingerprint is SHA256:abc123xyz...
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'myhost' (ED25519) to the list of known hosts.
alicedev@myhost's password:
Last login: Mon Apr 28 09:00:00 2026
alicedev@myhost:~$
Connect on a non-default port:
ssh -p 2222 alicedev@myhost
Output:
alicedev@myhost's password:
alicedev@myhost:~$
Run a one-off command without entering an interactive shell:
ssh alicedev@myhost "uptime && df -h /"
Output:
09:15:01 up 12 days, 3:22, 1 user, load average: 0.05, 0.04
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 40G 12G 26G 32% /
Key-based authentication
Key authentication replaces passwords with a public/private key pair. The private key stays on your Windows machine; the public key is appended to ~/.ssh/authorized_keys on each remote host you want to reach without a password.
Generate an Ed25519 key pair (preferred over RSA for new keys):
ssh-keygen -t ed25519 -C "alice@example.com"
Output:
Generating public/private ed25519 key pair.
Enter file in which to save the key (C:\Users\alice\.ssh\id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in C:\Users\alice\.ssh\id_ed25519
Your public key has been saved in C:\Users\alice\.ssh\id_ed25519.pub
The key fingerprint is:
SHA256:abc123xyz alice@example.com
Windows has no ssh-copy-id. Use this PowerShell one-liner to append your public key to the remote authorized_keys file:
type $HOME\.ssh\id_ed25519.pub | ssh alicedev@myhost "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
Output:
alicedev@myhost's password:
Verify key-based login — no password prompt should appear:
ssh alicedev@myhost
Output:
Last login: Mon Apr 28 09:15:00 2026
alicedev@myhost:~$
SSH config file
The file %USERPROFILE%\.ssh\config stores per-host settings so you can type ssh myhost instead of a full command string. Each Host block defines an alias and its connection parameters; Host * applies defaults to every connection.
Create or edit %USERPROFILE%\.ssh\config with these example blocks:
Host myhost
HostName 192.168.1.50
User alicedev
Port 22
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 60
Host jumpbox
HostName bastion.example.com
User alicedev
IdentityFile ~/.ssh/id_ed25519
Host internal-server
HostName 10.0.0.20
User alicedev
ProxyJump jumpbox
IdentityFile ~/.ssh/id_ed25519
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
Output: (none — config file saved)
Connect using the alias:
ssh myhost
Output:
Last login: Mon Apr 28 09:15:00 2026
alicedev@myhost:~$
Transferring files with scp
scp (Secure Copy) ships with OpenSSH and copies files over the SSH channel. It accepts the same -p, -i, and -P (capital for port) options as ssh. For large recursive syncs, rsync (via WSL) is faster due to delta transfers; scp is for quick one-off copies.
Copy a local file to a remote directory:
scp C:\Reports\report.csv alicedev@myhost:/home/alicedev/reports/
Output:
report.csv 100% 42KB 512.0KB/s 00:00
Copy a remote file to the local machine:
scp alicedev@myhost:/var/log/syslog C:\Logs\syslog.txt
Output:
syslog 100% 1024KB 2.1MB/s 00:00
Copy an entire directory recursively:
scp -r alicedev@myhost:/home/alicedev/project C:\Projects\
Output:
main.py 100% 2KB 1.2MB/s 00:00
requirements.txt 100% 512B 512.0KB/s 00:00
Port forwarding
Port forwarding tunnels TCP traffic through the encrypted SSH connection. Local forwarding (-L) makes a remote service reachable on a local port; remote forwarding (-R) exposes a local port on the remote machine; dynamic forwarding (-D) creates a SOCKS5 proxy. Combine with -N to open the tunnel without an interactive shell.
Local forward — reach a remote MySQL instance on localhost:3307:
ssh -N -L 3307:localhost:3306 alicedev@myhost
Output:
(none — tunnel open, blocks until Ctrl+C)
Remote forward — expose a local dev server on port 8080 of the remote:
ssh -N -R 8080:localhost:5173 alicedev@myhost
Output:
(none — tunnel open, blocks until Ctrl+C)
Dynamic SOCKS5 proxy — route browser traffic through the remote host:
ssh -N -D 1080 alicedev@myhost
Output:
(none — SOCKS5 proxy listening on localhost:1080)
SSH agent on Windows
The Windows OpenSSH agent service (ssh-agent) holds decrypted private keys in memory so you enter your passphrase once per session rather than on every connection. Enable and start the service from an elevated PowerShell prompt.
Set-Service -Name ssh-agent -StartupType Automatic
Start-Service ssh-agent
Output:
(none — service starts and is set to auto-start on reboot)
Load a private key into the agent:
ssh-add %USERPROFILE%\.ssh\id_ed25519
Output:
Enter passphrase for C:\Users\alice\.ssh\id_ed25519:
Identity added: C:\Users\alice\.ssh\id_ed25519 (alice@example.com)
List keys currently loaded:
ssh-add -l
Output:
256 SHA256:abc123xyz alice@example.com (ED25519)
Remove all keys from the agent (e.g. before locking a shared workstation):
ssh-add -D
Output:
All identities removed.
Common pitfalls
bad permissionson the private key — OpenSSH refuses keys that other users can read. Fix with:icacls %USERPROFILE%\.ssh\id_ed25519 /inheritance:r /grant:r "%USERNAME%:R".authorized_keysmust be mode 600 on the remote — a Linux SSH daemon silently ignores the file if it's group- or world-writable; runchmod 600 ~/.ssh/authorized_keyson the remote.- Windows line endings in config or key files — editors like Notepad may save
\r\n; the SSH daemon expects\n. Save config files with LF endings (VS Code: bottom-right status bar →CRLF→LF). - StrictHostKeyChecking blocks scripted connections — for automation, add
-o StrictHostKeyChecking=accept-newto auto-trust new hosts rather than using the unsafenosetting. ssh-agentservice not running —ssh-add: Could not open a connection to your authentication agentmeans the service is stopped; runStart-Service ssh-agentin an elevated prompt.known_hostsmismatch after server rebuild — if the remote host key changes, ssh exits withWARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!. Remove the old entry:ssh-keygen -R myhost.
Real-world recipes
Tunnel a database port through a bastion
Access a MySQL server on a private network without opening extra firewall rules — route through a jump host that has internet access.
ssh -N -L 3307:db-server:3306 alicedev@myhost
Output:
(none — tunnel open, blocks until Ctrl+C)
Point your SQL client at localhost:3307. Traffic routes through myhost to db-server:3306 on the private LAN.
Reach an internal server via a jump host
Connect to an internal VM that is not directly reachable from the internet by hopping through a public bastion host.
ssh -J alicedev@jumpbox alicedev@10.0.0.20
Output:
alicedev@10.0.0.20:~$
Or define ProxyJump jumpbox in the config Host internal-server block and connect with just ssh internal-server.
Sync a project folder via rsync over SSH (WSL)
rsync is not natively available in Windows but runs in WSL. Use the Windows SSH key directly from WSL for incremental syncs.
wsl rsync -avz --delete /mnt/c/Projects/myapp/ alicedev@myhost:/home/alicedev/myapp/
Output:
sending incremental file list
./
main.py
requirements.txt
sent 3,456 bytes received 94 bytes 7,100.00 bytes/sec
total size is 32,768 speedup is 4.50
Audit who is connected to a remote host
Run a one-off command to list active SSH sessions on the remote without entering an interactive shell.
ssh alicedev@myhost "who | grep pts"
Output:
alicedev pts/0 2026-04-29 09:00 (192.168.1.10)
bob pts/1 2026-04-29 09:12 (192.168.1.20)
Installing OpenSSH Server on Windows
OpenSSH Server (sshd) on Windows accepts incoming SSH connections — turning your Windows box into a managed host you can administer from a Linux laptop, a CI runner, or via PowerShell Remoting over SSH. It ships as a Windows Capability that must be installed and started explicitly. Once configured, it integrates with Windows authentication (local SAM, Active Directory, Microsoft Entra), the Windows event log, and Windows Firewall.
# Check availability
Get-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
# Install
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
# Start the service and set it to auto-start
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
# Open the firewall (the install usually creates the rule, but verify)
Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' |
Select-Object Name, Enabled, Action, Direction
# If the rule is missing, add it
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' `
-Enabled True -Direction Inbound -Protocol TCP -LocalPort 22 -Action Allow
Output (Add-WindowsCapability):
Path :
Online : True
RestartNeeded : False
Verify the service is listening and reachable:
Get-Service sshd
netstat -an | findstr ":22 "
Output:
Status Name DisplayName
------ ---- -----------
Running sshd OpenSSH SSH Server
TCP 0.0.0.0:22 0.0.0.0:0 LISTENING
TCP [::]:22 [::]:0 LISTENING
On Windows Server, use the same Add-WindowsCapability command — it works identically to client SKUs. For Windows Server 2019 specifically, install via Install-WindowsFeature instead:
Install-WindowsFeature -Name OpenSSH.Server
sshd_config — Windows-specific differences
The Windows sshd configuration file lives at C:\ProgramData\ssh\sshd_config (note: ProgramData, not Program Files, and not the user's .ssh). It follows the upstream OpenBSD syntax, but a few directives behave differently or have additional Windows-specific implications. The file is created with sane defaults on install, but most production hardening starts here.
# Open in Notepad (admin)
notepad C:\ProgramData\ssh\sshd_config
# Validate the syntax before restarting sshd
sshd -t
Open sshd_config and the common settings to tune:
# C:\ProgramData\ssh\sshd_config
# --- Network ---
Port 22
ListenAddress 0.0.0.0
ListenAddress ::
# --- Host keys ---
HostKey __PROGRAMDATA__/ssh/ssh_host_ed25519_key
HostKey __PROGRAMDATA__/ssh/ssh_host_rsa_key
HostKey __PROGRAMDATA__/ssh/ssh_host_ecdsa_key
# --- Authentication ---
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin no
PermitEmptyPasswords no
# --- Subsystems (Windows uses backslash paths) ---
Subsystem sftp sftp-server.exe
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo
# --- Override default shell (per-user via registry, see below) ---
# --- Logging ---
SyslogFacility LOCAL0
LogLevel INFO
# --- Per-user / per-group rules ---
AllowGroups "AD\\SSH Users"
DenyUsers "AD\\guest"
Match Group "AD\\Admins"
PasswordAuthentication no
AuthenticationMethods publickey
Differences from the Linux build worth knowing:
| Directive / behaviour | Linux OpenSSH | Windows OpenSSH |
|---|---|---|
| Config path | /etc/ssh/sshd_config | C:\ProgramData\ssh\sshd_config |
| Host keys | /etc/ssh/ssh_host_* | C:\ProgramData\ssh\ssh_host_* |
| Default shell | /bin/sh (per-user via /etc/passwd) | cmd.exe (override via registry — see below) |
Subsystem sftp | sftp-server binary | sftp-server.exe |
chroot directive | Honoured | Not supported — ChrootDirectory is silently ignored |
| Path separators | / | / or \\ accepted; \\ doubled when escaping |
Token %h (home) | Linux-style | Resolves to Windows profile path |
Token __PROGRAMDATA__ | n/a | Windows-only substitution for %PROGRAMDATA% |
AuthorizedKeysFile | usually .ssh/authorized_keys | same path but inside %USERPROFILE% |
Administrator's authorized_keys | per-user file | C:\ProgramData\ssh\administrators_authorized_keys (special case) |
| Reload signal | kill -HUP | Restart-Service sshd |
| Default port firewall | iptables/nftables | New-NetFirewallRule / Defender Firewall |
After editing, validate then restart:
sshd -t
Restart-Service sshd
Host keys on Windows
The sshd host key files live in C:\ProgramData\ssh\ and are generated automatically the first time sshd is started. Use these commands to inspect, regenerate, or sync host keys across machines.
# List host key files
Get-ChildItem C:\ProgramData\ssh\ssh_host_*
# Show fingerprints of all installed host keys
Get-ChildItem C:\ProgramData\ssh\ssh_host_*.pub | ForEach-Object {
ssh-keygen -lf $_.FullName
}
# Regenerate host keys (run elevated; service will re-create on next start)
Remove-Item C:\ProgramData\ssh\ssh_host_*
Restart-Service sshd
# Fix ACLs after copying host keys between machines
# (the OpenSSH install ships a helper)
& 'C:\Windows\System32\OpenSSH\OpenSSHUtils.psm1' -Confirm:$false
Import-Module 'C:\Windows\System32\OpenSSH\OpenSSHUtils.psm1' -Force
Repair-SshdHostKeyPermission -FilePath C:\ProgramData\ssh\ssh_host_ed25519_key
Output (ssh-keygen -lf):
256 SHA256:Tg7m...abc no comment (ED25519)
3072 SHA256:Xf8c...xyz no comment (RSA)
256 SHA256:Yp4e...mno no comment (ECDSA)
Distribute the new fingerprints to client known_hosts files to avoid the change-detected warning on first reconnect after a key rotation.
administrators_authorized_keys — the special case
On Windows, members of the Administrators group do not read their authorized_keys from ~/.ssh/authorized_keys. Instead, sshd reads a single shared file at C:\ProgramData\ssh\administrators_authorized_keys for every admin user. This is a Windows-specific security design — it prevents a compromised admin profile from being used to silently re-enable SSH access via a per-user file.
# Create the file with locked-down ACLs (admin and SYSTEM only)
$keyfile = 'C:\ProgramData\ssh\administrators_authorized_keys'
New-Item -ItemType File -Path $keyfile -Force | Out-Null
# Append a public key
type C:\Users\alicedev\.ssh\id_ed25519.pub | Out-File -Append -Encoding ASCII $keyfile
# Repair ACLs so sshd will read it
icacls.exe $keyfile /inheritance:r `
/grant 'Administrators:F' `
/grant 'SYSTEM:F'
Output (icacls):
processed file: C:\ProgramData\ssh\administrators_authorized_keys
Successfully processed 1 files; Failed processing 0 files
If sshd refuses your key as Administrator and the log says Authentication refused: bad ownership or modes, the cause is almost always the ACL on this file. Use Repair-AuthorizedKeyPermission from OpenSSHUtils:
Import-Module 'C:\Windows\System32\OpenSSH\OpenSSHUtils.psm1' -Force
Repair-AuthorizedKeyPermission -FilePath C:\ProgramData\ssh\administrators_authorized_keys
To disable the admin-specific path and let admins use per-user authorized_keys (not recommended), comment out the Match Group administrators block at the bottom of sshd_config:
# Match Group administrators
# AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
Default shell — using PowerShell instead of cmd.exe
sshd hands sessions to whatever is registered as the default shell for the system. Out of the box that's C:\Windows\System32\cmd.exe, which is ugly to work in. Switch the default to PowerShell 7 (or pwsh) via a single registry value.
# Set PowerShell 7 as the default SSH shell
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell `
-Value 'C:\Program Files\PowerShell\7\pwsh.exe' -PropertyType String -Force
# Optional: pass extra args
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShellCommandOption `
-Value '/c' -PropertyType String -Force
# Set Windows PowerShell 5.1 instead
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell `
-Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' `
-PropertyType String -Force
After setting the value, reconnect — no service restart needed for the registry change.
Output (when connected with PS7 default):
PowerShell 7.4.2
PS C:\Users\alicedev>
For per-user shells, define a Subsystem block in sshd_config and tell the client to use it:
# In sshd_config
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo
Connect from a remote OpenSSH client into that subsystem (this is also how PowerShell Remoting over SSH works — see powershell-remoting):
ssh -s alicedev@myhost powershell
Output:
PowerShell 7.4.0
PS C:\Users\alicedev>
Logging — where SSH logs go on Windows
Unlike Linux, Windows OpenSSH does not write to a syslog file by default. Logs are sent to the Windows Event Log under the OpenSSH/Operational channel and Application log, with optional file logging via the SyslogFacility LOCAL0 directive in sshd_config.
# View recent sshd events
Get-WinEvent -LogName 'OpenSSH/Operational' -MaxEvents 20 |
Format-Table TimeCreated, Id, LevelDisplayName, Message -Wrap
# Filter for authentication failures
Get-WinEvent -FilterHashtable @{
LogName = 'OpenSSH/Operational'
Id = 4
} -MaxEvents 50 | Select-Object TimeCreated, Message
# Enable file logging by editing sshd_config
# (add `SyslogFacility LOCAL0` and `LogLevel DEBUG3`)
# Then logs land in C:\ProgramData\ssh\logs\sshd.log
Output:
TimeCreated Id LevelDisplayName Message
----------- -- ---------------- -------
5/25/2026 9:15:01 4 Information sshd: Accepted publickey for alicedev from 192.168.1.10 port 51234 ssh2
5/25/2026 9:14:55 5 Warning sshd: Authentication refused: bad ownership or modes for ...
Key event IDs:
| ID | Meaning |
|---|---|
| 1 | sshd started |
| 2 | sshd stopped |
| 4 | Connection accepted |
| 5 | Bad ownership / modes on authorized_keys |
| 6 | Authentication failed |
| 100 | Subsystem error |
For LogLevel DEBUG3 to take effect, also raise the level in sshd_config and restart:
LogLevel DEBUG3
SyslogFacility LOCAL0
Restart-Service sshd
Get-Content C:\ProgramData\ssh\logs\sshd.log -Wait -Tail 50
sshd hardening checklist
A minimal hardening pass to apply to any Internet-exposed Windows sshd:
# /ProgramData/ssh/sshd_config
# Disable password auth — keys only
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
# Require pubkey + something else for admins
Match Group administrators
AuthenticationMethods publickey,keyboard-interactive
# Restrict listen address (e.g. management VLAN only)
ListenAddress 10.0.10.5
# Modern algorithms only
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Rate-limiting and timeouts
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 5
ClientAliveInterval 60
ClientAliveCountMax 3
# Lock down to a security group
AllowGroups "AD\\SSH Users" "AD\\SSH Admins"
Then test the config without committing the running daemon:
sshd -t
sshd -T # dump effective config
Restart-Service sshd
Finally, restrict the listening port at the firewall to your management subnet rather than the whole internet:
Set-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' `
-RemoteAddress '10.0.0.0/16' -Profile Domain
PowerShell Remoting over SSH
PowerShell 7 can use SSH instead of WinRM as its transport, which gives you cross-platform remoting between Linux and Windows hosts. The Windows sshd must declare a PowerShell subsystem that PowerShell invokes when establishing the session.
Add the subsystem to sshd_config:
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo
Reload and connect from any PowerShell 7 client:
Restart-Service sshd
# From a Linux/Mac client running pwsh
Enter-PSSession -HostName myhost -UserName alicedev
# Or from Windows client into Linux pwsh target
Enter-PSSession -HostName ubuntu-host -UserName alicedev -KeyFilePath ~/.ssh/id_ed25519
Output:
[myhost]: PS C:\Users\alicedev> Get-Process | Select -First 3
See the powershell-remoting cheat sheet for the full set of Invoke-Command / New-PSSession recipes, credential helpers, and SSH-vs-WinRM trade-offs.
sftp — interactive file transfers
sftp ships with OpenSSH and runs over the same SSH connection as ssh/scp but adds an interactive prompt with familiar cd, ls, get, put, mget, mput, chmod, mkdir commands. It's the right tool when you need an interactive browse-and-pull session against an unfamiliar tree.
sftp alicedev@myhost
Output:
Connected to myhost.
sftp>
Common commands inside the prompt:
sftp> pwd # remote pwd
sftp> lpwd # local pwd
sftp> cd /var/log
sftp> ls -la
sftp> get syslog # remote → local
sftp> get -r project # recursive
sftp> put C:\Reports\daily.csv # local → remote
sftp> mput *.log # multi
sftp> rename old.txt new.txt
sftp> chmod 600 secrets.env
sftp> bye # disconnect
Non-interactive batch mode:
echo get /var/log/syslog C:\Logs\syslog.txt > batch.txt
sftp -b batch.txt alicedev@myhost
Output:
Connected to myhost.
Fetching /var/log/syslog to C:\Logs\syslog.txt
/var/log/syslog 100% 142KB 3.1MB/s 00:00
For the Windows sshd subsystem, sftp uses sftp-server.exe and supports the metadata flag for Unix permissions on /mnt/c-style paths.
scp vs sftp vs rsync — which to use
scp, sftp, and rsync all use SSH for transport but have different ergonomics and performance profiles. For the windows-from-windows case:
| Tool | Best for | Native Windows? | Resume | Delta sync |
|---|---|---|---|---|
scp | Quick one-off copies, scripted output | yes | no | no |
sftp | Interactive browse + transfer | yes | yes (reget/reput) | no |
rsync | Large recursive trees, repeated syncs | no — via WSL or cwRsync | yes (--partial) | yes (--delete) |
pscp (PuTTY) | Legacy scripts; supports older protocols | yes | no | no |
robocopy /ssh | (not supported — robocopy is SMB-only) | n/a | n/a | n/a |
The general rule: scp for ad-hoc transfers, sftp when you don't know exactly what you want, rsync via WSL for any sync that runs more than once.
Tunneling and proxies — see also
For the full set of -L/-R/-D tunnel recipes, ssh-agent forwarding, autossh persistence, jump-host chains, and ssh_config Match directives, see the linux ssh-tunnels cheat sheet — the syntax is identical between the Linux and Windows OpenSSH builds.
Common pitfalls (continued)
administrators_authorized_keysACLs — Windows admins read keys from this special file, not from~/.ssh/authorized_keys. If your admin key doesn't work, this is almost always why.HKLM:\SOFTWARE\OpenSSH\DefaultShellnot respected — values must be a literal path; environment variables aren't expanded.%ProgramFiles%\PowerShell\7\pwsh.exewill silently fail.sshd_configpath uses ProgramData — not Program Files, not%APPDATA%, not~/.ssh. The first wrong guess wastes an hour.- Service restart needed for most changes — almost every
sshd_configchange needsRestart-Service sshd; theDefaultShellregistry key is the rare exception. - Windows Firewall not opened by
Add-WindowsCapability— on some builds, the install creates the rule but leaves it disabled. Verify withGet-NetFirewallRule -Name 'OpenSSH-Server-In-TCP'. sshd -tdoesn't catch ACL mistakes — it only validates syntax. UseRepair-SshdHostKeyPermissionandRepair-AuthorizedKeyPermissionfromOpenSSHUtilsfor ACL issues.
Real-world recipes (continued)
Bootstrap a brand-new Windows server for remote management
# Enable OpenSSH Server
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Set-Service sshd -StartupType Automatic
Start-Service sshd
# Default shell → PowerShell 7
winget install --id Microsoft.PowerShell --silent --accept-package-agreements
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell `
-Value 'C:\Program Files\PowerShell\7\pwsh.exe' -PropertyType String -Force
# Add my admin key
$keyfile = 'C:\ProgramData\ssh\administrators_authorized_keys'
'ssh-ed25519 AAAA...abc alice@example.com' | Out-File -Append -Encoding ASCII $keyfile
icacls $keyfile /inheritance:r /grant 'Administrators:F' /grant 'SYSTEM:F'
# Harden
@'
PasswordAuthentication no
PubkeyAuthentication yes
AllowGroups administrators
'@ | Add-Content C:\ProgramData\ssh\sshd_config
sshd -t
Restart-Service sshd
Mirror authorized_keys to a fleet via PowerShell Remoting
$key = Get-Content C:\Keys\team_ed25519.pub -Raw
$servers = 'web01','web02','db01'
Invoke-Command -ComputerName $servers -ScriptBlock {
param($key)
$f = 'C:\ProgramData\ssh\administrators_authorized_keys'
$existing = if (Test-Path $f) { Get-Content $f -Raw } else { '' }
if ($existing -notmatch [regex]::Escape($key.Trim())) {
$key | Out-File -Append -Encoding ASCII $f
icacls $f /inheritance:r /grant 'Administrators:F' /grant 'SYSTEM:F' | Out-Null
"Updated on $env:COMPUTERNAME"
} else {
"Already present on $env:COMPUTERNAME"
}
} -ArgumentList $key
Stream sshd logs in real time during a connect-fail debug session
# Terminal 1 — tail the operational log
Get-WinEvent -LogName 'OpenSSH/Operational' -Oldest -ContinueAfter $true |
Format-Table TimeCreated, Id, Message -Wrap
# Terminal 2 — reproduce the failed connection
ssh -vvv alicedev@myhost
Pair with sshd -d (run in the foreground from an elevated prompt — first Stop-Service sshd) for full server-side trace output.
Upgrading from in-box OpenSSH to the Win32-OpenSSH release
The OpenSSH bundled with Windows ships through the Windows Capability mechanism and trails the upstream OpenBSD release by months. The PowerShell/Win32-OpenSSH project provides Microsoft's own fresher build as an MSI that installs into C:\Program Files\OpenSSH and supersedes the in-box copy on PATH. Use it when you need a recent ciphers/KEX list, a security fix that hasn't landed in Windows servicing yet, or an upcoming feature (Microsoft has publicly discussed Entra ID authentication as a future addition to the Windows fork).
# Check the in-box version first
ssh -V
# Download and install the latest MSI from GitHub Releases
$msi = "$env:TEMP\OpenSSH-Win64-v9.8.1.0p1.msi"
Invoke-WebRequest -Uri 'https://github.com/PowerShell/Win32-OpenSSH/releases/latest/download/OpenSSH-Win64-v9.8.1.0p1.msi' -OutFile $msi
Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /quiet ADDLOCAL=Client,Server" -Wait -Verb RunAs
# Confirm the MSI build is now first on PATH
ssh -V
where.exe ssh
Output:
OpenSSH_for_Windows_9.5p1, LibreSSL 3.8.2
OpenSSH_for_Windows_9.8p1, LibreSSL 3.9.2
C:\Program Files\OpenSSH\ssh.exe
C:\Windows\System32\OpenSSH\ssh.exe
The MSI registers its own service entries (named ssh-agent and sshd like the in-box ones) and keeps existing keys, host keys, and sshd_config in place — C:\ProgramData\ssh is untouched. To revert, uninstall the MSI from Apps & Features and the in-box copy takes over again automatically.
The MSI build is not updated via Windows Update — you must re-run
msiexecwhenever a new release lands. For fleets, package the MSI in your software-distribution tool (Intune, Configuration Manager, Chocolatey, winget) and treat OpenSSH like any other third-party app.
Related cheat sheets
- linux/ssh-tunnels —
-L,-R,-D,ProxyJump, agent forwarding (identical syntax) - powershell-remoting — PowerShell over SSH transport
- whoami — confirm identity inside the SSH session
- winget — install PowerShell 7 / OpenSSH-related tools
- wsl-interop — forward the Windows ssh-agent into WSL
- netstat — confirm sshd is listening
Sources
- PowerShell/Win32-OpenSSH on GitHub — upstream Windows port; readme covers feature parity with OpenBSD
- Win32-OpenSSH Releases — MSI downloads and per-version changelogs
- Upgrade in-box OpenSSH to the Latest OpenSSH Release | Microsoft Learn — Microsoft-documented MSI upgrade path
- Install Win32 OpenSSH Wiki — installation modes, service registration, ACL helpers
- OpenSSH for Windows overview | Microsoft Learn —
Add-WindowsCapability,sshd_configpaths, default shell