Skip to content
ExponaBLOG
PROMETHEUS, GRAFANA & MONITORING · DAY 035 / 100

Monitoring SSL certificate expiry: a step-by-step guide

Never let a certificate expire again.

Expona Admin7 min read

Introduction

We've all heard the story: a production certificate silently expires at 2 AM on a Friday, customers can't reach your platform, and the on-call engineer spends the weekend fighting fires. Monitoring SSL certificate expiry: a step-by-step guide will show you how to catch these disasters weeks before they happen. With Prometheus and Grafana, you can automatically check every domain and subdomain, alert your team with plenty of lead time, and sleep soundly knowing your certificates are under constant watch.

Why certificate monitoring matters

SSL certificates don't fail gracefully. When they expire, browsers show security warnings, APIs refuse connections, and your users experience hard failures with no fallback. Manual renewal calendars are unreliable—people go on vacation, change jobs, or simply forget. Even certificate authorities that send reminder emails can't guarantee you'll see them in time to coordinate a renewal and deployment.

The stakes are higher when you run multiple environments and services. A staging certificate expiring won't wake anyone up, but it will silently break your integration tests. A subdomain certificate that you forgot existed will expire and take down a specific feature or partner integration without affecting your main site, making it hard to debug.

Blackbox exporter TLS metrics

The Prometheus blackbox exporter is purpose-built to probe endpoints and extract metrics. When configured to check TLS certificates, it connects to your domain, validates the certificate chain, and exposes two critical metrics:

  • probe_ssl_earliest_cert_expiry — Unix timestamp of the soonest expiring certificate in the chain
  • probe_ssl_last_chain_expiry_timestamp_seconds — timestamp of the final certificate in the chain

The first metric is what matters most: it catches expiry of the leaf certificate, intermediate certificates, and any other cert in the presented chain.

Start by installing the blackbox exporter. On most systems, you can use your package manager or pull the binary directly:

yaml
curl -L https://github.com/prometheus/blackbox_exporter/releases/download/v0.24.0/blackbox_exporter-0.24.0.linux-amd64.tar.gz | tar xz
cd blackbox_exporter-0.24.0.linux-amd64
./blackbox_exporter --config.file=blackbox.yml

Next, configure it to probe HTTPS endpoints. Create or update your blackbox.yml:

yaml
modules:
  https_2xx:
    prober: http
    timeout: 5s
    http:
      valid_http_versions:
        - HTTP/1.1
        - HTTP/2.0
      valid_status_codes:
        - 200
        - 301
        - 302
      no_follow_redirects: false
  https_cert:
    prober: http
    timeout: 5s
    http:
      valid_http_versions:
        - HTTP/1.1
      no_follow_redirects: true
      tls_config:
        insecure_skip_verify: false

The https_cert module focuses purely on TLS validation without requiring a specific HTTP response code. This is ideal for checking certificate validity regardless of what your endpoint returns.

Configure Prometheus to scrape the blackbox exporter for each domain you want to monitor. In your prometheus.yml:

yaml
scrape_configs:
  - job_name: 'blackbox-https'
    metrics_path: /probe
    static_configs:
      - targets:
        - https://api.example.com
        - https://app.example.com
        - https://admin.example.com
        - https://partner-api.example.com
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: localhost:9115

Run this for five minutes and you'll see probe_ssl_earliest_cert_expiry appear in Prometheus. Graph it in Grafana to visualize when each certificate expires.

Alert 21 days before expiry

A 21-day alert window gives you time to renew, test, and deploy without panic. It also catches cases where renewal automation fails silently—your monitoring catches it before your users do.

Add this alert rule to your Prometheus configuration. We'll use the probe_ssl_earliest_cert_expiry metric and calculate how many days until expiry:

yaml
groups:
  - name: ssl_certificates
    interval: 300s
    rules:
      - alert: SSLCertificateExpiringSoon
        expr: |
          (probe_ssl_earliest_cert_expiry - time()) < 21 * 24 * 3600
        for: 1h
        labels:
          severity: warning
          team: platform
        annotations:
          summary: "SSL certificate for {{ $labels.instance }} expires in less than 21 days"
          description: "Certificate for {{ $labels.instance }} will expire on {{ humanize (probe_ssl_earliest_cert_expiry{instance=\"{{ $labels.instance }}\"}) }}. Renew it now."
      
      - alert: SSLCertificateExpiryCritical
        expr: |
          (probe_ssl_earliest_cert_expiry - time()) < 7 * 24 * 3600
        for: 15m
        labels:
          severity: critical
          team: platform
        annotations:
          summary: "SSL certificate for {{ $labels.instance }} expires in less than 7 days"
          description: "Certificate for {{ $labels.instance }} is expiring very soon. Renew immediately."
      
      - alert: SSLCertificateExpired
        expr: |
          probe_ssl_earliest_cert_expiry - time() < 0
        for: 5m
        labels:
          severity: critical
          team: platform
        annotations:
          summary: "SSL certificate for {{ $labels.instance }} has EXPIRED"
          description: "Certificate for {{ $labels.instance }} expired at {{ humanize (probe_ssl_earliest_cert_expiry{instance=\"{{ $labels.instance }}\"}) }}. This is a production incident."

