kms-website: auto-fetch + auto-install GVLKs (no manual key lookup)

Scripts now detect the running edition and fetch the matching GVLK from a
published key list instead of requiring the user to copy one from the table.

- data/products.yaml: add editionid to every Windows/Server entry, plus build
  numbers where an EditionID spans releases (LTSC, Server). Azure Edition left
  unmapped on purpose (collides with Datacenter; KMS may fail there anyway).
- /keys.json: Hugo KEYS output format renders products.yaml as JSON
  (single source of truth). layouts/index.keys.json.
- setup-kms.ps1: when no VL key is installed, read registry EditionID
  (+build/ProductType for server) -> fetch /keys.json -> slmgr /ipk the match
  -> activate. Only acts when not already licensed (never clobbers retail).
- kms-bootstrap.ps1: same for Windows; for Office/Project/Visio, read
  Click-to-Run ProductReleaseIds -> ospp /inpkey the matching GVLK -> /act.
- $env:KMS_KEYS_URL overrides the key-list URL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-01 10:12:03 +00:00
parent c27077549c
commit 9059dbc85b
5 changed files with 170 additions and 21 deletions

View file

@ -2,93 +2,123 @@ windows:
# Windows 11 / Windows 10 — desktop SKUs (modern)
- family: "Windows 11 / 10"
edition: "Pro"
editionid: "Professional"
gvlk: "W269N-WFGWX-YVC9B-4J6C9-T83GX"
notes: "Same GVLK for Win11 and Win10 Pro."
current: true
- family: "Windows 11 / 10"
edition: "Pro N"
editionid: "ProfessionalN"
gvlk: "MH37W-N47XK-V7XM9-C7227-GCQG9"
notes: "EU 'N' edition (no media pack)."
current: true
- family: "Windows 11 / 10"
edition: "Pro for Workstations"
editionid: "ProfessionalWorkstation"
gvlk: "NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J"
current: true
- family: "Windows 11 / 10"
edition: "Pro for Workstations N"
editionid: "ProfessionalWorkstationN"
gvlk: "9FNHH-K3HBT-3W4TD-6383H-6XYWF"
current: true
- family: "Windows 11 / 10"
edition: "Pro Education"
editionid: "ProfessionalEducation"
gvlk: "6TP4R-GNPTD-KYYHQ-7B7DP-J447Y"
current: true
- family: "Windows 11 / 10"
edition: "Pro Education N"
editionid: "ProfessionalEducationN"
gvlk: "YVWGF-BXNMC-HTQYQ-CPQ99-66QFC"
current: true
- family: "Windows 11 / 10"
edition: "Education"
editionid: "Education"
gvlk: "NW6C2-QMPVW-D7KKK-3GKT6-VCFB2"
current: true
- family: "Windows 11 / 10"
edition: "Education N"
editionid: "EducationN"
gvlk: "2WH4N-8QGBV-H22JP-CT43Q-MDWWJ"
current: true
- family: "Windows 11 / 10"
edition: "Enterprise"
editionid: "Enterprise"
gvlk: "NPPR9-FWDCX-D2C8J-H872K-2YT43"
current: true
- family: "Windows 11 / 10"
edition: "Enterprise N"
editionid: "EnterpriseN"
gvlk: "DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4"
current: true
- family: "Windows 11 / 10"
edition: "Enterprise G"
editionid: "EnterpriseG"
gvlk: "YYVX9-NTFWV-6MDM3-9PT4T-4M68B"
notes: "German government SKU."
current: true
- family: "Windows 11 / 10"
edition: "Enterprise G N"
editionid: "EnterpriseGN"
gvlk: "44RPN-FTY23-9VTTB-MP9BX-T84FV"
current: true
# LTSC editions
- family: "Windows LTSC"
edition: "Enterprise LTSC 2024 / 2021 / 2019"
editionid: "EnterpriseS"
builds: ["26100", "19044", "17763"]
gvlk: "M7XTQ-FN8P6-TTKYV-9D4CC-J462D"
notes: "Single GVLK across Win11 LTSC 2024 and Win10 LTSC 2021/2019."
current: true
- family: "Windows LTSC"
edition: "Enterprise N LTSC 2024 / 2021 / 2019"
editionid: "EnterpriseSN"
builds: ["26100", "19044", "17763"]
gvlk: "92NFX-8DJQP-P6BBQ-THF9C-7CG2H"
current: true
- family: "Windows LTSC"
edition: "IoT Enterprise LTSC 2024 / 2021"
editionid: "IoTEnterpriseS"
builds: ["26100", "19044"]
gvlk: "KBN8V-HFGQ4-MGXVD-347P6-PDQGT"
notes: "Same GVLK for x64 and ARM64 IoT."
current: true
- family: "Windows LTSC"
edition: "Enterprise LTSB 2016"
editionid: "EnterpriseS"
builds: ["14393"]
gvlk: "DCPHK-NFMTC-H88MJ-PFHPY-QJ4BJ"
notes: "Out of mainstream support; security updates ended Oct 2026."
- family: "Windows LTSC"
edition: "Enterprise N LTSB 2016"
editionid: "EnterpriseSN"
builds: ["14393"]
gvlk: "QFFDN-GRT3P-VKWWX-X7T3R-8B639"
- family: "Windows LTSC"
edition: "Enterprise LTSB 2015"
editionid: "EnterpriseS"
builds: ["10240"]
gvlk: "WNMTR-4C88C-JK8YV-HQ7T2-76DF9"
notes: "End of support reached."
- family: "Windows LTSC"
edition: "Enterprise N LTSB 2015"
editionid: "EnterpriseSN"
builds: ["10240"]
gvlk: "2F77B-TNFGY-69QQF-B8YKP-D69TJ"
windows_server:
- family: "Windows Server 2025"
edition: "Standard"
editionid: "ServerStandard"
builds: ["26100"]
gvlk: "TVRH6-WHNXV-R9WG3-9XRFY-MY832"
current: true
- family: "Windows Server 2025"
edition: "Datacenter"
editionid: "ServerDatacenter"
builds: ["26100"]
gvlk: "D764K-2NDRG-47T6Q-P8T8W-YP6DF"
current: true
- family: "Windows Server 2025"
@ -98,10 +128,14 @@ windows_server:
current: true
- family: "Windows Server 2022"
edition: "Standard"
editionid: "ServerStandard"
builds: ["20348"]
gvlk: "VDYBN-27WPP-V4HQT-9VMD4-VMK7H"
current: true
- family: "Windows Server 2022"
edition: "Datacenter"
editionid: "ServerDatacenter"
builds: ["20348"]
gvlk: "WX4NM-KYWYW-QJJR4-XV3QB-6VM33"
current: true
- family: "Windows Server 2022"
@ -111,25 +145,37 @@ windows_server:
current: true
- family: "Windows Server 2019"
edition: "Standard"
editionid: "ServerStandard"
builds: ["17763"]
gvlk: "N69G4-B89J2-4G8F4-WWYCC-J464C"
current: true
- family: "Windows Server 2019"
edition: "Datacenter"
editionid: "ServerDatacenter"
builds: ["17763"]
gvlk: "WMDGN-G9PQG-XVVXX-R3X43-63DFG"
current: true
- family: "Windows Server 2019"
edition: "Essentials"
editionid: "ServerSolution"
builds: ["17763"]
gvlk: "WVDHN-86M7X-466P6-VHXV7-YY726"
current: true
- family: "Windows Server 2016"
edition: "Standard"
editionid: "ServerStandard"
builds: ["14393"]
gvlk: "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY"
notes: "EOL Jan 2027."
- family: "Windows Server 2016"
edition: "Datacenter"
editionid: "ServerDatacenter"
builds: ["14393"]
gvlk: "CB7KF-BWN84-R7R2Y-793K2-8XDDG"
- family: "Windows Server 2016"
edition: "Essentials"
editionid: "ServerSolution"
builds: ["14393"]
gvlk: "JCKRF-N37P4-C2D82-9YXRT-4M63B"
office:
# Office LTSC 2024 — current

View file

@ -14,6 +14,18 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
kmsPort = 1688
bootstrapURL = "/scripts/kms-bootstrap.ps1"
setupURL = "/scripts/setup-kms.ps1"
keysURL = "/keys.json"
# Publish data/products.yaml as machine-readable JSON at /keys.json so the
# activation scripts can fetch the GVLKs (single source of truth, no hardcoding).
# /keys.json is carved out of Anubis alongside /scripts (see stacks/kms).
[outputs]
home = ["HTML", "KEYS"]
[outputFormats.KEYS]
mediaType = "application/json"
baseName = "keys"
isPlainText = true
[markup]
[markup.goldmark.renderer]

2
layouts/index.keys.json Normal file
View file

@ -0,0 +1,2 @@
{{- /* Machine-readable GVLK list for the activation scripts. Served at /keys.json. */ -}}
{{- dict "windows" .Site.Data.products.windows "windows_server" .Site.Data.products.windows_server "office" .Site.Data.products.office | jsonify (dict "indent" " ") -}}

View file

@ -3,6 +3,8 @@
# Interactive KMS activator. Asks what you want to activate (Windows /
# already-installed Office / Project / Visio) and runs only what you confirm.
# Points each product at the public KMS host (default: vlmcs.viktorbarzin.me:1688).
# When a Volume License key is missing it auto-detects the edition/product and
# fetches the matching GVLK from the published key list (no manual key lookup).
#
# Usage:
# iwr -UseBasicParsing https://kms.viktorbarzin.me/scripts/kms-bootstrap.ps1 | iex
@ -20,7 +22,8 @@
[CmdletBinding()]
param(
[string]$KmsHost = $(if ($env:KMS_HOST) { $env:KMS_HOST } else { 'vlmcs.viktorbarzin.me' }),
[int] $KmsPort = $(if ($env:KMS_PORT) { [int]$env:KMS_PORT } else { 1688 })
[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' })
)
$ErrorActionPreference = 'Stop'
@ -83,17 +86,50 @@ function Get-WindowsLicense {
[pscustomobject]@{ Licensed = ($p.LicenseStatus -eq 1); DaysLeft = [int]([math]::Round($p.GracePeriodRemaining / 1440)) }
}
# Fetch the published GVLK list once (single source of truth, no hardcoding).
$script:KeysCache = $null
function Get-Keys {
if ($script:KeysCache) { return $script:KeysCache }
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
try { $script:KeysCache = (Invoke-WebRequest -UseBasicParsing -Uri $KeysUrl -TimeoutSec 20).Content | ConvertFrom-Json }
catch { Warn "Could not fetch the key list from $KeysUrl"; $script:KeysCache = $null }
return $script:KeysCache
}
# Auto-select the GVLK for THIS machine's edition (registry EditionID, locale-
# independent; narrowed by build for LTSC/Server which share an EditionID).
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 {}
$keys = Get-Keys; if (-not $keys) { 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
}
function Activate-Windows {
Step "Windows activation"
$slmgr = "$env:WINDIR\System32\slmgr.vbs"
& cscript //Nologo $slmgr /skms "$KmsHost`:$KmsPort" | Out-Host
if ($LASTEXITCODE -ne 0) { Bad "slmgr /skms failed"; return }
$lic = Get-WindowsLicense
if ($null -eq $lic) { Bad "No Volume License Windows SKU - install a GVLK first (slmgr /ipk <GVLK>)."; return }
if ($lic.Licensed) { OK "Windows already licensed ($($lic.DaysLeft) days) - host pinned, skipping /ato"; return }
if ($lic -and $lic.Licensed) { OK "Windows already licensed ($($lic.DaysLeft) days) - host pinned, skipping /ato"; return }
if ($null -eq $lic) {
Step "No Volume License key - fetching the GVLK for this edition"
$gvlk = Resolve-WindowsGvlk
if (-not $gvlk) { Bad "Couldn't auto-select a GVLK. Pick one from https://kms.viktorbarzin.me/#windows and run: slmgr /ipk <GVLK>"; return }
Write-Host " installing GVLK $gvlk"
& cscript //Nologo $slmgr /ipk $gvlk | Out-Host
if ($LASTEXITCODE -ne 0) { Bad "slmgr /ipk failed"; return }
}
& cscript //Nologo $slmgr /ato | Out-Host
$lic = Get-WindowsLicense
if ($lic.Licensed) { OK "Windows licensed ($($lic.DaysLeft) days)" }
if ($lic -and $lic.Licensed) { OK "Windows licensed ($($lic.DaysLeft) days)" }
else { Bad "Windows not licensed - likely not a VL edition (Home/retail/OEM reject KMS). See https://kms.viktorbarzin.me/#faq" }
}
if ($doWin) { Activate-Windows }
@ -113,6 +149,16 @@ function Find-Ospp {
return $null
}
# Installed Click-to-Run Volume products, e.g. ProPlus2024Volume, VisioPro2024Volume.
# These IDs match the `product` field in the published key list exactly.
function Get-OfficeReleaseIds {
$c = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -ErrorAction SilentlyContinue
if ($c -and $c.ProductReleaseIds) {
return $c.ProductReleaseIds.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ -match 'Volume$' }
}
return @()
}
function Activate-Ospp([string]$label) {
$ospp = Find-Ospp
if (-not $ospp) {
@ -126,9 +172,19 @@ function Activate-Ospp([string]$label) {
# in ospp output is a fixed literal, not localized).
$st = & cscript //Nologo $ospp /dstatus 2>&1 | Out-String
if ($st -match '---LICENSED---') { OK "$label already licensed - host set, skipping /act"; return }
# Not licensed: install the matching GVLK for each installed VL product of
# this family (Office = anything that isn't Project/Visio), fetched from the list.
$rels = Get-OfficeReleaseIds | Where-Object {
switch ($label) { 'Project' { $_ -match 'Project' } 'Visio' { $_ -match 'Visio' } default { $_ -notmatch 'Project|Visio' } }
}
$keys = Get-Keys
foreach ($rel in $rels) {
$k = if ($keys) { ($keys.office | Where-Object { $_.product -eq $rel } | Select-Object -First 1).gvlk } else { $null }
if ($k) { Write-Host " $rel -> installing GVLK $k"; & cscript //Nologo $ospp /inpkey:$k | Out-Host }
}
& cscript //Nologo $ospp /act | Out-Host
$st = & cscript //Nologo $ospp /dstatus 2>&1 | Out-String
if ($st -match '---LICENSED---') { OK "$label licensed" } else { Warn "$label status not LICENSED yet (no VL Office SKU? See https://kms.viktorbarzin.me/#office)" }
if ($st -match '---LICENSED---') { OK "$label licensed" } else { Warn "$label status not LICENSED yet (no VL $label SKU installed? See https://kms.viktorbarzin.me/#office)" }
}
if ($doOfficeAct) { Activate-Ospp 'Office' }
if ($doProjAct) { Activate-Ospp 'Project' }

View file

@ -1,10 +1,11 @@
# setup-kms.ps1
#
# Minimal KMS-host wiring for an already-installed Volume License Windows.
# Pins the KMS host, then activates only if not already licensed.
# Idempotent - safe to re-run: if Windows is already licensed it reports the
# remaining days and exits without re-contacting the KMS server.
# Does NOT install Office. Does NOT change DNS suffix. Pin only.
# 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.
#
# Usage:
# iwr -UseBasicParsing https://kms.viktorbarzin.me/scripts/setup-kms.ps1 | iex
@ -18,7 +19,8 @@
[CmdletBinding()]
param(
[string]$KmsHost = $(if ($env:KMS_HOST) { $env:KMS_HOST } else { 'vlmcs.viktorbarzin.me' }),
[int] $KmsPort = $(if ($env:KMS_PORT) { [int]$env:KMS_PORT } else { 1688 })
[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' })
)
$ErrorActionPreference = 'Stop'
@ -44,6 +46,27 @@ function Get-WindowsLicense {
[pscustomobject]@{ Licensed = ($p.LicenseStatus -eq 1); DaysLeft = [int]([math]::Round($p.GracePeriodRemaining / 1440)) }
}
# 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
}
$slmgr = "$env:WINDIR\System32\slmgr.vbs"
# Pin the KMS host. Idempotent: re-running just re-sets the same value.
@ -53,19 +76,29 @@ if ($LASTEXITCODE -ne 0) { Bad "slmgr /skms failed (exit $LASTEXITCODE): $out";
OK "KMS host pinned"
$lic = Get-WindowsLicense
if ($null -eq $lic) {
Bad "No KMS-client (Volume License) Windows SKU detected."
Write-Host " Install a GVLK first: slmgr /ipk <GVLK> (see https://kms.viktorbarzin.me/#windows)"
# 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) {
Write-Host ""
Write-Host "==> Already licensed ($($lic.DaysLeft) days remaining). Nothing to do." -ForegroundColor Green
Write-Host " Host is pinned to $KmsHost; Windows auto-renews every 7 days."
return
}
# Idempotent: already activated -> don't re-hit the KMS server. The pinned host
# above means Windows keeps auto-renewing every 7 days on its own.
if ($lic.Licensed) {
Write-Host ""
Write-Host "==> Already licensed via KMS ($($lic.DaysLeft) days remaining). Nothing to do." -ForegroundColor Green
Write-Host " Host is pinned to $KmsHost; Windows auto-renews every 7 days."
return
# 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) {
Bad "Could not auto-select a GVLK for this edition."
Write-Host " Pick one from https://kms.viktorbarzin.me/#windows and run: slmgr /ipk <GVLK>"
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"
}
Step "Activating (slmgr /ato)"