Skip to content

Expiration Validator

This is the validator that catches the most common certificate incident there is: a certificate that has expired, or is about to.

It reports how long until the certificate's notAfter date, and it flags the situations you care about: the cert is already expired, isn't valid yet, is approaching expiration, or was issued for longer than your lifetime policy allows.

Enabled by default

You don't have to turn this one on. expiration is one of the three default validators, along with hostname and root_certificate.

Try it

Let's run it against a host:

from certmonitor import CertMonitor

with CertMonitor("example.com") as monitor:
    monitor.get_cert_info()
    print(monitor.validate()["expiration"])

A healthy certificate comes back valid, with the days remaining:

These examples show selected fields from illustrative scans. validate() also adds status and code, described in the result contract.

{
  "is_valid": true,
  "days_to_expiry": 56,
  "expires_on": "2026-08-08T22:14:02+00:00",
  "lifetime_days": 365,
  "lifetime_limit_days": 398,
  "warnings": []
}

An expired one flips is_valid to false and adds a reason you can drop straight into an alert:

{
  "is_valid": false,
  "days_to_expiry": -4080,
  "expires_on": "2015-04-12T23:59:59+00:00",
  "warnings": ["Certificate is expired and has been expired for (-4080 days)"],
  "reason": "Certificate expired 4080 days ago (expired on 2015-04-12).",
  "lifetime_days": 3,
  "lifetime_limit_days": 1187
}

A valid certificate can still warn you

is_valid only turns false when the certificate is expired or not yet valid. A certificate can be perfectly valid and still carry a warning: it expires within the warning threshold (time to renew), including when less than one day remains, or its total lifetime exceeds the limit. So watch the warnings list, and status, not just is_valid.

Arguments

Pass via validator_args={"expiration": {...}}. The arguments and their defaults are documented in the reference below.

Thresholds accept fractional days and require 0 <= critical_days <= warning_days.

The lifetime policy defaults to "public": the CA/Browser Forum limit that applied on the certificate's issue date. That is 825 days from March 2018, 398 from September 2020, 200 from March 2026, 100 from March 2027, and 47 from March 2029, so a certificate is judged by the rule it was issued under, and the limit used is reported in lifetime_limit_days. The check warns rather than fails. Pass a number for a private PKI policy, or None to skip the check:

results = monitor.validate({"expiration": {"max_lifetime_days": 825}})

When notBefore is missing from the certificate data, the validity-start and lifetime checks are skipped and lifetime_days is omitted.

Reference

certmonitor.validators.expiration.ExpirationValidator

Bases: BaseCertValidator

A validator for checking the expiration date of an SSL certificate.

Attributes:

Name Type Description
name str

The name of the validator.

name class-attribute instance-attribute

name: str = 'expiration'

validate

validate(cert: dict[str, Any], host: str, port: int, *, warning_days: float = 7, critical_days: float = 1, max_lifetime_days: float | str | None = PUBLIC_TLS_POLICY) -> ExpirationResult

Validates the validity window of the provided SSL certificate and its total lifetime.

The certificate fails when it is expired or not yet valid. Approaching expiry and an over-long total lifetime are reported as warnings, so is_valid stays True while status becomes warn.

Parameters:

Name Type Description Default
cert dict

The SSL certificate.

required
host str

The hostname (not used in this validator).

required
port int

The port number (not used in this validator).

required
warning_days float

Warn when this many days or fewer remain. Defaults to 7.

7
critical_days float

Use a critical warning within this many days. Defaults to 1.

1
max_lifetime_days float | str

Warn when the total lifetime from notBefore to notAfter exceeds this many days. Defaults to "public", the CA/Browser Forum limit that applied on the certificate's issue date (825 days from March 2018, 398 from September 2020, 200 from March 2026, 100 from March 2027, 47 from March 2029). Pass a number to set your own limit for a private PKI, or None to disable the check. Fractional-day thresholds are supported, including less than one day remaining.

PUBLIC_TLS_POLICY

Returns:

Name Type Description
dict ExpirationResult

A dictionary containing the validation results, including whether the certificate is valid, the number of days until expiry, the expiration date, the total lifetime in days and the limit it was compared with (when notBefore is available), and any warnings.

Raises:

Type Description
ValueError

If thresholds do not satisfy 0 <= critical_days <= warning_days, or max_lifetime_days is neither "public", a positive number, nor None.

Examples:

Example output (success): This example shows a certificate that is valid and has 120 days until expiration, so no warnings are present.

```json
{
    "is_valid": true,
    "days_to_expiry": 120,
    "expires_on": "2025-09-01T23:59:59+00:00",
    "lifetime_days": 365,
    "lifetime_limit_days": 398,
    "warnings": []
}
```

Example output (failure): This example shows a certificate that expired 10 days ago, so validation fails and a warning is included.

