When working with HashiCorp Packer to create golden images, joining an Active Directory Domain is typically not necessary for most use cases. However, there are scenarios where a domain join becomes essential. For instance, an application installer might need to access a file share that isn’t available in an isolated deployment. Initially, the solution seems straightforward: create a PowerShell script and execute it using a PowerShell provisioner in your Packer template. But, believe me, it’s not always that simple.
Whats the issue? Lets start from the beginning. This following script is responsible for joining the machine to the Active Directory Domain. Nothing fancy, with the help of an environment variable ($env:PACKER_DOMAIN_SECRET) I am parsing the password to the script that it is not stored in clear text. This can be added on your local machine as an environment variable or through a variable in your pipeline.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<# .AUTHOR: Julian Mooren .DATE: 09.04.2024 #> $AppName = "Packer-PreReqs-JoinDomain" $Version = "1.0" $PackerDir = "C:\PackerBuild" Start-Transcript -Path "$PackerDir\Logs\$($AppName)_$($Version).txt" try { [string]$domain = "contoso.com" [string]$ou = "OU=Staging,DC=contoso,DC=com" [string]$userName = "svc-join@$($domain)" [securestring]$secStringPassword = ConvertTo-SecureString $env:PACKER_DOMAIN_SECRET -AsPlainText -Force [pscredential]$joinCred = New-Object System.Management.Automation.PSCredential ($userName, $secStringPassword) Add-Computer -Domain $domain -OUPath $ou -Credential $joinCred -PassThru -Verbose } catch { throw "Error: $($_.Exception.Message)" } Stop-Transcript |
Inside the packer template file this the mentioned provisioner block.
1 2 3 4 5 6 7 8 9 |
provisioner "powershell" { environment_vars = ["PACKER_DOMAIN_SECRET=${var.PACKER_DOMAIN_SECRET}"] scripts = ["scripts/000_PreReqs/007_Packer-PreReqs-JoinDomain.ps1"] valid_exit_codes = [0, 3010] } provisioner "windows-restart" { restart_check_command = "powershell -command \"& {Write-Output 'Machine restarted'}\"" } |
After having everything in place I tried to build the image and the join to the domain completed successfully.
That was too easy, whats the whole purpose of this blog post? Well we need to reboot the machine after the domain join and thats where the problems are beginning to occur….. The following provisioner block “windows-restart” was triggered successful after the domain join but it was impossible to establish a WinRM connection again.
The following outputs have been visible in the packer log file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
2024/04/10 09:29:38 ui: ==> azure-arm.avd: Waiting for machine to restart... 2024/04/10 09:29:38 packer.exe plugin: Check if machine is rebooting... 2024/04/10 09:29:39 [INFO] 0 bytes written for 'stdout' 2024/04/10 09:29:39 [INFO] 0 bytes written for 'stderr' 2024/04/10 09:29:39 packer.exe plugin: [INFO] 0 bytes written for 'stdout' 2024/04/10 09:29:39 packer.exe plugin: [INFO] 0 bytes written for 'stderr' 2024/04/10 09:29:39 packer.exe plugin: Waiting for machine to reboot with timeout: 5m0s 2024/04/10 09:29:39 packer.exe plugin: Waiting for machine to become available... 2024/04/10 09:29:39 packer.exe plugin: Checking that communicator is connected with: 'powershell -command "& {Write-Output 'Machine restarted'}"' 2024/04/10 09:29:51 [INFO] 0 bytes written for 'stdout' 2024/04/10 09:29:51 [INFO] 0 bytes written for 'stderr' 2024/04/10 09:29:51 packer.exe plugin: [INFO] 0 bytes written for 'stdout' 2024/04/10 09:29:51 packer.exe plugin: [INFO] 0 bytes written for 'stderr' 2024/04/10 09:29:51 packer.exe plugin: Communication connection err: unknown error Post "https://10.106.9.4:5986/wsman": dial tcp 10.106.9.4:5986: connectex: No connection could be made because the target machine actively refused it. 2024/04/10 09:29:57 [INFO] 0 bytes written for 'stderr' 2024/04/10 09:29:57 [INFO] 0 bytes written for 'stdout' 2024/04/10 09:29:57 packer.exe plugin: [INFO] 0 bytes written for 'stdout' 2024/04/10 09:29:57 packer.exe plugin: [INFO] 0 bytes written for 'stderr' 2024/04/10 09:29:57 packer.exe plugin: Communication connection err: http response error: 401 - invalid content type |
The WinRM connection could not be established because the target machine is refusing the connection. Whats going on there? Lets check the security logs of the build VM.
With the entries of the Event ID 4625 it was very clear what is happening there. The WinRM login before the domain join was using the local provisioned user account “packer” but after joining the machine the user account specified under the option “winrm_username” was attached with the joined domain. In this case: “packer@contoso.com”. With that piece of information my first guess was to change the “winrm_username” option to the following syntax:
Before
1 2 3 4 5 6 |
#WinRM communicator = "winrm" winrm_insecure = "true" winrm_timeout = "5m" winrm_use_ssl = "true" winrm_username = "packer" |
After
1 2 3 4 5 6 |
#WinRM communicator = "winrm" winrm_insecure = "true" winrm_timeout = "5m" winrm_use_ssl = "true" winrm_username = ".\packer" |
Unfortunately the build didn’t even start… During the first provisioning phase I received an error message (screenshot below) telling me that the usage of some characters is not allowed for the winrm username. This includes using a dot (.) or the @ character. Well slowly this is getting a bit tricky 🤔
Reading through the Packer Azure ARM Builder documentation and checking my options, I was not really finding a useful hint. I knew that the Azure builder is creating a KeyVault which is storing the certificate for the authentication for WinRM and the local packer user account password which is being provisioned. There have been some unsolved Github issues about this problem and a thread under HashiCorp discussions. But they never came to a solution for the problem.
With a background thought in my head and little bit of curiosity I tried to fill both options “winrm_username” “winrm_password” with custom values and started the packer build again.
1 2 3 4 5 6 7 |
#WinRM communicator = "winrm" winrm_insecure = "true" winrm_timeout = "5m" winrm_use_ssl = "true" winrm_username = "svc-packer" winrm_password = "P@ssw0rd!#" |
The VM was provisioned and the WinRM connection was established. Boom! 💣 With that information we can put the pieces together the solve the issue with a workaround. Until today I didn’t know that its possible to specify your own password for the provisioned user account when using the azure-arm plugin. I have done this with the VMware vSphere Packer plugin several years ago (using the autounattend.xml) but was believing that the password for the local user account can only be created and grabbed through the integrated azure-arm plugin procedure (randomization) including the temporary provisioned KeyVault. At least for me that information was not visible when reading the packer documentation. What can we do with the information we have gathered?
We are still not able to use special characters when working with the “winrm_username” option. But thats something we can ignore! The workaround is simple but effective. You just need to create an user (service) account in the domain you are joining the virtual machine. In the example above I have been using the username “svc-packer”. This user account (svc-packer@contoso.com) needs to have the identical password which is being specified for the “winrm_password” option. Before the domain join script is being executed the WinRM connection will be established with the local user “svc-packer”. After the machine has been joined to the domain and the reboot is done, communication will take place over the service account “svc-packer@contoso.com”. The WinRM connection will not break anymore and you can continue the deployment with additional steps.
Important: Make sure that you add the service account as a local administrator on the Packer VM. This can be done directly after using the “Add-Computer” Cmdlet. Otherwise you will run into issues, because the connected user is not authorized to install software or doing further modification on the system.
1 |
Add-LocalGroupMember -Name Administrators -Member "svc-packer" |
Never ever put the password for the “winrm_password” in clear text. I simplified the configuration for this post to make it easier to understand the problem with the current azure-arm plugin. I always recommend creating a dedicated variable and using that as a reference in the template file. Example:
1 2 3 4 5 6 7 8 9 10 |
variable "PACKER_DOMAIN_SECRET" { type = string default = "${env("PACKER_DOMAIN_SECRET")}" sensitive = true } #WinRM .... winrm_password = var.PACKER_DOMAIN_SECRET .... |
To be honest: I would prefer to work with the local user account which is being provisioned during the VM creation for 95% of the time. The “elevated_user” and “elevated_password” option would be way to go for special application installations which are requiring Active Directory connectivity. Maybe some day the Azure ARM plugin will be fixed that you can at least specify using a dot (.) for choosing a local account. Since there are issues for this topic on Github for several years, I do not expect it to be fixed anytime soon. Hope this helps people out there who are dealing with the same issue.
Happy to hear about this workaround! I’ll have to fiddle again when I have time because I believe I have tried to set a username with credentials from AD as winrm_username and winrm_password and it didn’t work for me. However, I did so many tests back then, I may have missed a setting, or our domain is restrictive somehow on remote winrm connections. Glad you found something that works for you!
JF (u362jsim)
Thank you very much for sharing your solution, I’m just starting to work with Packer now. I confess that I’ve been trying to join the VM to the domain for a few days, I finally managed to join it, but after it restarts, I still get errors to continue with the other configurations:
2024/09/13 11:54:16 packer-provisioner-windows-restart plugin: [INFO] 0 bytes written to ‘stdout’
2024/09/13 11:54:16 packer-provisioner-windows-restart plugin: [INFO] 0 bytes written to ‘stderr’
2024/09/13 11:54:16 packer-provisioner-windows-restart plugin: Communication connection err: http response error: 401 – invalid content type
The password for the user svc-packer is exactly the same in AD and in the template. I can log in to the machine via RDP when packer is working.
In the winrm logs on the machine, I have this log after rebooting:
The authorization of the user failed with error 5
I’m trying to figure out what it could be, but if you already know this behavior and can help me, I’d really appreciate it.
I don’t know if there is any additional configuration that needs to be done for authentication to work correctly after joining the domain.
My version of packer: Packer v1.11.2
Hello Rodrigo have you tried „Test-WSMan“ after the VM has rebooted? If the passwords really are identical it should work. Have you tried joining the machine to a OU without any GPOs applied?