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]"
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
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
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
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
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
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
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
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
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)