Skip to content

Native gMSA Support for Windows #27047

@Elpaggio

Description

@Elpaggio

Proposal

Originally proposed in #9424 by @rgl

Add native support for Group Managed Service Accounts (gMSA) in the Windows executor.
The patch detects usernames ending with $ (gMSA convention) and uses the special Windows SERVICE_ACCOUNT_PASSWORD placeholder (_SA_{262E99C9-6160-4871-ACEC-4E61736B6F21}) when calling LogonUserW with LOGON32_LOGON_SERVICE flag. This leverages Windows native gMSA password retrieval and token creation, enabling jobs to run under gMSA identities seamlessly.

Use-cases

Many enterprises increasingly adopt gMSA for secure service account management on Windows nodes.
Enabling Nomad to run tasks as gMSA accounts without exposing passwords aligns with these security best practices and enables integration with Active Directory managed service accounts, simplifying credential management and rotation for orchestrated workloads.

Attempted Solutions

Current Nomad Windows executor implementation cannot imperonate gMSA accounts only builtin local Windows accounts.
I have developed and tested a minimal patch that conditionally sets the special service account placeholder password when a gMSA account is detected, allowing Windows to handle authentication internally.
The patch has been successfully tested in a real environment, demonstrating functional gMSA support without breaking existing functionality.

Note:
This patch is a minimal viable implementation designed to demonstrate how native gMSA support can be integrated into the Windows executor with the very least amount of code changes. It is not intended to be a full production-quality PR. Further work on error handling, testing, and code cleanup would be needed before merging. This approach serves primarily as a proof of concept and starting point for collaboration.

diff --git a/drivers/shared/executor/executor_windows.go b/drivers/shared/executor/executor_windows.go
index 9446a11bef..8f79b080c5 100644
--- a/drivers/shared/executor/executor_windows.go
+++ b/drivers/shared/executor/executor_windows.go
@@ -49,9 +49,34 @@ func setCmdUser(cmd *exec.Cmd, user string) error {
 	}
 	nameParts := strings.Split(user, "\\")
 	if len(nameParts) != 2 {
-		return errors.New("user name must contain domain")
+		return errors.New("user name must be in format DOMAIN\\username")
+	}
+
+	domain := nameParts[0]
+	username := nameParts[1]
+
+	var token *syscall.Token
+	var err error
+
+	// Determine if this is a gMSA (ends with $)
+	isGMSA := strings.HasSuffix(username, "$")
+	if isGMSA {
+		/*
+			If user context is SYSTEM (or another account with the "Act as part of the operating system" privledge)
+			during the LogonUserW call, and if the logontype is specified as LOGON32_LOGON_SERVICE, the cleartext password
+			can be provided as _SA_{262E99C9-6160-4871-ACEC-4E61736B6F21}, which is defined in lmaccess.h as the
+			constant SERVICE_ACCOUNT_PASSWORD .
+			This is detailed in this MS discussion (https://learn.microsoft.com/en-us/archive/msdn-technet-forums/9c88e74d-0710-46a0-8eb8-d0fcd9d18191)
+			and the constant is noted in this Rust winapi wrapper (among other places like the windows sdk )
+			https://docs.rs/winapi/latest/winapi/um/lmaccess/constant.SERVICE_ACCOUNT_PASSWORD.html.
+		*/
+		password := "_SA_{262E99C9-6160-4871-ACEC-4E61736B6F21}"
+		token, err = createUserToken(domain, username, &password)
+	} else {
+		// Create user token
+		token, err = createUserToken(domain, username, nil)
 	}
-	token, err := createUserToken(nameParts[0], nameParts[1])
+
 	if err != nil {
 		return fmt.Errorf("failed to create user token: %w", err)
 	}
@@ -78,7 +103,7 @@ const (
 	_PROVIDER_DEFAULT uint32 = 0
 )
 
-func createUserToken(domain, username string) (*syscall.Token, error) {
+func createUserToken(domain, username string, password *string) (*syscall.Token, error) {
 	userw, err := syscall.UTF16PtrFromString(username)
 	if err != nil {
 		return nil, fmt.Errorf("failed to convert username to UTF-16: %w", err)
@@ -87,11 +112,18 @@ func createUserToken(domain, username string) (*syscall.Token, error) {
 	if err != nil {
 		return nil, fmt.Errorf("failed to convert user domain to UTF-16: %w", err)
 	}
+	var passwordw *uint16
+	if password != nil && *password != "" {
+		passwordw, err = syscall.UTF16PtrFromString(*password)
+		if err != nil {
+			return nil, fmt.Errorf("failed to convert password to UTF-16: %w", err)
+		}
+	}
 	var token syscall.Token
 	ret, _, e := procLogonUserW.Call(
 		uintptr(unsafe.Pointer(userw)),
 		uintptr(unsafe.Pointer(domainw)),
-		uintptr(unsafe.Pointer(nil)),
+		uintptr(unsafe.Pointer(passwordw)),
 		uintptr(_LOGON_SERVICE),
 		uintptr(_PROVIDER_DEFAULT),
 		uintptr(unsafe.Pointer(&token)),

I'd like to gather maintainers and community feedback to improve and formally merge this feature.
Please advise on best practices for testing, documentation, or further improvements desired.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Needs Roadmapping

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions