Dateien nach "/" hochladen

This commit is contained in:
2026-08-13 23:10:22 +02:00
parent ba2cbd5730
commit 4d4ab979fa
2 changed files with 433 additions and 1 deletions
+338
View File
@@ -0,0 +1,338 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Sichere Zertifikatsrotation für SMTPGraphRelay.
.DESCRIPTION
- Muss mit Windows PowerShell 5.1 ausgeführt werden.
- Liest TenantId, ClientId und aktuellen Thumbprint aus config.json.
- Erzeugt ein neues nicht exportierbares RSA-Zertifikat in LocalMachine\My.
- Meldet einen Entra-Administrator interaktiv an.
- Fügt das neue öffentliche Zertifikat zusätzlich zur bestehenden App Registration hinzu.
- Testet App-only Authentication mit dem neuen Zertifikat.
- Aktualisiert erst danach config.json.
- Startet den SMTPGraphRelay Scheduled Task neu.
- Entfernt das alte App-Zertifikat / lokale Zertifikat nur auf Wunsch.
Exchange Application RBAC bleibt unverändert, da ClientId/App Registration gleich bleibt.
#>
[CmdletBinding()]
param(
[string]$ConfigPath = "$PSScriptRoot\config.json",
[int]$ValidityYears = 2
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if ($PSVersionTable.PSEdition -ne "Desktop" -or $PSVersionTable.PSVersion.Major -ne 5) {
Write-Host ""
Write-Host "Dieses Skript muss mit Windows PowerShell 5.1 ausgeführt werden." -ForegroundColor Red
Write-Host "Bitte starten:" -ForegroundColor Yellow
Write-Host " C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
Write-Host ""
Read-Host "Enter drücken zum Beenden"
exit 1
}
if (-not (Test-Path -LiteralPath $ConfigPath)) {
throw "config.json nicht gefunden: $ConfigPath"
}
if ($ValidityYears -lt 1 -or $ValidityYears -gt 5) {
throw "ValidityYears muss zwischen 1 und 5 liegen."
}
$config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
$TenantId = [string]$config.Graph.TenantId
$ClientId = [string]$config.Graph.ClientId
$OldThumbprint = [string]$config.Graph.CertificateThumbprint
if ([string]::IsNullOrWhiteSpace($TenantId) -or
[string]::IsNullOrWhiteSpace($ClientId) -or
[string]::IsNullOrWhiteSpace($OldThumbprint)) {
throw "TenantId, ClientId oder CertificateThumbprint fehlen in config.json."
}
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host " SMTPGraphRelay - Zertifikatsrotation" -ForegroundColor Cyan
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Tenant ID: $TenantId"
Write-Host "Client ID: $ClientId"
Write-Host "Alter Thumbprint:$OldThumbprint"
Write-Host ""
$modules = @(
"Microsoft.Graph.Authentication",
"Microsoft.Graph.Applications"
)
foreach ($module in $modules) {
if (-not (Get-Module -ListAvailable -Name $module)) {
Write-Host "-> $module fehlt. Wird für alle Benutzer installiert..." -ForegroundColor Yellow
Install-Module $module -Scope AllUsers -Repository PSGallery -Force -AllowClobber
}
}
Import-Module Microsoft.Graph.Authentication -Force -ErrorAction Stop
Import-Module Microsoft.Graph.Applications -Force -ErrorAction Stop
$oldCert = Get-Item -LiteralPath "Cert:\LocalMachine\My\$OldThumbprint" -ErrorAction Stop
Write-Host "Altes Zertifikat gültig bis: $($oldCert.NotAfter)" -ForegroundColor DarkGray
Write-Host ""
Write-Host "Schritt 1: Neues Zertifikat erzeugen..." -ForegroundColor Cyan
$certSubject = "CN=SMTPGraphRelay-$env:COMPUTERNAME"
$newCert = New-SelfSignedCertificate `
-Subject $certSubject `
-CertStoreLocation "Cert:\LocalMachine\My" `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-HashAlgorithm SHA256 `
-KeyExportPolicy NonExportable `
-KeySpec Signature `
-NotAfter (Get-Date).AddYears($ValidityYears)
Write-Host "-> Neues Zertifikat: $($newCert.Thumbprint)" -ForegroundColor Green
Write-Host "-> Gültig bis: $($newCert.NotAfter)" -ForegroundColor Green
Write-Host ""
try {
Write-Host "Schritt 2: Entra-Administrator anmelden..." -ForegroundColor Cyan
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
Connect-MgGraph `
-TenantId $TenantId `
-Scopes "Application.ReadWrite.All" `
-NoWelcome
$ctx = Get-MgContext
if (-not $ctx -or $ctx.TenantId -ne $TenantId) {
throw "Graph-Anmeldung am erwarteten Tenant fehlgeschlagen."
}
Write-Host "-> Angemeldet an Tenant $($ctx.TenantId)." -ForegroundColor Green
Write-Host ""
Write-Host "Schritt 3: Bestehende App Registration laden..." -ForegroundColor Cyan
$app = Get-MgApplication -Filter "appId eq '$ClientId'" -Property "id,appId,displayName,keyCredentials" | Select-Object -First 1
if (-not $app) {
throw "App Registration mit ClientId '$ClientId' wurde nicht gefunden."
}
Write-Host "-> App gefunden: $($app.DisplayName)" -ForegroundColor Green
$alreadyPresent = $false
foreach ($key in @($app.KeyCredentials)) {
if ($key.CustomKeyIdentifier) {
$keyThumb = ([BitConverter]::ToString($key.CustomKeyIdentifier)).Replace("-","")
if ($keyThumb -eq $newCert.Thumbprint) {
$alreadyPresent = $true
break
}
}
}
if (-not $alreadyPresent) {
Write-Host "Schritt 4: Neues Zertifikat zusätzlich zur App hinzufügen..." -ForegroundColor Cyan
$existingKeys = @()
foreach ($key in @($app.KeyCredentials)) {
$existingKeys += @{
CustomKeyIdentifier = $key.CustomKeyIdentifier
DisplayName = $key.DisplayName
EndDateTime = $key.EndDateTime
KeyId = $key.KeyId
StartDateTime = $key.StartDateTime
Type = $key.Type
Usage = $key.Usage
}
}
$newKey = @{
Type = "AsymmetricX509Cert"
Usage = "Verify"
Key = $newCert.GetRawCertData()
DisplayName = "SMTPGraphRelay Certificate $($newCert.NotAfter.ToString('yyyy-MM-dd'))"
StartDateTime = $newCert.NotBefore.ToUniversalTime()
EndDateTime = $newCert.NotAfter.ToUniversalTime()
}
$allKeys = @($existingKeys) + @($newKey)
Update-MgApplication `
-ApplicationId $app.Id `
-KeyCredentials $allKeys
Write-Host "-> Neues Zertifikat wurde zusätzlich registriert." -ForegroundColor Green
}
else {
Write-Host "-> Neues Zertifikat ist bereits in der App registriert." -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Schritt 5: App-only Anmeldung mit NEUEM Zertifikat testen..." -ForegroundColor Cyan
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
$authSucceeded = $false
$lastAuthError = $null
# Entra kann kurz brauchen, bis das neue Credential überall verfügbar ist.
for ($attempt = 1; $attempt -le 6; $attempt++) {
try {
Connect-MgGraph `
-TenantId $TenantId `
-ClientId $ClientId `
-Certificate $newCert `
-NoWelcome | Out-Null
$appCtx = Get-MgContext
if ($appCtx -and $appCtx.AuthType -eq "AppOnly" -and $appCtx.ClientId -eq $ClientId) {
$authSucceeded = $true
break
}
throw "Graph-Kontext ist nicht AppOnly oder hat eine unerwartete ClientId."
}
catch {
$lastAuthError = $_.Exception.Message
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
if ($attempt -lt 6) {
Write-Host "-> Noch nicht verfügbar (Versuch $attempt/6), neuer Versuch..." -ForegroundColor Yellow
Start-Sleep -Seconds 10
}
}
}
if (-not $authSucceeded) {
throw "Neues Zertifikat konnte nicht zur App-only Anmeldung verwendet werden: $lastAuthError"
}
Write-Host "-> App-only Anmeldung mit neuem Zertifikat erfolgreich." -ForegroundColor Green
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
Write-Host ""
Write-Host "Schritt 6: config.json atomar auf neuen Thumbprint umstellen..." -ForegroundColor Cyan
$backupPath = "$ConfigPath.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Copy-Item -LiteralPath $ConfigPath -Destination $backupPath -Force
$config.Graph.CertificateThumbprint = $newCert.Thumbprint
$tmpConfig = "$ConfigPath.tmp"
$config | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $tmpConfig -Encoding UTF8
Move-Item -LiteralPath $tmpConfig -Destination $ConfigPath -Force
Write-Host "-> config.json aktualisiert." -ForegroundColor Green
Write-Host "-> Backup: $backupPath" -ForegroundColor DarkGray
Write-Host ""
Write-Host "Schritt 7: SMTPGraphRelay neu starten..." -ForegroundColor Cyan
$task = Get-ScheduledTask -TaskName "SMTPGraphRelay" -ErrorAction SilentlyContinue
if ($task) {
Stop-ScheduledTask -TaskName "SMTPGraphRelay" -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Start-ScheduledTask -TaskName "SMTPGraphRelay"
Start-Sleep -Seconds 2
Write-Host "-> Scheduled Task neu gestartet." -ForegroundColor Green
}
else {
Write-Host "-> Scheduled Task 'SMTPGraphRelay' nicht gefunden. Bitte Relay manuell neu starten." -ForegroundColor Yellow
}
Write-Host ""
Write-Host "==========================================================" -ForegroundColor Green
Write-Host " ZERTIFIKATSROTATION ERFOLGREICH" -ForegroundColor Green
Write-Host "==========================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Alter Thumbprint: $OldThumbprint"
Write-Host "Neuer Thumbprint: $($newCert.Thumbprint)" -ForegroundColor Yellow
Write-Host ""
Write-Host "Sicherheitsreserve:" -ForegroundColor Cyan
Write-Host "Das alte Zertifikat bleibt zunächst parallel registriert."
Write-Host "Damit ist ein Rollback über das config.json-Backup möglich."
Write-Host ""
$removeOld = Read-Host "Altes Zertifikat jetzt aus Entra UND LocalMachine entfernen? [j/N]"
if ($removeOld -match '^(?i)j|ja|y|yes$') {
Write-Host ""
Write-Host "Schritt 8: Altes Zertifikat entfernen..." -ForegroundColor Cyan
# Für die Änderung wieder Admin-Kontext herstellen.
Connect-MgGraph `
-TenantId $TenantId `
-Scopes "Application.ReadWrite.All" `
-NoWelcome
$appFresh = Get-MgApplication -ApplicationId $app.Id -Property "id,keyCredentials"
$remainingKeys = @()
$oldFound = $false
foreach ($key in @($appFresh.KeyCredentials)) {
$isOld = $false
if ($key.CustomKeyIdentifier) {
$keyThumb = ([BitConverter]::ToString($key.CustomKeyIdentifier)).Replace("-","")
if ($keyThumb -eq $OldThumbprint) {
$isOld = $true
$oldFound = $true
}
}
if (-not $isOld) {
$remainingKeys += @{
CustomKeyIdentifier = $key.CustomKeyIdentifier
DisplayName = $key.DisplayName
EndDateTime = $key.EndDateTime
KeyId = $key.KeyId
StartDateTime = $key.StartDateTime
Type = $key.Type
Usage = $key.Usage
}
}
}
if ($oldFound) {
Update-MgApplication `
-ApplicationId $app.Id `
-KeyCredentials $remainingKeys
Write-Host "-> Altes Zertifikat aus Entra entfernt." -ForegroundColor Green
}
else {
Write-Host "-> Alter Thumbprint war in Entra nicht mehr vorhanden." -ForegroundColor Yellow
}
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
Remove-Item -LiteralPath "Cert:\LocalMachine\My\$OldThumbprint" -Force -ErrorAction Stop
Write-Host "-> Altes lokales Zertifikat entfernt." -ForegroundColor Green
}
else {
Write-Host "-> Altes Zertifikat bleibt als Rollback-Reserve bestehen." -ForegroundColor Yellow
}
}
catch {
Write-Host ""
Write-Host "FEHLER BEI DER ZERTIFIKATSROTATION:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
Write-Host "Die bestehende config.json wurde nur nach erfolgreichem Auth-Test geändert." -ForegroundColor Yellow
Write-Host "Falls das neue Zertifikat bereits erzeugt/hochgeladen wurde, kann es später manuell bereinigt werden." -ForegroundColor Yellow
throw
}
finally {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}
Write-Host ""
Read-Host "Enter drücken zum Beenden"