Please note – this script was updated – you find the updated post here.
One of the challenges in most daily IT operations is onboarding of workstations and servers (respective domain join). Over the years I came across and tried many ways to accomplish this. Today I wanted to share a script and solution others might find helpful, but first lets get down to some theory and background.
The goals and challenges:
- simple domain join after a system was imaged
- this is in theory possible in a fully automated process via various imaging solutions – I found that WDS (Microsoft Windows Deployment Services are in most cases the easiest way to accomplish this while having the possibility to use this in consulting for various clients, in enterprise for various departments etc. Since Windows 10 came in to the equation some of the automation with WDS became more challenging – so keeping it simple with some additional manual labor is often the easiest way to accomplish this – to simplify the process a PowerShell script became a perfect solution).
- systems should have a local admin account (not administrator / SID 500 / who should remain disabled) with an individual password
- typing this manual you always risk that the password is misspelled either in your password database or on the actual operating system
- if you think it is a good idea to have the same password on all your clients I actually suggest you do some security related research!
The PowerShell script below will do the following for you:
- Ask for the name of the system (this will change the hostname/computername)
- Ask for credentials for KeePass / Pleasant Password Server
- Ask for credentials to join the system to the domain
- Create a local admin user account on the system
- Generate a password for this account
- Check if there is an existing KeePass / Pleasant Password Server entry for this system
- If not – it will proceed and create a entry with the machine name, username, password and various additional information like
- manufacturer
- model
- serial number / service tag
- UEFI BIOS Windows license key
- MAC addresses of all network cards Windows knows about
- And finally it will join the domain and put the system right away in to the defined OU
The whole script is only an example – you don’t have to use KeePass / Pleasant Password Server nor is the script perfect for any situation – you can take it and modify it as you need it – point it to various IT Asset databases or let you chose from predefined OUs etc. – adjust it as you needed – in general it is a very useful baseline and I wanted to share it.
One of the challenges is to execute the script as administrator (elevated rights) and as well bypass the script execution restrictions without compromising them in a default image, like disabling this important security feature on the image itself. To accomplish this, a simple CMD-Script actually will execute the PowerShell script. CMD-Script can right-clicked and executed as administrator and gain elevated rights. This is as of today not possible by default with PowerShell scripts (.ps1).
Create the following two files “Execute-DomainJoin.cmd” and “Execute-DomainJoin.ps1” and save them in the same directory or e.g. a portable flash drive. Adjust the PowerShell script so it connects to your domain and local systems.
1 2 | powershell.exe -executionpolicy unrestricted -file "%~dp0\Execute-DomainJoin.ps1" pause |
Please note – this script was updated – you find the updated post here.
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | $ScriptVersion = "1.0 - 6/15/2018 - Florian Rossmark Clear-Host Write-Host "" Write-Host "" Write-Host "Welcome to the Execute Domain-Join script" Write-Host "=========================================" Write-Host "" Write-Host "" Write-Host "This script will do the following Steps:" Write-Host "----------------------------------------" Write-Host "- You enter the Computer Name / Host Name" Write-Host "- You enter Domain Admin credentials for a Domain-Join" Write-Host "- You enter credentials to access KeyPass Password server" Write-Host "" Write-Host "" Write-Host "Note: always type credentials without any domain-information (MYDOMAIN\) - the script will fail otherwise." Write-Host "" Write-Host "" Write-Host "" Write-Host "- The script will automatically create a KeyPass entry with all information" Write-Host "- The script will automatically create a specific local admin account for this machine" Write-Host "- The script will automatically rename the system to the given name" Write-Host "- The script will automatically join the system to the domain" Write-Host "" Write-Host "" Write-Host "Script Version: $ScriptVersion" Write-Host "" Write-Host "" pause #Upper area holds functions.. Function MakeUp-String([Int]$Size = 8, [Char[]]$CharSets = "ULNS", [Char[]]$Exclude) { $Chars = @(); $TokenSet = @() If (!$TokenSets) {$Global:TokenSets = @{ U = [Char[]]'ABCDEFGHJKLMNPQRSTUVWXYZ' #Upper case L = [Char[]]'abcdefghijkmnpqrstuvwxyz' #Lower case N = [Char[]]'23456789' #Numerals S = [Char[]]'!@$!@$!@$!@$' #Symbols }} $CharSets | ForEach { $Tokens = $TokenSets."$_" | ForEach {If ($Exclude -cNotContains $_) {$_}} If ($Tokens) { $TokensSet += $Tokens If ($_ -cle [Char]"Z") {$Chars += $Tokens | Get-Random} #Character sets defined in upper case are mandatory } } While ($Chars.Count -lt $Size) {$Chars += $TokensSet | Get-Random} ($Chars | Sort-Object {Get-Random}) -Join "" #Mix the (mandatory) characters and output string }; #bypass/avoid certificate issues Add-Type @" using System; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public class ServerCertificateValidationCallback { public static void Ignore() { ServicePointManager.ServerCertificateValidationCallback += delegate ( Object obj, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors ) { return true; }; } } "@ [ServerCertificateValidationCallback]::Ignore(); #Script starts here... Clear-Host Write-Host "" Write-Host "" Write-Host "This script must be executed in a PowerShell with elevated rights!" Write-Host "" Write-Host "" Write-Host "Stop the script with: CTRL + C any time..." Write-Host "" Write-Host "" pause Clear-Host $ComputerName = Read-Host "Please enter the new Computer Name" $DomainJoinCredentials = Get-Credential -Message "Enter your credentials for the domain join" $KeePassCredentials = Get-Credential -Message “Enter your credentials to access Keepass Server” $localAdminUser = $("$ComputerName" + "_Admin") $PW = MakeUp-String(12) $PWencrypted = ConvertTo-SecureString $PW -AsPlainText -Force Clear-Host Write-Host "" Write-Host "" Write-Host "Checking KeePass to see if those credentials already exist" Write-Host "" Write-Host "" #Collecting MACs $colMACItems = get-wmiobject Win32_NetworkAdapter | Where-Object {$_.MACAddress -like "*:*"} foreach ($objMACItem in $colMACItems) { $MACobj = $objMACItem |select Description,MACAddress $MACs += $MACobj.MACAddress + " - " + $MACobj.Description + " " } #Collecting SN/ServiceTag and HW information $SN = "ServiceTag / SN: "+(Get-WmiObject win32_bios | select SerialNumber).SerialNumber $Model = "Model: "+(Get-WmiObject win32_computersystem | select Model).Model $Manufacturer = "Manufacturer: "+(Get-WmiObject win32_computersystem | select Manufacturer).Manufacturer $UEFIKey = wmic path softwarelicensingservice get OA3xOriginalProductKey $KeepassURL = "https://passwordserver.mydomain.local:10001" $PWFolder = "Local Admin Accounts" $PWName = "Individual Admin Account on $ComputerName" $PWUser = $localAdminUser $PWPassword = $PW $PWDescription = "Hardware information: ===================== $Manufacturer $Model $SN UEFI BIOS Windows Key: ====================== $UEFIKey Known MAC Addresses: ==================== $MACs (MACs might be subject to change with Docking-Stations)" $tokenParams = @{ grant_type='password'; username=$KeePassCredentials.UserName; password=$KeePassCredentials.GetNetworkCredential().password;} $JSON = Invoke-WebRequest -Uri "$KeepassURL/OAuth2/Token" -Method POST -Body $tokenParams -ContentType "application/x-www-form-urlencoded" $Token = (ConvertFrom-Json $JSON.Content).access_token $headers = @{ "Accept" = "application/json" "Authorization" = "$Token"} $GroupSearch = @{“search” = $PWFolder} $Group = Invoke-RestMethod -Method post -Uri "$KeepassURL/api/v4/rest/search"-body (ConvertTo-Json $GroupSearch) -Headers $headers -ContentType 'application/json' $GroupID = $Group.Groups.Id $SearchPhrase = $PWUser $searchbody = @{“search” = $SearchPhrase} $Search = Invoke-RestMethod -Method post -Uri "$KeepassURL/api/v4/rest/search"-body (ConvertTo-Json $searchbody) -Headers $headers -ContentType 'application/json' $SearchID = $Search.Credentials.Id $KeePassResultscnt = 0 ForEach($Result in $Search.credentials) { $CredentialID = $Result.id If ($Result.GroupId -eq $GroupID) { $KeePassResultscnt += 1 } } If ($KeePassResultscnt -ge 1) { Write-Host "Total number of results found: $cnt" Write-Host "" Write-Host "" Write-Host "ALERT - KeePass already has credentials for this system - can not proceed." Write-Host "==========================================================================" Write-Host "" Write-Host "Search-Phrase: $SearchPhrase" Write-Host "Search-Folder: $PWFolder" Write-Host "Amount of results found: $KeePassResultscnt" Write-Host "" Write-Host "" Write-Host "" Write-Host "Next Steps:" Write-Host "===========" Write-Host "- This script will exit now!" Write-Host "- No changes to the system of KeePass have been applied" Write-Host "- Go to KeePass and check if this is an old entry and can be renamed/moved" Write-Host "- Execute the script again" Write-Host "" Write-Host "" pause EXIT } Else { Write-Host "" Write-Host "" Write-Host "Writing new credentials to the KeePass server/database" Write-Host "" Write-Host "" $PWEntryDT = Get-Date -Format "O" $CredEntry = @{ Id = "00000000-0000-0000-0000-000000000000" Name = "$PWName" Username = "$PWUser" Password = "$PWPassword" Created = "$PWEntryDT" Modified = "$PWEntryDT" GroupId = "$GroupID" Notes = "$PWDescription" Url = "$ComputerName" } Invoke-RestMethod -Method post -Uri "$KeepassURL/api/v4/rest/credential/00000000-0000-0000-0000-000000000000"-body @(ConvertTo-Json $CredEntry) -Headers $headers -ContentType 'application/json' Write-Host "" Write-Host "" Write-Host "Finished writing to KeePass server/database" Write-Host "" Write-Host "" } pause Clear-Host Write-Host "" Write-Host "" Write-Host "Please check the following information:" Write-Host "=======================================" Write-Host "" Write-Host "Computer Name: $ComputerName" Write-Host "local Admin: $localAdminUser" Write-Host "Admin-PW: $PW" Write-Host "" Write-Host "" Write-Host "Make sure the information above are correct!" Write-Host "" Write-Host "" Write-Host "Verify the local Admin and Admin-PW in KeePass!" Write-Host "" Write-Host "" pause Write-Host "" Write-Host "" Write-Host "All information are verified?" Write-Host "" Write-Host "" pause Clear-Host Write-Host "Checking and removing (if exists) old local user: $localAdminUser" Get-LocalUser -Name $localAdminUser | Remove-LocalUser Clear-Host Write-Host "Adding new local administrator account: $localAdminUser" New-LocalUser -Name $localAdminUser -Password $PWencrypted -AccountNeverExpires -PasswordNeverExpires -Description "System specific local administrator account" $NewUser = Get-LocalUser -Name $localAdminUser $AdminGroup = Get-LocalGroup -Name "Administrators" Add-LocalGroupMember -Name $AdminGroup -Member $NewUser Write-Host "" Write-Host "" Write-Host "Finished." Write-Host "" Write-Host "" pause Write-Host "" Write-Host "" Write-Host "Joining this system to the domain and reboot automatically - stay put" Write-Host "" Write-Host "" Write-Host "Note: The system will not reboot if an error occurs. Check the error message in case you see a error." Write-Host "" Write-Host "" Add-Computer -Domain "mydomain.local" -Credential $DomainJoinCredentials -NewName $ComputerName -Restart -Force -OUPath "OU=Computers,OU=San Diego,OU=USA,DC=mydomain,DC=local" Write-Host "" Write-Host "" Write-Host "Computer exists already:" Write-Host " Check Domain - remove Computer if needed" Write-Host " Reboot this system" Write-Host " Rename this system manually after the reboot" Write-Host "" Write-Host "Credentials for Domain are wrong:" Write-Host " Start the script over" Write-Host "" Write-Host "" pause |
Explaining, adjusting and guiding your through the PowerShell script
It is important that you understand the script so you can make adjustments to it. I will try to explain everything that is important and reference some line-numbers while doing so.
- Lines 1-30 are just a general introduction and show some generic information
- Lines 31-76 hold some functions to generate a password, to bypass some certificate issues etc
- Lines 35-38 are worth taking a look at, here are all the characters of the four categories that will be used to generate a password. Excluded are already usually hard to read characters in some fonts and other characters that might cause issues – of course, adjust especially line 38 to your preferences and add more symbols or remove what you don’t want to use
- Lines 77-89 are just informational
- Lines 90-96 expect some user-input
- new computername
- get credentials for the domain join (admin)
- the script will not validate the credentials, in theory this could be done but I never found it that important
- get credentials to read/write on the password database server (often not the actual admin-credentials, therefor I separated those two)
- the script will not validate the credentials, in theory this could be done but I never found it that important
- the local admin username that will be created
- $localAdminUser = $(“$ComputerName” + “_Admin”)
- the above line will create a hostname_admin account – you can adjust this to your preferences
- 94-95 will generate a password and encrypt it so it can be used to create the local account
- Lines 97-103 are just informational
- Lines 104-216 – this is actually the whole password server communication and entry check and generation
- 104-115 those lines gather various information from your current system like serial number, UEFI Windows keys, etc. – you can keep em as is
- 116 – please enter the URL to your password server here
- 117 – here your need to enter the folder where the generated credentials are going to be put in on your password server
- 118 – this is the subject of the entry that will be generated – adjust this to your preferences
- 119-120 – those are username/password for the entry – you should leave this as is
- 121-134 – those lines are the details in your password server entry – adjust them to your likes
- 135-165 – this actually will execute the following on the REST API on your password server
- connect to it
- check if a entry with the same username already exists
- 166-189 – this will raise an alert that this user already exists on your password server – 189 will actually exit the whole script
- 190-216 – this block will write to the password server – cause it did not find an entry with the new username
- Lines 217-241 this shows the new created username and password – it actually suggests you compare the entries on your password server to the information shown to make sure everything is correct
- Lines 242-251 will create the new local admin account on the system and set the password
- Lines 252-267 are informational
- Line 268 will execute the actual domain join
- please adjust the -Domain and the -OUPath parameter to your specific needs
- note that the command will automatically restart the system
- Lines 269-282 Those lines are informational – actually – if anything would go wrong those lines would be shown and help to take further steps after the failed domain join – in most cases those suggestions will help – in the end the error output shown by the command for the domain join (line 268) would indicate what went wrong. The restart of the system actually would bypass this message in the end (more or less)
If you have any questions, feel free to reach out to me. The script could be cleaned up more – but I wanted to provide a working version of it – so I just did a quick clean up or some special stuff and posted it here. Personally I like things a bit more structured, but as said – this is just a general example.
Please note – this script was updated – you find the updated post here.
This script is also mentioned on the API Examples page on the Pleasant Solutions web site here.