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.

powershell
Get-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0

Output:

yaml
Name  : OpenSSH.Client~~~~0.0.1.0
State : Installed

Install if the state shows NotPresent:

powershell
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0

Output:

yaml
Path          :
Online        : True
RestartNeeded : False

Confirm the installed version:

cmd
ssh -V

Output:

code
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.

cmd
ssh [options] [user@]host [command]

Output: (none — opens an interactive shell, or executes the command and exits)

Essential options

OptionMeaning
-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
-NNo remote command — forward ports only
-TDisable pseudo-TTY allocation
-J <jump_host>Route through a jump / bastion host
-AForward the local SSH agent to the remote host
-vVerbose — 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.

cmd
ssh alicedev@myhost

Output:

vbnet
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:

cmd
ssh -p 2222 alicedev@myhost

Output:

css
alicedev@myhost's password:
alicedev@myhost:~$

Run a one-off command without entering an interactive shell:

cmd
ssh alicedev@myhost "uptime && df -h /"

Output:

bash
 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):

cmd
ssh-keygen -t ed25519 -C "alice@example.com"

Output:

vbnet
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:

powershell
type $HOME\.ssh\id_ed25519.pub | ssh alicedev@myhost "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Output:

css
alicedev@myhost's password:

Verify key-based login — no password prompt should appear:

cmd
ssh alicedev@myhost

Output:

ruby
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:

bash
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:

cmd
ssh myhost

Output:

ruby
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:

cmd
scp C:\Reports\report.csv alicedev@myhost:/home/alicedev/reports/

Output:

bash
report.csv                               100%   42KB 512.0KB/s   00:00

Copy a remote file to the local machine:

cmd
scp alicedev@myhost:/var/log/syslog C:\Logs\syslog.txt

Output:

bash
syslog                                   100%  1024KB   2.1MB/s   00:00

Copy an entire directory recursively:

cmd
scp -r alicedev@myhost:/home/alicedev/project C:\Projects\

Output:

bash
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:

cmd
ssh -N -L 3307:localhost:3306 alicedev@myhost

Output:

lua
(none — tunnel open, blocks until Ctrl+C)

Remote forward — expose a local dev server on port 8080 of the remote:

cmd
ssh -N -R 8080:localhost:5173 alicedev@myhost

Output:

lua
(none — tunnel open, blocks until Ctrl+C)

Dynamic SOCKS5 proxy — route browser traffic through the remote host:

cmd
ssh -N -D 1080 alicedev@myhost

Output:

csharp
(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.

powershell
Set-Service -Name ssh-agent -StartupType Automatic
Start-Service ssh-agent

Output:

vbnet
(none — service starts and is set to auto-start on reboot)

Load a private key into the agent:

cmd
ssh-add %USERPROFILE%\.ssh\id_ed25519

Output:

less
Enter passphrase for C:\Users\alice\.ssh\id_ed25519:
Identity added: C:\Users\alice\.ssh\id_ed25519 (alice@example.com)

List keys currently loaded:

cmd
ssh-add -l

Output:

ruby
256 SHA256:abc123xyz alice@example.com (ED25519)

Remove all keys from the agent (e.g. before locking a shared workstation):

cmd
ssh-add -D

Output:

css
All identities removed.

Common pitfalls

  1. bad permissions on the private key — OpenSSH refuses keys that other users can read. Fix with: icacls %USERPROFILE%\.ssh\id_ed25519 /inheritance:r /grant:r "%USERNAME%:R".
  2. authorized_keys must be mode 600 on the remote — a Linux SSH daemon silently ignores the file if it's group- or world-writable; run chmod 600 ~/.ssh/authorized_keys on the remote.
  3. 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 → CRLFLF).
  4. StrictHostKeyChecking blocks scripted connections — for automation, add -o StrictHostKeyChecking=accept-new to auto-trust new hosts rather than using the unsafe no setting.
  5. ssh-agent service not runningssh-add: Could not open a connection to your authentication agent means the service is stopped; run Start-Service ssh-agent in an elevated prompt.
  6. known_hosts mismatch after server rebuild — if the remote host key changes, ssh exits with WARNING: 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.

