<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>OpenSSH on Windows Server · Loosely Decoupled</title>
    <link>https://looselydecoupled.com/series/openssh-windows-server/</link>
    <description>From a stopped sshd to key-only login for named accounts, on Server 2022 and 2025. Three guides that were written separately and read as one procedure.</description>
    <language>en-us</language>
    
    <copyright>© 2026 Emmanuel Tsouris</copyright>
    <lastBuildDate>Mon, 28 Sep 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://looselydecoupled.com/series/openssh-windows-server/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>How to Enable SSH on Windows Server 2025</title>
      <link>https://looselydecoupled.com/enable-ssh-windows-server-2025/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/enable-ssh-windows-server-2025/</guid>
      <pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Windows Server</category>
      <category>SSH</category>
      <category>PowerShell</category>
      <category>Automation</category>
      
      <description>Enabling SSH on Windows Server 2025 lets only Administrators and the OpenSSH Users group sign in. You turn it on with PowerShell, then choose who&#39;s in it.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>On Windows Server 2025 the OpenSSH server is installed but not running. In an
elevated PowerShell session:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-Service</span> <span class="n">-Name</span> <span class="n">sshd</span> <span class="n">-StartupType</span> <span class="n">Automatic</span>
</span></span><span class="line"><span class="cl"><span class="nb">Start-Service</span> <span class="n">-Name</span> <span class="n">sshd</span>
</span></span><span class="line"><span class="cl"><span class="nb">Get-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Format-Table</span> <span class="n">Name</span><span class="p">,</span> <span class="n">Enabled</span><span class="p">,</span> <span class="n">Profile</span></span></span></code></pre></div></div>
  </div>
</div><p>The stock rule covers the Private profile only. Before connecting, add the
profile the server&rsquo;s network is on; the firewall step below shows how to tell.
On a domain-joined server:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-Profile</span> <span class="n">Domain</span><span class="p">,</span> <span class="n">Private</span></span></span></code></pre></div></div>
  </div>
</div><p>On a cloud VM or a workgroup server, whose network is usually Public:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-Profile</span> <span class="n">Public</span><span class="p">,</span> <span class="n">Private</span></span></span></code></pre></div></div>
  </div>
</div><p>Run one, not both: <code>-Profile</code> replaces the rule&rsquo;s list rather than adding to it.</p>
<p>Then connect from a client:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ssh</span> <span class="console__token">Administrator@server-address</span></span></code></pre>
    </div>
  </div></div><p>If <code>Set-Service</code> reports that the service does not exist, the capability was
removed from your image; the first step below puts it back.</p>

</div>

<p>The visible change from Server 2022 is that the install step is usually gone:
Server 2025 images carry the <code>OpenSSH.Server</code> capability by default. Server
Manager gained a <strong>Remote SSH Access</strong> toggle on the Local Server page that
enables the service. The commands below do the
same thing from PowerShell, where they can be scripted, and they check each
assumption rather than relying on the image being stock.</p>
<!-- style-check: procedure -->
<h2 id="confirm-the-server-capability-is-present">Confirm the server capability is present<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#confirm-the-server-capability-is-present"
     aria-label="Link to section: Confirm the server capability is present" data-pagefind-ignore></a>
</h2>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-WindowsCapability</span> <span class="n">-Online</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="n">Name</span> <span class="o">-like</span> <span class="s1">&#39;OpenSSH*&#39;</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Format-Table</span> <span class="n">Name</span><span class="p">,</span> <span class="n">State</span></span></span></code></pre></div></div>
  </div>
</div><p>On a stock image <code>OpenSSH.Server</code> reads <code>Installed</code>. If it reads <code>NotPresent</code>
— a hardened or minimised image may have dropped it — add it the same way as
on Server 2022:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Add-WindowsCapability</span> <span class="n">-Online</span> <span class="n">-Name</span> <span class="n">OpenSSH</span><span class="p">.</span><span class="n">Server</span><span class="p">~~~~</span><span class="mf">0.0</span><span class="p">.</span><span class="py">1</span><span class="p">.</span><span class="mf">0</span></span></span></code></pre></div></div>
  </div>
</div><h2 id="start-sshd-and-make-it-start-at-boot">Start sshd and make it start at boot<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#start-sshd-and-make-it-start-at-boot"
     aria-label="Link to section: Start sshd and make it start at boot" data-pagefind-ignore></a>
</h2>
<p>The service is registered but stopped. Set the start type before starting it,
so a reboot cannot leave the server unreachable:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-Service</span> <span class="n">-Name</span> <span class="n">sshd</span> <span class="n">-StartupType</span> <span class="n">Automatic</span>
</span></span><span class="line"><span class="cl"><span class="nb">Start-Service</span> <span class="n">-Name</span> <span class="n">sshd</span>
</span></span><span class="line"><span class="cl"><span class="nb">Get-Service</span> <span class="n">-Name</span> <span class="n">sshd</span></span></span></code></pre></div></div>
  </div>
