DEV Community

Billy
Billy

Posted on

Catch expiring TLS certs and weak security headers in CI — with one small CLI

Your CI proves the app builds, the tests pass, and maybe that there are no known-vulnerable dependencies. Then it ships — behind a TLS certificate that will quietly expire in three weeks, serving responses with a security-headers grade of F.

Nobody notices until a browser does.

These two failure modes — an expiring certificate and weak HTTP security headers — are cheap to check and easy to forget. Here's a small, dependency-light CLI that checks them (plus DNS posture and subdomains) from your terminal, and a one-line GitHub Action that fails the build before your users find out.

Install

pip install "secably[dns]"
Enter fullscreen mode Exit fullscreen mode

The core checks have zero dependencies; the optional [dns] extra pulls in dnspython for SPF/DMARC/DNSSEC. Everything runs locally — nothing about your domain is sent to a third party.

Check a TLS certificate

secably ssl example.com
Enter fullscreen mode Exit fullscreen mode
TLS — github.com:443
--------------------
  Subject          github.com
  Issuer           Sectigo Public Server Authentication CA DV E36
  Valid until      2026-09-30T23:59:59+00:00  (84 days left)
  Protocol         TLSv1.3
  Cipher           TLS_AES_128_GCM_SHA256
  Trusted          yes
  SAN              github.com, www.github.com
Enter fullscreen mode Exit fullscreen mode

It completes the handshake, reads the leaf certificate, and reports the issuer, validity window, days remaining and negotiated protocol. It also flags the things you actually care about: an expired or not-yet-valid certificate, a chain that doesn't verify, or a server still negotiating an obsolete protocol like TLS 1.0.

One implementation detail worth knowing if you build something similar: ssl.getpeercert() returns an empty dict unless the certificate verifies, so you can't use it to inspect an already-expired or self-signed cert — exactly the case you want to catch. The trick is to pull the certificate in DER form with getpeercert(binary_form=True) and decode that instead, then run verification separately just to set a "trusted" flag.

The CI-friendly part is the exit code:

secably ssl example.com --fail-if-expires 21
Enter fullscreen mode Exit fullscreen mode

Exits non-zero if the certificate expires within 21 days. Drop it in a scheduled job and you get weeks of warning instead of an incident.

Grade your security headers

secably headers https://example.com
Enter fullscreen mode Exit fullscreen mode
Security headers — https://example.com
--------------------------------------
  Grade: A  (90/100, HTTP 200)
  Present:
  • strict-transport-security
  • content-security-policy
  • x-frame-options
  • x-content-type-options
  • referrer-policy
  Missing:
  ✗ permissions-policy — Set a Permissions-Policy to restrict powerful browser features.
  Info leaks:
  ! server: nginx
Enter fullscreen mode Exit fullscreen mode

It scores the response A–F across the six headers that matter most — HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy — and calls out headers that leak your stack, like a verbose Server or X-Powered-By.

Same CI story:

secably headers https://example.com --fail-below B
Enter fullscreen mode Exit fullscreen mode

Fails the build if the grade drops below B.

Bonus: DNS posture and subdomains

Two more checks that are handy for recon and hygiene:

secably dns example.com          # A/AAAA/MX/NS/TXT + SPF / DMARC / DNSSEC
secably subdomains example.com   # passive discovery via Certificate Transparency
Enter fullscreen mode Exit fullscreen mode

dns tells you whether your domain can be spoofed (no SPF, no DMARC, no DNSSEC — the CLI warns on each). subdomains queries public Certificate Transparency logs — every name a CA has ever issued a certificate for — so there's no brute force and no traffic to the target. Add --resolve to get IPs, and --json to any command for a machine-readable object you can pipe into jq.

Put it in CI

You can wire the CLI into any pipeline, but on GitHub there's a ready-made action so you don't have to script the exit-code handling yourself:

name: Security check
on:
  schedule:
    - cron: '0 6 * * *'   # every morning
  workflow_dispatch:

jobs:
  secably:
    runs-on: ubuntu-latest
    steps:
      - uses: Secably/secably-security-check@v1
        with:
          url: your-domain.com
          fail-if-expires: 21
          headers-min-grade: B
Enter fullscreen mode Exit fullscreen mode

That's the whole thing. The job runs entirely inside your runner, prints a job summary, and turns red — with a GitHub error annotation — the moment the certificate is close to expiry or the headers slip below your bar. Running it on a daily cron (not just on push) is the point: certificates expire on the calendar, not on your commits.

Where it runs

To be upfront about it: ssl, headers, dns and subdomains all run on your machine with no account and no rate limit. There's also a secably scan command for a deeper server-side website/port scan, which uses a free API key — but the four checks above need nothing.

A CI run is a snapshot at deploy time, though. Certificates still expire, new subdomains still appear, and ports still open up between deploys. If you want these same checks running continuously with alerts, that's what the hosted side at Secably does — but the CLI and the Action are open source and stand on their own.

If you add it to a pipeline, I'd genuinely like to hear which check caught something first — mine was an expiring cert on a staging box nobody was watching.

Top comments (0)