Skip to content

fix(ssh): repair managed Windows SSH config permissions - #1110

Open
EhabY wants to merge 3 commits into
mainfrom
fix/windows-ssh-config-acls
Open

EhabY wants to merge 3 commits into
mainfrom
fix/windows-ssh-config-acls

Conversation

@EhabY

@EhabY EhabY commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #1108. On Windows, OpenSSH refuses to read an Include when any file it matches is too permissive, so connections fail with Bad owner or permissions. This repairs the permissions of the extension's own SSH config directory using built-in commands, with no Windows Script Host, ADSI, native dependency, or elevation.

  • Before each managed write, run whoami.exe to find the current user, then icacls.exe /reset and /inheritance:r /grant:r on %APPDATA%\coder.coder-remote\ssh. Only that user, SYSTEM, and Administrators keep inheritable full control.
  • Reset every direct *.conf match to inherit the directory, which also repairs other deployments and editors on the first connection after an upgrade, plus the temporary file before its rename.
  • Check command exit codes. Never read ACLs back or parse SDDL.
  • Reject links and non-files before the repair, and leave your own SSH config and anything outside the managed directory alone.

SshConfig takes an optional ManagedPermissions. Its absence is the marker for "not Coder-managed", which is why it has no no-op default: a default would make the extension enumerate ~/.ssh on every save on every platform. createManagedPermissions() returns undefined off Windows, so the whole repair path stays Windows-only.

What each file changes

File Lines What it does
src/remote/windowsAcl.ts +122 New. Exports WINDOWS_ACL, createManagedPermissions, and system32; everything else is private, ordered by first use.
src/remote/sshConfig.ts +91 / -14 Adds the ManagedPermissions interface. Splits save() into getFileMode, repairIncludedFiles, writeTemp, repairPermissions, and replaceWithTemp, and makes the temp write exclusive (wx).
src/remote/remote.ts +3 Passes createManagedPermissions() to the per-deployment SshConfig.
test/unit/remote/windowsAcl.test.ts +186 New. 23 cases over mocked icacls.exe and whoami.exe: the command transcript, 9 rejection rows, failure propagation, and 12 path-form rows through the public API.
test/unit/remote/windowsAcl.native.test.ts +221 New. 5 cases against the real icacls.exe, whoami.exe, and OpenSSH, skipped off Windows.
test/unit/remote/sshConfig.test.ts +188 / -1 8 cases for the managed-permissions path, plus one for the exclusive temp write.
test/env-check.ts, vitest.config.mts +15 Global setup that fails the run when process.arch does not match EXPECTED_ARCH.
.github/workflows/ci.yaml +6 Adds a windows-11-arm unit-test job and sets EXPECTED_ARCH.
CONTRIBUTING.md, CHANGELOG.md +30 Documents the approach, its limits, and the user-facing fix.
.gitattributes, .vscodeignore +3 / -1 Marks lock files as generated and adds a trailing newline. Unrelated to the fix.

Total: 865 added, 16 removed across 13 files.

Simplification pass

The last commit reworks the first implementation rather than adding to it:

  • Replaced the stateful WindowsAcl class with a WINDOWS_ACL const holding two functions. The old class tracked the last prepared directory so that secure() could refuse a file outside it, but SshConfig.save() is the only caller and already calls them in order. Removing that state removed three tests with it.
  • Folded parseWhoamiUserSid into currentUserSid and stopped exporting isFullyQualifiedWindowsPath, which existed only so a test could assert on it. The same decision is reachable through WINDOWS_ACL.secure().
  • Renamed FilePermissions to ManagedPermissions. It prepares a directory, so "File" was wrong, and the name now hints that absence means unmanaged.
  • Flattened src/remote/windows/acl.ts to src/remote/windowsAcl.ts, since the directory held one file.
  • Cut the test files from 721 to 592 lines with the same coverage. Rejection cases became it.each tables, command assertions became flat transcript strings instead of nested arrays, and the 60-second native test lost a duplicate fixture.
  • Shortened the CONTRIBUTING.md section and rewrote the changelog entry around the error users see.