</div><p>The first start writes the host keys and <code>sshd_config</code> to <code>C:\ProgramData\ssh\</code>.</p>
<h2 id="confirm-the-firewall-rule">Confirm the firewall rule<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#confirm-the-firewall-rule"
     aria-label="Link to section: Confirm the firewall rule" data-pagefind-ignore></a>
</h2>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-ErrorAction</span> <span class="n">SilentlyContinue</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Format-Table</span> <span class="n">Name</span><span class="p">,</span> <span class="n">Enabled</span><span class="p">,</span> <span class="n">Direction</span><span class="p">,</span> <span class="n">Action</span><span class="p">,</span> <span class="n">Profile</span></span></span></code></pre></div></div>
  </div>
</div><p>The rule is normally present with <code>Enabled</code> set to <code>True</code>. If it is present
but disabled, enable it; if it is absent, create it:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Enable-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">New-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-DisplayName</span> <span class="s1">&#39;OpenSSH Server (sshd)&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Enabled</span> <span class="n">True</span> <span class="n">-Direction</span> <span class="n">Inbound</span> <span class="n">-Protocol</span> <span class="n">TCP</span> <span class="n">-Action</span> <span class="n">Allow</span> <span class="n">-LocalPort</span> <span class="mf">22</span></span></span></code></pre></div></div>
  </div>
</div><p>Run whichever of the two applies, not both. A rule created this way covers
every profile.</p>
<p>The stock rule covers the Private profile only, so check which profile the
server&rsquo;s network is on:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-NetConnectionProfile</span> <span class="p">|</span> <span class="nb">Format-Table</span> <span class="n">InterfaceAlias</span><span class="p">,</span> <span class="n">NetworkCategory</span></span></span></code></pre></div></div>
  </div>
</div><p>A domain-joined server usually reads <code>DomainAuthenticated</code>, and a cloud VM or
a workgroup server usually reads <code>Public</code>. The stock rule admits neither. For
<code>DomainAuthenticated</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-Profile</span> <span class="n">Domain</span><span class="p">,</span> <span class="n">Private</span></span></span></code></pre></div></div>
  </div>
</div><p>For <code>Public</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-Profile</span> <span class="n">Public</span><span class="p">,</span> <span class="n">Private</span></span></span></code></pre></div></div>
  </div>
</div><p>Run one, not both: <code>-Profile</code> replaces the rule&rsquo;s list rather than adding to
it, so the second command would undo the first. A server with more than one
network adapter can report more than one category; then name them all in one
command, such as <code>-Profile Domain, Public, Private</code>.</p>
<p>Public applies the rule on any network the server joins, so on a public
address let the network firewall in front of the server decide who reaches
port 22.</p>
<h2 id="connect-from-a-client">Connect from a client<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#connect-from-a-client"
     aria-label="Link to section: Connect from a client" data-pagefind-ignore></a>
</h2>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ssh</span> <span class="console__token">Administrator@server-address</span></span></code></pre>
    </div>
  </div></div><p>Accept the host key and enter the account&rsquo;s Windows password. You arrive in
<code>cmd.exe</code>; that is the default shell for Windows OpenSSH. To land in PowerShell
instead:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">New-ItemProperty</span> <span class="n">-Path</span> <span class="s1">&#39;HKLM:\SOFTWARE\OpenSSH&#39;</span> <span class="n">-Name</span> <span class="n">DefaultShell</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-PropertyType</span> <span class="n">String</span> <span class="n">-Force</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Value</span> <span class="s1">&#39;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe&#39;</span></span></span></code></pre></div></div>
  </div>
</div><!-- style-check: end procedure -->
<h2 id="expected-result">Expected result<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#expected-result"
     aria-label="Link to section: Expected result" data-pagefind-ignore></a>
</h2>
<p><code>Get-Service sshd</code> reports <code>Running</code>, something is listening on port 22:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-NetTCPConnection</span> <span class="n">-LocalPort</span> <span class="mf">22</span> <span class="n">-State</span> <span class="n">Listen</span></span></span></code></pre></div></div>
  </div>
</div><p>and a password login from a client succeeds for an administrator. Nobody else
can sign in yet, and the next section is why.</p>

<!-- style-check: procedure -->
<h2 id="choose-who-can-sign-in">Choose who can sign in<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#choose-who-can-sign-in"
     aria-label="Link to section: Choose who can sign in" data-pagefind-ignore></a>
</h2>
<p>On Server 2025, the generated <code>C:\ProgramData\ssh\sshd_config</code> already
limits SSH to two local groups:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">AllowGroups administrators &#34;openssh users&#34;</span></span></code></pre></div></div>
  </div>
