Skip to content

Latest commit

 

History

History
357 lines (267 loc) · 14.5 KB

File metadata and controls

357 lines (267 loc) · 14.5 KB

Deployment Guide

End-to-end deployment of BitLockerKeyMonitor to a Windows Server using the two helper scripts shipped with the repository:

  1. tools/Setup-BitLockerMonitorGmsa.ps1 — provisions the gMSA service account and all required permissions (AD, SQL, certs, services).
  2. Deploy-Remote.ps1 — publishes the .NET solution from the build machine and deploys it to the target server via WinRM.

Architecture overview

┌─────────────────┐     WinRM      ┌──────────────────────────────────┐
│  Build machine  │ ─────────────▶ │  Target server (SRVBLKMON01)     │
│ (not domain     │                │  ┌────────────────────────────┐  │
│  joined OK)     │                │  │ Worker service (Quartz)    │  │
└─────────────────┘                │  │ Web service (Kestrel, 443) │  │
                                   │  │ SQL Server Express (local) │  │
                                   │  │ Both services run as gMSA  │  │
                                   │  └────────────────────────────┘  │
                                   └──────────────────────────────────┘
                                                │
                                                │ Kerberos (gMSA)
                                                ▼
                                   ┌──────────────────────────────────┐
                                   │  Domain Controller (LDAP/ADWS)   │
                                   │  msFVE-RecoveryInformation       │
                                   └──────────────────────────────────┘

Prerequisites

Build machine

  • .NET 10 SDK
  • PowerShell 5.1+
  • WinRM connectivity to the target (typically TCP 5985 over the management VLAN)
  • A credential with local administrator rights on the target server

The build machine does NOT need to be domain-joined — it connects to the target via WinRM with explicit credentials.

Target server

  • Windows Server 2022+, domain-joined
  • .NET 10 ASP.NET Core Runtime (Hosting Bundle)
  • SQL Server Express (local) — or a remote SQL instance with the equivalent grants
  • WinRM enabled (default on domain-joined servers)
  • TLS certificate in LocalMachine\My (for Kestrel HTTPS)
  • RSAT ActiveDirectory module (Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0) — required for Install-ADServiceAccount

Domain

  • A Domain Admin account (or equivalent) able to:
    • Create gMSA accounts and assign PrincipalsAllowedToRetrieveManagedPassword
    • Modify ACLs on the OUs that hold BitLocker recovery key objects
    • Register SPNs on the gMSA
  • A KDS Root Key exists, or you can create one (Add-KdsRootKey -EffectiveImmediately)

Entra ID (App Registration)

For the Entra scan to work, the App Registration linked to Authentication.ClientId in appsettings.json needs these Application permissions (admin consent required):

Permission Purpose
Device.Read.All Enumerate devices and correlate with AD computer objects
BitlockerKey.ReadBasic.All List recovery keys (KeyId, VolumeId, CreatedDate)
BitlockerKey.Read.All Read the actual recovery password value (getKey) — only needed if IncludeRecoveryKeyValue=true

Verify the granted permissions by decoding an access token issued to the app (the roles claim is the source of truth — see Troubleshooting).


Step 1 — Provision the gMSA account

Run this script once per environment, from a domain-joined administrative workstation (or a DC). The build machine does not need to be domain-joined.

$gmsaParams = @{
    TargetServer            = "SRVBLKMON01"
    GmsaName                = "svc-BlkMon"
    DomainDnsName           = "contoso.local"
    SearchBaseOUs           = @("OU=Workstations,DC=contoso,DC=local")
    SqlInstance             = ".\SQLEXPRESS"
    DatabaseName            = "BitLockerKeyMonitor"
    CertificateThumbprints  = @(
        "ae803d83c7326852729aad9da891d8df17a6c549",   # Kestrel HTTPS cert
        "855cdf6182de9cbd6c7d3c95340b5caa7222d2ba"    # Entra client auth cert
    )
}

.\tools\Setup-BitLockerMonitorGmsa.ps1 @gmsaParams

For lab/test environments where you do not want to wait the 10-hour KDS replication window, add -LabMode:

.\tools\Setup-BitLockerMonitorGmsa.ps1 @gmsaParams -LabMode

What the script does