Validation

  • pnpm test: 2,710 passed, 6 skipped, 181 files. The native Windows tests are among the skips on Linux.
  • pnpm typecheck and pnpm lint pass.
  • Windows CI covers x64 and ARM64.
  • Checked the *.conf directory case against OpenSSH 9.6p1 on Linux: glibc opens a directory successfully, so OpenSSH reads zero lines and ignores it unless it is group-writable or unreadable. The Windows CRT fails the open instead, which aborts the whole Include, so the extension reports that entry rather than trying to fix it with an ACL change.

Limits

A directory-preparation failure stops the write. A single file-reset failure logs a warning and still attempts the connection, so OpenSSH may reject a fragment the repair missed. The two directory commands are not atomic: a failure after /reset can leave the directory with its parent's grants. The path and link checks stop mistakes, not an attacker racing the check.

Generated by Coder Agents on behalf of @EhabY.

@EhabY EhabY changed the title fix(ssh): repair Windows ACLs with a bundled static helper fix(ssh): repair managed Windows config files with icacls Sep 14, 2026
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch 2 times, most recently from b895e47 to 3635e17 Compare September 14, 2026 16:50
@EhabY
EhabY marked this pull request as ready for review September 14, 2026 16:55
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch 6 times, most recently from 72e1d72 to 91612cb Compare September 14, 2026 17:54
@EhabY
EhabY marked this pull request as draft September 14, 2026 18:24
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch 4 times, most recently from 6ebe186 to ad4b2dc Compare September 16, 2026 10:46
@EhabY
EhabY marked this pull request as ready for review September 16, 2026 11:01
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch 4 times, most recently from 4a896a4 to 16adc12 Compare September 16, 2026 13:22
@EhabY EhabY changed the title fix(ssh): repair managed Windows config files with icacls fix(ssh): repair managed Windows SSH config permissions Sep 16, 2026
Comment thread vitest.config.mts Outdated
async function savedAcl(pathname: string): Promise<string> {
return withAclBackup(async (backup) => {
await execFile(system32("icacls.exe"), [pathname, "/save", backup]);
return await fs.readFile(backup, "utf16le");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why does this have to be utf-16 ?

icacls /save Defaults to UTF-16LE
When the extension runs icacls.exe [target] /save [backup], it asks Windows to dump the Access Control List (ACL) of the target SSH config file into a text file. By default, Windows writes this backup file using UTF-16LE (Little Endian) encoding.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was written in the prod file, but I've added a small comment here as well 🙏

@untra untra Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

just gotta make sure utf-16 files and strings don't leak out to much, are used minimally. I do see it in the prod code; I'm not a fan of alternative content encoding, as they can cause unintended data pollution.

icacls.exe

I think another windows expert should give this PR a better review

EhabY and others added 2 commits September 16, 2026 18:00
Repair included fragments with built-in Windows tools and retain best-effort
SSH setup. Inject permission repair as a collaborator so the config writer
stays platform agnostic, skip files whose permissions already verify, and
drive the real Windows tools from the native tests.

Fixes #1108

Generated by Coder Agents.
Keep vitest.config.mts declarative, and record why the icacls backup is
read as UTF-16LE.
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch from 16adc12 to dddc9e9 Compare September 16, 2026 16:00
@EhabY
EhabY requested a review from untra September 16, 2026 16:02
@phorcys420

phorcys420 commented Sep 17, 2026

Copy link
Copy Markdown
Member

is there any reason you didn't use the icacls utility or PowerShell's Set-Acl all the way to set the permissions instead of depending on JScript (assets/wsh/acl.js)?

Scripts ran by the Windows Script Host are super sketchy patterns that AVs love to flag for no reason. Spawning PowerShell is similar but less so.

@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch 2 times, most recently from 8cf3d1c to dd84e8c Compare September 17, 2026 16:16
Replace the WSH setter and runtime ACL parsing with checked icacls commands. Repair existing deployment fragments on upgrade, retain path and link guards, and consolidate ACL helpers and tests.
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch from dd84e8c to 98bbb22 Compare September 17, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated Coder SSH config contains invalid/unknown user permissions

3 participants