</div><p>This is the change from Server 2022, where the generated file has no such line
and every account that can sign in to the server can sign in over SSH. On
Server 2025, an account that isn&rsquo;t an administrator gets in only once it&rsquo;s a
member of OpenSSH Users:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Add-LocalGroupMember</span> <span class="n">-Group</span> <span class="s1">&#39;OpenSSH Users&#39;</span> <span class="n">-Member</span> <span class="s1">&#39;deploy&#39;</span></span></span></code></pre></div></div>
  </div>
</div><p>There is no root account, so <code>PermitRootLogin</code> does nothing. To keep
administrators off SSH entirely, deny their group, which leaves OpenSSH Users
as the only way in:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">DenyGroups administrators</span></span></code></pre></div></div>
  </div>
</div><p><code>AllowUsers</code> narrows access further rather than widening it. An account has to
pass <code>AllowUsers</code> and <code>AllowGroups</code> both, so a name listed in <code>AllowUsers</code> that
isn&rsquo;t in either group is still refused.</p>
<p>And once key-based login works for the accounts that need it, turn passwords
off:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">PasswordAuthentication no</span></span></code></pre></div></div>
  </div>
</div><p>Put each new line above the <code>Match Group administrators</code> block at the end of
the file. A line added after it belongs to that block: it applies only to
administrators, or, for a directive the block doesn&rsquo;t allow, stops sshd from
starting.</p>
<p>Restart the service after any change:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Restart-Service</span> <span class="n">-Name</span> <span class="n">sshd</span></span></span></code></pre></div></div>
  </div>
</div><p>Where an administrator&rsquo;s public key has to go is the Windows-specific part; the
related article scripts it.</p>
<!-- style-check: end procedure -->
<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<h3 id="administrator-gets-permission-denied-publickey">Administrator gets <code>Permission denied (publickey)</code><a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#administrator-gets-permission-denied-publickey"
     aria-label="Link to section: Administrator gets Permission denied (publickey)" data-pagefind-ignore></a>
</h3>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">Permission</span> <span class="console__token">denied</span> <span class="console__token">(publickey)</span></span></code></pre>
    </div>
  </div></div><p>Windows OpenSSH doesn&rsquo;t read <code>~\.ssh\authorized_keys</code> for members of the
Administrators group. The key belongs in
<code>C:\ProgramData\ssh\administrators_authorized_keys</code>, with inheritance removed
and only Administrators and SYSTEM granted access.</p>
<h3 id="sshd-stopped-while-the-toggle-says-on"><code>sshd</code> stopped while the toggle says on<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#sshd-stopped-while-the-toggle-says-on"
     aria-label="Link to section: sshd stopped while the toggle says on" data-pagefind-ignore></a>
</h3>
<p>Refresh Server Manager. If they still disagree, the service failed to start,
and the usual cause is a broken edit to <code>sshd_config</code>. Event Viewer won&rsquo;t say
so: a broken file leaves the OpenSSH <strong>Admin</strong> and <strong>Operational</strong> channels
empty, and the System log records only that the service terminated
unexpectedly. Check the file without starting anything:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">sshd</span> <span class="n">-t</span></span></span></code></pre></div></div>
  </div>
</div><p>It prints the line number and what is wrong with it, and nothing when the file
is valid.</p>
<h3 id="only-administrators-can-sign-in">Only administrators can sign in<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#only-administrators-can-sign-in"
     aria-label="Link to section: Only administrators can sign in" data-pagefind-ignore></a>
</h3>
<p>Even after adding an account to OpenSSH Users, on a non-English installation
the group can carry a translated name, such as <code>OpenSSH-Benutzer</code> in German,
while <code>sshd_config</code> still names <code>openssh users</code>. Find the local name, put it in
the <code>AllowGroups</code> line in lower case, quoted if it contains a space, and
restart sshd:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-LocalGroup</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="n">Name</span> <span class="o">-like</span> <span class="s1">&#39;*SSH*&#39;</span></span></span></code></pre></div></div>
  </div>
</div><h3 id="connection-times-out-with-the-service-running">Connection times out with the service running<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2025/#connection-times-out-with-the-service-running"
     aria-label="Link to section: Connection times out with the service running" data-pagefind-ignore></a>
</h3>
<p>The rule exists but is disabled, or doesn&rsquo;t cover the network profile the
server is on. The firewall drops the packets, so the client waits and times
out rather than being refused:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="p">|</span> <span class="nb">Format-Table</span> <span class="n">Name</span><span class="p">,</span> <span class="n">Enabled</span><span class="p">,</span> <span class="n">Profile</span></span></span></code></pre></div></div>
  </div>
</div><p><code>Enabled</code> should read <code>True</code>, and <code>Profile</code> must include the profile the
server is on; the firewall step earlier covers both.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>OS</dt> <dd>Windows Server 2025 Datacenter (build 26100.33438), OpenSSH.Server removed and added again for a clean install</dd></div>
    <div class="tested__row"><dt>OpenSSH</dt> <dd>9.5p2</dd></div>
    <div class="tested__row"><dt>Client</dt> <dd>OpenSSH on macOS 27</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>Configure SSH Key Authentication for an Administrator on Windows Server with PowerShell</title>
      <link>https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/</guid>
      <pubDate>Sun, 05 May 2024 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Windows Server</category>
      <category>SSH</category>
      <category>PowerShell</category>
      <category>Automation</category>
      
      <description>Windows OpenSSH ignores an administrator&#39;s authorized_keys. Put the key in administrators_authorized_keys with the right ACL, scripted for a fresh server.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>For any account in the local Administrators group, the public key goes in one
shared file, and that file must be accessible only by Administrators and SYSTEM.
In an elevated PowerShell session on the server, with your public key in
<code>$publicKey</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$authorizedKeys</span> <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$env:ProgramData</span><span class="s2">\ssh\administrators_authorized_keys&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Add-Content</span> <span class="n">-Path</span> <span class="nv">$authorizedKeys</span> <span class="n">-Value</span> <span class="nv">$publicKey</span> <span class="n">-Encoding</span> <span class="n">ascii</span>
</span></span><span class="line"><span class="cl"><span class="n">icacls</span><span class="p">.</span><span class="py">exe</span> <span class="nv">$authorizedKeys</span> <span class="p">/</span><span class="n">inheritance</span><span class="err">:</span><span class="nb">r </span><span class="p">/</span><span class="n">grant</span> <span class="s1">&#39;Administrators:F&#39;</span> <span class="p">/</span><span class="n">grant</span> <span class="s1">&#39;SYSTEM:F&#39;</span></span></span></code></pre></div></div>
  </div>
</div><p>Then from the client:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ssh</span> <span class="console__token">-i</span> <span class="console__token">~/.ssh/id_ed25519</span> <span class="console__token">Administrator@server-address</span></span></code></pre>
    </div>
  </div></div><p>No password prompt means it worked. A non-administrator account uses
<code>~\.ssh\authorized_keys</code> as on any other OpenSSH server and needs none of this.</p>

</div>

<p>The OpenSSH port for Windows makes one deliberate departure from the
<code>sshd_config</code> defaults you know. Its stock configuration ends with:</p>
<div class="code-block" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Match Group administrators
</span></span><span class="line"><span class="cl">       AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys</span></span></code></pre></div></div>
  </div>
</div><p>So for an administrator, the per-user <code>authorized_keys</code> is never consulted, and
<code>ssh-copy-id</code> — which doesn&rsquo;t exist on Windows anyway — would put the key in
a file sshd never reads. The key goes in the shared file, and sshd ignores that
file without saying why if an ordinary user can write to it.</p>
<!-- style-check: procedure -->
<h2 id="generate-a-key-pair-on-the-client">Generate a key pair on the client<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#generate-a-key-pair-on-the-client"
     aria-label="Link to section: Generate a key pair on the client" data-pagefind-ignore></a>
</h2>
<p>If you do not already have one:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ssh-keygen</span> <span class="console__token">-t</span> <span class="console__token">ed25519</span></span></code></pre>
    </div>
  </div></div><p>Take the defaults. Key formats, <code>ssh-keygen</code> and the rest of the client side
are unmodified OpenSSH, so a key you already use elsewhere works here too. The
public half is <code>~/.ssh/id_ed25519.pub</code>, one line beginning <code>ssh-ed25519</code>.
That line is what the server needs; the private half never leaves the client.</p>
<h2 id="get-the-public-key-onto-the-server">Get the public key onto the server<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#get-the-public-key-onto-the-server"
     aria-label="Link to section: Get the public key onto the server" data-pagefind-ignore></a>
</h2>
<p>Pick whichever route reaches the server. In a password-authenticated SSH
session, or an RDP session, paste the one line into a variable on the server:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># Paste the whole public key line between the quotes.</span>
</span></span><span class="line"><span class="cl"><span class="nv">$publicKey</span> <span class="p">=</span> <span class="s1">&#39;ssh-ed25519 AAAA... you@laptop&#39;</span></span></span></code></pre></div></div>
  </div>
</div><p>Or, from the client, copy the line in one step, which avoids pasting a long
line through a terminal:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">scp</span> <span class="console__token">~/.ssh/id_ed25519.pub</span> <span class="console__token">Administrator@server-address:id_ed25519.pub</span></span></code></pre>
    </div>
  </div></div><p>and on the server:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$publicKey</span> <span class="p">=</span> <span class="nb">Get-Content</span> <span class="s2">&#34;</span><span class="nv">$env:USERPROFILE</span><span class="s2">\id_ed25519.pub&#34;</span></span></span></code></pre></div></div>
  </div>
</div><h2 id="write-the-key-and-fix-the-permissions">Write the key and fix the permissions<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#write-the-key-and-fix-the-permissions"
     aria-label="Link to section: Write the key and fix the permissions" data-pagefind-ignore></a>
</h2>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$sshDir</span>        <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$env:ProgramData</span><span class="s2">\ssh&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$authorizedKeys</span> <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$sshDir</span><span class="s2">\administrators_authorized_keys&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="p">(</span><span class="nb">Test-Path</span> <span class="nv">$sshDir</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">New-Item</span> <span class="n">-ItemType</span> <span class="n">Directory</span> <span class="n">-Path</span> <span class="nv">$sshDir</span> <span class="p">|</span> <span class="nb">Out-Null</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Add-Content</span> <span class="n">-Path</span> <span class="nv">$authorizedKeys</span> <span class="n">-Value</span> <span class="nv">$publicKey</span> <span class="n">-Encoding</span> <span class="n">ascii</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">icacls</span><span class="p">.</span><span class="py">exe</span> <span class="nv">$authorizedKeys</span> <span class="p">/</span><span class="n">inheritance</span><span class="err">:</span><span class="nb">r </span><span class="p">/</span><span class="n">grant</span> <span class="s1">&#39;Administrators:F&#39;</span> <span class="p">/</span><span class="n">grant</span> <span class="s1">&#39;SYSTEM:F&#39;</span></span></span></code></pre></div></div>
  </div>
</div><p><code>Add-Content</code> creates the file if it is missing and appends if it is not, so
the same lines add a second administrator&rsquo;s key later. <code>-Encoding ascii</code> is
not decoration: see the first common failure.</p>
<p><code>/inheritance:r</code> strips the inherited entries. The two <code>/grant</code> entries put
back the only two principals Microsoft&rsquo;s documentation allows on this file.
sshd&rsquo;s own permission check is looser, and rejects the file only when another
account has write access. Confirm:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">icacls</span><span class="p">.</span><span class="py">exe</span> <span class="nv">$authorizedKeys</span></span></span></code></pre></div></div>
  </div>
</div><p>The output should list exactly <code>BUILTIN\Administrators:(F)</code> and
<code>NT AUTHORITY\SYSTEM:(F)</code>.</p>
<h2 id="test-from-the-client">Test from the client<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#test-from-the-client"
     aria-label="Link to section: Test from the client" data-pagefind-ignore></a>
</h2>
<p>sshd reads the keys file at every sign-in, so no restart is needed. From the
client:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ssh</span> <span class="console__token">-i</span> <span class="console__token">~/.ssh/id_ed25519</span> <span class="console__token">Administrator@server-address</span></span></code></pre>
    </div>
  </div></div><!-- style-check: end procedure -->
<h2 id="expected-result">Expected result<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#expected-result"
     aria-label="Link to section: Expected result" data-pagefind-ignore></a>
</h2>
<p>You&rsquo;re signed in without a password prompt. Once that holds for every account
that needs access, turn password authentication off in
<code>C:\ProgramData\ssh\sshd_config</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">PasswordAuthentication no</span></span></code></pre></div></div>
  </div>
</div><p>and restart the service, which reads <code>sshd_config</code> only when it starts. Leave
password authentication on until the key login is confirmed, or you&rsquo;ll lock
yourself out of the server you&rsquo;re configuring.</p>

<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<h3 id="login-still-asks-for-a-password">Login still asks for a password<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#login-still-asks-for-a-password"
     aria-label="Link to section: Login still asks for a password" data-pagefind-ignore></a>
</h3>
<p>The key is in the file and the ACL is right, but the file isn&rsquo;t plain ASCII.
In Windows PowerShell 5.1, <code>&gt;</code> and <code>Out-File</code> write UTF-16 with a byte-order
mark, and sshd can&rsquo;t parse a key that starts with one. Delete the file and
write it again with <code>Add-Content -Encoding ascii</code> or <code>Set-Content -Encoding ascii</code>. Inspect a suspect file: a good file starts <code>73 73 68</code> (<code>ssh</code>), a bad
one starts <code>FF FE</code>.</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Format-Hex</span> <span class="n">-Path</span> <span class="nv">$authorizedKeys</span> <span class="p">|</span> <span class="nb">Select-Object</span> <span class="n">-First</span> <span class="mf">1</span></span></span></code></pre></div></div>
  </div>
</div><h3 id="acl-shows-extra-principals-after-icacls">ACL shows extra principals after <code>icacls</code><a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#acl-shows-extra-principals-after-icacls"
     aria-label="Link to section: ACL shows extra principals after icacls" data-pagefind-ignore></a>
</h3>
<p>Inheritance wasn&rsquo;t removed before granting. Run the <code>/inheritance:r</code> form
earlier, which does both steps in one call, and check again.</p>
<h3 id="icacls-reports-no-mapping-for-an-account-name"><code>icacls</code> reports no mapping for an account name<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#icacls-reports-no-mapping-for-an-account-name"
     aria-label="Link to section: icacls reports no mapping for an account name" data-pagefind-ignore></a>
</h3>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">No</span> <span class="console__token">mapping</span> <span class="console__token">between</span> <span class="console__token">account</span> <span class="console__token">names</span> <span class="console__token">and</span> <span class="console__token">security</span> <span class="console__token">IDs</span> <span class="console__token">was</span> <span class="console__token">done.</span></span></code></pre>
    </div>
  </div></div><p>The Windows installation isn&rsquo;t in English, and the Administrators group has a
localized name. Grant it by SID instead, with <code>/grant '*S-1-5-32-544:F'</code> in
place of <code>/grant 'Administrators:F'</code>.</p>
<h3 id="works-for-one-account-and-not-another">Works for one account and not another<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#works-for-one-account-and-not-another"
     aria-label="Link to section: Works for one account and not another" data-pagefind-ignore></a>
</h3>
<p>The second account isn&rsquo;t an administrator. For it, the key belongs in that
user&rsquo;s own <code>C:\Users\&lt;name&gt;\.ssh\authorized_keys</code>, and the shared file is
irrelevant.</p>
<h3 id="client-output-doesnt-explain-the-refusal">Client output doesn&rsquo;t explain the refusal<a class="heading-anchor" href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/#client-output-doesnt-explain-the-refusal"
     aria-label="Link to section: Client output doesn’t explain the refusal" data-pagefind-ignore></a>
</h3>
<p>Ask the server. When the ACL is the cause, Server 2025 logs it under
<strong>Applications and Services Logs &gt; OpenSSH &gt; Operational</strong>, naming the file and
the account that can write to it. Server 2022&rsquo;s OpenSSH 8.1 logs only
&ldquo;Authentication refused&rdquo; there; the line naming the file goes to the
<strong>OpenSSH &gt; Debug</strong> channel, which is off by default. An encoding fault logs nothing there, so check the bytes
as described earlier. <code>ssh -v</code> on the client shows only that the key was
offered and declined.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>OS</dt> <dd>Windows Server 2025 Datacenter (26100.33438) and Windows Server 2022 Datacenter (20348.5622)</dd></div>
    <div class="tested__row"><dt>OpenSSH</dt> <dd>9.5p2 on 2025, 8.1p1 on 2022</dd></div>
    <div class="tested__row"><dt>Client</dt> <dd>OpenSSH on macOS 27</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to Enable SSH on Windows Server 2022</title>
      <link>https://looselydecoupled.com/enable-ssh-windows-server-2022/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/enable-ssh-windows-server-2022/</guid>
      <pubDate>Sat, 04 May 2024 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Windows Server</category>
      <category>SSH</category>
      <category>PowerShell</category>
      <category>Automation</category>
      
      <description>Install the OpenSSH server with PowerShell, start sshd at boot, confirm the port 22 rule, and connect, plus where an administrator&#39;s key really goes.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>In an elevated PowerShell session on the server:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Add-WindowsCapability</span> <span class="n">-Online</span> <span class="n">-Name</span> <span class="n">OpenSSH</span><span class="p">.</span><span class="n">Server</span><span class="p">~~~~</span><span class="mf">0.0</span><span class="p">.</span><span class="py">1</span><span class="p">.</span><span class="py">0</span>
</span></span><span class="line"><span class="cl"><span class="nb">Set-Service</span> <span class="n">-Name</span> <span class="n">sshd</span> <span class="n">-StartupType</span> <span class="n">Automatic</span>
</span></span><span class="line"><span class="cl"><span class="nb">Start-Service</span> <span class="n">-Name</span> <span class="n">sshd</span>
</span></span><span class="line"><span class="cl"><span class="nb">Get-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Format-Table</span> <span class="n">Name</span><span class="p">,</span> <span class="n">Enabled</span><span class="p">,</span> <span class="n">Direction</span><span class="p">,</span> <span class="n">Action</span></span></span></code></pre></div></div>
  </div>
</div><p>Then from any machine with an SSH client:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ssh</span> <span class="console__token">Administrator@server-address</span></span></code></pre>
    </div>
  </div></div><p>Sign in with the Windows account&rsquo;s password. Key-based login for an
administrator has one Windows-specific rule, covered under common failures.</p>

</div>

<p>Windows Server 2022 ships the OpenSSH server as an optional capability, not as
an installed service. Adding it takes one command; the rest of the work is
making sure it starts on boot and that the firewall lets the connection in.
Everything below runs in a PowerShell session opened as Administrator.</p>
<!-- style-check: procedure -->
<h2 id="install-the-openssh-server-capability">Install the OpenSSH server capability<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#install-the-openssh-server-capability"
     aria-label="Link to section: Install the OpenSSH server capability" data-pagefind-ignore></a>
</h2>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Add-WindowsCapability</span> <span class="n">-Online</span> <span class="n">-Name</span> <span class="n">OpenSSH</span><span class="p">.</span><span class="n">Server</span><span class="p">~~~~</span><span class="mf">0.0</span><span class="p">.</span><span class="py">1</span><span class="p">.</span><span class="mf">0</span></span></span></code></pre></div></div>
  </div>
</div><p>The four tildes are part of the name. The command downloads the capability
from Windows Update, so the server needs a route to it, or a configured
alternate source for features on demand.</p>
<p>Confirm it landed:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-WindowsCapability</span> <span class="n">-Online</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="n">Name</span> <span class="o">-like</span> <span class="s1">&#39;OpenSSH*&#39;</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Format-Table</span> <span class="n">Name</span><span class="p">,</span> <span class="n">State</span></span></span></code></pre></div></div>
  </div>
</div><p><code>OpenSSH.Server</code> should show <code>Installed</code>. If <code>OpenSSH.Client</code> shows
<code>NotPresent</code> and you want it on the server too, add it the same way with
<code>OpenSSH.Client~~~~0.0.1.0</code>.</p>
<p>The same install is available without a shell under <strong>Settings &gt; Apps &gt; Apps &amp;
features &gt; Optional features &gt; Add a feature</strong>, searching for &ldquo;OpenSSH Server&rdquo;. It is the
same capability, so the commands that follow apply either way.</p>
<h2 id="start-sshd-and-make-it-start-at-boot">Start sshd and make it start at boot<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#start-sshd-and-make-it-start-at-boot"
     aria-label="Link to section: Start sshd and make it start at boot" data-pagefind-ignore></a>
</h2>
<p>The install registers the <code>sshd</code> service but doesn&rsquo;t start it or set it to
start at boot. Set the start type first, so a reboot before you finish does not
leave the server unreachable:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-Service</span> <span class="n">-Name</span> <span class="n">sshd</span> <span class="n">-StartupType</span> <span class="n">Automatic</span>
</span></span><span class="line"><span class="cl"><span class="nb">Start-Service</span> <span class="n">-Name</span> <span class="n">sshd</span>
</span></span><span class="line"><span class="cl"><span class="nb">Get-Service</span> <span class="n">-Name</span> <span class="n">sshd</span></span></span></code></pre></div></div>
  </div>
</div><p><code>Status</code> should read <code>Running</code>. The first start generates the host keys under
<code>C:\ProgramData\ssh\</code>, which is also where <code>sshd_config</code> lives from now on.</p>
<h2 id="confirm-the-firewall-rule">Confirm the firewall rule<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#confirm-the-firewall-rule"
     aria-label="Link to section: Confirm the firewall rule" data-pagefind-ignore></a>
</h2>
<p>Installing the capability with <code>Add-WindowsCapability</code> normally creates and
enables an inbound rule named <code>OpenSSH-Server-In-TCP</code> for port 22. Check rather than
assume:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-ErrorAction</span> <span class="n">SilentlyContinue</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Format-Table</span> <span class="n">Name</span><span class="p">,</span> <span class="n">Enabled</span><span class="p">,</span> <span class="n">Direction</span><span class="p">,</span> <span class="n">Action</span></span></span></code></pre></div></div>
  </div>
</div><p>If the rule is missing, create it:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">New-NetFirewallRule</span> <span class="n">-Name</span> <span class="s1">&#39;OpenSSH-Server-In-TCP&#39;</span> <span class="n">-DisplayName</span> <span class="s1">&#39;OpenSSH Server (sshd)&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Enabled</span> <span class="n">True</span> <span class="n">-Direction</span> <span class="n">Inbound</span> <span class="n">-Protocol</span> <span class="n">TCP</span> <span class="n">-Action</span> <span class="n">Allow</span> <span class="n">-LocalPort</span> <span class="mf">22</span></span></span></code></pre></div></div>
  </div>
</div><p>Run it only when the check finds nothing. Rule names are unique, so
<code>New-NetFirewallRule</code> fails if <code>OpenSSH-Server-In-TCP</code> already exists.</p>
<h2 id="connect-from-a-client">Connect from a client<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#connect-from-a-client"
     aria-label="Link to section: Connect from a client" data-pagefind-ignore></a>
</h2>
<p>From another machine, using its own OpenSSH client (macOS, Linux, and Windows
10 version 1809 or later all have one):</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ssh</span> <span class="console__token">Administrator@server-address</span></span></code></pre>
    </div>
  </div></div><p>Accept the host key on first connection and enter the account&rsquo;s Windows
password. Windows OpenSSH is a port of the same OpenSSH that ships on Linux and
macOS, so the client and its keys behave the way you already expect.
Authentication is the difference: sshd hands it to Windows. The default
<code>sshd_config</code> has no <code>AllowUsers</code> or <code>AllowGroups</code> line, so any local or
domain account Windows accepts can sign in. SSH keeps no user list of its own
until you add one.</p>
<!-- style-check: end procedure -->
<h2 id="expected-result">Expected result<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#expected-result"
     aria-label="Link to section: Expected result" data-pagefind-ignore></a>
</h2>
<p>You land in a <code>cmd.exe</code> prompt on the server. That&rsquo;s the default shell for
Windows OpenSSH, not a sign that something went wrong. To get PowerShell
instead, set the default shell once on the server:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">New-ItemProperty</span> <span class="n">-Path</span> <span class="s1">&#39;HKLM:\SOFTWARE\OpenSSH&#39;</span> <span class="n">-Name</span> <span class="n">DefaultShell</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-PropertyType</span> <span class="n">String</span> <span class="n">-Force</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Value</span> <span class="s1">&#39;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe&#39;</span></span></span></code></pre></div></div>
  </div>
</div><p>Point it at <code>pwsh.exe</code> instead if PowerShell 7 is installed and that&rsquo;s what
you want to arrive in.</p>

<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<h3 id="administrator-gets-permission-denied-publickey">Administrator gets <code>Permission denied (publickey)</code><a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#administrator-gets-permission-denied-publickey"
     aria-label="Link to section: Administrator gets Permission denied (publickey)" data-pagefind-ignore></a>
</h3>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">Permission</span> <span class="console__token">denied</span> <span class="console__token">(publickey)</span></span></code></pre>
    </div>
  </div></div><p>The key is in <code>~\.ssh\authorized_keys</code>, and for any account in the local
Administrators group Windows OpenSSH ignores that file entirely. The key has to
be in <code>C:\ProgramData\ssh\administrators_authorized_keys</code>, and sshd refuses
that file unless only SYSTEM and Administrators have access to it. The client
is never told why; the reason goes to the server&rsquo;s OpenSSH event log. The
procedure, scripted, is in <a href="https://looselydecoupled.com/windows-server-ssh-key-authentication-powershell/">the article on key authentication for an
administrator</a>.
Non-administrator accounts use <code>~\.ssh\authorized_keys</code> as usual.</p>
<h3 id="connection-refused-or-timing-out">Connection refused or timing out<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#connection-refused-or-timing-out"
     aria-label="Link to section: Connection refused or timing out" data-pagefind-ignore></a>
</h3>
<p>A stopped service usually gives a refusal. A missing firewall rule usually
gives a timeout, because the firewall drops the packets. <code>Get-Service sshd</code> and
the firewall check earlier tell you which. If both look right, confirm
something is listening:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-NetTCPConnection</span> <span class="n">-LocalPort</span> <span class="mf">22</span> <span class="n">-State</span> <span class="n">Listen</span></span></span></code></pre></div></div>
  </div>
</div><h3 id="add-windowscapability-fails-with-0x800f0954"><code>Add-WindowsCapability</code> fails with 0x800f0954<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#add-windowscapability-fails-with-0x800f0954"
     aria-label="Link to section: Add-WindowsCapability fails with 0x800f0954" data-pagefind-ignore></a>
</h3>
<p>The server can&rsquo;t reach Windows Update, or can&rsquo;t find the source. The usual
cause is WSUS: Server 2022 can&rsquo;t install OpenSSH from WSUS at all. One fix is
the Group Policy setting <strong>Specify settings for optional component installation
and component repair</strong>, under Computer Configuration &gt; Administrative Templates</p>
<blockquote>
  <p>System. Enable it and select <strong>Download repair content and optional features
directly from Windows Update instead of Windows Server Update Services</strong>. The
other is to mount the Server 2022 Features on Demand ISO and pass its drive to
<code>-Source</code>. The same fix applies to 0x800F0950 and the 0x8024xxxx connection
errors.</p>

</blockquote><h3 id="ssh-hangs-after-the-password"><code>ssh</code> hangs after the password<a class="heading-anchor" href="https://looselydecoupled.com/enable-ssh-windows-server-2022/#ssh-hangs-after-the-password"
     aria-label="Link to section: ssh hangs after the password" data-pagefind-ignore></a>
</h3>
<p>Every command works, but the session never opens. Check Event Viewer under
<strong>Applications and Services Logs &gt; OpenSSH &gt; Operational</strong>, which records each
sign-in and refusal. Don&rsquo;t wait for the <strong>Admin</strong> channel: it stays empty even
when sshd can&rsquo;t start. For that case, <code>sshd -t</code> names the broken line in
<code>sshd_config</code>, and prints nothing when the file is valid.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>OS</dt> <dd>Windows Server 2022 Datacenter (build 20348.5622)</dd></div>
    <div class="tested__row"><dt>OpenSSH</dt> <dd>8.1p1 server, from the OpenSSH.Server capability</dd></div>
    <div class="tested__row"><dt>Client</dt> <dd>OpenSSH on macOS 27</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
