-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDeploy-BitLockerKeyMonitor.ps1
More file actions
525 lines (450 loc) · 24.8 KB
/
Deploy-BitLockerKeyMonitor.ps1
File metadata and controls
525 lines (450 loc) · 24.8 KB
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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
<#
.SYNOPSIS
Deploys BitLockerKeyMonitor (Worker + Web) as Windows Services using Kestrel.
No IIS required — minimal software footprint.
.DESCRIPTION
This script:
1. Publishes both Worker and Web projects (framework-dependent, win-x64)
2. Creates install directories under $InstallRoot
3. Stops existing services before overwriting binaries
4. Registers both as Windows Services (idempotent — creates or updates)
5. Creates log, output, and secure-export directories
6. Adds a firewall rule for the Kestrel HTTPS port
7. Starts the services in the correct order (Worker first, then Web)
Prerequisites on the target machine:
- Windows Server 2022+ (domain-joined)
- .NET 10 ASP.NET Core Runtime (not SDK)
- SQL Server Express (local or remote)
- A TLS certificate in LocalMachine\My for Kestrel HTTPS
.PARAMETER InstallRoot
Root installation folder. Default: C:\Program Files\BitLockerKeyMonitor
.PARAMETER HttpsPort
HTTPS port for the Kestrel-hosted Blazor portal. Default: 5443
.PARAMETER CertificateThumbprint
Thumbprint of the TLS certificate in LocalMachine\My for Kestrel HTTPS binding.
If omitted, the script will list available certificates and prompt.
.PARAMETER ServiceAccount
The account to run both services under. Use a gMSA (DOMAIN\svc-account$) for
production. Default: LocalSystem (for initial testing only).
.PARAMETER SqlInstanceName
SQL Server instance name. Default: SQLEXPRESS
.PARAMETER SqlSysAdminAccounts
Account(s) granted sysadmin on SQL Server during install.
Default: BUILTIN\ADMINISTRATORS
.PARAMETER SkipPrerequisites
Skip automatic download/install of .NET and SQL Server Express.
.PARAMETER SkipPublish
Skip the dotnet publish step (useful when deploying pre-built artifacts).
.PARAMETER SkipFirewall
Skip firewall rule creation.
.EXAMPLE
.\Deploy-BitLockerKeyMonitor.ps1 -CertificateThumbprint "AB12CD34..." -ServiceAccount "YOURLAB\svc-blkmonitor$"
.EXAMPLE
.\Deploy-BitLockerKeyMonitor.ps1 -SkipPublish -InstallRoot "D:\Apps\BitLockerKeyMonitor"
.EXAMPLE
.\Deploy-BitLockerKeyMonitor.ps1 -SkipPrerequisites -SkipPublish
#>
[CmdletBinding()]
param(
[string]$InstallRoot = "C:\Program Files\BitLockerKeyMonitor",
[int] $HttpsPort = 443,
[string]$CertificateThumbprint,
[string]$ServiceAccount = "LocalSystem",
[string]$SqlInstanceName = "SQLEXPRESS",
[string]$SqlSysAdminAccounts = "BUILTIN\ADMINISTRATORS",
[switch]$SkipPrerequisites,
[switch]$SkipPublish,
[switch]$SkipFirewall
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ── Paths ────────────────────────────────────────────────────────────────────
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$SrcDir = Join-Path $ScriptDir "src"
$WorkerProj = Join-Path $SrcDir "BitLockerKeyMonitor.Worker\BitLockerKeyMonitor.Worker.csproj"
$WebProj = Join-Path $SrcDir "BitLockerKeyMonitor.Web\BitLockerKeyMonitor.Web.csproj"
$WorkerDest = Join-Path $InstallRoot "Worker"
$WebDest = Join-Path $InstallRoot "Web"
$LogDir = Join-Path $InstallRoot "logs"
$OutputDir = Join-Path $InstallRoot "output"
$SecureDir = Join-Path $InstallRoot "output\secure"
$WorkerSvc = "BitLockerKeyMonitor.Worker"
$WebSvc = "BitLockerKeyMonitor.Web"
# ── Helper functions ─────────────────────────────────────────────────────────
function Write-Step { param([string]$msg) Write-Host "`n▶ $msg" -ForegroundColor Cyan }
function Write-Ok { param([string]$msg) Write-Host " ✓ $msg" -ForegroundColor Green }
function Write-Warn { param([string]$msg) Write-Host " ⚠ $msg" -ForegroundColor Yellow }
function Write-Err { param([string]$msg) Write-Host " ✗ $msg" -ForegroundColor Red }
# ── Version constants ────────────────────────────────────────────────────────
$DotnetMajorMinor = "10.0"
$DotnetInstallScript = "https://dot.net/v1/dotnet-install.ps1"
$SqlExpressMedia = "https://go.microsoft.com/fwlink/p/?linkid=2216019" # SQL2022-SSEI-Expr.exe
$TempDir = Join-Path $env:TEMP "BitLockerKeyMonitor-Setup"
# ── Pre-flight checks ───────────────────────────────────────────────────────
Write-Step "Pre-flight checks"
# Must run elevated
$currentPrincipal = [Security.Principal.WindowsPrincipal]([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Err "This script must be run as Administrator."
exit 1
}
Write-Ok "Running elevated"
# ── Prerequisites: .NET ASP.NET Core Runtime ─────────────────────────────────
if (-not $SkipPrerequisites) {
Write-Step "Checking prerequisites"
# ── .NET 10 ASP.NET Core Runtime ─────────────────────────────────────────
$dotnetInstalled = $false
$aspnetInstalled = $false
# Check if dotnet CLI exists and has the right runtime version
$dotnetExe = Get-Command dotnet -ErrorAction SilentlyContinue
if ($dotnetExe) {
$runtimes = & dotnet --list-runtimes 2>$null
if ($runtimes -match "Microsoft\.NETCore\.App $DotnetMajorMinor") {
$dotnetInstalled = $true
Write-Ok ".NET Runtime $DotnetMajorMinor already installed"
}
if ($runtimes -match "Microsoft\.AspNetCore\.App $DotnetMajorMinor") {
$aspnetInstalled = $true
Write-Ok "ASP.NET Core Runtime $DotnetMajorMinor already installed"
}
}
if (-not $dotnetInstalled -or -not $aspnetInstalled) {
Write-Step "Installing .NET $DotnetMajorMinor ASP.NET Core Runtime"
if (-not (Test-Path $TempDir)) { New-Item -ItemType Directory -Path $TempDir -Force | Out-Null }
$dotnetInstallPs1 = Join-Path $TempDir "dotnet-install.ps1"
Write-Host " Downloading dotnet-install.ps1 ..." -ForegroundColor Gray
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $DotnetInstallScript -OutFile $dotnetInstallPs1 -UseBasicParsing
if (-not $dotnetInstalled) {
Write-Host " Installing .NET Runtime $DotnetMajorMinor (machine-wide) ..." -ForegroundColor Gray
& $dotnetInstallPs1 -Channel $DotnetMajorMinor -Runtime dotnet -InstallDir "C:\Program Files\dotnet" -NoPath
if ($LASTEXITCODE -ne 0) { Write-Err ".NET Runtime install failed."; exit 1 }
Write-Ok ".NET Runtime $DotnetMajorMinor installed"
}
if (-not $aspnetInstalled) {
Write-Host " Installing ASP.NET Core Runtime $DotnetMajorMinor (machine-wide) ..." -ForegroundColor Gray
& $dotnetInstallPs1 -Channel $DotnetMajorMinor -Runtime aspnetcore -InstallDir "C:\Program Files\dotnet" -NoPath
if ($LASTEXITCODE -ne 0) { Write-Err "ASP.NET Core Runtime install failed."; exit 1 }
Write-Ok "ASP.NET Core Runtime $DotnetMajorMinor installed"
}
# Ensure dotnet is on PATH for the rest of this script
$dotnetDir = "C:\Program Files\dotnet"
if ($env:PATH -notlike "*$dotnetDir*") {
$env:PATH = "$dotnetDir;$env:PATH"
}
# Persist to system PATH if not already there
$machinePath = [Environment]::GetEnvironmentVariable("PATH", "Machine")
if ($machinePath -notlike "*$dotnetDir*") {
[Environment]::SetEnvironmentVariable("PATH", "$dotnetDir;$machinePath", "Machine")
Write-Ok "Added dotnet to system PATH"
}
}
# ── SQL Server Express ───────────────────────────────────────────────────
$sqlInstance = Get-Service -Name "MSSQL`$$SqlInstanceName" -ErrorAction SilentlyContinue
if (-not $sqlInstance) {
# Also check default instance
$sqlDefault = Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue
if ($sqlDefault -and $SqlInstanceName -eq "SQLEXPRESS") {
Write-Warn "SQL Server default instance found but SQLEXPRESS not found."
Write-Warn "Update ConnectionStrings in appsettings.json to use Server=. instead of .\SQLEXPRESS"
}
}
if (-not $sqlInstance) {
Write-Step "Installing SQL Server 2022 Express"
if (-not (Test-Path $TempDir)) { New-Item -ItemType Directory -Path $TempDir -Force | Out-Null }
$sqlMediaDir = Join-Path $TempDir "SqlExpress"
$sqlSsei = Join-Path $TempDir "SQL2022-SSEI-Expr.exe"
$sqlExtracted = Join-Path $TempDir "SqlExpressSetup"
# Step 1: Download the SQL Server Express media downloader
if (-not (Test-Path $sqlSsei)) {
Write-Host " Downloading SQL Server 2022 Express installer ..." -ForegroundColor Gray
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $SqlExpressMedia -OutFile $sqlSsei -UseBasicParsing
Write-Ok "Downloaded SQL2022-SSEI-Expr.exe"
}
# Step 2: Download the full media (Core edition)
if (-not (Test-Path $sqlMediaDir)) {
New-Item -ItemType Directory -Path $sqlMediaDir -Force | Out-Null
}
Write-Host " Downloading SQL Server Express media (this may take several minutes) ..." -ForegroundColor Gray
$downloadArgs = "/ACTION=Download /MEDIATYPE=Core /MEDIAPATH=`"$sqlMediaDir`" /QUIET"
$proc = Start-Process -FilePath $sqlSsei -ArgumentList $downloadArgs -Wait -PassThru -NoNewWindow
if ($proc.ExitCode -ne 0) {
Write-Err "SQL Express media download failed (exit code $($proc.ExitCode))."
exit 1
}
Write-Ok "SQL Express media downloaded"
# Step 3: Extract the setup files
$sqlCoreExe = Get-ChildItem -Path $sqlMediaDir -Filter "SQLEXPR*.exe" | Select-Object -First 1
if (-not $sqlCoreExe) {
Write-Err "Could not find SQLEXPR*.exe in $sqlMediaDir"
exit 1
}
if (-not (Test-Path $sqlExtracted)) {
New-Item -ItemType Directory -Path $sqlExtracted -Force | Out-Null
}
Write-Host " Extracting SQL Server setup ..." -ForegroundColor Gray
$extractProc = Start-Process -FilePath $sqlCoreExe.FullName -ArgumentList "/Q /X:`"$sqlExtracted`"" -Wait -PassThru -NoNewWindow
if ($extractProc.ExitCode -ne 0) {
Write-Err "SQL Express extraction failed (exit code $($extractProc.ExitCode))."
exit 1
}
Write-Ok "SQL Express setup extracted"
# Step 4: Silent install
$setupExe = Join-Path $sqlExtracted "setup.exe"
if (-not (Test-Path $setupExe)) {
# Sometimes extracted to a subdirectory
$setupExe = Get-ChildItem -Path $sqlExtracted -Filter "setup.exe" -Recurse | Select-Object -First 1
if (-not $setupExe) { Write-Err "setup.exe not found in $sqlExtracted"; exit 1 }
$setupExe = $setupExe.FullName
}
Write-Host " Installing SQL Server 2022 Express (silent, instance=$SqlInstanceName) ..." -ForegroundColor Gray
$installArgs = @(
"/Q"
"/ACTION=Install"
"/IACCEPTSQLSERVERLICENSETERMS"
"/FEATURES=SQLEngine"
"/INSTANCENAME=$SqlInstanceName"
"/SQLSYSADMINACCOUNTS=`"$SqlSysAdminAccounts`""
"/TCPENABLED=1"
"/NPENABLED=0"
"/SECURITYMODE=SQL"
"/SAPWD=`"B1tL0ck3rM0n!t0r_$(Get-Random -Minimum 10000 -Maximum 99999)`""
"/UPDATEENABLED=False"
"/SQLSVCSTARTUPTYPE=Automatic"
"/BROWSERSVCSTARTUPTYPE=Disabled"
)
$installProc = Start-Process -FilePath $setupExe -ArgumentList ($installArgs -join " ") -Wait -PassThru -NoNewWindow
if ($installProc.ExitCode -ne 0 -and $installProc.ExitCode -ne 3010) {
Write-Err "SQL Express install failed (exit code $($installProc.ExitCode))."
Write-Err "Check log at: C:\Program Files\Microsoft SQL Server\*.0\Setup Bootstrap\Log"
exit 1
}
if ($installProc.ExitCode -eq 3010) {
Write-Warn "SQL Express installed — a REBOOT is required to complete setup."
} else {
Write-Ok "SQL Server 2022 Express installed (instance: $SqlInstanceName)"
}
# Verify the service exists
Start-Sleep -Seconds 3
$sqlCheck = Get-Service -Name "MSSQL`$$SqlInstanceName" -ErrorAction SilentlyContinue
if ($sqlCheck) {
if ($sqlCheck.Status -ne 'Running') { Start-Service -Name "MSSQL`$$SqlInstanceName" }
Write-Ok "SQL Server service MSSQL`$$SqlInstanceName is running"
} else {
Write-Warn "SQL Server service not found yet — may need a reboot."
}
} else {
Write-Ok "SQL Server instance $SqlInstanceName already installed"
if ($sqlInstance.Status -ne 'Running') {
Start-Service -Name "MSSQL`$$SqlInstanceName"
Write-Ok "Started SQL Server service"
}
}
} else {
Write-Ok "Skipping prerequisites (--SkipPrerequisites)"
}
# Check .NET runtime is available (either pre-existing or just installed)
$dotnetVersion = & dotnet --version 2>$null
if (-not $dotnetVersion) {
Write-Err ".NET runtime not found. Run without -SkipPrerequisites or install .NET 10 ASP.NET Core Runtime manually."
exit 1
}
Write-Ok ".NET version: $dotnetVersion"
# Validate source projects exist (only if publishing)
if (-not $SkipPublish) {
if (-not (Test-Path $WorkerProj)) { Write-Err "Worker project not found: $WorkerProj"; exit 1 }
if (-not (Test-Path $WebProj)) { Write-Err "Web project not found: $WebProj"; exit 1 }
Write-Ok "Source projects found"
}
# ── Certificate selection ────────────────────────────────────────────────────
Write-Step "TLS Certificate"
if (-not $CertificateThumbprint) {
$certs = Get-ChildItem Cert:\LocalMachine\My |
Where-Object { $_.NotAfter -gt (Get-Date) -and $_.HasPrivateKey } |
Sort-Object NotAfter -Descending
if ($certs.Count -eq 0) {
Write-Err "No valid certificates with private keys found in LocalMachine\My."
Write-Err "Import a TLS certificate and re-run with -CertificateThumbprint."
exit 1
}
Write-Host "`n Available certificates:" -ForegroundColor White
for ($i = 0; $i -lt $certs.Count; $i++) {
$c = $certs[$i]
Write-Host " [$i] $($c.Thumbprint) Subject=$($c.Subject) Expires=$($c.NotAfter.ToString('yyyy-MM-dd'))"
}
$selection = Read-Host "`n Select certificate index [0]"
if ([string]::IsNullOrWhiteSpace($selection)) { $selection = 0 }
$CertificateThumbprint = $certs[[int]$selection].Thumbprint
}
$cert = Get-ChildItem "Cert:\LocalMachine\My\$CertificateThumbprint" -ErrorAction SilentlyContinue
if (-not $cert) {
Write-Err "Certificate with thumbprint $CertificateThumbprint not found in LocalMachine\My."
exit 1
}
Write-Ok "Using certificate: $($cert.Subject) ($CertificateThumbprint)"
# ── Publish ──────────────────────────────────────────────────────────────────
if (-not $SkipPublish) {
Write-Step "Publishing Worker"
$workerPubDir = Join-Path $ScriptDir "artifacts\Worker"
& dotnet publish $WorkerProj -c Release -r win-x64 --self-contained false -o $workerPubDir --nologo -v q
if ($LASTEXITCODE -ne 0) { Write-Err "Worker publish failed."; exit 1 }
Write-Ok "Worker published to $workerPubDir"
Write-Step "Publishing Web"
$webPubDir = Join-Path $ScriptDir "artifacts\Web"
& dotnet publish $WebProj -c Release -r win-x64 --self-contained false -o $webPubDir --nologo -v q
if ($LASTEXITCODE -ne 0) { Write-Err "Web publish failed."; exit 1 }
Write-Ok "Web published to $webPubDir"
} else {
$workerPubDir = Join-Path $ScriptDir "artifacts\Worker"
$webPubDir = Join-Path $ScriptDir "artifacts\Web"
if (-not (Test-Path $workerPubDir) -or -not (Test-Path $webPubDir)) {
Write-Err "Pre-built artifacts not found. Run without -SkipPublish first."
exit 1
}
Write-Ok "Using pre-built artifacts"
}
# ── Stop existing services ───────────────────────────────────────────────────
Write-Step "Stopping existing services (if any)"
foreach ($svcName in @($WebSvc, $WorkerSvc)) {
$svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -ne 'Stopped') {
Stop-Service -Name $svcName -Force
Write-Ok "Stopped $svcName"
} elseif ($svc) {
Write-Ok "$svcName already stopped"
} else {
Write-Ok "$svcName not installed yet"
}
}
# ── Create directories ───────────────────────────────────────────────────────
Write-Step "Creating directories"
foreach ($dir in @($InstallRoot, $WorkerDest, $WebDest, $LogDir, $OutputDir, $SecureDir)) {
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Write-Ok "Created $dir"
}
}
# ── Copy binaries ────────────────────────────────────────────────────────────
Write-Step "Copying Worker binaries"
Copy-Item -Path "$workerPubDir\*" -Destination $WorkerDest -Recurse -Force
Write-Ok "Worker → $WorkerDest"
Write-Step "Copying Web binaries"
Copy-Item -Path "$webPubDir\*" -Destination $WebDest -Recurse -Force
Write-Ok "Web → $WebDest"
# ── Patch Web appsettings with certificate thumbprint ────────────────────────
Write-Step "Configuring Kestrel HTTPS"
$webAppSettings = Join-Path $WebDest "appsettings.json"
$config = Get-Content $webAppSettings -Raw | ConvertFrom-Json
# Ensure Kestrel section exists and set certificate thumbprint
if (-not ($config.PSObject.Properties.Name -contains 'Kestrel')) {
$config | Add-Member -NotePropertyName "Kestrel" -NotePropertyValue ([PSCustomObject]@{})
}
$config.Kestrel = [PSCustomObject]@{
Endpoints = [PSCustomObject]@{
Https = [PSCustomObject]@{
Url = "https://*:$HttpsPort"
Certificate = [PSCustomObject]@{
Store = "My"
Location = "LocalMachine"
Thumbprint = $CertificateThumbprint
AllowInvalid = $false
}
}
}
}
# Also set the WebServer section which Program.cs reads for manual Kestrel configuration
if (-not ($config.PSObject.Properties.Name -contains 'WebServer')) {
$config | Add-Member -NotePropertyName "WebServer" -NotePropertyValue ([PSCustomObject]@{})
}
$config.WebServer = [PSCustomObject]@{
HttpsPort = $HttpsPort
CertificateThumbprint = $CertificateThumbprint
}
$config | ConvertTo-Json -Depth 10 | Set-Content $webAppSettings -Encoding UTF8
Write-Ok "Kestrel HTTPS configured on port $HttpsPort with cert $CertificateThumbprint"
# ── Register Windows Services ────────────────────────────────────────────────
Write-Step "Registering Windows Services"
$workerExe = Join-Path $WorkerDest "BitLockerKeyMonitor.Worker.exe"
$webExe = Join-Path $WebDest "BitLockerKeyMonitor.Web.exe"
foreach ($entry in @(
@{ Name = $WorkerSvc; Exe = $workerExe; Display = "BitLocker Key Monitor - Worker" },
@{ Name = $WebSvc; Exe = $webExe; Display = "BitLocker Key Monitor - Web (Kestrel)" }
)) {
$existing = Get-Service -Name $entry.Name -ErrorAction SilentlyContinue
if ($existing) {
# Update existing service binary path
& sc.exe config $entry.Name binPath= "`"$($entry.Exe)`"" start= delayed-auto | Out-Null
Write-Ok "Updated $($entry.Name)"
} else {
if ($ServiceAccount -eq "LocalSystem") {
& sc.exe create $entry.Name `
binPath= "`"$($entry.Exe)`"" `
DisplayName= $entry.Display `
start= delayed-auto | Out-Null
} else {
& sc.exe create $entry.Name `
binPath= "`"$($entry.Exe)`"" `
DisplayName= $entry.Display `
start= delayed-auto `
obj= $ServiceAccount password= "" | Out-Null
}
Write-Ok "Created $($entry.Name)"
}
# Set recovery: restart after 60s on first/second failure, no action on subsequent
& sc.exe failure $entry.Name reset= 86400 actions= restart/60000/restart/60000// | Out-Null
Write-Ok "Recovery policy set for $($entry.Name)"
# Set description
& sc.exe description $entry.Name "$($entry.Display) - BitLockerKeyMonitor Kestrel deployment" | Out-Null
}
# ── Firewall ─────────────────────────────────────────────────────────────────
if (-not $SkipFirewall) {
Write-Step "Configuring firewall"
$ruleName = "BitLockerKeyMonitor Web (HTTPS $HttpsPort)"
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($existing) {
Set-NetFirewallRule -DisplayName $ruleName -LocalPort $HttpsPort -Protocol TCP -Action Allow -Enabled True
Write-Ok "Updated firewall rule: $ruleName"
} else {
New-NetFirewallRule -DisplayName $ruleName `
-Direction Inbound -Protocol TCP -LocalPort $HttpsPort `
-Action Allow -Enabled True -Profile Domain | Out-Null
Write-Ok "Created firewall rule: $ruleName (Domain profile only)"
}
}
# ── Start services ───────────────────────────────────────────────────────────
Write-Step "Starting services"
# Worker first (initializes DB)
Start-Service -Name $WorkerSvc
Write-Ok "Started $WorkerSvc"
# Brief pause to let DB initialize on first run
Start-Sleep -Seconds 5
Start-Service -Name $WebSvc
Write-Ok "Started $WebSvc"
# ── Summary ──────────────────────────────────────────────────────────────────
Write-Host "`n" -NoNewline
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Green
Write-Host " BitLockerKeyMonitor deployed successfully!" -ForegroundColor Green
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Green
Write-Host ""
Write-Host " Install root : $InstallRoot"
Write-Host " Worker service : $WorkerSvc (delayed-auto start)"
Write-Host " Web service : $WebSvc (delayed-auto start)"
Write-Host " Portal URL : https://$($env:COMPUTERNAME):$HttpsPort"
Write-Host " Logs : $LogDir"
Write-Host " Service account : $ServiceAccount"
Write-Host ""
Write-Host " Post-deployment checklist:" -ForegroundColor Yellow
Write-Host " 1. Update appsettings.json in Worker and Web with your AD/Entra/SQL settings"
Write-Host " 2. Register SPNs for Kerberos auth (if using gMSA):"
Write-Host " setspn -S HTTP/$($env:COMPUTERNAME) $ServiceAccount"
Write-Host " setspn -S HTTP/$($env:COMPUTERNAME).$($env:USERDNSDOMAIN) $ServiceAccount"
Write-Host " 3. Grant private key access to the service account on the TLS certificate"
Write-Host " 4. Verify portal: https://$($env:COMPUTERNAME):$HttpsPort"
Write-Host ""
# ── Cleanup temp files ───────────────────────────────────────────────────────
if (Test-Path $TempDir) {
Write-Step "Cleaning up temporary files"
Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Ok "Removed $TempDir"
}