From b8c85b3b321a1cdc96827a04565447e492b1dc40 Mon Sep 17 00:00:00 2001 From: TinaMor Date: Tue, 16 Jul 2024 11:44:22 +0000 Subject: [PATCH] Fix gMSA setup on Windows and add (back) WS2025 CI --- docs/book/src/capi/windows/windows.md | 2 +- .../files/install-gmsa-keyvault-plugin.ps1 | 203 +++++++++++++----- .../roles/gmsa/tasks/gmsa_keyvault.yml | 35 +-- images/capi/packer/goss/goss-command.yaml | 4 - 4 files changed, 174 insertions(+), 70 deletions(-) diff --git a/docs/book/src/capi/windows/windows.md b/docs/book/src/capi/windows/windows.md index 1aa6d38ae4..46b24c0224 100644 --- a/docs/book/src/capi/windows/windows.md +++ b/docs/book/src/capi/windows/windows.md @@ -50,7 +50,7 @@ Ansible doesn't run on directly on Windows (wsl works) but can used to configure ## Set up Windows machine Follow the [WinRM Setup](https://docs.ansible.com/ansible/latest/os_guide/windows_setup.html) in the Ansible documentation for configuring WinRM on the Windows machine. Note the [ConfigureRemotingForAnsible.ps1](https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1) is for development only. Refer to [Ansible WinRM documentation](https://docs.ansible.com/ansible/latest/user_guide/windows_winrm.html) for details for advance configuration. -After WinRM is installed you can edit or `/etc/ansible/hosts` file with the following: +After WinRM is installed you can edit the `/etc/ansible/hosts` file with the following: ``` [winhost] diff --git a/images/capi/ansible/windows/roles/gmsa/files/install-gmsa-keyvault-plugin.ps1 b/images/capi/ansible/windows/roles/gmsa/files/install-gmsa-keyvault-plugin.ps1 index a670b07d84..3f7f26302f 100644 --- a/images/capi/ansible/windows/roles/gmsa/files/install-gmsa-keyvault-plugin.ps1 +++ b/images/capi/ansible/windows/roles/gmsa/files/install-gmsa-keyvault-plugin.ps1 @@ -13,11 +13,12 @@ # limitations under the License. # script modified from https://github.com/Azure/AgentBaker/blob/8d5323f3b1a622d558e624e5a6b0963229f80b2a/staging/cse/windows/configfunc.ps1 under MIT + $ErrorActionPreference = 'Stop' function Enable-Privilege { - param($Privilege) - $Definition = @' + param($Privilege) + $Definition = @' using System; using System.Runtime.InteropServices; public class AdjPriv { @@ -55,80 +56,184 @@ function Enable-Privilege { } } '@ - $ProcessHandle = (Get-Process -id $pid).Handle - $type = Add-Type $definition -PassThru - $type[0]::EnablePrivilege($processHandle, $Privilege) + $ProcessHandle = (Get-Process -id $pid).Handle + $type = Add-Type $definition -PassThru + $type[0]::EnablePrivilege($processHandle, $Privilege) } function Aquire-Privilege { param($Privilege) write-output "Acquiring the $Privilege privilege" - $enablePrivilegeResponse=$false - for($i = 0; $i -lt 10; $i++) { - write-output "Retry $i : Trying to enable the $Privilege privilege" - $enablePrivilegeResponse = Enable-Privilege -Privilege "$Privilege" -ErrorAction 'Continue' - if ($enablePrivilegeResponse) { - break - } - Start-Sleep 1 + $enablePrivilegeResponse = $false + for ($i = 0; $i -lt 10; $i++) { + write-output "Retry $i : Trying to enable the $Privilege privilege" + $enablePrivilegeResponse = Enable-Privilege -Privilege "$Privilege" + if ($enablePrivilegeResponse) { + break + } + Start-Sleep 1 } - if(!$enablePrivilegeResponse) { - write-output "Failed to enable the $Privilege privilege." - exit 1 + if (!$enablePrivilegeResponse) { + write-error "Failed to enable the $Privilege privilege." + exit 1 } } -# Enable the PowerShell privilege to set the registry permissions. -Aquire-Privilege -Privilege "SeTakeOwnershipPrivilege" +function Set-RegistryKeyPermissions { + param ( + [string]$RegistryKeyPath, + [string]$TargetOwner = "BUILTIN\Administrators" + ) -# Set the registry permissions. -write-output "Setting GMSA plugin registry permissions" -try { - $ccgKeyPath = "System\CurrentControlSet\Control\CCG\COMClasses" - $owner = [System.Security.Principal.NTAccount]"BUILTIN\Administrators" + try { + $owner = [System.Security.Principal.NTAccount]$TargetOwner + # Open the key with permission to take ownership $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey( - $ccgKeyPath, - [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, - [System.Security.AccessControl.RegistryRights]::TakeOwnership) + $RegistryKeyPath, + [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, + [System.Security.AccessControl.RegistryRights]::TakeOwnership) + + if (-not $key) { + write-host "Failed to open registry key $RegistryKeyPath. Registry key does not exist." + return + } + + # Get ACL and set owner $acl = $key.GetAccessControl() $originalOwner = $acl.owner $acl.SetOwner($owner) $key.SetAccessControl($acl) - + + # Reopen the key with permission to change permissions $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey( - $ccgKeyPath, - [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, - [System.Security.AccessControl.RegistryRights]::ChangePermissions) + $RegistryKeyPath, + [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, + [System.Security.AccessControl.RegistryRights]::ChangePermissions) $acl = $key.GetAccessControl() - $rule = New-Object System.Security.AccessControl.RegistryAccessRule( - $owner, - [System.Security.AccessControl.RegistryRights]::FullControl, - [System.Security.AccessControl.AccessControlType]::Allow) + + # Remove any deny permissions + $RemoveAcl = $acl.Access | Where-Object { $_.AccessControlType -eq "Deny" } + if ($RemoveAcl) { + $Acl.RemoveAccessRule($RemoveAcl) + } + + # Disable protection (enable inheritance) + $acl.SetAccessRuleProtection($false, $true) # False disables protection; true preserves existing entries + + # Add a new access rule + $rule = New-Object System.Security.AccessControl.RegistryAccessRule ( + $owner, + [System.Security.AccessControl.RegistryRights]::FullControl, + [System.Security.AccessControl.AccessControlType]::Allow + ) $acl.SetAccessRule($rule) + + # Apply the updated ACL back to the registry key $key.SetAccessControl($acl) -} catch { - write-output "Failed to set GMSA plugin registry permissions. $_" + + return @{ + OriginalOwner = $originalOwner + RegistryKey = $key + Rule = $rule + } + } + catch { + write-error "Failed to set GMSA plugin registry permissions. $_" exit 1 + } } -# Set the appropriate registry values. -try { - write-output "Setting the appropriate GMSA plugin registry values" - reg.exe import "registerplugin.reg" -} catch { - write-output "Failed to set GMSA plugin registry values. $_" +function Restore-RegistryKeyOriginalAccess { + param ( + [Microsoft.Win32.RegistryKey]$Key, + [System.Security.AccessControl.RegistryAccessRule]$Rule, + [string]$OriginalOwner + ) + + try { + $acl = $key.GetAccessControl() + $acl.RemoveAccessRule($rule) | Out-Null + $acl.SetOwner([System.Security.Principal.NTAccount]$originalowner) + + # Apply the updated ACL to the key + $key.SetAccessControl($acl) + $key.close() + } + catch { + Write-Error "Failed to restore original registry access. $_" exit 1 + } +} + +############################################################### +######################### MAIN SCRIPT ######################### +############################################################### + +# Check if the registerplugin.reg file exists +$pluginPath = "$PSScriptRoot\registerplugin.reg" +if (-not (Test-Path "$pluginPath")) { + write-error "Couldn't find file: $pluginPath" + exit 1 +} + +# Enable the PowerShell privilege to set the registry permissions +Aquire-Privilege -Privilege "SeTakeOwnershipPrivilege" + +# Get the registry key paths from the plugin file to set permissions +[System.Array]$registryKeyPaths = @( "System\CurrentControlSet\Control\CCG\COMClasses" ) +$registryKeyPaths += Get-Content -Path $pluginPath | ForEach-Object { + if ($_ -match '^\[HKEY_LOCAL_MACHINE\\(.*)]$') { + return $matches[1] + } } -write-output "Restore original access to registry key" -$acl = $key.GetAccessControl() -$acl.RemoveAccessRule($rule) -$acl.SetOwner([System.Security.Principal.NTAccount]$originalowner) -Aquire-Privilege -Privilege "SeRestorePrivilege" -$key.SetAccessControl($acl) -$key.close() +[System.Array]$registryResults = @() +try { + # Set the registry owner and permissions + Write-Output "Setting registry owner and permissions" + foreach ($registryKeyPath in $registryKeyPaths) { + write-output "Setting permissions: { KeyPath: $RegistryKeyPath }" + $result = Set-RegistryKeyPermissions -RegistryKeyPath "$registryKeyPath" + $registryResults += $result + } + + # HACK: Set the error action preference to 'Continue' to avoid script-terminating errors + # Restore the original error action preference after the registry import is done + # In Windows PowerShell (v5.1), 2>&1 redirection in the presence of $ErrorActionPreference = 'Stop' + # generates a script-terminating error if stderr output is written. + # https://github.com/PowerShell/PowerShell/issues/3996 + # https://www.reddit.com/r/PowerShell/comments/16j43tx/howto_properly_capture_error_output_from_external/ + $ErrorActionPreference = 'Continue' + + # Import the registry values from the plugin file + Write-Output "Setting the appropriate GMSA plugin registry values" + $cmdOutput = reg.exe import "$pluginPath" 2>&1 + + # Reset the error action preference to 'Stop' + $ErrorActionPreference = 'Stop' + + if ($LASTEXITCODE -ne 0) { + throw "Failed to import GMSA plugin registry values. $cmdOutput" + } + Write-Output "Successfully imported the GMSA plugin registry values" +} +catch { + Write-Error "Couldn't install the GMSA plugin. $_" + exit 1 +} +finally { + # Restore the original registry permissions + Write-Output "Restoring original access to registry key" + + # Acquire necessary privileges for restoring owner + Aquire-Privilege -Privilege SeRestorePrivilege + + foreach ($result in $registryResults) { + Restore-RegistryKeyOriginalAccess -Key $result.RegistryKey -Rule $result.Rule -OriginalOwner $result.OriginalOwner + } +} write-output "Successfully installed the GMSA plugin" diff --git a/images/capi/ansible/windows/roles/gmsa/tasks/gmsa_keyvault.yml b/images/capi/ansible/windows/roles/gmsa/tasks/gmsa_keyvault.yml index a50f87557d..82ef232bcb 100644 --- a/images/capi/ansible/windows/roles/gmsa/tasks/gmsa_keyvault.yml +++ b/images/capi/ansible/windows/roles/gmsa/tasks/gmsa_keyvault.yml @@ -43,19 +43,22 @@ dest: "{{ kubernetes_install_path }}" - name: Register gMSA Key Vault plugin - ansible.windows.win_shell: | - {{ kubernetes_install_path }}\install-gmsa-keyvault-plugin.ps1 -- name: Install registry CCG logging manifest - ansible.windows.win_shell: | - wevtutil.exe um {{ kubernetes_install_path }}\CCGEvents.man - wevtutil.exe im {{ kubernetes_install_path }}\CCGEvents.man -- name: Install registry Key Vault plugin logging manifest - ansible.windows.win_shell: | - wevtutil.exe um {{ kubernetes_install_path }}\CCGAKVPluginEvents.man - wevtutil.exe im {{ kubernetes_install_path }}\CCGAKVPluginEvents.man -- name: Clean up gMSA install files - ansible.windows.win_shell: | - Remove-Item {{ kubernetes_install_path }}\CCGEvents.man - Remove-Item {{ kubernetes_install_path }}\CCGAKVPluginEvents.man - Remove-Item {{ kubernetes_install_path }}\registerplugin.reg - Remove-Item {{ kubernetes_install_path }}\install-gmsa-keyvault-plugin.ps1 + block: + - name: Import gMSA Key Vault plugin + ansible.windows.win_shell: | + {{ kubernetes_install_path }}\install-gmsa-keyvault-plugin.ps1 + - name: Install registry CCG logging manifest + ansible.windows.win_shell: | + wevtutil.exe um {{ kubernetes_install_path }}\CCGEvents.man + wevtutil.exe im {{ kubernetes_install_path }}\CCGEvents.man + - name: Install registry Key Vault plugin logging manifest + ansible.windows.win_shell: | + wevtutil.exe um {{ kubernetes_install_path }}\CCGAKVPluginEvents.man + wevtutil.exe im {{ kubernetes_install_path }}\CCGAKVPluginEvents.man + always: + - name: Cleanup gMSA install files + ansible.windows.win_shell: | + Remove-Item {{ kubernetes_install_path }}\CCGEvents.man + Remove-Item {{ kubernetes_install_path }}\CCGAKVPluginEvents.man + Remove-Item {{ kubernetes_install_path }}\registerplugin.reg + Remove-Item {{ kubernetes_install_path }}\install-gmsa-keyvault-plugin.ps1 diff --git a/images/capi/packer/goss/goss-command.yaml b/images/capi/packer/goss/goss-command.yaml index 7ea9925e84..aa2179c6d0 100644 --- a/images/capi/packer/goss/goss-command.yaml +++ b/images/capi/packer/goss/goss-command.yaml @@ -231,16 +231,12 @@ command: stdout: - "C:\\Windows\\System32\\CCGAKVPlugin.dll" timeout: 30000 - {{if ne .Vars.distribution_version "2025"}} - # TODO: [SEPTEMEBER 2024] Known issue in Windows Server 2025 preview image. - # WIP to fix the bug: The property value is null/empty Key Vault gMSA CCG interface is registered: exec: powershell -command "(Get-Item 'HKLM:SOFTWARE\Classes\Interface\{6ECDA518-2010-4437-8BC3-46E752B7B172}') | Ft -autosize -wrap" exit-status: 0 stdout: - "ICcgDomainAuthCredentials" timeout: 30000 - {{end}} {{end}} {{ if ne .Vars.ssh_source_url "" }}