Skip to content

Adds support to OpenSSH Servers on Windows.#389

Closed
fdcastel wants to merge 12 commits into
openpubkey:mainfrom
fdcastel:issue-370-pr
Closed

Adds support to OpenSSH Servers on Windows.#389
fdcastel wants to merge 12 commits into
openpubkey:mainfrom
fdcastel:issue-370-pr

Conversation

@fdcastel
Copy link
Copy Markdown
Contributor

@fdcastel fdcastel commented Oct 30, 2025

⚠️ RFC: Do not merge. ⚠️

How to test

⚠️ This is an early alpha release. It goes without saying, but... do NOT use this in production! ⚠️

#
# Setup OpenSSH Server
#   https://gist.github.com/fdcastel/9648325ae5be548d1664c5433fd6e9ae
#
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0        # NOP in Server 2025 (already installed)
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0        # NOP in Server 2025 (already installed)

# Enable OpenSSH Server
Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'

# Enable OpenSSH for ALL profiles in Firewall
Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" | Set-NetFirewallRule -Profile Any
#
# Setup opkssh server
#   Custom build from https://github.com/fdcastel/opkssh/releases which includes the updates from this PR.
#
$version = 'v0.13.0-win1'
$repo = 'fdcastel/opkssh'

# Download the install script to $env:TEMP folder
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri https://github.com/fdcastel/opkssh/releases/latest/download/Install-OpksshServer.ps1 -OutFile $env:TEMP/Install-OpksshServer.ps1

$canUseSystemAccount = (Get-Command sshd).Version -ge [version]'8.9'
if ($canUseSystemAccount) {
    & $env:TEMP/Install-OpksshServer.ps1 -InstallVersion $version -GitHubRepo $repo
} else {
    & $env:TEMP/Install-OpksshServer.ps1 -InstallVersion $version -GitHubRepo $repo -AuthCmdUser 'opksshuser'
}

# Reload PATH
$env:Path = (Get-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Session Manager\Environment').Path + ';' + `
            (Get-ItemProperty 'HKCU:\Environment').Path
# Authorize user
opkssh add administrator <YOUR-EMAIL> <YOUR-PROVIDER>

Tested on:

  • Windows Server 2025
  • Windows Server 2022
  • Windows Server 2019
  • Windows 11
  • Windows 10

Long version (grab a ☕!)

I began migrating the existing scripts/install-linux.sh code to PowerShell, aiming to maintain as much fidelity as possible.

During the first run, I quickly encountered an issue with hardcoded POSIX filesystem paths. I adapted these to Windows paths to the best of my (admittedly limited) knowledge of Go 😅 (kudos to Claude Sonnet 4.5 for the assist!)

Some parts of the code depend on specific file permissions to run correctly. Initially, I tried to preserve this behavior by emulating the same permissions through Windows ACLs, hoping to keep the original Go code untouched. However, this turned out to be an ungrateful and impractical task, so I eventually abandoned that path.

A few smaller issues also came up, such as differences in version string formats between Windows builds. I adjusted the Go code to handle those as well.

Then I discovered a major difference in the OpenSSH Server shipped with Windows Server 2025: This PR, merged on March 26, 2021, enables the use of AuthorizedKeysCommandUser = System, eliminating the need to create a dedicated user for running opkssh.

Browsing the Win32-OpenSSH repository, I couldn’t find a clear mapping between specific PRs and releases. However, based on the PR’s merge date and the release history

# https://github.com/powershell/Win32-OpenSSH
05/26/2021   8.6.0.0   https://github.com/PowerShell/openssh-portable/releases/tag/v8.6.0.0
03/17/2022   8.9.0.0   https://github.com/PowerShell/openssh-portable/releases/tag/v8.9.0.0

it’s reasonable to assume that versions 8.9.0.0 and up includes this change.

Unfortunately, it seems that all versions shipped with Windows prior to Server 2025 are earlier than this one. I added a safeguard to detect this condition and alert the user, instructing them to use the appropriate CLI option to create the opksshuser when necessary.

Finally, modifying the system PATH on Windows is not exactly straightforward. I updated the script to handle PATH expansion correctly, avoiding one of Windows’ many quirks.

And so… this is it! Now,

  • it needs tests;
  • it needs feedback; and
  • it surely needs some polishing around the edges.

But... hey! It's working. 😄

To Do:

  • Port GitHub OIDC test for Windows
  • Run Go tests (opkssh binary) on Windows
  • Port Bash tests (opkssh installer) for Windows
  • Review current Windows folder structure ($env:ProgramData/opk) -- Is it acceptable?
  • Enforce stricter permissions of opkssh configuration files -- is this necessary on Windows?
  • Per-user configurations? (stricter permissions, and the -NoHomeProfile installer option)
  • Plugin support

Fix #370.

@fdcastel fdcastel marked this pull request as draft October 30, 2025 07:21
@fdcastel
Copy link
Copy Markdown
Contributor Author

Fixed failing test. Rebased onto the latest main branch.

Comment thread .github/workflows/gha-windows.yml Fixed
@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Oct 31, 2025

I’ve just pushed an initial Windows container port of the gha.yml workflow (Test GitHub Provider).

Unfortunately, I discovered that docker/build-push-action isn’t compatible with Windows containers. As a result, we have to build the image manually using docker build. This also means we lose the caching benefits provided by docker/build-push-action (via its cache-from and cache-to options).

UPDATE: This version no longer uses Windows containers. See below.

Comment thread .github/workflows/go.yml Fixed
@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Nov 1, 2025

  • Added test runners on Windows.

    • The tests were passing before because they were running only on Linux.
    • Now, on Windows, they fail due to permission differences -- making the problem more visible.
  • Rewrote the GitHub OIDC test to run natively on the GitHub Runner VM instead of using Windows containers (which were painfully slow 🐢).

    • The test now uses the SSH client and server on the same VM -- I'm not sure if this has any side effects, but I believe it doesn't.
    • I can also port this approach to the Linux workflow if you approve (removing the need for Docker).
  • Fixed a minor issue in the providers files (missing newline at the end of the file).

  • Rebased with latest main.

@EthanHeilman EthanHeilman added the enhancement New feature or request label Nov 1, 2025
@EthanHeilman
Copy link
Copy Markdown
Member

@fdcastel This is looking really nice!

I saw you were doing some refactoring of permissions between windows and linux. OPKSSH doesn't handle file permissions in the most organized way.

We have const ModeSystemPerms = fs.FileMode(0640) but also have adhoc permissions like

if err := afs.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {

If you ended up refactoring my current code into unified permissions struct across all OPKSSH, I would not object. I've been thinking about doing some similar and it seems you probably have to do it to support windows.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Nov 3, 2025

If you ended up refactoring my current code into unified permissions struct across all OPKSSH, I would not object.

That's great news! 😄
Give me a few days to put together a plan. I believe it can bring significant benefits to the codebase.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Nov 3, 2025

BTW: should we consider removing the Docker container from the gha workflow (like what was done in the Windows version)?

I’d also say the existing workflows could benefit from a little cleanup 😅.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Nov 7, 2025

@EthanHeilman I'm considering adding a new opkssh command: something like fix-permissions.

The idea is to centralize all platform-specific permission settings within the application itself.

This would simplify our installer scripts (on both Linux and Windows), which currently handle much of this logic, thereby reducing code duplication and keeping all permission management contained within the app.

It could also be useful for quickly repairing user installations that (for any reason) may have become corrupted.

What do you think?

@EthanHeilman
Copy link
Copy Markdown
Member

I'm considering adding a new opkssh command: something like fix-permissions.

@fdcastel I think that makes sense. Could you do that as a separate PR or is it required here?

There is some similar work happening here that you might want to coordinate with.
#388

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Nov 8, 2025

Sure! This entire PR (which is getting quite large, by the way) is meant to serve as a proof of concept for now.

My intention isn’t to merge it as-is. Once everything is working properly and looks good, I plan to split it into several smaller PRs and rebase them as needed -- e.g. to adapt to new changes like #388.

It will likely take me a few more weeks to finish, but progress is looking very promising. There’s still quite a bit of work to do, though (trying to build a good abstract layer for permissions in both Posix and Windows platforms).

@fdcastel
Copy link
Copy Markdown
Contributor Author

@EthanHeilman Just a quick follow-up:

I had to put this work on hold for a while since things are hectic here toward year-end. 😅

But, just to reinforce, I’m still really interested in getting this done, and I expect to pick it back up soon (most likely at the beginning of January).

I’m currently refactoring the work from this PR to make it simpler and more robust across both platforms. The updated work is happening here: https://github.com/fdcastel/opkssh/tree/issue-370 (note: this is not the same source branch as this PR).

Disclaimer: The branch above is still very much a work in progress and may undergo significant changes or force-pushes. Not exactly reliable for code review 😄

Also, I noticed you’re doing some merges here. If you’d like, I can rebase this PR when I’m back.

Best regards, and happy holidays!

Fabio.

@EthanHeilman
Copy link
Copy Markdown
Member

Happy holidays! Looking forward to working with you when you get back in early Jan

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Feb 3, 2026

TL;DR

What about adding a --fix option to the existing audit command?

Long version

I’m upgrading the audit command to support Windows as well.

In my earlier code -- based on v0.10, before the audit command existed (no in this PR) -- I added a new opkssh permissions command with three subcommands:

  • permissions check - Verify ACLs/permissions
  • permissions fix - Repair ACLs/permissions (requires admin)
  • permissions install - Non-interactive fixer for installers

The check subcommand has mostly been replaced by audit.

The other two, I believe, can be handled with a single audit --fix.

@Basti-Fantasti @EthanHeilman Opinions?

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Feb 3, 2026

On second thought… the permissions check/fix/install command may still be useful.

For installers, it’s better to explicitly run permissions install than to rely on audit to “fix” something that was never set up in the first place.

We could refactor this so that the permissions check becomes a smaller component of the overall audit process.

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish GitHub release
run: gh release edit "${{ github.ref_name }}" --draft=false --repo "${{ github.repository }}"

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Feb 4, 2026

I’ve pushed the latest version and now I would love reviews and opinions.

Testing instructions have been updated. This version has been tested on Windows Server 2022 and 2025, but it should also work on any Windows edition that supports Microsoft’s OpenSSH server.

What’s new:

  • Permissions overhaul for both Windows and Linux
  • New permissions command (still WIP). I’m on the fence about keeping it and would appreciate feedback
  • New workflow for publishing releases on forks (e.g. https://github.com/fdcastel/opkssh/releases)

Please take a look and let me know what you think.

@EthanHeilman
Copy link
Copy Markdown
Member

On second thought… the permissions check/fix/install command may still be useful.

I think a command like this could be useful. Feel free to create a ticket for it with a description of what it would do and I'll provide feedback

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Feb 4, 2026

I think a command like this could be useful. Feel free to create a ticket for it with a description of what it would do and I'll provide feedback

Agreed. But I will also need this functionality in this current PR.

My design relies on this command to eliminate duplicated code in the installation scripts, centralizing the permission logic in a single place (inside the application). This logic may be reused by the fix command, too.

This approach is already used in the Windows installer.

And the plan is to extend it to the Linux installer as well.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Feb 4, 2026

If you’re good with this direction, I can split this PR in two:

  • one for the permissions command; and
  • one for Windows support (this one)

and then rebasing this PR on top of the first.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Feb 4, 2026

That said, this only makes sense if the group find the permissions command useful.

Alternatively, if a little permission code duplication between the scripts and the app is tolerable -- and a need for a dedicated “fix” command doesn't exists -- I could remove this code entirely.

I’m just looking for guidance on what the team thinks is best for the project.

After giving it some thought, my preference would be to keep the permissions subcommand, but that’s just my two cents.

@Basti-Fantasti
Copy link
Copy Markdown
Contributor

TL;DR

What about adding a --fix option to the existing audit command?

Long version

I’m upgrading the audit command to support Windows as well.

In my earlier code -- based on v0.10, before the audit command existed (no in this PR) -- I added a new opkssh permissions command with three subcommands:

  • permissions check - Verify ACLs/permissions
  • permissions fix - Repair ACLs/permissions (requires admin)
  • permissions install - Non-interactive fixer for installers

The check subcommand has mostly been replaced by audit.

The other two, I believe, can be handled with a single audit --fix.

@Basti-Fantasti @EthanHeilman Opinions?

Sorry for my delayed response. I'm a bit late to the party 😄
but I also think that this would be a useful addition and I will definitely test it.

@fdcastel
Copy link
Copy Markdown
Contributor Author

Thanks, guys, for the feedback!

I’m currently abroad, but I plan to revisit this next week (making a PR for the new permissions command and then rebasing this one on top of it).

P.S.: That said, we’re still looking for more Windows testers! 😅
(I haven’t heard from anyone about the changes of this PR) 😉

Comment thread .github/workflows/build.yml
Comment thread commands/audit.go Outdated
} else if err != nil {
return nil, fmt.Errorf("failed to read /etc/passwd: %v", err)
if isWindows() {
fmt.Fprint(a.ErrOut, "skipping user policy audit on Windows (no /etc/passwd)\n")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense for right now to not have to support this, but there is probably a way to get all windows users on a server

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will investigate 👍🏻

Comment thread commands/permissions.go Outdated

// Providers dir
if _, err := ops.Stat(providersDir); err != nil {
if err := ops.MkdirAllWithPerm(providersDir, 0750); err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should a file that has the correct permissions for each file type. Hardcoding them this way is likely to cause bugs down the road. I started this anti-pattern, but it would be great to fix it in this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I THINK I understand what you want 😅.

But... If you could elaborate a bit more -- with a few examples -- I’d be glad to look into it further!

Copy link
Copy Markdown
Member

@EthanHeilman EthanHeilman Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this, not exactly this, I haven't really thought it through, but in this sort of ballpark

type PermInfo struct {
	Mode  fs.FileMode
	Owner string
	Group string
	MustExist bool
}


var PermsEum = struct {
	HOME_AUTHID FileInfo
	ETC_AUTHID  FileInfo
	ETC_CONFIG_YML FileInfo
}{
	HOME_AUTHID: PermInfo {
		Mode:  fs.FileMode(0600),
		Owner: "",
		Group: "",
                MustExist: false
		},
	ETC_AUTHID:  PermInfo {
		Mode:  fs.FileMode(0640),
		Owner: "opksshuser",
		Group: "opksshuser",
		MustExist: true
		},
	ETC_CONFIG_YML: PermInfo {
		Mode:  fs.FileMode(0640),
		Owner: "opksshuser",
		Group: "opksshuser",
		MustExist: true
		},
}

Something close to this exists in Perms Checker, but missing details
https://github.com/openpubkey/opkssh/blob/main/policy/files/permschecker.go#L28-L37

You'd probably want Perminfo to support Linux and Windows permissions.

Introduces a PermInfo struct in policy/files that defines the expected
mode, owner, group, and existence requirements for each opkssh resource
type (SystemPolicy, HomePolicy, ProvidersDir, PluginsDir, PluginFile).

Platform-specific values are provided via build-tagged files
(perminfo_unix.go and perminfo_windows.go) so the correct owner
('root' vs 'Administrators') is selected at compile time.

Replaces hardcoded permission values scattered across
commands/permissions.go, commands/audit.go, and commands/platform.go
with references to the centralized files.RequiredPerms definitions.

Addresses review feedback from EthanHeilman on PR openpubkey#389.
Replaces the 'skipping user policy audit on Windows' stub with actual
user profile enumeration via the Windows registry ProfileList at
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList.

New files:
- commands/audit_windows_profiles.go: reads ProfileList registry keys,
  filters to real user SIDs (S-1-5-21-*), resolves usernames via
  os/user.LookupId, and returns profile paths.
- commands/audit_enum_windows.go: Windows enumerateUserHomeDirs() method
  that delegates to getHomeDirsFromProfileList().
- commands/audit_enum_unix.go: Unix enumerateUserHomeDirs() method that
  reads /etc/passwd (extracted from audit.go).

Changes:
- commands/audit.go: unified user enumeration loop that calls the
  platform-specific enumerateUserHomeDirs(); removed isWindows() skip.
- NewAuditCmd no longer defaults SkipUserPolicy to true on Windows.
- commands/audit_windows_test.go: updated tests to verify enumeration
  runs and SkipUserPolicy flag works.

No new dependencies - uses golang.org/x/sys/windows/registry which is
already available via the existing golang.org/x/sys dependency.
The isWindows() function in commands/platform.go is no longer called
after the audit user enumeration was refactored to use platform-specific
build-tagged files. The golangci linter correctly flagged this as unused.
- Create permissions_mocks_test.go with shared mockFilePermsOps and
  mockACLVerifier types used by both Unix and Windows tests
- Remove duplicated mockOpsNW/mockVerifierNW and mockOps/mockVerifier
- Fix test file naming: rename *_test_windows.go to *_windows_test.go
  so Go's test runner properly discovers the test functions
- Refactor SetupAuditCmdMocks to use platform-aware paths
  (policy.SystemDefaultPolicyPath) instead of hardcoded Unix paths
- Make etcPasswdContent parameter optional (empty string skips writing)
- Update audit_windows_test.go to call SetupAuditCmdMocks instead of
  duplicating AuditCmd setup inline
- Fix test expectations to use platform-aware policy path strings
- Create permcheck.go with CheckFilePermissions() that centralises the
  existence, mode/ownership, and ACL checks shared by audit and permissions
- Update audit.go auditPolicyFileWithStatus to use CheckFilePermissions
- Update permissions.go runPermissionsCheck to use CheckFilePermissions
- Both commands now use the same checking logic, ensuring consistent results
Replace the 'not supported' stub with a full implementation that:
- Validates the username format
- Looks up the user via user.Lookup to get their SID and home directory
- Reads the policy file from <HomeDir>\.opk\auth_id
- Verifies file ownership by comparing the file owner SID against the
  expected user SID using the existing ACL verification infrastructure
- Reports any ACL problems found on the file
Add the standard Apache 2.0 license header to permissions.go and
fileperms_ops.go, consistent with all other source files.
Add a comparison table and usage guidance to docs/audit.md explaining
when to use 'opkssh audit' vs 'opkssh permissions check' and how
they relate to each other.
Add TestAuditAndPermissionsCheckConsistency to verify that the audit
command and CheckFilePermissions report the same results for a given
system policy file. Also add TestAuditAndPermissionsCheckBadPerms
(Unix-only) to verify both detect incorrect mode bits consistently.
- Remove unused expectedSystemACL() from platform.go (golangci-lint unused)
- Fix gofmt formatting in audit_permissions_test.go
- Fix CmdRunner mock in SetupAuditCmdMocks to return 'opksshuser' instead
  of 'opkssh', matching RequiredPerms.SystemPolicy.Group on Linux. This
  was exposed by CheckFilePermissions now passing owner/group to CheckPerm.
@fdcastel
Copy link
Copy Markdown
Contributor Author

I pushed the latest improvements and fixes, as well the suggestions from the reviews.

I'll start to split this into smaller PRs.

  • GitHub Actions workflows changes
  • Add Permissions command and audit refactors
  • The remaining content of this PR (main task: support OpenSSH servers on Windows)

@fdcastel
Copy link
Copy Markdown
Contributor Author

All set. The work continues in PRs #478, #479, and #480.

I’m closing this one now.

@fdcastel fdcastel closed this Feb 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Windows OpenSSH Server?

4 participants