tls-sentinel
Check TLS endpoints for expiring or untrusted certificates, hostname mismatches, missing intermediates, weak keys and signatures, TLS 1.0/1.1, weak ciphers and missing TLS 1.3.
tls-sentinel connects to each host[:port], inspects the certificate chain the server sends, and probes which protocol versions and cipher families it accepts. It uses Node's own TLS stack, so there is no openssl binary or external service involved, and it only performs handshakes (no HTTP request is sent), so it works for any TLS service: HTTPS, SMTPS, IMAPS, LDAPS, databases. CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies.
$ tls-sentinel example.com mail.example.com:993 --warn-days 21
What it checks
- Certificate: expired, not yet valid, expiring within
--critical-days(default 14, high) or--warn-days(default 30, medium), host name not covered, self-signed, chain not trusted, intermediates missing, RSA < 2048 / EC < 256 bits, SHA-1/MD5 signatures, lifetimes over 398 days, no OCSP stapling. - Protocols and ciphers: accepts TLS 1.0 or 1.1, no TLS 1.3, accepts 3DES/RC4/NULL/EXPORT/anonymous suites, accepts static-RSA key exchange (no forward secrecy), negotiates CBC on TLS 1.2.
Probe results depend on what the local OpenSSL can still offer: OpenSSL 3 (Node 18+) has dropped RC4 and EXPORT suites, so a server that only accepts those shows up as a connect-error. Use --quick to skip the probes and make one handshake per target (good for frequent expiry monitoring). Unreachable targets exit with code 2. --ca adds an internal CA bundle on top of Node's root store.
Rules
| Rule | Default | Meaning |
|---|---|---|
cert-expired | critical | Certificate has expired |
cert-not-yet-valid | critical | Certificate is not valid yet |
hostname-mismatch | critical | Certificate does not cover the host name |
untrusted-chain | critical | Chain does not lead to a trusted root |
self-signed | critical | Self-signed certificate |
incomplete-chain | high | Server does not send the intermediate certificates |
cert-expiring | high | Certificate expires within the critical window (default 14 days) |
cert-expiring-soon | medium | Certificate expires within the warning window (default 30 days) |
weak-key | high | RSA key < 2048 bits or EC key < 256 bits |
weak-signature | high | Certificate signed with SHA-1 or MD5 |
legacy-protocol | high | Server accepts TLS 1.0 or TLS 1.1 |
weak-cipher | high | Server accepts 3DES, RC4, NULL, EXPORT or anonymous ciphers |
no-forward-secrecy | low | Server accepts static-RSA key exchange (no forward secrecy) |
cbc-cipher | low | Server prefers a CBC-mode cipher on TLS 1.2 |
no-tls13 | low | TLS 1.3 is not supported |
long-validity | low | Certificate lifetime is longer than 398 days |
no-ocsp-stapling | info | No OCSP response stapled |
connect-error | high | Could not connect or complete a TLS handshake |
Install and run
npx tls-sentinel example.com www.example.com:443 api.example.com:8443
npx tls-sentinel internal.corp:636 --ca corp-root.pem --quick --warn-days 45
npx tls-sentinel https://example.com --format sarif > tls.sarif
Run it on a schedule (for example a daily GitHub Actions cron) to catch expiring certificates.
Library
import { check, exitCode } from 'tls-sentinel';
const report = await check(['example.com'], { quick: true, warnDays: 21 });
console.log(report.targets[0].summary.daysLeft);
process.exitCode = exitCode(report, 'high');
Configuration
Put a .tls-sentinel.json in the working directory (or pass --config <file>). Every key is optional; CLI flags override the file.
{
"failOn": "high",
"severity": { "no-tls13": "high" },
"allow": [
{ "rule": "no-tls13", "match": "legacy.example.com*", "reason": "Accepted risk, ticket SEC-123", "expires": "2027-03-31" }
]
}
severitychanges the severity of any rule, or turns itoff.allowsuppresses findings. Each entry needs arule(globs such asno-*work) and areason, and can be narrowed withtargetandmatch(a glob tested against the target, location and value). Suppressed findings are listed in the JSON report. Whenexpires(YYYY-MM-DD) has passed, the finding comes back together with anallow-expiredfinding, so exceptions can't be forgotten. Entries that match nothing are reported asallow-unused(info).
Output and exit codes
--format text|json|sarif|markdownfor stdout, plus--json,--sarifand--markdown <file>to write extra copies in one run. SARIF 2.1.0 works with GitHub code scanning.--fail-on critical|high|medium|low|info|none(defaulthigh).- Exit code 0: passed. 1: at least one finding at or above
--fail-on. 2: usage or config error, or a target could not be checked. --list-rulesprints every rule with its default severity.
GitHub Action
permissions:
contents: read
security-events: write # only for the SARIF upload
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { persist-credentials: false }
- uses: MrLouisWright/tls-sentinel@v1
with:
targets: 'example.com'
fail-on: high
sarif: tls-sentinel.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: tls-sentinel.sarif }
Inputs: targets, fail-on, config, sarif, json, job-summary (Markdown report in the job summary, default true), args (extra CLI flags), warn-days. Outputs: markdown-file, exit-code.
Requirements
Node.js 18 or newer. No runtime dependencies.
Support
If this project is useful to you, you can support its development with a donation: Buy me a coffee. It helps keep these tools maintained, documented and free for everyone.
Credit (optional)
Free to use under MIT, no attribution required. If tls-sentinel helps you, a link back is appreciated:
<a href="https://louiswright.co.uk">Built with tls-sentinel by Louis Wright</a>
[](https://louiswright.co.uk)
Revocation, Certificate Transparency, STARTTLS and grades
npx tls-sentinel example.com --revocation # query OCSP if the server does not staple
npx tls-sentinel mail.example.com --starttls smtp # port 587 unless you give one
npx tls-sentinel db.internal:5432 --starttls postgres --ca internal-ca.pem
npx tls-sentinel example.com --min-grade A- # fail below an SSL Labs-style grade
- OCSP: a stapled response is always parsed.
--revocationsends one OCSP request to the URL in the certificate's AIA extension. A revoked certificate iscert-revoked(critical). Must-Staple certificates without a stapled response, and certificates with no OCSP or CRL URL at all, are reported too. Only the certificate status is read; the responder's signature is not verified. - Certificate Transparency: a publicly trusted certificate without embedded SCTs is
no-sct. - STARTTLS: SMTP (EHLO/STARTTLS), IMAP, POP3 (STLS), FTP (AUTH TLS) and PostgreSQL (SSLRequest), then the normal checks.
- Grade: each target's summary has
gradeandgradeReasons. Trust problems give T (M for a name mismatch). RC4/NULL/EXPORT ciphers give F and 3DES gives C. TLS 1.0/1.1, no forward secrecy or a missing intermediate cap it at B, and no TLS 1.3 at A-. A+ is never given, because it needs HSTS, which header-sentinel checks. tls-sentinel only does TLS handshakes and certificates.
Module formats and types
Ships ESM (import) and a bundled CommonJS build (require), with TypeScript declarations generated from the source:
import * as lib from 'tls-sentinel'; // ESM
const lib = require('tls-sentinel'); // CommonJS
Tests live in test/ and run on Node 18, 20 and 22 in GitHub Actions, including a require+import check. Releases are published with npm publish --provenance.
License
MIT © Louis Wright (Louis Wright)