# Step Detail
1 KDS Root Key Creates one if missing (-LabMode skips the 10h wait)
2 Create gMSA svc-BlkMon$ with $TargetServer in PrincipalsAllowedToRetrieveManagedPassword
3 AD ACLs on each OU GenericRead on msFVE-RecoveryInformation descendants
ReadProperty on msFVE-RecoveryPassword
ExtendedRight on msFVE-RecoveryPassword (required for confidential attribute)
3b HTTP SPNs HTTP/<server> + HTTP/<server.fqdn> on the gMSA (required for Negotiate/Kerberos on the Web)
4 Install gMSA Install-ADServiceAccount + Test-ADServiceAccount on the target
5 SQL login Creates MSLABS\svc-BlkMon$ login and assigns db_owner on the application DB
6 "Log on as a service" right Grants SeServiceLogonRight via secedit
7 Certificate private key ACLs Adds Read on each cert's private key file in C:\ProgramData\Microsoft\Crypto\Keys
8 Service configuration Configures Worker and Web services to run as the gMSA

Critical detail: confidential attributes

msFVE-RecoveryPassword is a confidential attribute in AD (searchFlags bit 0x80). AD silently omits the value from LDAP query results unless the caller has the ExtendedRight (Control Access) right — ReadProperty alone is not enough.

The script grants both. If you set ACLs manually (e.g. through ADUC delegation), make sure the Control Access right on msFVE-RecoveryPassword is also delegated, otherwise the AD scan will run successfully but every KeyValue will be NULL and the consistency check will report ValueComparisonIncomplete.

Verification

After the script completes:

# On the target server
Get-Service BitLockerKeyMonitor.Worker, BitLockerKeyMonitor.Web
Test-ADServiceAccount -Identity svc-BlkMon
Get-WmiObject Win32_Service -Filter "Name='BitLockerKeyMonitor.Worker'" |
    Select-Object Name, State, StartName
# StartName should read DOMAIN\svc-BlkMon$

Step 2 — Configure appsettings.json

The Deploy-Remote.ps1 script copies the appsettings.json from the source tree as-is. Before publishing, edit both src/BitLockerKeyMonitor.Worker/appsettings.json and src/BitLockerKeyMonitor.Web/appsettings.json with values appropriate for the target:

{
    "ConnectionStrings": {
        "DefaultConnection": "Server=.\\SQLEXPRESS;Database=BitLockerKeyMonitor;Trusted_Connection=True;TrustServerCertificate=True;"
    },
    "Authentication": {
        "AdAuthMode":     "WindowsIntegrated",          // gMSA uses Kerberos automatically
        "EntraAuthMode":  "ClientCertificate",
        "TenantId":       "<your-tenant-guid>",
        "ClientId":       "<your-app-client-id>",
        "CertificateThumbprint": "855cdf61..."          // matches Entra App Registration
    },
    "ScanSettings": {
        "LdapServer":     "dc01.contoso.local",
        "LdapSearchBase": "OU=Workstations,DC=contoso,DC=local",
        "LdapPort":       389,
        "LdapUseSsl":     false
    },
    "WebServer": {
        "HttpsPort":              443,
        "CertificateThumbprint":  "ae803d83..."         // Kestrel HTTPS cert (Web only)
    },
    "Authorization": {
        "AdminGroup":  "CONTOSO\\BitLockerMonitor-Admins",
        "ViewerGroup": "CONTOSO\\BitLockerMonitor-Viewers"
    }
}

Important values:

  • Authentication.AdAuthMode = "WindowsIntegrated" — required when running as gMSA. Do not set Credential mode (the gMSA has no static password).
  • WebServer.CertificateThumbprint — must reference a cert whose private key the gMSA can read (already handled by -CertificateThumbprints in Step 1).

Step 3 — Deploy with Deploy-Remote.ps1

Run from the repository root on the build machine.

$cred = Get-Credential   # local admin on the target server
.\Deploy-Remote.ps1 -TargetServer SRVBLKMON01 -Credential $cred

What the script does

# Step Detail
1 Pre-flight Verifies source projects exist; pings the target
2 Publish Worker dotnet publish -c Release to artifacts\Worker (skip with -SkipPublish)
3 Publish Web dotnet publish -c Release to artifacts\Web
4 PSSession Opens a WinRM session to the target
5 Stop services Stops Web first, then Worker
6 Copy artifacts Copies the published trees into C:\Program Files\BitLockerKeyMonitor\{Worker,Web}
7 Register services Creates/updates the two Windows Services and sets MSSQL$SQLEXPRESS as a dependency
8 Start services Starts Worker, then Web; verifies both are Running

Parameters

Parameter Default Purpose
-TargetServer (required) Hostname or FQDN
-Credential prompts Local admin on the target
-InstallRoot C:\Program Files\BitLockerKeyMonitor Install path
-SqlInstanceName SQLEXPRESS Local SQL instance for service dependency
-SkipPublish off Reuse existing artifacts\ (faster re-deploy)
-SkipServiceRestart off Copy bits but leave services stopped (maintenance window)

