Monitor je PMG met Zabbix

Ingediend door Dirk Hornstra op 13-jul-2026 21:35

Onze vorige hosting-partner was Vevida, die verschillende SMTP-servers had om mail te versturen, dat werkte als een trein. Your Hosting heeft heel veel hostingbedrijven overgenomen, zo ook Vevida en zo konden we gebruik blijven maken van deze dienst. Maar na verloop van tijd leek het toch beter om over te stappen naar een andere partij. Dat hebben we gedaan en dat is over het algemeen best soepel verlopen.

Naast dat we "websites maken", wordt er via die websites ook mail verstuurd. Soms een mailtje vanaf een contactformulier, maar ook grotere hoeveelheden als de eigenaar van de website naar een groep mensen een nieuwsbrief of iets dergelijks wil mailen. We hebben eerst naar "producten in de markt" gekeken, maar dat bood ons niet wat we wilden. We wilden namelijk een soort SMTP-server die meteen een true/false teruggeeft of een mail succesvol verzonden kon worden. De online diensten bieden vaak de mogelijkheid om de mail aan te bieden, maar daar wordt het in een queue gezet die later verwerkt wordt. Je krijgt dus geen directe response of het mailen nu wel of niet gelukt is en ook kon je niet na de tijd nog opvragen of een mail nu wel of niet verzonden kon worden. Daarom heeft onze hostingpartner een PMG-omgeving opgezet (Proxmox Mail Gateway).

Dat is een soort proxy, je kunt zien welke mails verstuurd zijn (en welke niet), een heel mooi pakket.

 

Eind februari 2026 klaagde een klant dat mails niet aankwamen. We zagen dat alle mails netjes verzonden werden, behalve de adressen waarbij het domein eindigde op @hotmail.com en @live.nl. Microsoft dus… en je kon ook in de response-headers zien dat de protectie van Microsoft ons IP als onbetrouwbaar zag en er een tijdelijke rate-limit op gegooid had. Tijdelijk was bijna 2 dagen en het lag dus ook nog eens niet aan ons. Zie deze thread op learn.microsoft.com en op status.yourhosting.nl werd het onderstaande bericht gedeeld:

 

Current Status: Prestatieproblemen 
Started: February 25, 2026 19:59 CET 
Resolved: 

Affected Infrastructure 
Components: Yourhosting, Argeweb, De Heeg, RealHosting, SoHosted, Vevida, VIP Internet 
Locations: E-mail Hosting, Webmail 

Update: 
Op dit moment ontvangen wij op alle labels meldingen dat e-mailverkeer richting specifieke Microsoft-ontvangers (zoals outlook.com, hotmail.com, live.nl, enz.) wordt geweigerd.

Aan onze zijde nemen wij alle mogelijke maatregelen om de aflevering te optimaliseren en eventuele oorzaken uit te sluiten. Op basis van onze bevindingen en signalen uit de markt vermoeden wij echter dat er momenteel sprake is van overmatige ratelimiting en/of blacklisting aan de zijde van Microsoft. Dit vermoeden wordt versterkt door het feit dat het probleem gelijktijdig optreedt bij meerdere providers.

Wij blijven de situatie actief monitoren en ondernemen waar mogelijk verdere actie. Zodra er nieuwe ontwikkelingen zijn, informeren wij u direct.

Mocht je ooit tegen dit probleem aanlopen, vul dan dit online formulier in, bij ons loste dat redelijk snel het probleem op.

 

Uiteindelijk is het probleem (gelukkig) opgelost, maar het feit dat we pas actie konden ondernemen nadat een klant ons er op attent gemaakt had, triggerde de vraag: hadden we dit ook zelf eerder kunnen zien? Want dan hadden we misschien de tijd dat "de boel eruit lag" kunnen verkorten.

Het antwoord is ja, in de portal heb je een menu-item Queues, daar kun je zien hoeveel mails er een x tijd al staan te wachten om verzonden te worden (omdat de eerste keer niet lukte). En een top 5 qua aantallen per domein. Totaal was toen iets van 1200 en 500 nog wat voor @hotmail.com domeinen.

We willen natuurlijk niet elke dag handmatig inloggen op het portal, naar deze pagina gaan en dan kijken hoe het met de queue's zit. En dat hoeft ook niet. Met Zabbix kun je scripts uitvoeren en zo waardes die je terugkrijgt als "items" registreren. Daar kun je dan weer triggers aan koppelen die actief worden als bepaalde waardes overschreden worden. 

