Skip to content

[client] Derive Windows SSH privilege checks from the token and group membership - #6966

Open
lixmal wants to merge 5 commits into
mainfrom
ssh-windows-privilege-check
Open

[client] Derive Windows SSH privilege checks from the token and group membership#6966
lixmal wants to merge 5 commits into
mainfrom
ssh-windows-privilege-check

Conversation

@lixmal

@lixmal lixmal commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes

The SSH server decided Windows privilege from usernames and SID string patterns, which does not match how Windows expresses privilege, so the allow-root-login setting was enforced against the wrong set of accounts. Privilege is now derived from the process token and from actual group membership.

  • Determine whether the daemon runs elevated from its process token instead of matching the current user's name or SID
  • Classify a target account from its SID (well-known service accounts, built-in Administrator by RID) and from local Administrators membership, replacing the hardcoded list of privileged usernames
  • Resolve Administrators membership for domain accounts through an S4U token so nested and universal groups are covered, with local group enumeration as fallback
  • Deny when an account cannot be resolved or its membership cannot be determined
  • Add tests for the SID and membership checks

With root login disabled, the set of refused accounts changes: members of the local Administrators group are now included, and unprivileged accounts whose names merely resemble privileged ones (such as admin) are not.

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)
  • This change does not modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — OR I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See CONTRIBUTING.md

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

No configuration, flags or documented behaviour change: the allow-root-login setting keeps its documented meaning, this makes enforcement match it on Windows.

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • Strengthened Windows SSH privilege handling with fail-closed checks for privileged accounts and more reliable Administrators membership evaluation for both local and domain users.
    • Added an option to control whether root login is allowed.
    • Standardized username/domain parsing across Windows SSH command execution, SFTP command creation, and user switching.
  • Tests
    • Expanded Windows privilege and local group membership coverage, including localized account names and consistency/failure scenarios; updated non-Windows expectations and skip behavior.
  • Refactor
    • Reworked the cross-platform privilege-check flow for consistent, platform-appropriate results.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 64e8f894-50d2-4c42-b80b-2e240defd37c

📥 Commits

Reviewing files that changed from the base of the PR and between c4fec48 and 7aa9395.

📒 Files selected for processing (1)
  • client/ssh/server/privileges_windows_test.go

📝 Walkthrough

Walkthrough

Windows SSH privilege detection now uses platform-specific SID, token, S4U, and local-group helpers with injected routing and expanded tests. Username parsing is package-level and reused by Windows command, PTY, SFTP, and user-switching paths.

Changes

Windows SSH privilege evaluation

Layer / File(s) Summary
Privilege check routing and injection
client/ssh/server/user_utils.go
Privilege evaluation delegates Windows account and process checks to injected helpers while retaining Unix checks and shared privilege-check entry points.
Platform-specific privilege helpers
client/ssh/server/privileges_windows.go, client/ssh/server/privileges_other.go
Windows elevation, SID, Administrator, S4U, local-group, and NetAPI checks are implemented with non-Windows stubs.
Privilege behavior validation
client/ssh/server/privileges_windows_test.go, client/ssh/server/server_config_test.go, client/ssh/server/user_utils_test.go
Tests cover Windows privilege classification, membership paths, fail-closed behavior, platform dispatch, and injected helper cleanup.

Windows username parsing

Layer / File(s) Summary
Shared username parsing helper
client/ssh/server/userswitching_windows.go, client/ssh/server/command_execution_windows.go, client/ssh/server/sftp_windows.go
The parser is converted to a package-level helper and used by command execution, PTY, SFTP, and user-switching flows without changing parsing behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SSHServer
  participant isPrivilegedUsername
  participant isWindowsAccountPrivileged
  participant WindowsToken
  participant NetAPI
  SSHServer->>isPrivilegedUsername: evaluate Windows username
  isPrivilegedUsername->>isWindowsAccountPrivileged: classify account
  isWindowsAccountPrivileged->>WindowsToken: resolve SID and test token membership
  isWindowsAccountPrivileged->>NetAPI: enumerate local groups
  WindowsToken-->>isWindowsAccountPrivileged: elevation or S4U membership
  NetAPI-->>isWindowsAccountPrivileged: local group names
  isWindowsAccountPrivileged-->>SSHServer: privilege result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change to Windows SSH privilege checks.
Description check ✅ Passed The template is mostly filled out with a clear change summary, checklist, and docs section; only the issue ticket/link and stack details are missing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ssh-windows-privilege-check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
client/ssh/server/user_utils.go (1)

190-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Minor duplication between userNameLookup and userPrivilegeCheck.

Both build an identical PrivilegeCheckRequest and only differ in what they extract from the result. userNameLookup could simply call userPrivilegeCheck and return result.User, err to avoid the two staying in sync manually as the request shape evolves.