```json
{
    "is_valid": false,
    "days_to_expiry": -10,
    "expires_on": "2025-04-30T23:59:59+00:00",
    "lifetime_days": 365,
    "lifetime_limit_days": 398,
    "warnings": [
        "Certificate is expired and has been expired for (-10 days)"
    ],
    "reason": "Certificate expired 10 days ago (expired on 2025-04-30)."
}
```
Source code in certmonitor/validators/expiration.py
def validate(
    self,
    cert: dict[str, Any],
    host: str,
    port: int,
    *,
    warning_days: float = 7,
    critical_days: float = 1,
    max_lifetime_days: float | str | None = PUBLIC_TLS_POLICY,
) -> ExpirationResult:
    """
    Validates the validity window of the provided SSL certificate and its total lifetime.

    The certificate fails when it is expired or not yet valid. Approaching
    expiry and an over-long total lifetime are reported as warnings, so
    `is_valid` stays `True` while `status` becomes `warn`.

    Args:
        cert (dict): The SSL certificate.
        host (str): The hostname (not used in this validator).
        port (int): The port number (not used in this validator).
        warning_days (float): Warn when this many days or fewer remain. Defaults to 7.
        critical_days (float): Use a critical warning within this many days. Defaults to 1.
        max_lifetime_days (float | str, optional): Warn when the total lifetime from
            notBefore to notAfter exceeds this many days. Defaults to `"public"`, the
            CA/Browser Forum limit that applied on the certificate's issue date (825
            days from March 2018, 398 from September 2020, 200 from March 2026, 100
            from March 2027, 47 from March 2029). Pass a number to set your own limit
            for a private PKI, or `None` to disable the check. Fractional-day
            thresholds are supported, including less than one day remaining.

    Returns:
        dict: A dictionary containing the validation results, including whether the certificate is valid,
              the number of days until expiry, the expiration date, the total lifetime in days
              and the limit it was compared with (when notBefore is available), and any warnings.

    Raises:
        ValueError: If thresholds do not satisfy 0 <= critical_days <= warning_days,
            or max_lifetime_days is neither `"public"`, a positive number, nor `None`.

    Examples:
        Example output (success):
            This example shows a certificate that is valid and has 120 days until expiration, so no warnings are present.

            ```json
            {
                "is_valid": true,
                "days_to_expiry": 120,
                "expires_on": "2025-09-01T23:59:59+00:00",
                "lifetime_days": 365,
                "lifetime_limit_days": 398,
                "warnings": []
            }
            ```

        Example output (failure):
            This example shows a certificate that expired 10 days ago, so validation fails and a warning is included.

            ```json
            {
                "is_valid": false,
                "days_to_expiry": -10,
                "expires_on": "2025-04-30T23:59:59+00:00",
                "lifetime_days": 365,
                "lifetime_limit_days": 398,
                "warnings": [
                    "Certificate is expired and has been expired for (-10 days)"
                ],
                "reason": "Certificate expired 10 days ago (expired on 2025-04-30)."
            }
            ```
    """
    if not 0 <= critical_days <= warning_days:
        raise ValueError("Require 0 <= critical_days <= warning_days")
    if (
        isinstance(max_lifetime_days, str)
        and max_lifetime_days != PUBLIC_TLS_POLICY
    ):
        raise ValueError(
            f'max_lifetime_days must be "{PUBLIC_TLS_POLICY}", a positive number, or None'
        )
    if isinstance(max_lifetime_days, (int, float)) and max_lifetime_days <= 0:
        raise ValueError("max_lifetime_days must be positive")
    utc = datetime.timezone.utc
    now = datetime.datetime.now(utc)
    not_after = parse_not_after(cert).replace(tzinfo=utc)
    parsed_before = parse_not_before(cert)
    not_before = parsed_before.replace(tzinfo=utc) if parsed_before else None
    remaining = not_after - now
    warnings: list[str] = []
    result: ExpirationResult = {
        "is_valid": now < not_after and (not_before is None or now >= not_before),
        "days_to_expiry": remaining.days,
        "expires_on": not_after.isoformat(),
        "warnings": warnings,
    }
    if now >= not_after:
        result["reason"] = (
            f"Certificate expired {abs(remaining.days)} days ago "
            f"(expired on {not_after.date().isoformat()})."
        )
        warnings.append(
            f"Certificate is expired and has been expired for ({remaining.days} days)"
        )
    elif not_before is not None and now < not_before:
        result["reason"] = "Certificate is not yet valid."
    elif remaining <= datetime.timedelta(days=critical_days):
        warnings.append(
            f"Certificate is expiring within the critical threshold "
            f"({remaining.days} days remaining, threshold {critical_days} days)."
        )
    elif remaining <= datetime.timedelta(days=warning_days):
        warnings.append(
            f"Certificate is expiring within the warning threshold "
            f"({remaining.days} days remaining, threshold {warning_days} days)."
        )

    if not_before is not None:
        lifetime = not_after - not_before
        result["lifetime_days"] = lifetime.days
        limit: float | None
        if max_lifetime_days == PUBLIC_TLS_POLICY:
            limit = public_tls_lifetime_limit(not_before.date())
            label = (
                f"{limit}-day public TLS limit for certificates issued on "
                f"{not_before.date().isoformat()}"
            )
        else:
            limit = cast("float | None", max_lifetime_days)
            label = f"{limit}-day limit"
        if limit is not None:
            result["lifetime_limit_days"] = int(limit)
            if lifetime > datetime.timedelta(days=limit):
                warnings.append(
                    f"Certificate total lifetime ({lifetime.days} days) exceeds the {label}."
                )
    return result