2026-05-09 17:54:38 +00:00
|
|
|
# setup-kms.ps1
|
|
|
|
|
#
|
2026-06-01 10:12:03 +00:00
|
|
|
# Minimal KMS wiring for Volume License Windows. Pins the KMS host and activates
|
|
|
|
|
# only if not already licensed. If no VL key is installed it auto-detects the
|
|
|
|
|
# Windows edition and fetches the matching GVLK from the published key list
|
|
|
|
|
# (no manual key lookup). Idempotent - safe to re-run: an already-licensed
|
|
|
|
|
# machine reports the remaining days and exits without re-contacting KMS.
|
|
|
|
|
# Does NOT install Office. Does NOT change DNS suffix.
|
2026-05-09 17:54:38 +00:00
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# iwr -UseBasicParsing https://kms.viktorbarzin.me/scripts/setup-kms.ps1 | iex
|
|
|
|
|
#
|
|
|
|
|
# Or with a custom KMS host (e.g. self-hosted):
|
|
|
|
|
# $env:KMS_HOST = 'kms.example.com'; iwr ... | iex
|
|
|
|
|
#
|
2026-05-09 22:12:21 +00:00
|
|
|
# Source: https://kms.viktorbarzin.me/scripts/setup-kms.ps1
|
2026-05-09 17:54:38 +00:00
|
|
|
# Licence: MIT, no warranty, KMS activates Volume License SKUs only.
|
|
|
|
|
|
|
|
|
|
[CmdletBinding()]
|
|
|
|
|
param(
|
2026-06-01 08:05:31 +00:00
|
|
|
[string]$KmsHost = $(if ($env:KMS_HOST) { $env:KMS_HOST } else { 'vlmcs.viktorbarzin.me' }),
|
2026-06-01 10:12:03 +00:00
|
|
|
[int] $KmsPort = $(if ($env:KMS_PORT) { [int]$env:KMS_PORT } else { 1688 }),
|
|
|
|
|
[string]$KeysUrl = $(if ($env:KMS_KEYS_URL) { $env:KMS_KEYS_URL } else { 'https://kms.viktorbarzin.me/keys.json' })
|
2026-05-09 17:54:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
|
|
|
|
|
function Step($m) { Write-Host "==> $m" -ForegroundColor Cyan }
|
|
|
|
|
function OK($m) { Write-Host " OK: $m" -ForegroundColor Green }
|
|
|
|
|
function Bad($m) { Write-Host " !! $m" -ForegroundColor Red }
|
|
|
|
|
|
|
|
|
|
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
|
|
|
Bad "Must run as Administrator. Right-click PowerShell -> 'Run as administrator', then retry."
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 08:53:20 +00:00
|
|
|
# Locale-independent license probe (slmgr /dlv text is localized; the WMI
|
|
|
|
|
# LicenseStatus integer is not). 1 = Licensed. Returns $null if no KMS-client
|
|
|
|
|
# Windows SKU is installed. GracePeriodRemaining is in minutes.
|
|
|
|
|
function Get-WindowsLicense {
|
|
|
|
|
$q = "SELECT LicenseStatus, GracePeriodRemaining FROM SoftwareLicensingProduct WHERE Name LIKE 'Windows%' AND PartialProductKey IS NOT NULL"
|
|
|
|
|
$p = $null
|
|
|
|
|
try { $p = Get-CimInstance -Query $q -ErrorAction Stop | Select-Object -First 1 }
|
|
|
|
|
catch { try { $p = Get-WmiObject -Query $q -ErrorAction Stop | Select-Object -First 1 } catch {} }
|
|
|
|
|
if (-not $p) { return $null }
|
|
|
|
|
[pscustomobject]@{ Licensed = ($p.LicenseStatus -eq 1); DaysLeft = [int]([math]::Round($p.GracePeriodRemaining / 1440)) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 10:12:03 +00:00
|
|
|
# Auto-select the GVLK for THIS machine's edition from the published key list,
|
|
|
|
|
# so the user never has to look one up. Matches on the registry EditionID
|
|
|
|
|
# (locale-independent); for editions that share an EditionID across releases
|
|
|
|
|
# (LTSC, Server) it also narrows by the OS build number.
|
|
|
|
|
function Resolve-WindowsGvlk {
|
|
|
|
|
$cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue
|
|
|
|
|
if (-not $cv) { return $null }
|
|
|
|
|
$editionId = $cv.EditionID
|
|
|
|
|
$build = "$($cv.CurrentBuildNumber)"
|
|
|
|
|
$isServer = $false
|
|
|
|
|
try { $isServer = ((Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).ProductType -ne 1) } catch {}
|
|
|
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
|
try { $keys = (Invoke-WebRequest -UseBasicParsing -Uri $KeysUrl -TimeoutSec 20).Content | ConvertFrom-Json }
|
|
|
|
|
catch { Bad "Could not fetch the key list from $KeysUrl"; return $null }
|
|
|
|
|
$pool = if ($isServer) { $keys.windows_server } else { $keys.windows }
|
|
|
|
|
$m = $pool | Where-Object { $_.editionid -eq $editionId -and ( -not $_.builds -or ($_.builds -contains $build) ) } | Select-Object -First 1
|
|
|
|
|
if ($m) { Write-Host " detected $editionId (build $build) -> $($m.edition)"; return $m.gvlk }
|
|
|
|
|
Write-Host " no published GVLK matches EditionID '$editionId' (build $build)"
|
|
|
|
|
return $null
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 17:54:38 +00:00
|
|
|
$slmgr = "$env:WINDIR\System32\slmgr.vbs"
|
|
|
|
|
|
2026-06-01 08:53:20 +00:00
|
|
|
# Pin the KMS host. Idempotent: re-running just re-sets the same value.
|
|
|
|
|
Step "Pinning KMS host -> $KmsHost`:$KmsPort"
|
2026-05-09 17:54:38 +00:00
|
|
|
$out = & cscript //Nologo $slmgr /skms "$KmsHost`:$KmsPort" 2>&1
|
2026-06-01 08:53:20 +00:00
|
|
|
if ($LASTEXITCODE -ne 0) { Bad "slmgr /skms failed (exit $LASTEXITCODE): $out"; return }
|
2026-05-09 17:54:38 +00:00
|
|
|
OK "KMS host pinned"
|
|
|
|
|
|
2026-06-01 08:53:20 +00:00
|
|
|
$lic = Get-WindowsLicense
|
|
|
|
|
|
2026-06-01 10:12:03 +00:00
|
|
|
# Idempotent: already activated (retail OR KMS) -> don't re-hit the KMS server and
|
|
|
|
|
# never clobber a working key. The pinned host keeps Windows auto-renewing.
|
|
|
|
|
if ($lic -and $lic.Licensed) {
|
2026-05-09 17:54:38 +00:00
|
|
|
Write-Host ""
|
2026-06-01 10:12:03 +00:00
|
|
|
Write-Host "==> Already licensed ($($lic.DaysLeft) days remaining). Nothing to do." -ForegroundColor Green
|
2026-06-01 08:53:20 +00:00
|
|
|
Write-Host " Host is pinned to $KmsHost; Windows auto-renews every 7 days."
|
2026-05-09 17:54:38 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 10:12:03 +00:00
|
|
|
# No Volume License key installed -> fetch + install the GVLK for this edition.
|
|
|
|
|
if ($null -eq $lic) {
|
|
|
|
|
Step "No Volume License key installed - detecting edition and fetching its GVLK"
|
|
|
|
|
$gvlk = Resolve-WindowsGvlk
|
|
|
|
|
if (-not $gvlk) {
|
2026-06-01 14:35:56 +00:00
|
|
|
Bad "This edition has no KMS GVLK - Home/retail can't KMS-activate as-is."
|
|
|
|
|
Write-Host " To UPGRADE it in place to a Volume License edition (e.g. Home -> Pro) and"
|
|
|
|
|
Write-Host " activate, run the interactive bootstrap (it shows the consequences first):"
|
|
|
|
|
Write-Host " iwr -UseBasicParsing https://kms.viktorbarzin.me/scripts/kms-bootstrap.ps1 | iex"
|
2026-06-01 10:12:03 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
Step "Installing GVLK $gvlk"
|
|
|
|
|
$out = & cscript //Nologo $slmgr /ipk $gvlk 2>&1
|
|
|
|
|
if ($LASTEXITCODE -ne 0) { Bad "slmgr /ipk failed (exit $LASTEXITCODE): $out"; return }
|
|
|
|
|
OK "GVLK installed"
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 08:53:20 +00:00
|
|
|
Step "Activating (slmgr /ato)"
|
|
|
|
|
$out = & cscript //Nologo $slmgr /ato 2>&1
|
2026-05-09 17:54:38 +00:00
|
|
|
Write-Host $out
|
|
|
|
|
|
2026-06-01 08:53:20 +00:00
|
|
|
$lic = Get-WindowsLicense
|
|
|
|
|
if ($lic.Licensed) {
|
2026-05-09 17:54:38 +00:00
|
|
|
Write-Host ""
|
2026-06-01 08:53:20 +00:00
|
|
|
Write-Host "==> SUCCESS: Windows is now licensed via KMS ($($lic.DaysLeft) days)." -ForegroundColor Green
|
|
|
|
|
Write-Host " Renews automatically every 7 days; lasts 180 days per renewal."
|
2026-05-09 17:54:38 +00:00
|
|
|
} else {
|
|
|
|
|
Write-Host ""
|
2026-06-01 08:53:20 +00:00
|
|
|
Write-Host "==> Activation did not stick." -ForegroundColor Yellow
|
|
|
|
|
Write-Host " Most common cause: this Windows is not a Volume License edition"
|
|
|
|
|
Write-Host " (Home / retail / OEM reject KMS). See https://kms.viktorbarzin.me/#faq"
|
2026-05-09 17:54:38 +00:00
|
|
|
}
|