春江暮客

春江暮客的个人学习分享网站

Check TLS Certificate Expiry with Python: Verification, Warnings, and Local Tests

2026-10-18 Technology
Check TLS Certificate Expiry with Python: Verification, Warnings, and Local Tests

Port 443 accepts connections, but the certificate has only two days left. Waiting for browsers to report an error can turn routine renewal into an outage. This tutorial builds a Python standard-library checker that validates TLS, reads certificate expiry, and reports JSON with exit codes for existing automation.

For beginner-to-intermediate readers, no pip packages are needed. We first reproduce valid, expiring, untrusted, and hostname-mismatch results using a local certificate, then check a real site.

1. Method: validate identity, then calculate remaining time

  1. Create a client context with ssl.create_default_context(), keeping certificate-chain and hostname verification enabled.
  2. Connect over TCP, then pass the expected identity as server_hostname; a DNS hostname also enables SNI.
  3. After a successful handshake, read notAfter from the server’s leaf certificate and convert it to a UTC timestamp.
  4. Return expiring when remaining seconds are at most --warn-days × 86400; otherwise return ok.

This follows the official Python TLS client example and getpeercert() documentation. Use ssl.cert_time_to_seconds() for conversion so GMT in a certificate is not interpreted as local time.

If the connection fails before TLS, start with Python TCP port checks. A successful TCP connection does not establish that the certificate is valid.

2. Setup: get the scripts and local test tools

Use Python 3.9 or newer with a working ssl module. The local experiment also needs an OpenSSL command supporting -addext, plus curl for downloading. Examples were tested on Linux with Python 3.11.4 and the system Python 3.13; no global dependency installation is required.

  1. Create a demo directory and download both complete scripts. Run subsequent commands in this directory:
python3 --version
openssl version
mkdir tls-expiry-demo
cd tls-expiry-demo
curl -fS https://www.bobobk.com/downloads/tls-expiry/check_tls.py -o check_tls.py
curl -fS https://www.bobobk.com/downloads/tls-expiry/test_tls.py -o test_tls.py
python3 check_tls.py --help
  1. check_tls.py contains the complete implementation in the next section. test_tls.py is the companion local self-test. Without curl, save both downloads in a browser. Checking an ordinary public HTTPS certificate only requires check_tls.py; the OpenSSL command is for local testing.

3. Complete implementation: one JSON line and clear exit codes

"""Check one TLS endpoint's verified leaf certificate. Python 3.9+."""
import argparse
from datetime import datetime, timezone
import json
import math
import socket
import ssl
import time


def check(host, port, timeout, warn_days, cafile=None):
    report = {"host": host, "port": port, "status": "error"}
    try:
        context = ssl.create_default_context(cafile=cafile)
        with socket.create_connection((host, port), timeout=timeout) as raw:
            with context.wrap_socket(raw, server_hostname=host) as tls:
                certificate = tls.getpeercert()
        expires = ssl.cert_time_to_seconds(certificate["notAfter"])
        remaining = expires - time.time()
        report.update(
            status="expiring" if remaining <= warn_days * 86400 else "ok",
            expires_utc=datetime.fromtimestamp(expires, timezone.utc).isoformat(),
            days_remaining=round(remaining / 86400, 3),
        )
    except ssl.SSLCertVerificationError as exc:
        report.update(status="verify_error", error=exc.verify_message)
    except (OSError, ValueError, KeyError, OverflowError) as exc:
        report.update(error=str(exc))
    return report


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("host", help="DNS hostname or IP address, without a URL")
    parser.add_argument("--port", type=int, default=443)
    parser.add_argument("--timeout", type=float, default=3)
    parser.add_argument("--warn-days", type=int, default=14)
    parser.add_argument("--cafile", help="explicit PEM trust file, e.g. for a lab")
    args = parser.parse_args()
    if not args.host or any(c.isspace() for c in args.host) or "/" in args.host:
        parser.error("host must be a hostname or IP address without a URL")
    if not 1 <= args.port <= 65535:
        parser.error("--port must be between 1 and 65535")
    if not math.isfinite(args.timeout) or not 0 < args.timeout <= 60:
        parser.error("--timeout must be finite and greater than 0, at most 60")
    if not 0 <= args.warn_days <= 36500:
        parser.error("--warn-days must be between 0 and 36500")
    report = check(args.host, args.port, args.timeout, args.warn_days, args.cafile)
    print(json.dumps(report, ensure_ascii=False))
    return 0 if report["status"] == "ok" else 1


if __name__ == "__main__":
    raise SystemExit(main())

days_remaining is rounded only for display; the warning comparison uses unrounded seconds. An expired certificate normally fails during the handshake and produces verify_error. The checker keeps verification enabled.

Exit code JSON status or behavior
0 ok: verification succeeded and remaining time exceeds the threshold.
1 expiring: valid but too close to expiry; verify_error: verification failed; error: another TLS, network, or file error.
2 Invalid arguments; a message goes to standard error and no JSON is emitted.

socket.create_connection() resolves hostnames and can try the resulting addresses. --timeout applies to connection and handshake operations, not a whole-process deadline; DNS resolution and multiple address attempts can extend total runtime. This tool checks direct TLS services, without SMTP STARTTLS or HTTP page requests.

4. Local experiment: a two-day certificate triggers a three-day warning

  1. In terminal A, inside the demo directory, create a dedicated configuration and test certificate. Start a TLS server listening only on loopback:
