Files
shopdb-flask/scripts/generate-agent-cert.ps1
Proudlock 5cd5332397
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled
Add agent fingerprinting, client certificate auth, and security audit documentation
This commit adds:
- Database migrations for app_delta fingerprints and client certificates
- Scripts for certificate generation and fingerprint backfilling
- Comprehensive security audit reports and findings tracking
- Implementation documentation for certificate authentication
- Test utilities for certificate renewal and fingerprinting
- GE-ENFORCE collector fix documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-09-15 09:18:12 -04:00

134 lines
5.6 KiB
PowerShell

param(
[Parameter(Mandatory=$false)]
[string]$Hostname = $env:COMPUTERNAME,
[Parameter(Mandatory=$false)]
[int]$ValidityYears = 2
)
# SECURITY: Validate hostname matches actual machine name to prevent cert for wrong host
$actualHostname = [System.Net.Dns]::GetHostName()
if ($Hostname -ne $actualHostname) {
Write-Warning "Hostname mismatch detected!"
Write-Warning " Requested: $Hostname"
Write-Warning " Actual machine: $actualHostname"
Write-Warning "Using actual hostname to prevent certificate mismatch."
$Hostname = $actualHostname
}
# Generate self-signed cert for this endpoint
$cert = New-SelfSignedCertificate `
-Subject "CN=$Hostname" `
-DnsName $Hostname `
-CertStoreLocation "Cert:\LocalMachine\My" `
-KeyExportPolicy NonExportable `
-KeyUsage DigitalSignature, KeyEncipherment `
-NotAfter (Get-Date).AddYears($ValidityYears) `
-HashAlgorithm SHA256
# SECURITY WARNING: Grant Users group read access to the private key so the tray
# (running as interactive user) can use it when shelling out to shopdbagent.exe --once.
# The service already works because it runs as SYSTEM.
#
# RISK: This grants ALL interactive users on the machine read access to the private
# key, which means any user with console/RDP access can extract the key and impersonate
# this endpoint. Only acceptable in environments where ALL interactive users are trusted.
#
# MITIGATION OPTIONS:
# 1. Best: Remove tray dependency on cert (tray calls service API, service makes HTTP)
# 2. Acceptable: Grant only to specific user account running the tray
# 3. Document: Only deploy in environments where all console users are trusted operators
try {
$rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
$keyPath = $rsaKey.Key.UniqueName
# CNG keys are in SystemKeys, not MachineKeys
$keyFile = Join-Path "$env:ProgramData\Microsoft\Crypto\SystemKeys" $keyPath
if (-not (Test-Path $keyFile)) {
$keyFile = Join-Path "$env:ProgramData\Microsoft\Crypto\Keys" $keyPath
}
if (-not (Test-Path $keyFile)) {
$keyFile = Join-Path "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys" $keyPath
}
if (Test-Path $keyFile) {
# Grant Read + Execute to BUILTIN\Users
$acl = Get-Acl $keyFile
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"BUILTIN\Users", "Read,ReadAndExecute", "Allow")
$acl.AddAccessRule($rule)
Set-Acl -Path $keyFile -AclObject $acl
Write-Host " Granted Users group access to private key"
} else {
Write-Host " WARNING: Could not find private key file to set permissions"
}
} catch {
Write-Host " WARNING: Could not set private key permissions: $($_.Exception.Message)"
}
Write-Host "Generated certificate:"
Write-Host " Thumbprint: $($cert.Thumbprint)"
Write-Host " Subject: $($cert.Subject)"
Write-Host " Expires: $($cert.NotAfter)"
# Compute SHA1 fingerprint to match nginx $ssl_client_fingerprint variable
# SECURITY NOTE: Nginx only provides SHA1 via $ssl_client_fingerprint variable.
# SHA1 is deprecated for cryptographic signing but remains acceptable for certificate
# fingerprinting in air-gapped environments with:
# - Short-lived certificates (2 years max)
# - Certificate revocation support (cert_revoked_at column)
# - No external CA trust (self-signed only)
# Future migration to SHA256 requires: nginx module for SHA256, or IIS deployment
# which exposes full PEM cert for server-side hashing.
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
$sha1 = [System.Security.Cryptography.SHA1]::Create()
$fingerprintBytes = $sha1.ComputeHash($certBytes)
$fingerprint = ($fingerprintBytes | ForEach-Object { $_.ToString("x2") }) -join ""
Write-Host ""
Write-Host "========================================="
Write-Host "CERTIFICATE GENERATED SUCCESSFULLY"
Write-Host "========================================="
Write-Host "Fingerprint (SHA1): $fingerprint"
Write-Host "Length: 40 hex chars (nginx limitation)"
Write-Host ""
Write-Host "Register this certificate in ShopDB database:"
Write-Host ""
Write-Host " UPDATE assets"
Write-Host " SET client_cert_fingerprint = '$fingerprint',"
Write-Host " cert_issued_at = CURRENT_TIMESTAMP,"
Write-Host " cert_expires_at = '$($cert.NotAfter.ToString("yyyy-MM-dd HH:mm:ss"))',"
Write-Host " cert_revoked_at = NULL,"
Write-Host " cert_revocation_reason = NULL"
Write-Host " WHERE assetnumber = '(REPLACE_WITH_ASSET_NUMBER)';"
Write-Host ""
Write-Host "To revoke this certificate later (compromised/lost endpoint):"
Write-Host ""
Write-Host " UPDATE assets"
Write-Host " SET cert_revoked_at = CURRENT_TIMESTAMP,"
Write-Host " cert_revocation_reason = 'Compromised - TICKET-123'"
Write-Host " WHERE client_cert_fingerprint = '$fingerprint';"
Write-Host ""
Write-Host "========================================="
# Update agent config
$configPath = "C:\ProgramData\ShopDB\config.json"
if (Test-Path $configPath) {
$config = Get-Content $configPath | ConvertFrom-Json
$config | Add-Member -NotePropertyName clientCertThumbprint -NotePropertyValue $cert.Thumbprint -Force
$config | ConvertTo-Json | Set-Content $configPath
Write-Host ""
Write-Host "Updated $configPath with thumbprint"
} else {
Write-Host ""
Write-Host "WARNING: $configPath not found - agent may not be configured"
}
# Output for automation
Write-Host ""
Write-Host "---MACHINE-READABLE---"
Write-Host "THUMBPRINT=$($cert.Thumbprint)"
Write-Host "FINGERPRINT=$fingerprint"
Write-Host "HOSTNAME=$Hostname"
Write-Host "EXPIRES=$($cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss'))"