The three tiers—21 days, 7 days, and expired—let you escalate response proportionally. At 21 days, send it to your team Slack channel. At 7 days, page the on-call engineer. If it's already expired, it's a SEV-1 incident.

Configure your alertmanager to route these alerts where your team actually watches:

yaml
route:
  receiver: 'platform-team'
  routes:
    - match:
        alertname: SSLCertificate.*
        severity: warning
      receiver: slack-warnings
      group_wait: 10m
    - match:
        alertname: SSLCertificate.*
        severity: critical
      receiver: pagerduty
      group_wait: 1m

Cover every domain and subdomain

The most common mistake is assuming your monitoring covers everything. You monitor api.example.com and app.example.com, but miss internal.example.com, staging-api.example.com, or an old subdomain that's still being used by a legacy partner.

Start by auditing all domains and subdomains actually serving traffic. Check your infrastructure-as-code repositories, your CDN configuration, your load balancer rules, and your DNS records. Ask your DevOps and security teams if they know of any endpoints you missed. Document them in a spreadsheet or config file:

yaml
domains:
  production:
    - https://example.com
    - https://www.example.com
    - https://api.example.com
    - https://app.example.com
    - https://websocket.example.com
  staging:
    - https://staging.example.com
    - https://staging-api.example.com
  internal:
    - https://admin.example.com
    - https://metrics.example.com
    - https://internal-api.example.com
  legacy:
    - https://partner-old.example.com
    - https://legacy.example.com

Add all of these to your Prometheus blackbox scrape config. If you have dozens of subdomains, use a script or dynamic discovery to generate the target list. Some teams use consul or file-based discovery to automatically pick up new endpoints.

Set a quarterly reminder to re-audit your domains. Endpoints get created, retired, and forgotten. A cert that nobody is monitoring will still expire and cause an outage.

Check intermediate chains too

Most certificate chains include at least one intermediate certificate between your leaf certificate and the root. If an intermediate certificate in your chain expires, your certificate becomes invalid even if the leaf cert is fresh.

The blackbox exporter's probe_ssl_earliest_cert_expiry metric already checks the entire chain and returns the earliest expiry date. This is exactly what you want—it catches expiring intermediates automatically.

However, there's a catch: some CAs rotate intermediate certificates on a predictable schedule, and if your server isn't configured to serve the latest intermediate in its chain, clients may reject your certificate. Validate that your web server is actually sending the complete chain.

Test this manually for each domain:

yaml
openssl s_client -connect api.example.com:443 -showcerts

Look for "chain depth" output. You should see the leaf certificate, one or more intermediate certificates, and ultimately a self-signed root. If you only see the leaf certificate, your web server configuration may be incomplete. Some certificate providers include setup guides with the exact intermediate bundle to serve.

In production, misconfigured chains are often invisible until the old intermediate expires. Your users will see "certificate not trusted" errors, and some clients (particularly older mobile apps) may fail silently. By monitoring the entire chain, you catch this months in advance.

Common mistakes

  • Not monitoring staging and internal domains. A staging certificate that expires won't page anyone, but it will break your CI/CD pipeline and integration tests. Include every endpoint in your monitoring, not just production-facing services.
  • Alerting too late or too early. Alerting at 1 day before expiry is too aggressive and noisy; alerting at 1 day before it expires is too late to coordinate safely. 21 days is the sweet spot for most teams.
  • Assuming your web server is sending the full chain. Just because a certificate is valid doesn't mean your web server is configured to serve the full chain. Clients may reject it. Periodically verify with openssl s_client.
  • Forgetting about wildcard and multi-domain certificates. A wildcard cert covering *.example.com is still one certificate. It expires as a unit. Make sure you're monitoring the primary domain it's issued to, not just a few subdomains under it.
  • Not handling certificate rotation in your automation. Even if you set up perfect monitoring and alerts, if your certificate renewal and deployment pipeline doesn't work, you're just getting paged more often. Test your renewal process quarterly.

Key takeaways

  • Use the Prometheus blackbox exporter to automatically check TLS certificate expiry for all your domains and subdomains.
  • Alert at 21 days before expiry (warning), 7 days before (critical), and immediately if expired.
  • Audit and document every domain serving traffic, including staging, internal, and legacy subdomains—missing one will eventually bite you.
  • The blackbox exporter checks the entire certificate chain, catching expiring intermediates automatically.
  • Periodically verify that your web servers are actually sending the complete certificate chain, not just the leaf certificate.
  • Pair monitoring with an automated renewal and deployment pipeline so alerts lead to action, not just noise.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles