Zertifikatsbericht per NITRO: warum abgelaufene Zertifikate fehlen
Ein Skript fragt über NITRO, die REST-Schnittstelle der NetScaler, alle Zertifikate einer Appliance ab und sortiert sie nach Restlaufzeit. Es läuft ohne Fehler, der Zertifikatsbericht ist sauber formatiert, und trotzdem tauchen abgelaufene Zertifikate darin nicht auf. Der Grund liegt in einem einzigen Feld der API, und die Lösung ist, ein anderes Feld zur Grundlage zu machen.
Die Werte in diesem Beitrag stammen von meiner Testinstanz mit NetScaler 14.1, auf der ich dafür Testzertifikate mit bekanntem Ablaufdatum angelegt habe.
Warum abgelaufene Zertifikate im Zertifikatsbericht fehlen
Die Ressource sslcertkey liefert je Zertifikat unter anderem drei Felder, die für einen Bericht interessant sind: status, daystoexpiration und clientcertnotafter. Die Dokumentation beschreibt daystoexpiration als die Tage, die dem Zertifikat bis zum Ablauf bleiben. Was sie nicht sagt: Der Wert wird nie negativ.
So sieht das für fünf Zertifikate aus, abgefragt am 02.10.2026 gegen 13:40 UTC:
| Zertifikat | clientcertnotafter | status | daystoexpiration |
|---|---|---|---|
| seit vier Wochen abgelaufen | Sep 5 12:00:00 2026 GMT | Expired | 0 |
| läuft am selben Abend ab | Oct 2 22:00:00 2026 GMT | Valid | 0 |
| läuft in knapp sieben Tagen ab | Oct 9 12:00:00 2026 GMT | Valid | 6 |
| läuft in einem Jahr ab | Oct 2 13:36:23 2027 GMT | Valid | 364 |
| internes Zertifikat der Appliance | Jan 17 20:33:25 4764 GMT | Valid | 999775 |
Ein Filter wie daystoexpiration -lt 0 liefert damit keinen einzigen Treffer, obwohl ein abgelaufenes Zertifikat in der Liste steht. Es gibt keine Fehlermeldung und keine Warnung. Das Ergebnis ist einfach leer.
Ein Skript, das mit einem Fehler abbricht, fällt sofort auf. Ein leeres Ergebnis dagegen sieht aus wie die Meldung „keine abgelaufenen Zertifikate“.
Null heißt nicht abgelaufen
Die Tabelle zeigt eine zweite Eigenheit: Den Wert 0 tragen zwei Zertifikate, ein abgelaufenes und ein gültiges. Der Zähler wird auf ganze Tage abgeschnitten, ein Zertifikat mit sechs Tagen und 22 Stunden Restlaufzeit steht auf 6, eines mit acht Stunden auf 0.
Ein Filter auf daystoexpiration -eq 0 findet also das abgelaufene Zertifikat, meldet aber zugleich ein gültiges als abgelaufen. Unterscheiden lassen sich die beiden nur über status oder über das Ablaufdatum.
Auf der Kommandozeile ist der Unterschied sichtbar, weil show ssl certkey bei einem abgelaufenen Zertifikat gar keine Tage ausgibt:
Name: tz_abgel Status: -->Expired<--
Name: tz_bald Status: Valid, Days to expiration:6
Wer den Bericht aus der API baut, sieht nur die Zahl.
Zertifikate mit Ablauf im Jahr 4764
Am anderen Ende der Skala stehen die internen Zertifikate der Appliance. Auf meiner Testinstanz läuft ns-sftrust-certificate im Jahr 4764 ab, daystoexpiration steht entsprechend auf 999775. Der Wert ist echt gerechnet und wird jeden Tag um eins kleiner.
Ungefiltert landen solche Einträge am Ende jeder Sortierung, und im Bericht steht ein Ablauf in gut 2700 Jahren. Solche Zeilen lenken vom Wesentlichen ab. Weil der Wert sich ändert, taugt er nicht als fester Vergleichswert. Eine Schwelle tut es: Alles, was weiter als 100 Jahre in der Zukunft liegt, kommt in eine eigene Stufe.
Die Restlaufzeit ist eine Momentaufnahme
Der Tageswert stimmt im Moment der Abfrage. Ein Bericht, der drei Wochen später aus dem Postfach geholt wird, nennt eine Restlaufzeit, die es nicht mehr gibt. Wer Ablaufdatum und Zeitpunkt des Laufs mit ausgibt, macht den Bericht auch später noch lesbar.
Wie du es löst: das Ablaufdatum als Grundlage
Das Ablaufdatum in clientcertnotafter ist ein absoluter Wert. Es bleibt nicht bei null stehen, kennt keine Sonderwerte und altert nicht zwischen Abfrage und Lektüre. Die Restlaufzeit lässt sich daraus jederzeit neu berechnen, umgekehrt geht das nicht.
- Das Ablaufdatum in einen echten Datumstyp umwandeln, nicht als Zeichenkette weiterreichen.
- Abgelaufen ist, was ein Ablaufdatum in der Vergangenheit hat. Kein Zählerfilter.
- Sehr ferne Ablaufdaten über eine Schwelle abfangen und in eine eigene Stufe legen.
- Ablaufdatum, Resttage und den Zeitpunkt des Laufs gemeinsam ausgeben.
Die Falle im Datumsformat
clientcertnotafter füllt einen einstelligen Tag mit einem zusätzlichen Leerzeichen auf: Sep 5 12:00:00 2026 GMT. Ein festes Muster wie MMM d HH:mm:ss yyyy 'GMT' scheitert daran mit String was not recognized as a valid DateTime, und zwar nur an den Tagen 1 bis 9 eines Monats. Ein Skript, das im Test am 15. lief, fällt dann am 5. des Folgemonats aus.
Vor dem Parsen die Leerzeichen auf eines reduzieren:
$text = $text -replace '\s+', ' '
Alles in einer Bewertungsfunktion
Die Sonderfälle gehören an eine Stelle, ganz an den Anfang der Verarbeitung. Eine kleine Funktion macht aus dem Ablaufdatum eine eindeutige Stufe: abgelaufen, kritisch, demnächst, unkritisch oder ohne praktisches Ablaufdatum. Ab da arbeitet der Rest des Skripts nur noch mit der Stufe, nie mehr mit den Rohwerten.
Dazu kommt eine Prüfung, die wenige Zeilen kostet: ein Referenzobjekt. Ein Zertifikat, von dem feststeht, dass es existiert. Fehlt es im Ergebnis, bricht der Lauf ab, statt einen Bericht zu erzeugen. ns-server-certificate eignet sich dafür, weil es auf jeder Appliance vorhanden ist.
Der folgende Block läuft unter Windows PowerShell 5.1 und PowerShell 7. Er meldet sich per NITRO über HTTPS am FQDN an, mit regulärer Zertifikatsprüfung, fragt die Zertifikate ab und meldet sich wieder ab. Leserechte genügen.
$Appliance = 'adc01.example.com' # FQDN, passend zum Zertifikat der Verwaltung
$Referenz = 'ns-server-certificate' # gibt es auf jeder Appliance
$Kultur = [Globalization.CultureInfo]::InvariantCulture
function Get-ZertifikatsStufe {
param([string]$NotAfter, [datetime]$Jetzt)
$text = $NotAfter -replace '\s+', ' ' # "Sep 5" wird zu "Sep 5"
$ablauf = [datetime]::ParseExact($text, "MMM d HH:mm:ss yyyy 'GMT'", $Kultur,
[Globalization.DateTimeStyles]'AssumeUniversal, AdjustToUniversal')
$tage = ($ablauf - $Jetzt).TotalDays
$stufe = if ($tage -lt 0) { 'abgelaufen' }
elseif ($tage -lt 14) { 'kritisch' }
elseif ($tage -lt 60) { 'demnaechst' }
elseif ($tage -gt 36500) { 'ohne praktisches Ablaufdatum' }
else { 'unkritisch' }
[pscustomobject]@{ Ablauf = $ablauf; Tage = [int][math]::Truncate($tage); Stufe = $stufe }
}
if (-not $Credential) { $Credential = Get-Credential -Message 'NetScaler-Anmeldung (Leserechte genuegen)' }
$basis = "https://$Appliance/nitro/v1/config"
$rumpf = @{ login = @{ username = $Credential.UserName.Split('\')[-1]
password = $Credential.GetNetworkCredential().Password } } | ConvertTo-Json
$null = Invoke-RestMethod -Method Post -Uri "$basis/login" -Body $rumpf `
-ContentType 'application/json' -SessionVariable sitzung
try {
$lauf = [datetime]::UtcNow
$liste = (Invoke-RestMethod -Uri "$basis/sslcertkey?attrs=certkey,status,clientcertnotafter,daystoexpiration" `
-WebSession $sitzung).sslcertkey
if ($liste.certkey -notcontains $Referenz) { throw "Referenz $Referenz fehlt im Ergebnis, kein Bericht" }
$bericht = foreach ($z in @($liste)) {
$s = Get-ZertifikatsStufe -NotAfter $z.clientcertnotafter -Jetzt $lauf
[pscustomobject]@{ Zertifikat = $z.certkey; Stufe = $s.Stufe; Ablauf = $s.Ablauf.ToString('yyyy-MM-dd HH:mm')
Tage = $s.Tage; NITRO = "$($z.status), $($z.daystoexpiration)"; Lauf = $lauf.ToString('yyyy-MM-dd HH:mm') }
}
$bericht | Sort-Object Tage | Format-Table -AutoSize
}
finally {
try { $null = Invoke-RestMethod -Method Post -Uri "$basis/logout" -Body '{"logout":{}}' `
-ContentType 'application/json' -WebSession $sitzung } catch { }
}
Auf meiner Testinstanz sieht das Ergebnis so aus (gekürzt):
Zertifikat Stufe Ablauf Tage NITRO Lauf
---------- ----- ------ ---- ----- ----
tz_abgel abgelaufen 2026-09-05 12:00 -27 Expired, 0 2026-10-02 13:38
tz_heute kritisch 2026-10-02 22:00 0 Valid, 0 2026-10-02 13:38
tz_bald kritisch 2026-10-09 12:00 6 Valid, 6 2026-10-02 13:38
ns-sftrust-certificate ohne praktisches Ablaufdatum 4764-01-17 20:33 999775 Valid, 999775 2026-10-02 13:38
Die Spalte NITRO steht nur zum Vergleich da: Zweimal derselbe Rohwert 0, zwei verschiedene Stufen. Die Schwellen von 14 und 60 Tagen sind ein Vorschlag und lassen sich an einer Stelle ändern.
Serverseitig filtern lässt sich sslcertkey übrigens nicht: Ein filter= auf status oder daystoexpiration beantwortet die Appliance mit Invalid filter in query parameters. Die ganze Liste abzuholen und lokal zu bewerten, ist deshalb ohnehin der Weg.
Wer die Abfrage über die NetScaler Console als Proxy an die Appliance weiterreicht, bekommt die Antwort der Appliance zurück. Was auf dem Weg dorthin schiefgehen kann, steht im Beitrag Instanz nicht verfügbar beim ADM-Proxy.
Wenn der Bericht trotzdem nicht stimmt
| Bild | Ursache und Lösung |
|---|---|
| Ein bekannt abgelaufenes Zertifikat fehlt | Filter auf negative Resttage. Über das Ablaufdatum gehen |
| Ein gültiges Zertifikat steht als abgelaufen im Bericht | Filter auf daystoexpiration -eq 0. Über das Ablaufdatum oder status gehen |
| Ein Eintrag läuft in über 2700 Jahren ab | Internes Zertifikat. Über eine Schwelle in eine eigene Stufe legen |
| Das Datum lässt sich an manchen Tagen nicht parsen | Doppeltes Leerzeichen vor einstelligem Tag. Vor dem Parsen ersetzen |
| Der Bericht ist leer oder auffällig kurz | Abfrage unvollständig. Referenzobjekt prüfen und den Lauf abbrechen lassen |
Was du dir merkst
Bei einem abgelaufenen Zertifikat steht
daystoexpirationauf 0, nicht auf einem negativen Wert, und ein gültiges Zertifikat mit weniger als einem Tag Restlaufzeit ebenso. Ein Filter auf negative Werte findet nie etwas. Grundlage eines Zertifikatsberichts ist das Ablaufdatum, die Restlaufzeit wird daraus berechnet.
Quellen
- NITRO API: sslcertkey, NetScaler-Dokumentation: Felder
clientcertnotafter,daystoexpiration,status
Gemessen an meiner Testinstanz mit NetScaler 14.1. Das Verhalten der Felder kann sich mit künftigen Builds ändern. Deshalb das Ablaufdatum als Grundlage nehmen und nicht den Zähler.

Hinterlasse einen Kommentar
An der Diskussion beteiligen?Hinterlasse uns deinen Kommentar!