From 97bd9dfc7647ede9c5fed5dd510e76fdfceac0ed Mon Sep 17 00:00:00 2001 From: X9 Date: Sat, 14 Mar 2026 19:15:30 +0100 Subject: [PATCH] Add admin account creation and Windows activation steps - 00-admin-account.ps1: create/update adminx9, add to Administrators, hide from login screen via SpecialAccounts\UserList - 08-activation.ps1: activate via config key or GVLK fallback matched by OS edition; supports optional KMS server; skips if already active - config.json: add adminAccount block (password), activation block (productKey placeholder, kmsServer) - Deploy-Windows.ps1: add Step 0a and Step 0b before bloatware removal - Test-Deployment.ps1: add checks for admin account and activation - SPEC.md: document new steps, close open question #4 Co-Authored-By: Claude Sonnet 4.6 --- Deploy-Windows.ps1 | 14 +++++ SPEC.md | 22 ++++++- config/config.json | 12 +++- scripts/00-admin-account.ps1 | 94 ++++++++++++++++++++++++++++++ scripts/08-activation.ps1 | 109 +++++++++++++++++++++++++++++++++++ tests/Test-Deployment.ps1 | 32 ++++++++++ 6 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 scripts/00-admin-account.ps1 create mode 100644 scripts/08-activation.ps1 diff --git a/Deploy-Windows.ps1 b/Deploy-Windows.ps1 index 512845a..f77f65e 100644 --- a/Deploy-Windows.ps1 +++ b/Deploy-Windows.ps1 @@ -96,6 +96,20 @@ Invoke-Step -Name "Load config.json" -Action { Write-Log "Config loaded from $ConfigFile" -Level INFO } +# ----------------------------------------------------------------------- +# Step 0a - Admin account +# ----------------------------------------------------------------------- +Invoke-Step -Name "Step 0a - Admin account" -Action { + & "$ScriptRoot\scripts\00-admin-account.ps1" -Config $Config -LogFile $LogFile +} + +# ----------------------------------------------------------------------- +# Step 0b - Windows activation +# ----------------------------------------------------------------------- +Invoke-Step -Name "Step 0b - Windows activation" -Action { + & "$ScriptRoot\scripts\08-activation.ps1" -Config $Config -LogFile $LogFile +} + # ----------------------------------------------------------------------- # Step 1 - Bloatware removal # ----------------------------------------------------------------------- diff --git a/SPEC.md b/SPEC.md index 092a598..1c94f37 100644 --- a/SPEC.md +++ b/SPEC.md @@ -37,6 +37,26 @@ Script is divided into steps. Each step logs its result. Steps can be skipped wi --- +## STEP 0a - Admin account + +Creates local admin account `adminx9`: +- Password from `config.json` (`adminAccount.password`) +- Added to Administrators group +- Password never expires, user cannot change password +- Hidden from Windows login screen (SpecialAccounts\UserList = 0) + +--- + +## STEP 0b - Windows activation + +Activates Windows using product key from config: +- Key from `config.json` (`activation.productKey`) - set to real MAK/retail key for production +- Falls back to GVLK (KMS client key) matched by detected OS edition +- Optional KMS server via `activation.kmsServer` +- If already activated, skips silently + +--- + ## STEP 1 - Bloatware removal ### 1a - AppX packages (UWP apps) @@ -270,4 +290,4 @@ Custom PowerShell scheduled task. No external dependencies. | 1 | BackInfo replacement | DONE - custom PS scheduled task DesktopInfo | | 2 | Complete SW list for winget | TODO | | 3 | Per-client variability via config.json | FUTURE | -| 4 | Admin account adminx9 - script or manual? | OPEN | +| 4 | Admin account adminx9 - script or manual? | DONE - script (00-admin-account.ps1) | diff --git a/config/config.json b/config/config.json index b46c1d1..39b600b 100644 --- a/config/config.json +++ b/config/config.json @@ -1,8 +1,16 @@ { "deployment": { "timezone": "Central Europe Standard Time", - "locale": "cs-CZ", - "adminAccount": "adminx9" + "locale": "cs-CZ" + }, + "adminAccount": { + "username": "adminx9", + "password": "AdminX9.AdminX9", + "description": "X9 MSP admin account" + }, + "activation": { + "productKey": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", + "kmsServer": "" }, "software": { "install": [ diff --git a/scripts/00-admin-account.ps1 b/scripts/00-admin-account.ps1 new file mode 100644 index 0000000..bbd3390 --- /dev/null +++ b/scripts/00-admin-account.ps1 @@ -0,0 +1,94 @@ +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +# ----------------------------------------------------------------------- +# Read account config +# ----------------------------------------------------------------------- +$accountName = "adminx9" +$accountPass = "AdminX9.AdminX9" +$accountDesc = "X9 MSP admin account" + +if ($Config -and $Config.adminAccount) { + if ($Config.adminAccount.username) { $accountName = $Config.adminAccount.username } + if ($Config.adminAccount.password) { $accountPass = $Config.adminAccount.password } + if ($Config.adminAccount.description) { $accountDesc = $Config.adminAccount.description } +} + +Write-Log "Creating admin account: $accountName" -Level INFO + +$securePass = ConvertTo-SecureString $accountPass -AsPlainText -Force + +# ----------------------------------------------------------------------- +# Create or update account +# ----------------------------------------------------------------------- +$existing = Get-LocalUser -Name $accountName -ErrorAction SilentlyContinue + +if ($existing) { + Write-Log " Account already exists - updating password" -Level INFO + try { + Set-LocalUser -Name $accountName -Password $securePass -PasswordNeverExpires $true + Enable-LocalUser -Name $accountName + Write-Log " Account updated: $accountName" -Level OK + } + catch { + Write-Log " Failed to update account: $_" -Level ERROR + } +} else { + try { + New-LocalUser -Name $accountName ` + -Password $securePass ` + -Description $accountDesc ` + -PasswordNeverExpires ` + -UserMayNotChangePassword ` + -ErrorAction Stop | Out-Null + Write-Log " Account created: $accountName" -Level OK + } + catch { + Write-Log " Failed to create account: $_" -Level ERROR + } +} + +# ----------------------------------------------------------------------- +# Add to Administrators group +# ----------------------------------------------------------------------- +try { + $adminsGroup = (Get-LocalGroup | Where-Object { $_.SID -eq "S-1-5-32-544" }).Name + $members = Get-LocalGroupMember -Group $adminsGroup -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "*$accountName" } + if (-not $members) { + Add-LocalGroupMember -Group $adminsGroup -Member $accountName -ErrorAction Stop + Write-Log " Added to $adminsGroup" -Level OK + } else { + Write-Log " Already in $adminsGroup" -Level INFO + } +} +catch { + Write-Log " Failed to add to Administrators: $_" -Level ERROR +} + +# ----------------------------------------------------------------------- +# Hide account from login screen +# ----------------------------------------------------------------------- +try { + $specialPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" + if (-not (Test-Path $specialPath)) { + New-Item -Path $specialPath -Force | Out-Null + } + Set-ItemProperty -Path $specialPath -Name $accountName -Value 0 -Type DWord -Force + Write-Log " Account hidden from login screen" -Level OK +} +catch { + Write-Log " Failed to hide account from login screen: $_" -Level ERROR +} + +Write-Log "Step 0a - Admin account complete" -Level OK diff --git a/scripts/08-activation.ps1 b/scripts/08-activation.ps1 new file mode 100644 index 0000000..c3cbef3 --- /dev/null +++ b/scripts/08-activation.ps1 @@ -0,0 +1,109 @@ +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +# ----------------------------------------------------------------------- +# KMS Generic Volume License Keys (GVLK) +# Source: https://docs.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys +# These are official Microsoft-published keys for use with KMS infrastructure. +# Replace with your MAK/retail key for standalone activation. +# ----------------------------------------------------------------------- +$KmsKeys = @{ + # Windows 11 + "Windows 11 Pro" = "W269N-WFGWX-YVC9B-4J6C9-T83GX" + "Windows 11 Pro N" = "MH37W-N47XK-V7XM9-C7227-GCQG9" + "Windows 11 Pro Education" = "6TP4R-GNPTD-KYYHQ-7B7DP-J447Y" + "Windows 11 Education" = "NW6C2-QMPVW-D7KKK-3GKT6-VCFB2" + "Windows 11 Enterprise" = "NPPR9-FWDCX-D2C8J-H872K-2YT43" + # Windows 10 + "Windows 10 Pro" = "W269N-WFGWX-YVC9B-4J6C9-T83GX" + "Windows 10 Pro N" = "MH37W-N47XK-V7XM9-C7227-GCQG9" + "Windows 10 Education" = "NW6C2-QMPVW-D7KKK-3GKT6-VCFB2" + "Windows 10 Enterprise" = "NPPR9-FWDCX-D2C8J-H872K-2YT43" + "Windows 10 Home" = "TX9XD-98N7V-6WMQ6-BX7FG-H8Q99" +} + +# ----------------------------------------------------------------------- +# Check current activation status +# ----------------------------------------------------------------------- +Write-Log "Checking Windows activation status" -Level INFO + +$licenseStatus = (Get-CimInstance SoftwareLicensingProduct -Filter "PartialProductKey IS NOT NULL AND Name LIKE 'Windows%'" -ErrorAction SilentlyContinue | + Select-Object -First 1).LicenseStatus +# LicenseStatus: 0=Unlicensed, 1=Licensed, 2=OOBGrace, 3=OOTGrace, 4=NonGenuineGrace, 5=Notification, 6=ExtendedGrace + +if ($licenseStatus -eq 1) { + Write-Log " Windows is already activated - skipping" -Level OK +} else { + Write-Log " Activation status: $licenseStatus (not activated)" -Level WARN + + # Detect Windows edition + $osCaption = (Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue).Caption + Write-Log " Detected OS: $osCaption" -Level INFO + + # Check if a key is configured in config + $customKey = $null + if ($Config -and $Config.activation -and $Config.activation.productKey) { + $customKey = $Config.activation.productKey + } + + if ($customKey -and $customKey -ne "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX") { + # Use key from config + $keyToUse = $customKey + Write-Log " Using product key from config" -Level INFO + } else { + # Find matching GVLK key by OS name + $keyToUse = $null + foreach ($entry in $KmsKeys.GetEnumerator()) { + if ($osCaption -like "*$($entry.Key)*") { + $keyToUse = $entry.Value + Write-Log " Matched GVLK key for: $($entry.Key)" -Level INFO + break + } + } + } + + if (-not $keyToUse) { + Write-Log " No matching key found for: $osCaption" -Level WARN + Write-Log " Skipping activation - set activation.productKey in config.json" -Level WARN + } else { + # Install key + Write-Log " Installing product key..." -Level INFO + $ipkResult = & cscript //nologo "$env:SystemRoot\System32\slmgr.vbs" /ipk $keyToUse 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Log " Key installed" -Level OK + } else { + Write-Log " Key install result: $ipkResult" -Level WARN + } + + # Set KMS server if configured + if ($Config -and $Config.activation -and $Config.activation.kmsServer) { + $kmsServer = $Config.activation.kmsServer + Write-Log " Setting KMS server: $kmsServer" -Level INFO + & cscript //nologo "$env:SystemRoot\System32\slmgr.vbs" /skms $kmsServer 2>&1 | Out-Null + } + + # Attempt activation + Write-Log " Attempting activation..." -Level INFO + $atoResult = & cscript //nologo "$env:SystemRoot\System32\slmgr.vbs" /ato 2>&1 + $atoOutput = $atoResult -join " " + + if ($atoOutput -match "successfully" -or $atoOutput -match "uspesn") { + Write-Log " Activation successful" -Level OK + } else { + Write-Log " Activation result: $atoOutput" -Level WARN + Write-Log " Activation may require a KMS server or valid MAK key" -Level WARN + } + } +} + +Write-Log "Step 8 - Activation complete" -Level OK diff --git a/tests/Test-Deployment.ps1 b/tests/Test-Deployment.ps1 index 7101c4c..4189f02 100644 --- a/tests/Test-Deployment.ps1 +++ b/tests/Test-Deployment.ps1 @@ -61,6 +61,38 @@ Test-Check "Deploy.log exists" { Test-Path "C:\Windows\Setup\Scripts\Deploy.log" } +# ----------------------------------------------------------------------- +# Admin account +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- Admin account ---" +Test-Check "Account adminx9 exists" { + Get-LocalUser -Name "adminx9" -ErrorAction SilentlyContinue +} +Test-Check "Account adminx9 is enabled" { + (Get-LocalUser -Name "adminx9" -ErrorAction SilentlyContinue).Enabled -eq $true +} +Test-Check "Account adminx9 in Administrators" { + $adminsGroup = (Get-LocalGroup | Where-Object { $_.SID -eq "S-1-5-32-544" }).Name + Get-LocalGroupMember -Group $adminsGroup -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "*adminx9" } +} +Test-Check "Account adminx9 hidden from login screen" { + $specialPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" + (Get-ItemProperty -Path $specialPath -Name "adminx9" -ErrorAction SilentlyContinue).adminx9 -eq 0 +} + +# ----------------------------------------------------------------------- +# Activation +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- Activation ---" +Test-Check "Windows activated" { + $status = (Get-CimInstance SoftwareLicensingProduct -Filter "PartialProductKey IS NOT NULL AND Name LIKE 'Windows%'" -ErrorAction SilentlyContinue | + Select-Object -First 1).LicenseStatus + $status -eq 1 +} -WarnOnly + # ----------------------------------------------------------------------- # Software # -----------------------------------------------------------------------