♻️ Suggested refactor
 func (s *Server) userNameLookup(username string) (*user.User, error) {
-	result := s.CheckPrivileges(PrivilegeCheckRequest{
-		RequestedUsername:         username,
-		FeatureSupportsUserSwitch: true,
-		FeatureName:               FeatureSSHLogin,
-	})
-
-	if !result.Allowed {
-		return nil, result.Error
-	}
-
-	return result.User, nil
+	result, err := s.userPrivilegeCheck(username)
+	if err != nil {
+		return nil, err
+	}
+	return result.User, nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/ssh/server/user_utils.go` around lines 190 - 218, Refactor
userNameLookup to call userPrivilegeCheck with the username, then return the
resulting User and error. Remove its duplicated CheckPrivileges request
construction while preserving the existing userPrivilegeCheck behavior and
return semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/ssh/server/privileges_windows.go`:
- Around line 142-158: Update localGroupsContainSID to track whether any group
SID resolution succeeds and capture LookupSID errors; if all returned groups
fail resolution, return a non-nil error instead of false, nil, while preserving
true for matching SIDs and false, nil when at least one group resolves without
matching.

---

Nitpick comments:
In `@client/ssh/server/user_utils.go`:
- Around line 190-218: Refactor userNameLookup to call userPrivilegeCheck with
the username, then return the resulting User and error. Remove its duplicated
CheckPrivileges request construction while preserving the existing
userPrivilegeCheck behavior and return semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f08072d9-a472-4d4a-8c84-37d05a064cd5

📥 Commits

Reviewing files that changed from the base of the PR and between 0b7e6a9 and c4bc479.

📒 Files selected for processing (9)
  • client/ssh/server/command_execution_windows.go
  • client/ssh/server/privileges_other.go
  • client/ssh/server/privileges_windows.go
  • client/ssh/server/privileges_windows_test.go
  • client/ssh/server/server_config_test.go
  • client/ssh/server/sftp_windows.go
  • client/ssh/server/user_utils.go
  • client/ssh/server/user_utils_test.go
  • client/ssh/server/userswitching_windows.go

Comment thread client/ssh/server/privileges_windows.go
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Release artifacts

Built for PR head 7aa9395 in workflow run #17262.

Artifact Link
All release artifacts Download
Linux packages Download
Windows packages Download
macOS packages Download
UI artifacts Download
UI macOS artifacts Download

GHCR images (amd64)

This comment is updated by the Release workflow. Artifact links expire according to the workflow retention policy.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
client/ssh/server/privileges_windows_test.go (3)

128-168: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require at least one S4U comparison.

Because ineligible users are skipped, this test can complete successfully without checking any account when all local users fail S4U logon. Track the number of comparisons and fail with a diagnostic if it remains zero.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/ssh/server/privileges_windows_test.go` around lines 128 - 168, The
TestS4UMembershipAgreesWithLocalGroups test must require at least one successful
S4U comparison. After iterating through users and incrementing checked only for
completed comparisons, assert that checked is greater than zero with a
diagnostic describing that all local accounts were skipped due to S4U logon
failure.

79-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve built-in accounts by SID instead of literal names.

isWindowsAccountPrivileged treats lookup failures as privileged, but these tests still call it with "Administrator" and "Guest". On systems where those accounts are renamed, the lookup can fail and the assertion can pass for the fail-closed path rather than the intended membership check. Drive these cases from well-known SIDs or a controlled fixture instead. This also affects the localGroupsContainSID tests at lines 170-220.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/ssh/server/privileges_windows_test.go` around lines 79 - 104, Update
TestIsWindowsAccountPrivileged and the localGroupsContainSID tests to resolve
built-in Administrator and Guest accounts through their well-known SIDs or a
controlled fixture instead of literal names. Ensure each assertion exercises
actual SID-based membership behavior and does not pass via
isWindowsAccountPrivileged’s fail-closed lookup path.

106-122: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test UAC elevation independently from Administrators membership.

Token.IsMember checks enabled Administrators membership, whereas isProcessElevated() uses TokenElevation. An Administrators token can be non-elevated under UAC, so this precondition can skip limited-admin cases or assert an unrelated property. Inject getProcessElevated or otherwise exercise the elevation result directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/ssh/server/privileges_windows_test.go` around lines 106 - 122, The
TestIsProcessElevated test incorrectly derives its assertion from Administrators
membership instead of independently testing TokenElevation. Update the test to
inject or otherwise control the getProcessElevated result used by
isProcessElevated, and assert elevation behavior directly without gating on
Token(0).IsMember(adminSid); retain only membership checks if they are
separately needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@client/ssh/server/privileges_windows_test.go`:
- Around line 128-168: The TestS4UMembershipAgreesWithLocalGroups test must
require at least one successful S4U comparison. After iterating through users
and incrementing checked only for completed comparisons, assert that checked is
greater than zero with a diagnostic describing that all local accounts were
skipped due to S4U logon failure.
- Around line 79-104: Update TestIsWindowsAccountPrivileged and the
localGroupsContainSID tests to resolve built-in Administrator and Guest accounts
through their well-known SIDs or a controlled fixture instead of literal names.
Ensure each assertion exercises actual SID-based membership behavior and does
not pass via isWindowsAccountPrivileged’s fail-closed lookup path.
- Around line 106-122: The TestIsProcessElevated test incorrectly derives its
assertion from Administrators membership instead of independently testing
TokenElevation. Update the test to inject or otherwise control the
getProcessElevated result used by isProcessElevated, and assert elevation
behavior directly without gating on Token(0).IsMember(adminSid); retain only
membership checks if they are separately needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d5566c6-4e70-4f94-b236-9a9d0f56eca2

📥 Commits

Reviewing files that changed from the base of the PR and between c4bc479 and 3c745a8.

📒 Files selected for processing (3)
  • client/ssh/server/privileges_windows.go
  • client/ssh/server/privileges_windows_test.go
  • client/ssh/server/user_utils.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/ssh/server/privileges_windows.go
  • client/ssh/server/user_utils.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/ssh/server/privileges_windows_test.go`:
- Around line 169-177: Update TestLocalGroupsContainSID_Administrator to locate
the local account whose SID satisfies isBuiltinAdministratorSID (RID 500)
instead of passing the literal "Administrator" name, then pass that resolved
account name to localGroupsContainSID while preserving the existing assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5dc669af-14b2-46d5-b914-9bb49c890102

📥 Commits

Reviewing files that changed from the base of the PR and between 3c745a8 and 6f8dd82.

📒 Files selected for processing (2)
  • client/ssh/server/privileges_windows.go
  • client/ssh/server/privileges_windows_test.go

Comment thread client/ssh/server/privileges_windows_test.go Outdated
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant