ObservabilityLXIII · Synthetic MonitoringSynthetic
TLS Probes
What you'll learn
- Describe what the http/tls probe of blackbox_exporter 0.26.x verifies about the certificate chain
- Predict certificate expiry from probe_ssl_earliest_cert_expiry and alert at 30 / 7 / 1 day thresholds
- Configure tls_config deliberately, including insecure_skip_verify and the CA bundle
- Distinguish a probe that fails on TLS from a probe that fails on the application layer
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
The HTTPS probe returns green. The dashboard says the public boundary is healthy. The customer opens a ticket with a screenshot of a browser warning: “Your connection is not private. NET::ERR_CERT_AUTHORITY_INVALID.” The certificate the application serves is signed by the internal CA; the browser does not have that CA in its trust store. The exporter, configured with the internal CA bundle, accepted the chain. The probe is green. The user’s browser refuses the connection.
The TLS probe, on its own, never claimed to be a “user-trust-store” probe. The lesson is about what the blackbox TLS configuration actually verifies and how to alert on certificate expiry before it bites.
What it is
The TLS probe is the http module of blackbox_exporter
0.26.x with a tls_config block. The exporter performs a
TLS handshake against the target, applies the configured
chain validation, and (if the handshake succeeds) emits
probe_ssl_earliest_cert_expiry as a Unix timestamp of the
earliest certificate expiry in the chain. The probe does
not have a separate “TLS module” — TLS verification is a
configuration of the HTTP prober.
The TLS-specific metrics are:
probe_ssl_earliest_cert_expiry— Unix timestamp of the earliestnotAfterin the chain presented by the server.probe_tls_version_bits— the negotiated TLS version encoded as a bit count (128 for TLS 1.2, 256 for TLS 1.3).probe_tls_cipher_suite— the negotiated cipher suite.probe_failed_due_to_tls— set to 1 when the TLS validation failed.
The exporter validates the chain against the system’s
default CA bundle (typically /etc/ssl/certs/ca-certificates.crt
on Debian-family systems) unless tls_config.ca_file or
tls_config.ca_pem overrides it. The exporter does not
consult the browser trust store; it consults the exporter
host’s trust store.
Why a sysadmin cares
TLS expiry is the canonical silent failure mode. The certificate expires at 00:00 UTC on a Tuesday. The exporter probe that runs every 30 s sees the expiry at 00:00:30 and the next alert fires shortly after. The customer sees a browser warning at the moment they try to load the page. The right cadence is to alert well in advance — 30 days, 7 days, 1 day — so the on-call has time to renew.
Three production questions map onto the TLS probe:
- Is the certificate chain valid now? A red probe
with
probe_failed_due_to_tls=1means the chain is broken: expired, wrong hostname, untrusted CA, or revoked. Symptom: probe red, browser warning, customer ticket. - When does the earliest certificate in the chain
expire?
probe_ssl_earliest_cert_expiryis a Unix timestamp. The alert is the delta between now and that timestamp. - Is the negotiated protocol and cipher acceptable?
probe_tls_version_bitsandprobe_tls_cipher_suiteare the inputs to a compliance dashboard that asserts “no TLS 1.0 or 1.1, no RC4.”
The canonical alert shape is three thresholds: 30 days, 7 days, 1 day. The 30-day warning gives the renewal process time. The 7-day warning escalates. The 1-day warning pages the on-call.
How it works
The exporter’s HTTP prober, when configured with TLS,
delegates the handshake to Go’s crypto/tls package. The
package validates the chain against the configured CA
bundle, checks the hostname against the certificate’s
SAN, and reports the negotiated protocol and cipher.
exporter host target host
| |
| --- TLS ClientHello --> |
| <-- TLS ServerHello, Certificate --- |
| --- TLS Certificate (chain) --> |
| --- TLS CertificateVerify --> |
| <-- TLS Finished --> |
| |
| exporter validates: |
| - chain against tls_config.ca_file |
| - hostname against cert SANs |
| - expiry against now |
| - revocation if configured |
| |
v v
emit: probe_ssl_earliest_cert_expiry
probe_tls_version_bits
probe_tls_cipher_suite
probe_failed_due_to_tls
probe_success
The exporter does not consult the Online Certificate Status Protocol (OCSP) by default. The chain is validated on signature, expiry, and hostname; revocation is not checked. Production environments that require OCSP stapling or short-lived certificates must either rely on the server’s stapled response or add an external revocation check.
How to configure it
Below is a production-shaped blackbox.yml with three
TLS variants a typical environment needs.
# /etc/blackbox/blackbox.yml
modules:
# Public boundary probe with strict chain validation.
# The CA bundle is the system default; the hostname
# is checked against the cert SAN.
http_2xx_https_strict:
prober: http
timeout: 5s
http:
preferred_ip_protocol: ip4
ip_protocol_fallback: true
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
valid_status_codes: [200]
method: GET
fail_if_not_ssl: true
tls_config:
insecure_skip_verify: false
# Internal-service probe with an internal CA bundle.
# The certificate is signed by the internal CA; the
# exporter host does not trust that CA by default.
http_2xx_https_internal:
prober: http
timeout: 5s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
valid_status_codes: [200]
method: GET
fail_if_not_ssl: true
tls_config:
insecure_skip_verify: false
ca_file: /etc/blackbox/ca/internal-ca.pem
# TLS-only assertion. Returns probe_success=0 unless
# the chain is valid. The path is a known TLS-only
# endpoint; the body is not validated.
https_tls_only:
prober: http
timeout: 5s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
valid_status_codes: [200, 204, 301, 302, 401, 403]
method: GET
fail_if_not_ssl: true
tls_config:
insecure_skip_verify: false
min_version: TLS12
The scrape job in Prometheus ties the module to the targets.
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: blackbox_tls_public
metrics_path: /probe
params:
module: [http_2xx_https_strict]
scrape_interval: 60s
scrape_timeout: 10s
static_configs:
- targets:
- https://example.com
- https://api.example.com
labels:
service: public-boundary
env: prod
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
regex: '(https?://[^/]+)'
replacement: '${1}'
target_label: instance
- target_label: __address__
replacement: blackbox.internal:9115
The tls_config choices are the operational levers.
insecure_skip_verify: false is the default and the only
disciplined choice for production. ca_file overrides
the system CA bundle for internal CAs.
How to validate it
# 1. The probe against the real target.
curl -sfG http://blackbox.internal:9115/probe \
--data-urlencode 'module=http_2xx_https_strict' \
--data-urlencode 'target=https://example.com' \
| grep -E '^probe_'
# probe_duration_seconds 0.214
# probe_failed_due_to_tls 0
# probe_http_status_code 200
# probe_ssl_earliest_cert_expiry 1.790e+09
# probe_success 1
# probe_tls_cipher_suite 4865
# probe_tls_version_bits 128
# 2. Predict days until expiry.
EXPIRY=$(curl -sfG http://blackbox.internal:9115/probe \
--data-urlencode 'module=http_2xx_https_strict' \
--data-urlencode 'target=https://example.com' \
| awk '/^probe_ssl_earliest_cert_expiry/ {print $2}')
python3 -c "import time; print(((${EXPIRY}) - time.time()) / 86400, 'days')"
# 47.3 days
# 3. The probe against an internal-CA target.
curl -sfG http://blackbox.internal:9115/probe \
--data-urlencode 'module=http_2xx_https_internal' \
--data-urlencode 'target=https://internal.example.com' \
| grep -E '^probe_failed_due_to_tls|^probe_success'
# probe_failed_due_to_tls 0
# probe_success 1
# 4. Cross-check with openssl.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates
# notBefore=...
# notAfter=...
# 5. Confirm Prometheus has the metric.
probe_ssl_earliest_cert_expiry{service="public-boundary",env="prod"}
# {target="https://example.com"} 1.790e+09
A green probe_success, a green probe_failed_due_to_tls=0,
and a probe_ssl_earliest_cert_expiry that matches the
expected notAfter are the headline assertions.
How it can fail
-
CA bundle mismatch. The certificate is signed by an internal CA. The exporter host does not trust that CA.
probe_failed_due_to_tls=1. Symptom: probe red, browser accepts the certificate because the user’s machine has the internal CA installed, exporter rejects it because its host does not. -
insecure_skip_verify: true. The exporter accepts any certificate, including expired or wrong-host. The probe is green regardless. Symptom: probe green, certificate expired, customer ticket, audit failure. -
SAN mismatch. The certificate is valid but issued for a different hostname. The exporter host’s TLS client refuses the connection because the SAN check fails.
probe_failed_due_to_tls=1. Symptom: probe red, the certificate is correct for another service. -
Cert chain order. The server presents the chain in the wrong order. Most TLS clients (Go included) tolerate this. Some do not. Symptom: probe intermittently red.
-
SNI mismatch. The exporter connects to a host that serves multiple TLS certificates. The Server Name Indication (SNI) does not match the certificate. The exporter host’s TLS client may refuse the connection. Symptom: probe red on a hostname that resolves to a shared endpoint.
-
Expiry alert thresholds wrong. The alert is set at 7 days. The certificate expires in 14 days. The renewal process takes 10 days. Symptom: alert fires, certificate expires before renewal completes, customer impact.
-
TLS version policy drift. The server begins accepting TLS 1.0 for a legacy client. The exporter’s
valid_http_versionsdoes not assert the TLS version; it asserts the HTTP version. The probe is green. Symptom: compliance dashboard shows TLS 1.0 traffic; the TLS probe was not the right place to look. -
probe_ssl_earliest_cert_expiryreads the wrong certificate. The chain presents an intermediate CA cert that expires sooner than the leaf. The exporter records the earliest expiry. Symptom: the leaf is good for sixty days; the alert fires at fourteen days; the on-call investigates the wrong certificate.
How to troubleshoot it
The order matters because the boundary at which the failure lives determines the remedy.
- Cross-check with
openssl s_client.echo | openssl s_client -connect target:443 -servername target. This is the one-line test. If this fails, the chain is the boundary; the probe was honest. - Inspect
probe_failed_due_to_tls. A red probe withprobe_failed_due_to_tls=1is a TLS problem; do not blame the HTTP layer. - Confirm
insecure_skip_verify. A red probe that goes green wheninsecure_skip_verify: trueis set is a chain-validation problem; the configuration bypassed the chain. - Confirm the CA bundle. The exporter’s
ca_filepath must be readable by the exporter user. A typo silently falls back to the system CA bundle. - Confirm the hostname in the probe target. A SAN mismatch is fixed by correcting the target URL or by renewing the certificate with the right SAN.
- Inspect
probe_ssl_earliest_cert_expiryagainst the expectednotAfter. A drift means the chain changed (intermediate renewed, CA rolled, certificate reissued). - Compare to
node_exporternode_textfile_mtime. If a sidecar writes the certificate’snotAfterto a file for backup alerting, the two values must agree.
Security implications
insecure_skip_verify: true is the single most dangerous
configuration in the TLS probe. It accepts any certificate,
including expired, wrong-host, or self-signed. The probe
returns green regardless. The audit will read the
production config; a configuration with this set is an
audit failure even if the certificate is otherwise valid.
The CA bundle must be kept current. An exporter host that trusts an old CA bundle fails to validate certificates signed by a CA that has rolled its root. The bundle update process must be part of the platform’s patch cycle.
The TLS probe does not check OCSP by default. A certificate that has been revoked is still considered valid by the probe. Production environments that require revocation checking must rely on the server’s OCSP stapling or on an external revocation check; the TLS probe alone is not sufficient.
The cipher and version metrics (probe_tls_cipher_suite,
probe_tls_version_bits) are inputs to a compliance
dashboard. They are not assertions; they are observations.
A compliance dashboard that asserts “no TLS 1.0, no RC4”
should alert on the server’s behaviour, not on the
probe’s observation.
Performance implications
The TLS handshake is the most expensive part of an HTTPS probe. The cost is:
- Exporter CPU. Each handshake runs RSA or ECDHE key exchange, signature verification, and chain validation. A modern four-core exporter handles roughly 100 HTTPS probes per second before saturating.
- Network egress. Each probe is a request from the exporter host to the target. The handshake adds two round-trips of overhead.
- TLS session resumption. The exporter does not currently cache TLS sessions. Each probe performs a full handshake. Session resumption is a future optimisation.
The right mitigation is right-sized. A 200-target TLS suite at 60 s intervals is 3.3 rps, well within budget. A 1 000-target TLS suite at 15 s intervals is 67 rps and will saturate the exporter.
Production guidance
- Alert on
probe_ssl_earliest_cert_expiry - time() < 30 * 86400first. The 30-day threshold is the warning that gives the renewal process time. - Escalate at 7 days. The on-call needs to know.
- Page at 1 day. The certificate is about to expire.
- Set
insecure_skip_verify: falsealways. The audit reads the production config. - Keep the CA bundle current. The exporter’s
ca_file(or the system CA bundle if not overridden) is the trust anchor. - Use
valid_http_versionsand a separate compliance check for the TLS version policy. The TLS probe observes the negotiated version; the HTTP probe asserts the HTTP version. - Cross-check the alert with an independent signal. The
cert-exportersidecar, thenode_exportertextfile collector, or a simple cron job that reads the cert directly is a useful second pair of eyes.
Verification
You should now be able to answer:
- What does
probe_ssl_earliest_cert_expiryactually record, and which certificate in the chain does it choose? - Why is
insecure_skip_verify: truea dangerous production configuration, even on a probe? - How do the 30 / 7 / 1 day thresholds map onto the renewal process, and which one pages the on-call?
- What is the difference between a probe that fails on
TLS (
probe_failed_due_to_tls=1) and a probe that fails on the application layer? - Why is a CA bundle mismatch a boundary that the probe can detect but the operator may not notice?
Quiz
Knowledge check · 8 questions
Q1. What does probe_ssl_earliest_cert_expiry record?
Q2. A TLS probe against an internal-CA-signed certificate is red with probe_failed_due_to_tls=1. The browser accepts the certificate. What is the most likely cause?
Q3. Which alert thresholds are appropriate for a TLS-expiry alert that respects a 14-day renewal process? Select all that apply.
Q4. insecure_skip_verify: true is an audit failure in production even when the probe is wired against an internal service the operator controls.
Q5. Name the metric that distinguishes a TLS-handshake failure from an HTTP-application failure for an HTTPS probe.
Q6. A certificate is renewed; the leaf now expires in 90 days. The intermediate CA in the chain expires in 30 days. What does the alert see?
Q7. Why is a TLS-version policy check best implemented as a separate compliance dashboard rather than as a blackbox probe assertion?
Q8. The exporter host trusts an old CA bundle. A new internal CA is rolled. What is the consequence for a probe against a service whose certificate is signed by the new CA?
Passing score: 75%. Answers are checked in this browser.