Dateien nach "/" hochladen

This commit is contained in:
2026-08-13 23:15:08 +02:00
parent 07f7cea199
commit c4ffc9b0cf
2 changed files with 243 additions and 75 deletions
+103 -70
View File
@@ -1,104 +1,137 @@
# SMTPGraphRelay V1.4 Zertifikats-Lifecycle # SMTPGraphRelay V1.5 Log-Rotation
V1.4 erweitert V1.3 um Zertifikatsüberwachung und sichere Rotation. V1.5 erweitert V1.4 um automatische Log-Rotation und Log-Aufbewahrung.
## Relay: automatische Zertifikatsüberwachung ## Standardverhalten
Beim Start wird das Zertifikat aus `Graph.CertificateThumbprint` geprüft: Ohne Änderungen an `config.json` gelten:
- Zertifikat vorhanden ```text
- privater Schlüssel vorhanden MaxFileSizeMB = 10
- Ablaufdatum RetentionDays = 30
CleanupHours = 12
```
Standardwerte: Das aktive Log bleibt:
- Warnung ab 60 Tagen Restlaufzeit ```text
- kritisch ab 14 Tagen logs\SMTPGraphRelay.log
- erneute Prüfung alle 12 Stunden ```
Optional in `config.json`: Sobald es 10 MB erreicht, wird es z. B. umbenannt in:
```text
logs\SMTPGraphRelay-20260813-231500.log
```
Danach wird automatisch eine neue `SMTPGraphRelay.log` begonnen.
Archiv-Logs, die älter als 30 Tage sind, werden automatisch entfernt.
## Optionale Konfiguration
Die bestehende Config funktioniert unverändert.
Optional kann auf Root-Ebene ergänzt werden:
```json ```json
"Graph": { "Logging": {
"TenantId": "...", "MaxFileSizeMB": 10,
"ClientId": "...", "RetentionDays": 30,
"CertificateThumbprint": "...", "CleanupHours": 12
"SenderMailbox": "info@example.com",
"ForceSender": true,
"CertificateWarningDays": 60,
"CertificateCriticalDays": 14,
"CertificateCheckHours": 12
} }
``` ```
Die zusätzlichen Werte sind optional; bestehende Configs funktionieren unverändert. Beispiel:
## Rotation ```json
{
`Renew-SMTPGraphRelayCertificate.ps1` muss als Administrator mit "Smtp": {
**Windows PowerShell 5.1** ausgeführt werden. "ListenAddress": "0.0.0.0",
"Port": 2525,
```powershell "Hostname": "SMTPRELAY01",
cd "C:\Program Files\SMTPGraphRelay" "AllowedNetworks": [
.\Renew-SMTPGraphRelayCertificate.ps1 "127.0.0.1/32",
"10.0.0.0/8"
],
"MaxMessageSizeMB": 25,
"ClientTimeoutSeconds": 120,
"MaxConcurrentClients": 20
},
"Graph": {
"TenantId": "...",
"ClientId": "...",
"CertificateThumbprint": "...",
"SenderMailbox": "info@example.com",
"ForceSender": true
},
"Queue": {
"PollSeconds": 10,
"MaxRetries": 8,
"RetryMinutes": [1,5,15,30,60,120,240,480]
},
"Paths": {
"Queue": "queue",
"Failed": "failed",
"Logs": "logs"
},
"Logging": {
"MaxFileSizeMB": 10,
"RetentionDays": 30,
"CleanupHours": 12
}
}
``` ```
Ablauf: ## Parallel-sicher
1. bestehende Config lesen V1.5 verwendet weiterhin den benannten Mutex:
2. neues nicht exportierbares Zertifikat in `LocalMachine\My` erzeugen
3. Entra-Admin interaktiv anmelden
4. bestehende App über ihre ClientId finden
5. neues Zertifikat **zusätzlich** in `keyCredentials` hinterlegen
6. App-only Login mit dem neuen Zertifikat testen
7. erst danach `config.json` atomar auf den neuen Thumbprint umstellen
8. Relay-Scheduled-Task neu starten
9. optional altes Cloud- und lokales Zertifikat entfernen
Exchange Application RBAC muss nicht neu aufgebaut werden, weil die App/ClientId
bei der Rotation unverändert bleibt.
## Warum das alte Zertifikat zunächst erhalten bleibt
Die sichere Reihenfolge ist:
```text ```text
OLD funktioniert Local\SMTPGraphRelay-Log
|
NEW erzeugen
|
OLD + NEW in Entra
|
NEW Auth testen
|
Config -> NEW
|
Relay neu starten
|
erst dann optional OLD entfernen
``` ```
So gibt es während der Rotation keinen Zeitpunkt ohne gültiges Credential und ein Rotation und Schreiben passieren innerhalb desselben Locks. Dadurch können parallele
Rollback bleibt möglich. SMTP-Runspaces und der Queue-/Graph-Worker nicht gleichzeitig dieselbe Logdatei
rotieren oder beschreiben.
## Update von V1.3 ## Update von V1.4
Task stoppen:
```powershell ```powershell
Stop-ScheduledTask -TaskName "SMTPGraphRelay" Stop-ScheduledTask -TaskName "SMTPGraphRelay"
Copy-Item `
"C:\Program Files\SMTPGraphRelay\SMTPGraphRelay.ps1" `
"C:\Program Files\SMTPGraphRelay\SMTPGraphRelay-V1.4-backup.ps1"
``` ```
V1.3 sichern und V1.4 als `SMTPGraphRelay.ps1` ablegen, dann: Dann V1.5 als:
```text
C:\Program Files\SMTPGraphRelay\SMTPGraphRelay.ps1
```
ablegen und:
```powershell ```powershell
Start-ScheduledTask -TaskName "SMTPGraphRelay" Start-ScheduledTask -TaskName "SMTPGraphRelay"
``` ```
Die `config.json` muss nicht geändert werden. Im Log sollte beim Start u. a. erscheinen:
Im Log erscheint bei einem gesunden Zertifikat beim Start beispielsweise:
```text ```text
Graph-Zertifikat gültig bis 2028-08-13 23:00:00 (729 Tage verbleibend). SMTPGraphRelay V1.5 gestartet ...
Log-Rotation: max. 10 MB pro Datei, Aufbewahrung 30 Tage.
``` ```
## Hinweis
Nur archivierte Dateien im Muster
```text
SMTPGraphRelay-*.log
```
werden automatisch gelöscht.
Die aktuell aktive `SMTPGraphRelay.log` wird nie durch die Retention-Regel gelöscht.
+140 -5
View File
@@ -7,7 +7,7 @@
Nimmt lokale SMTP-Mails an, speichert sie als .eml in einer Queue und sendet sie Nimmt lokale SMTP-Mails an, speichert sie als .eml in einer Queue und sendet sie
anschließend per Microsoft Graph sendMail mit App-only Zertifikatsauthentifizierung. anschließend per Microsoft Graph sendMail mit App-only Zertifikatsauthentifizierung.
V1.4: Zertifikatsüberwachung, parallele SMTP-Clients, separater Queue-Worker, robuste Queue und Graph-Retry V1.5: Log-Rotation, Zertifikatsüberwachung, parallele SMTP-Clients, Queue-Worker und robuste Graph-Retry-Logik
#> #>
[CmdletBinding()] [CmdletBinding()]
@@ -18,6 +18,114 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Get-LogSettings {
$maxSizeMB = 10
$retentionDays = 30
$cleanupHours = 12
try {
if ($script:Config.Logging) {
if ($script:Config.Logging.PSObject.Properties.Name -contains "MaxFileSizeMB") {
$v = [int]$script:Config.Logging.MaxFileSizeMB
if ($v -ge 1 -and $v -le 1024) { $maxSizeMB = $v }
}
if ($script:Config.Logging.PSObject.Properties.Name -contains "RetentionDays") {
$v = [int]$script:Config.Logging.RetentionDays
if ($v -ge 1 -and $v -le 3650) { $retentionDays = $v }
}
if ($script:Config.Logging.PSObject.Properties.Name -contains "CleanupHours") {
$v = [int]$script:Config.Logging.CleanupHours
if ($v -ge 1 -and $v -le 168) { $cleanupHours = $v }
}
}
} catch {}
return [pscustomobject]@{
MaxFileSizeMB = $maxSizeMB
RetentionDays = $retentionDays
CleanupHours = $cleanupHours
}
}
function Invoke-LogMaintenance {
param(
[switch]$Force
)
if (-not $script:Config -or -not $script:Config.Paths.Logs) {
return
}
$settings = Get-LogSettings
$logDir = $script:Config.Paths.Logs
if (-not [IO.Path]::IsPathRooted($logDir)) {
if ($PSScriptRoot) {
$logDir = Join-Path $PSScriptRoot $logDir
} else {
return
}
}
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
if (-not $script:NextLogCleanup) {
$script:NextLogCleanup = [DateTime]::MinValue
}
if (-not $Force -and (Get-Date) -lt $script:NextLogCleanup) {
return
}
$cutoff = (Get-Date).AddDays(-1 * $settings.RetentionDays)
foreach ($file in Get-ChildItem -LiteralPath $logDir -File -Filter "SMTPGraphRelay-*.log" -ErrorAction SilentlyContinue) {
if ($file.LastWriteTime -lt $cutoff) {
try {
Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop
} catch {}
}
}
$script:NextLogCleanup = (Get-Date).AddHours($settings.CleanupHours)
}
function Rotate-LogIfNeeded {
param(
[Parameter(Mandatory)][string]$LogFile
)
if (-not (Test-Path -LiteralPath $LogFile)) {
return
}
$settings = Get-LogSettings
try {
$file = Get-Item -LiteralPath $LogFile -ErrorAction Stop
$maxBytes = [int64]$settings.MaxFileSizeMB * 1024 * 1024
if ($file.Length -lt $maxBytes) {
return
}
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$dir = Split-Path $LogFile -Parent
$archive = Join-Path $dir "SMTPGraphRelay-$stamp.log"
$counter = 1
while (Test-Path -LiteralPath $archive) {
$archive = Join-Path $dir ("SMTPGraphRelay-{0}-{1}.log" -f $stamp, $counter)
$counter++
}
Move-Item -LiteralPath $LogFile -Destination $archive -Force
}
catch {}
}
function Write-Log { function Write-Log {
param( param(
[Parameter(Mandatory)][string]$Message, [Parameter(Mandatory)][string]$Message,
@@ -31,28 +139,42 @@ function Write-Log {
try { try {
if ($script:Config -and $script:Config.Paths.Logs) { if ($script:Config -and $script:Config.Paths.Logs) {
$logDir = $script:Config.Paths.Logs $logDir = $script:Config.Paths.Logs
if (-not [IO.Path]::IsPathRooted($logDir)) { $logDir = Join-Path $PSScriptRoot $logDir }
if (-not [IO.Path]::IsPathRooted($logDir)) {
if ($PSScriptRoot) {
$logDir = Join-Path $PSScriptRoot $logDir
} else {
return
}
}
New-Item -ItemType Directory -Path $logDir -Force | Out-Null New-Item -ItemType Directory -Path $logDir -Force | Out-Null
$logFile = Join-Path $logDir "SMTPGraphRelay.log" $logFile = Join-Path $logDir "SMTPGraphRelay.log"
# Mehrere SMTP-Runspaces und der Queue-Worker können gleichzeitig loggen. # Mehrere SMTP-Runspaces und der Queue-Worker können gleichzeitig loggen.
# Ein benannter Mutex verhindert kollidierende Schreibzugriffe. # Ein benannter Mutex schützt Rotation und Schreibzugriff gemeinsam.
$mutex = New-Object System.Threading.Mutex($false, "Local\SMTPGraphRelay-Log") $mutex = New-Object System.Threading.Mutex($false, "Local\SMTPGraphRelay-Log")
$lockTaken = $false $lockTaken = $false
try { try {
$lockTaken = $mutex.WaitOne(5000) $lockTaken = $mutex.WaitOne(5000)
if ($lockTaken) { if ($lockTaken) {
Rotate-LogIfNeeded -LogFile $logFile
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8 Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
Invoke-LogMaintenance
} }
} }
finally { finally {
if ($lockTaken) { if ($lockTaken) {
try { $mutex.ReleaseMutex() } catch {} try { $mutex.ReleaseMutex() } catch {}
} }
$mutex.Dispose() $mutex.Dispose()
} }
} }
} catch {} }
catch {}
} }
function Resolve-PathFromConfig { function Resolve-PathFromConfig {
@@ -999,6 +1121,9 @@ $script:Config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | Conv
New-Item -ItemType Directory -Path (Resolve-PathFromConfig $script:Config.Paths.Logs) -Force | Out-Null New-Item -ItemType Directory -Path (Resolve-PathFromConfig $script:Config.Paths.Logs) -Force | Out-Null
Initialize-QueueDirectories Initialize-QueueDirectories
# Alte Logarchive beim Start direkt bereinigen.
Invoke-LogMaintenance -Force
# Zertifikat beim Start immer prüfen und Status protokollieren. # Zertifikat beim Start immer prüfen und Status protokollieren.
[void](Test-RelayCertificateExpiry -ForceLog) [void](Test-RelayCertificateExpiry -ForceLog)
@@ -1039,6 +1164,9 @@ $script:WorkerConfig = New-WorkerConfig
# Nur die Funktionen, die ein SMTP-Client wirklich benötigt, werden in die # Nur die Funktionen, die ein SMTP-Client wirklich benötigt, werden in die
# Worker-Runspaces kopiert. Graph-/Queue-Versand bleibt in einem separaten Worker. # Worker-Runspaces kopiert. Graph-/Queue-Versand bleibt in einem separaten Worker.
$smtpFunctionNames = @( $smtpFunctionNames = @(
"Get-LogSettings",
"Invoke-LogMaintenance",
"Rotate-LogIfNeeded",
"Write-Log", "Write-Log",
"Resolve-PathFromConfig", "Resolve-PathFromConfig",
"Test-IPv4InCidr", "Test-IPv4InCidr",
@@ -1072,6 +1200,9 @@ $script:ActiveSmtpWorkers = New-Object System.Collections.ArrayList
# Damit ein langsamer Graph-Aufruf nicht mehr die Annahme neuer SMTP-Verbindungen # Damit ein langsamer Graph-Aufruf nicht mehr die Annahme neuer SMTP-Verbindungen
# blockiert, läuft Process-Queue dauerhaft in einem eigenen Runspace. # blockiert, läuft Process-Queue dauerhaft in einem eigenen Runspace.
$queueFunctionNames = @( $queueFunctionNames = @(
"Get-LogSettings",
"Invoke-LogMaintenance",
"Rotate-LogIfNeeded",
"Write-Log", "Write-Log",
"Resolve-PathFromConfig", "Resolve-PathFromConfig",
"Get-GraphConnection", "Get-GraphConnection",
@@ -1120,11 +1251,15 @@ $listenIp = [System.Net.IPAddress]::Parse($script:Config.Smtp.ListenAddress)
$listener = [System.Net.Sockets.TcpListener]::new($listenIp, [int]$script:Config.Smtp.Port) $listener = [System.Net.Sockets.TcpListener]::new($listenIp, [int]$script:Config.Smtp.Port)
$listener.Start() $listener.Start()
Write-Log "SMTPGraphRelay V1.3 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)" Write-Log "SMTPGraphRelay V1.5 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
Write-Log "Graph-Absender: $($script:Config.Graph.SenderMailbox)" Write-Log "Graph-Absender: $($script:Config.Graph.SenderMailbox)"
Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients" Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients"
Write-Log "Queue-/Graph-Worker läuft separat vom SMTP-Listener." Write-Log "Queue-/Graph-Worker läuft separat vom SMTP-Listener."
$logSettings = Get-LogSettings
Write-Log ("Log-Rotation: max. {0} MB pro Datei, Aufbewahrung {1} Tage." -f `
$logSettings.MaxFileSizeMB, $logSettings.RetentionDays)
try { try {
while ($true) { while ($true) {
Remove-CompletedSmtpWorkers Remove-CompletedSmtpWorkers