cmd
ssh -N -L 3307:db-server:3306 alicedev@myhost

Output:

lua
(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.

cmd
ssh -J alicedev@jumpbox alicedev@10.0.0.20

Output:

ruby
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.

cmd
wsl rsync -avz --delete /mnt/c/Projects/myapp/ alicedev@myhost:/home/alicedev/myapp/

Output:

python
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.

cmd
ssh alicedev@myhost "who | grep pts"

Output:

yaml
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.

powershell
# 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):

yaml
Path          :
Online        : True
RestartNeeded : False

Verify the service is listening and reachable:

powershell
Get-Service sshd
netstat -an | findstr ":22 "

Output:

ini
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:

powershell
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.

powershell
# 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:

text
# 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 / behaviourLinux OpenSSHWindows OpenSSH
Config path/etc/ssh/sshd_configC:\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 sftpsftp-server binarysftp-server.exe
chroot directiveHonouredNot supportedChrootDirectory is silently ignored
Path separators// or \\ accepted; \\ doubled when escaping
Token %h (home)Linux-styleResolves to Windows profile path
Token __PROGRAMDATA__n/aWindows-only substitution for %PROGRAMDATA%
AuthorizedKeysFileusually .ssh/authorized_keyssame path but inside %USERPROFILE%
Administrator's authorized_keysper-user fileC:\ProgramData\ssh\administrators_authorized_keys (special case)
Reload signalkill -HUPRestart-Service sshd
Default port firewalliptables/nftablesNew-NetFirewallRule / Defender Firewall

After editing, validate then restart:

powershell
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.

powershell
# 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):

yaml
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.

powershell
# 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):

csharp
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:

powershell
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:

text
# 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.

powershell
# 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):

code
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:

text
# 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):

cmd
ssh -s alicedev@myhost powershell

Output:

text
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.

powershell
# 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:

python
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:

IDMeaning
1sshd started
2sshd stopped
4Connection accepted
5Bad ownership / modes on authorized_keys
6Authentication failed
100Subsystem error

For LogLevel DEBUG3 to take effect, also raise the level in sshd_config and restart:

text
LogLevel DEBUG3
SyslogFacility LOCAL0
powershell
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:

text
# /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:

powershell
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:

powershell
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:

text
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo

Reload and connect from any PowerShell 7 client:

powershell
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:

markdown
[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.

cmd
sftp alicedev@myhost

Output:

css
Connected to myhost.
sftp>

Common commands inside the prompt:

text
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:

cmd
echo get /var/log/syslog C:\Logs\syslog.txt > batch.txt
sftp -b batch.txt alicedev@myhost

Output:

text
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:

ToolBest forNative Windows?ResumeDelta sync
scpQuick one-off copies, scripted outputyesnono
sftpInteractive browse + transferyesyes (reget/reput)no
rsyncLarge recursive trees, repeated syncsno — via WSL or cwRsyncyes (--partial)yes (--delete)
pscp (PuTTY)Legacy scripts; supports older protocolsyesnono
robocopy /ssh(not supported — robocopy is SMB-only)n/an/an/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)

  1. administrators_authorized_keys ACLs — 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.
  2. HKLM:\SOFTWARE\OpenSSH\DefaultShell not respected — values must be a literal path; environment variables aren't expanded. %ProgramFiles%\PowerShell\7\pwsh.exe will silently fail.
  3. sshd_config path uses ProgramData — not Program Files, not %APPDATA%, not ~/.ssh. The first wrong guess wastes an hour.
  4. Service restart needed for most changes — almost every sshd_config change needs Restart-Service sshd; the DefaultShell registry key is the rare exception.
  5. Windows Firewall not opened by Add-WindowsCapability — on some builds, the install creates the rule but leaves it disabled. Verify with Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP'.
  6. sshd -t doesn't catch ACL mistakes — it only validates syntax. Use Repair-SshdHostKeyPermission and Repair-AuthorizedKeyPermission from OpenSSHUtils for ACL issues.

Real-world recipes (continued)

Bootstrap a brand-new Windows server for remote management

powershell
# 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

powershell
$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

powershell
# 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).

powershell
# 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:

text
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 msiexec whenever 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.

  • 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