Skip to content

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

Merged
EhabY merged 2 commits into
mainfrom
fix/windows-ssh-config-acls
Sep 22, 2026
Merged

EhabY merged 2 commits into
mainfrom
fix/windows-ssh-config-acls

Conversation

@EhabY

@EhabY EhabY commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #1108. Windows files inherit their permissions from the directory they live in, so a config the extension generates under %APPDATA%\coder.coder-remote\ssh can end up readable by other accounts. OpenSSH rejects such a file with Bad owner or permissions and skips the whole Include, so one bad file blocks every Coder host, not just the one it came from.

Before each managed write, the extension locks that directory down with built-in commands and lets its files inherit from it. No Windows Script Host, ADSI, native dependency, or elevation:

Step Command
Read the current user's SID whoami.exe /user /fo csv /nh
Clear the directory's own grants icacls.exe <dir> /reset
Grant that user, SYSTEM, and Administrators full control icacls.exe <dir> /inheritance:r /grant:r <trustee>
Clear each *.conf file so it inherits the directory icacls.exe <file> /reset
  • Resetting every direct *.conf match, not only the file being written, also repairs other deployments and editors on the first connection after an upgrade. The temporary file is reset before its rename.
  • Check command exit codes. Never read ACLs back or parse SDDL.
  • Reject links and non-files before the repair, but tolerate an entry that disappears mid-scan: two connecting windows share the directory, and one can rename its temporary config away between readdir and lstat.
  • 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

  • src/remote/windowsAcl.ts: new. The icacls repair itself, exporting WINDOWS_ACL, createManagedPermissions, and system32.
  • src/remote/sshConfig.ts: adds ManagedPermissions, splits save() into named steps, and makes the temp write exclusive (wx).
  • src/remote/remote.ts: passes createManagedPermissions() to the per-deployment SshConfig.
  • Tests and docs: mocked and native Windows suites, CONTRIBUTING.md, and CHANGELOG.md.
  • CI: adds a windows-11-arm unit job, because the repair shells out to icacls.exe and whoami.exe and Windows on ARM64 is a platform users connect from. test/env-check.ts fails that job if it ever runs under x64 emulation, which would silently make it a duplicate.
  • .github/actions/setup/action.yml: moves pnpm store caching from setup-node to pnpm/action-setup. setup-node restores on an exact lockfile hash, so a dependency bump refetched the whole store on every runner; pnpm/action-setup keys on the same hash but falls back to the last store for the platform.
  • pnpm-workspace.yaml: turns off the bufferutil and utf-8-validate source builds. Neither ships a win32-arm64 prebuild, so node-gyp compiled both on every cold ARM64 install. Both are dev-only and fall back to JavaScript, and platforms with a prebuild keep theirs.
  • .gitattributes and .vscodeignore: a generated-file marker and a trailing newline, unrelated to the fix.

Validation

pnpm test (2,751 passed, 6 skipped), pnpm typecheck, pnpm lint, and pnpm format:check pass. Windows CI covers x64 and ARM64 on a cold pnpm store. Against OpenSSH 9.6p1, a *.conf directory aborts the whole Include on Windows, where glibc instead reads zero lines and ignores it, 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
Comment thread test/unit/remote/windows/acl.native.test.ts Outdated
@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
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch from dd84e8c to 98bbb22 Compare September 17, 2026 16:27
@EhabY

EhabY commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Simplified this a lot more by removing the pre/post ACL checks that ran to make sure everything was applied correctly. We now apply this on all runs on Windows (which takes the same amount of time as the check anyway). The only windows specific logic is in windowsAcl.ts!

@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch 4 times, most recently from b7a0f0c to 07c23ef Compare September 18, 2026 23:39
Comment thread src/remote/windowsAcl.ts Outdated
OpenSSH rejects a config file whose ACL grants access beyond the owner,
SYSTEM, and Administrators, so a stale explicit grant on a generated
fragment fails every connection with "Bad owner or permissions".

Lock the Coder-managed SSH directory with checked icacls commands and let
its inheritable grant repair the files beneath it. Inject the repair as a
collaborator so the config writer stays platform-independent, and reject
links, non-files, and paths that icacls could expand before granting
anything. Repair existing deployment fragments on upgrade, and drive the
real Windows tools from the native tests.

Move the runtime architecture check into a global setup so
vitest.config.mts stays declarative.

Speed up the Windows ARM64 unit test job, where installing dependencies
took eight minutes against one on x64. Neither bufferutil nor
utf-8-validate ships a win32-arm64 prebuild, and both fall back to
JavaScript, so skip their node-gyp builds. Hand the pnpm store cache to
pnpm/action-setup, which keys on the same lockfile hash but falls back to
the last store for the platform, so a dependency bump no longer refetches
the whole store. A cold setup step now takes 2m31s.

Fixes #1108
Two connecting editor windows share the Coder-managed SSH directory, and
each one writes its config through a temporary sibling file. A scan can
read a directory entry that the other window renames away before the
following lstat, and that ENOENT aborted directory preparation and the
connection.

Skip an entry that no longer exists, and rethrow every other error.
@EhabY
EhabY force-pushed the fix/windows-ssh-config-acls branch from 07c23ef to 7593d1b Compare September 21, 2026 15:45

@aslilac aslilac left a comment

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.

providing an authoritative stamp on @fioan89's behalf

@EhabY
EhabY merged commit f096fe1 into main Sep 22, 2026
15 of 16 checks passed
@EhabY
EhabY deleted the fix/windows-ssh-config-acls branch September 22, 2026 08:22
@EhabY EhabY mentioned this pull request Sep 23, 2026
@linear-code

linear-code Bot commented Sep 23, 2026

Copy link
Copy Markdown

ENG-3335

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

6 participants