umask 077
printf '[req]\ndistinguished_name = dn\n[dn]\n' > demo-openssl.cnf
openssl req -x509 -newkey rsa:2048 -nodes \
  -config demo-openssl.cnf -days 2 -subj '/CN=localhost' \
  -addext 'subjectAltName=DNS:localhost' \
  -addext 'basicConstraints=critical,CA:TRUE' \
  -addext 'keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign' \
  -addext 'subjectKeyIdentifier=hash' \
  -addext 'authorityKeyIdentifier=keyid:always' \
  -keyout demo-key.pem -out demo-cert.pem
openssl s_server -accept 127.0.0.1:18443 \
  -cert demo-cert.pem -key demo-key.pem -www

The minimal configuration declares the request’s distinguished-name section; extensions are supplied explicitly. This avoids duplicate extensions from a system configuration combined with -addext. See the official OpenSSL req manual and s_server manual for the options. -nodes produces an unencrypted demo private key, so we use umask 077. These files are for this local experiment.

  1. Leave terminal A running and enter the same directory in terminal B:
python3 check_tls.py localhost --port 18443 --cafile demo-cert.pem --warn-days 0
if python3 check_tls.py localhost --port 18443 --cafile demo-cert.pem --warn-days 3; then
  printf 'Certificate has enough time remaining\n'
else
  tls_exit=$?
  printf 'Check needs attention: exit=%s\n' "$tls_exit"
fi

The first command returns status: "ok". The second returns status: "expiring" and exit code 1 because roughly two days remain and the threshold is three days. JSON includes expires_utc and days_remaining; actual dates depend on when you run it.

  1. Verify trust and hostname failures separately:
if python3 check_tls.py localhost --port 18443; then
  printf 'Unexpected trust result\n'
else
  printf 'Expected trust failure: exit=%s\n' "$?"
fi
if python3 check_tls.py 127.0.0.1 --port 18443 --cafile demo-cert.pem; then
  printf 'Unexpected hostname result\n'
else
  printf 'Expected hostname failure: exit=%s\n' "$?"
fi

Both checks should return verify_error. The first does not trust the demo certificate. The second explicitly trusts it, but its SAN lists only DNS:localhost, without an IP identity for 127.0.0.1. --cafile selects trust for this invocation; it neither installs system certificates nor disables hostname checks. Use trust files whose source you have already verified.

Press Ctrl+C in terminal A when finished. If the port is occupied, choose 18444 and update terminal B’s --port too, without stopping another program. Regenerate the demo files if their two-day certificate has expired.

5. Validation: automatically test failure branches

Run the downloaded test_tls.py:

python3 test_tls.py

Expected output:

OK: valid, expiring, untrusted, hostname mismatch, refused, arguments, timeout

The self-test generates a certificate in a temporary directory and starts a local TLS server on a dynamically assigned 127.0.0.1 port. Subprocess calls verify JSON and exit codes. A bound but non-listening port exercises connection failure; invalid timeouts, ports, and warning periods exercise argument handling. A mocked timeout checks error handling without depending on public-network packet loss. After the thread ends, sockets close and its generated certificate and private key are removed automatically, leaving section 4’s files intact.

After downloading, the self-test makes no public-network requests. It exercises real local handshakes, trust rejection, and hostname rejection. The injected timeout tests handling rather than real network timing, and the test does not simulate an expired certificate.

6. Automation and troubleshooting

  1. Check a real domain you manage, passing a hostname without https://. For example, check this site:
python3 check_tls.py www.bobobk.com --warn-days 14 --timeout 3

Remaining time depends on renewal, so do not assert a fixed number of days in tests. When warned, follow the issuer’s renewal procedure and confirm the public service presents the replacement certificate. CDN or load-balancer nodes may present different certificates; one connection observes only the selected endpoint.

  1. Preserve both JSON and nonzero exits in the caller, routing exit code 1 to an existing alerting process. Avoid hiding failures with || true. For scheduled execution, use absolute interpreter, script, and trust-file paths; see scheduled-job path and environment troubleshooting.
Symptom Concrete action
expiring Renew, then recheck the public endpoint. Updating a disk file without loading it into the serving process leaves the warning in place.
verify_error mentioning expiry Check the monitoring host’s clock and the server certificate, deploy the renewal, then rerun the same check.
verify_error mentioning issuer/trust Check the intermediate chain served by the endpoint and the trusted CA files used by this Python installation. For a private CA, select a verified --cafile.
verify_error mentioning hostname/IP Use a DNS name covered by the certificate SAN, or obtain a certificate with the required IP SAN.
error mentioning timeout/refused Verify DNS resolution, target port, listening service, and the network path.
error mentioning PEM/file paths Check the --cafile path, read permissions, and PEM contents.
  1. After the certificate passes, check HTTPS status and actual page content using the curl deployment checks. A valid certificate does not prove the page loads correctly. This tool is not a complete revocation-status or chain-wide expiry audit.

7. Summary

Keeping TLS identity verification enabled and applying a remaining-time threshold to the server leaf certificate helps surface renewal windows, trust failures, and hostname errors in automation. Validate success and failure branches with a local two-day certificate before connecting the checker to existing monitoring. After renewal, recheck the real endpoint and continue with HTTP response validation.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它