De vraag was dus, biedt de PMG een API die dit mogelijk maakt? En (gelukkig) vond ik daar deze documentatie: link naar API. (ik weet trouwens niet of dit de correcte link is, maar met mijn voorbeelden moet je er wel uit komen).

Je moet eerst een ticket aanmaken, daarbij krijg je een CSRF token (niet nodig) en een key terug. Die key moet je in je volgende request als cookie meegeven, dan ben je geauthenticeerd.

Dat ging bij mij eerst nog fout, omdat in het voorbeeld gesproken wordt over PMV. Maar dat is iets anders, ons cookie moet met PMG (Proxmox Mail Gateway) beginnen.

 

Hierbij hoe "wij" het nu ingericht hebben;

We hebben 2 scripts gemaakt. Dit zijn Powershell-scripts. Met perl, ruby en andere programmeertalen kun je waarschijnlijk hetzelfde doen, omdat de code bij ons op een proxy draait op een Windows-machine, was Powershell voor ons de makkelijkste optie;

Het eerste script is pmg-queue-summary.ps1, deze geeft de samenvatting van de queue's:


 

$summaryUri = "https://domain:8006/api2/json/nodes/pmg/postfix/qshape/"
$ticketUri = "https://domain:8006/api2/json/access/ticket"

function CreateCookie($name, $value, $domain, $path="/"){
    $c=New-Object System.Net.Cookie;
    $c.Name=$name;
    $c.Path=$path;
    $c.Value = $value
    $c.Domain =$domain;
    return $c;
}

$header = @{
    "Accept" = "application/json"
    "Content-Type" = "application/json"
}
$postParams = @{
    "username" = "[USERNAME PROXMOX]"
    "password" = "[PASSWORD PROXMOX]"
} | ConvertTo-Json
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$result = Invoke-WebRequest -Uri $ticketUri -Method POST -Headers $header -Body $postParams -SessionVariable session -UseBasicParsing
$content = $result | Select-Object -ExpandProperty Content | ConvertFrom-Json
$header = @{
    "Accept" = "application/json"
    "Content-Type" = "application/json"
}
$session.Cookies.Add((CreateCookie -name "PMGAuthCookie" -value $content.data.ticket -domain "domain"));
$result = Invoke-WebRequest -Uri $summaryUri -Method GET -Headers $header -WebSession $session -UseBasicParsing
Write-Output $result.Content

Het tweede script is pmg-tracking-bounce-summary.ps1:

param(
    [string]$StartBackground = "0"
)

$fileName = "pmg-bounce-summary.json"
$outFile = Join-Path -Path (Split-Path -Parent $MyInvocation.MyCommand.Definition) -ChildPath $fileName

# If output already exists and no refresh requested, return it and optionally start background
if ((Test-Path $outFile) -and ($StartBackground -eq "0")) {
    Get-Content -Path $outFile -Raw -Encoding utf8
    return
}

$endTime = [int][double]::Parse((Get-Date -UFormat %s))
$startTime = $endTime - (1 * 24 * 60 * 60)

$summaryUri = "https://domain:8006/api2/json/nodes/pmg/tracker?xfilter=bounce&starttime=$startTime&endtime=$endTime"
$ticketUri = "https://domain:8006/api2/json/access/ticket"

function CreateCookie($name, $value, $domain, $path="/"){
    $c=New-Object System.Net.Cookie;
    $c.Name=$name;
    $c.Path=$path;
    $c.Value = $value
    $c.Domain =$domain;
    return $c;
}

$header = @{
    "Accept" = "application/json"
    "Content-Type" = "application/json"
}
$postParams = @{
    "username" = "[USERNAME PROXMOX]"
    "password" = "[PASSWORD PROXMOX]"
} | ConvertTo-Json
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$result = Invoke-WebRequest -Uri $ticketUri -Method POST -Headers $header -Body $postParams -SessionVariable session -UseBasicParsing
$content = $result | Select-Object -ExpandProperty Content | ConvertFrom-Json
$header = @{
    "Accept" = "application/json"
    "Content-Type" = "application/json"
}
$session.Cookies.Add((CreateCookie -name "PMGAuthCookie" -value $content.data.ticket -domain "domain"));
$result = Invoke-WebRequest -Uri $summaryUri -Method GET -Headers $header -WebSession $session -UseBasicParsing
$content = $result | Select-Object -ExpandProperty Content | ConvertFrom-Json   

$first = $true

"" | Out-File -FilePath $outFile -Encoding utf8

Add-Content -Path $outFile -Value "{`"data`":["
foreach ($item in $content.data) {
    $detailUri = "https://domain:8006/api2/extjs/nodes/pmg/tracker/$($item.qid)?starttime=$startTime&endtime=$endTime"
    try {     
        $result = Invoke-WebRequest -Uri $detailUri -Method GET -Headers $header -WebSession $session -UseBasicParsing
        if (-not $first) {
            Add-Content -Path $outFile -Value ","
        }
        Add-Content -Path $outFile -Value $result.Content
        $first = $false
    }
    catch {
        Write-Error "Failed to get details for qid $($item.qid): $_"
        continue
    }
}
Add-Content -Path $outFile -Value "]}"

Je ziet dat ik in het 2e script een parameter StartBackground mee geef. Dat komt omdat dit script langere tijd nodig heeft om de output terug te geven. Daarom laat ik op de machine via een scheduled taak op geregelde tijden het .JSON-bestand genereren, als er dan vanuit Zabbix een request komt, hoef ik alleen maar het .JSON-bestand terug te geven.

 

We hebben in de Zabbix configuratie deze User Action toegevoegd:



UserParameter=proxmox.summary[*],d:\locatie\scripts\run-pmg-queue-summary.bat $1
UserParameter=proxmox.bounce[*],d:\locatie\scripts\run-pmg-tracking-bounce-summary.bat $1
 

Je ziet dat we hier niet rechtstreeks de Powershell-scripts met Powershell opstarten, dat komt omdat zaken op de computer behoorlijk dicht staan: en dat is goed. Daarom starten we de processen op via een batch-bestand, want dan kun je aangeven dat je een Powershell-bestand met net iets meer rechten wilt starten:



@echo off
powershell -ExecutionPolicy Bypass -File d:\locatie\scripts\pmg-tracking-bounce-summary.ps1
 


Vervolgens deze items aangemaakt:

 

Item: Mailqueue Summary:
Key: proxmox.summary[{HOST.NAME}]
Update interval: 5m

Item Mailqueue Bounce Summary
Key: proxmox.bounce[{HOST.NAME}]
Update interval: 1h

 

Op basis van deze hoofd-items maak je sub-items aan. Die maak je type "dependent item" en koppel je aan 1 van bovenstaande items. Je krijgt dan de waardes binnen die de hoofditems genereren, vervolgens kun je met transformaties zorgen dat een sub-items zijn eigen data heeft. De sub-items die ik aangemaakt heb:

 

Mailqueue Bounce DKim Failure Count
JSONPath
$.data[*]["data"]["logs"]
JavaScript
return (value.match(/Dkim= Fail/g) || []).length

----------------------------------------------------
Mailqueue Bounce DMARC Failure Count
JSONPath
$.data[*]["data"]["logs"]
JavaScript
return (value.match(/DMARC= Fail/g) || []).length

----------------------------------------------------
Mailqueue Bounce Spf Failure Count
JSONPath
$.data[*]["data"]["logs"]
JavaScript
return (value.match(/Spf= Fail/g) || []).length

----------------------------------------------------
Mailqueue Bounce DKim Failure Senders
JSONPath
$.data[*]["data"]
JavaScript
var items = JSON.parse(value);
var frequency = [];
var max = 0;
var mostFrequent;

items.forEach(function(item) {
    var i = item["logs"].toString();
    if (i.match(/Dkim= Fail/g)) {
        frequency.push(item["from"]);
    }
});

var frequencyCount = {};
var maxCount = 0;
var mostFrequentItem = "";   

frequency.forEach(function(item) {
    frequencyCount[item] = (frequencyCount[item] || 0) + 1;
    if (frequencyCount[item] > maxCount) {
        maxCount = frequencyCount[item];
        mostFrequentItem = item;
    }
});

return mostFrequentItem;

----------------------------------------------------
Mailqueue Bounce DMARC Failure Senders
JSONPath
$.data[*]["data"]
JavaScript
var items = JSON.parse(value);
var frequency = [];
var max = 0;
var mostFrequent;

items.forEach(function(item) {
    var i = item["logs"].toString();
    if (i.match(/DMARC= Fail/g)) {
        frequency.push(item["from"]);
    }
});

var frequencyCount = {};
var maxCount = 0;
var mostFrequentItem = "";   

frequency.forEach(function(item) {
    frequencyCount[item] = (frequencyCount[item] || 0) + 1;
    if (frequencyCount[item] > maxCount) {
        maxCount = frequencyCount[item];
        mostFrequentItem = item;
    }
});

return mostFrequentItem;

----------------------------------------------------
Mailqueue Bounce Spf Failure Senders
JSONPath
$.data[*]["data"]
JavaScript
var items = JSON.parse(value);
var frequency = [];
var max = 0;
var mostFrequent;

items.forEach(function(item) {
    var i = item["logs"].toString();
    if (i.match(/Spf= Fail/g)) {
        frequency.push(item["from"]);
    }
});

var frequencyCount = {};
var maxCount = 0;
var mostFrequentItem = "";   

frequency.forEach(function(item) {
    frequencyCount[item] = (frequencyCount[item] || 0) + 1;
    if (frequencyCount[item] > maxCount) {
        maxCount = frequencyCount[item];
        mostFrequentItem = item;
    }
});

return mostFrequentItem;

----------------------------------------------------
Mailqueue Bounce Number of Mails
JSONPath
$.data.length()

----------------------------------------------------
Mailqueue Most Items Domain
JSONPath
$.data[1]["domain"]

----------------------------------------------------
Mailqueue Most Items Total
JSONPath
$.data[1]["total"]

----------------------------------------------------
Mailqueue Total
JSONPath
$.data[0]["total"]

 

 

We willen notificaties ontvangen bij problemen. Dus we moeten op deze items "triggers" instellen. De eerste wordt actief als de mailqueue volloopt, de andere als er meer dan 5 mails fout gaan op basis van DKIM en de laatste als er meer dan 5 mails fout gaan op basis van DMARC.

 

Naam: Monitoring queue proxmox mail gateway

Expressie: last(/mailhost/pmg.mailqueue.total)>70 or last(/mailhost/pmg.mailqueue.most.items.total)>50


Naam: Monitoring tracking center pmg – DKIM

Expressie: last(/mailhost/pmg.mailqueue.bounced.dkim.failuresenders)<>"" and last(/mailhost/pmg.mailqueue.bounced.dkim.failurecount)>5


Naam: Monitoring tracking center pmg.tres.by-cj2.nl – DMARC

Expressie: last(/mailhost/pmg.mailqueue.bounced.dmarc.failuresenders)<>"" and last(/mailhost/pmg.mailqueue.bounced.dmarc.failurecount)>5

 

 

Je ziet dat dit "willekeurige waardes" zijn. Wanneer moet je systeem beginnen te piepen en wanneer zijn de meldingen "terecht" en hoef je er niets van te weten? Want er zijn best veel mensen die "per ongeluk" niet "@hotmail.com" maar "@homail.com" invullen en andere foutieve mailadressen. Daarom hebben wij de keuze gemaakt om een alarm te activeren op het moment dat de totale queue van wachtende e-mails boven de 70 stuks komt of op het moment dat er van 1 domein meer dan 20 e-mails staan te wachten.

Voor jouw configuratie moet je hier misschien nog een beetje mee schuiven. Voor ons was het de afgelopen week een correcte configuratie: KPN en ZIGGO hadden namelijk beide besloten dat onze mailserver geblokkeerd moest worden. Nu werden er niet heel veel mails verstuurd, maar we kwamen boven de 70 items in de queue grens: dus de trigger werd actief. Onze hostingpartner heeft het opgepakt en daardoor was het deze keer binnen 4 uur opgelost (in plaats van die 2 dagen van de vorige keer).

Nu kwam het ook omdat er in een omgeving naar een mailadres mails werden gestuurd naar een mailbox die niet meer bestond: ook dat moest gefixt worden (en dat hebben we gedaan). Maar als die "bug" er niet was geweest, had het langer geduurd voordat we gezien hadden dat er een probleem was, daarom hebben we de notificatie aangepast, als er meer dan 50 items in de queue staan, maken we de trigger actief.