Known limitation

The script overwrites appsettings.json on the target at every deploy (it copies the published artifact verbatim). After a deploy you typically need to:

  • Restore environment-specific values like WebServer.CertificateThumbprint and any values you do not want to commit to source control, or
  • Use the recommended pattern of keeping environment overrides in a separate appsettings.Production.json that is not in source control and is placed manually on the target after the first deploy.

A future iteration could add an -EnvSettingsPath parameter that overlays an out-of-tree config file after each copy.


Step 4 — Validation

From the target server:

# Services running as the gMSA
Get-WmiObject Win32_Service -Filter "Name LIKE 'BitLockerKeyMonitor%'" |
    Select-Object Name, State, StartName

# Wait for the next scheduled scan (default: every 5 minutes), then check the log
Get-Content "C:\ProgramData\BitLockerKeyMonitor\logs\worker-$(Get-Date -Format yyyyMMdd).log" -Tail 30

Healthy output ends with something like:

=== Scan Completed in 00:00:02.4 | AD: 5 devices/16 keys | Entra: 9 devices/5 keys | Consistent: 3, Inconsistent: 2 ===

Then visit the Web portal:

https://<server.fqdn>/

You should be challenged for Windows credentials and authenticated via Kerberos. The portal lands on the dashboard with the latest scan results.


Troubleshooting

Value comparison NOT performed for any pair in the consistency check

The most likely cause is that the gMSA does not have ExtendedRight on msFVE-RecoveryPassword. Re-run the gMSA script — Step 3 is idempotent and will add the missing ACE.

Verify the DB after the next scan:

USE BitLockerKeyMonitor;
DECLARE @scan INT = (SELECT MAX(Id) FROM ScanRuns);
SELECT d.DnsHostName, k.KeyId, k.Source,
       CASE WHEN k.KeyValue IS NULL THEN 'NULL' ELSE 'len=' + CAST(LEN(k.KeyValue) AS VARCHAR) END AS Val
FROM RecoveryKeys k JOIN Devices d ON d.Id = k.DeviceId
WHERE k.ScanRunId = @scan AND k.Source = 0 AND k.KeyValue IS NULL;
-- If this returns rows, the AD ACL is still incomplete.

Web portal returns HTTP 400 after authentication

The HTTP SPNs are missing on the gMSA. Verify with:

Get-ADServiceAccount svc-BlkMon -Properties servicePrincipalName |
    Select-Object -ExpandProperty servicePrincipalName

If HTTP/<server> and HTTP/<server>.<domain> are not listed, re-run the gMSA script (Step 3b is idempotent) or add them manually:

Set-ADServiceAccount svc-BlkMon -ServicePrincipalNames @{
    Add = "HTTP/SRVBLKMON01","HTTP/SRVBLKMON01.contoso.local"
}

Services fail to start with timeout error

Two common causes:

  1. gMSA missing "Log on as a service" right — re-run Step 6 of the gMSA script.
  2. SQL login missing or insufficient grants — re-run Step 5 (creates the login and adds db_owner).

Check the Application event log on the target for the specific error message.

Entra scan returns 401/403 "Failed to authorize"

The App Registration is missing the required Graph permission. Verify by acquiring a token with the configured certificate and decoding the JWT — the roles claim must include BitlockerKey.Read.All for value retrieval to work. See the script in docs/RECOVERY_KEY_VAULT.md (or use any JWT decoder).

To grant via Azure CLI:

# BitlockerKey.Read.All app role ID = 33854624-cf6e-44b8-94cf-c4d6e6dad88f
az ad app permission add --id <client-id> \
    --api 00000003-0000-0000-c000-000000000000 \
    --api-permissions 33854624-cf6e-44b8-94cf-c4d6e6dad88f=Role
az ad app permission admin-consent --id <client-id>

Deploy fails to connect via WinRM

# Verify connectivity from the build machine
Test-WSMan -ComputerName SRVBLKMON01 -Credential $cred -Authentication Negotiate

If the build machine is not domain-joined, add the target to TrustedHosts:

Set-Item WSMan:\localhost\Client\TrustedHosts -Value "SRVBLKMON01" -Force

Re-deploy / upgrade workflow

Once the gMSA is provisioned, day-to-day deployments are a single command:

.\Deploy-Remote.ps1 -TargetServer SRVBLKMON01 -Credential $cred

The gMSA script only needs to be re-run when:

  • Changing the gMSA account name or target server
  • Adding new search OUs for BitLocker recovery keys
  • Rotating the Entra or Kestrel certificate (to grant private-key access to the new cert)
  • After a clean re-install of the target server