Aller au contenu

Open source / Sentinel / tls-sentinel

tls-sentinel

TLS endpoint checker: certificate expiry, hostname mismatch, untrusted or incomplete chains, self-signed certs, weak keys and signatures, TLS 1.0/1.1 and weak ciphers, missing TLS 1.3, long-lived certs. Uses Node's TLS stack, no OpenSSL binary needed. CLI, library and GitHub Action. Zero deps.

Sur npm
$ npm install tls-sentinel

README

tls-sentinel

npm version CI Node License: MIT provenance

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

RuleDefaultMeaning
cert-expiredcriticalCertificate has expired
cert-not-yet-validcriticalCertificate is not valid yet
hostname-mismatchcriticalCertificate does not cover the host name
untrusted-chaincriticalChain does not lead to a trusted root
self-signedcriticalSelf-signed certificate
incomplete-chainhighServer does not send the intermediate certificates
cert-expiringhighCertificate expires within the critical window (default 14 days)
cert-expiring-soonmediumCertificate expires within the warning window (default 30 days)
weak-keyhighRSA key < 2048 bits or EC key < 256 bits
weak-signaturehighCertificate signed with SHA-1 or MD5
legacy-protocolhighServer accepts TLS 1.0 or TLS 1.1
weak-cipherhighServer accepts 3DES, RC4, NULL, EXPORT or anonymous ciphers
no-forward-secrecylowServer accepts static-RSA key exchange (no forward secrecy)
cbc-cipherlowServer prefers a CBC-mode cipher on TLS 1.2
no-tls13lowTLS 1.3 is not supported
long-validitylowCertificate lifetime is longer than 398 days
no-ocsp-staplinginfoNo OCSP response stapled
connect-errorhighCould 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" }
  ]
}
  • severity changes the severity of any rule, or turns it off.
  • allow suppresses findings. Each entry needs a rule (globs such as no-* work) and a reason, and can be narrowed with target and match (a glob tested against the target, location and value). Suppressed findings are listed in the JSON report. When expires (YYYY-MM-DD) has passed, the finding comes back together with an allow-expired finding, so exceptions can't be forgotten. Entries that match nothing are reported as allow-unused (info).

Output and exit codes

  • --format text|json|sarif|markdown for stdout, plus --json, --sarif and --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 (default high).
  • 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-rules prints 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>
[![Built with tls-sentinel by Louis Wright](https://img.shields.io/badge/built%20with-tls--sentinel-blue)](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. --revocation sends one OCSP request to the URL in the certificate's AIA extension. A revoked certificate is cert-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 grade and gradeReasons. 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)