testssl.sh

testssl.sh is a single bash script that negotiates TLS with a service and reports exactly what it accepts: every protocol version, every cipher suite, the full certificate chain, and a checklist of named TLS flaws. It probes the protocol directly instead of trusting a banner, so the report is what the server actually does. Reach for it when a quick cipher list isn’t enough: proving a weak-TLS finding, auditing a reverse proxy, or confirming a hardening change really took effect.

Mental model: testssl [scope flags] [output flags] HOST:PORT. No port means 443.

New to TLS? Core concepts
  • Protocol version: the negotiated wire format. SSLv2/SSLv3/TLS 1.0/1.1 are legacy, TLS 1.2/1.3 are current
  • Cipher suite: one bundle of key-exchange + authentication + bulk cipher + MAC (e.g. ECDHE-RSA-AES256-GCM-SHA384)
  • Handshake / negotiation: client and server agree on a version and suite; the server may enforce its own order (-P) or take the client’s
  • Forward secrecy: ephemeral ECDHE/DHE key exchange, so one stolen key can’t decrypt past traffic
  • Certificate chain: leaf (subject + SANs), intermediates, root; plus validity window, key size, and signature algorithm
  • SNI: the hostname sent in the handshake so one IP can serve many certs; wrong SNI means the wrong cert or a name mismatch
  • STARTTLS: a plaintext protocol (SMTP/IMAP/LDAP) upgraded to TLS mid-session, not TLS from byte one (needs -t)
  • Named flaws: protocol/cipher bugs with brands - Heartbleed, ROBOT, POODLE, BEAST, SWEET32, FREAK, LOGJAM, DROWN
When to reach for testssl.sh (and when not)

Reach for it when a plain cipher list isn’t the whole answer: proving a weak-protocol or weak-cipher finding, reading the full certificate chain, running the named-vuln checklist (each actually probed, not guessed from a version string), auditing what a reverse proxy or a specific load-balancer node really negotiates, or diffing a saved scan before and after a hardening change.

Reach for something else when you need speed or clean automation across many hosts (sslscan, sslyze), a fast one-line cipher grade or to find which ports even speak TLS first (nmap ssl-enum-ciphers), or to confirm one odd result by hand at the raw-handshake level (openssl s_client -connect HOST:443).

Install, update, verify#

# Package: convenient, but can lag upstream
sudo apt install -y testssl.sh          # Debian / Ubuntu / Kali
sudo dnf install -y testssl             # Fedora / RHEL
brew install testssl                    # macOS

# Always-current: clone and run in place (newest vuln checks + cipher maps)
git clone --depth 1 https://github.com/testssl/testssl.sh.git
cd testssl.sh && ./testssl.sh --version

sudo apt install --only-upgrade -y testssl.sh   # update the package
git -C testssl.sh pull                           # or update the clone

command -v testssl        # confirm PATH (the package installs it as plain `testssl`)
testssl --version         # version + the OpenSSL build it bundles

The clone bundles cipher and signature maps under etc/ and a patched OpenSSL under bin/, so a fresh checkout fingerprints ciphers and flags new flaws more accurately than an old distro package. Use the package on a daily driver; clone when validating a recent advisory.

Flags you’ll actually use#

testssl groups its work into scope flags (what to test), output flags (where results go), and single-vuln flags (one advisory each). Skim these first; the cheat sheet below is these flags combined.

FlagDoesFlagDoes
-pProtocols only-t <proto>STARTTLS (smtp/imap/ldap/…)
-SServer defaults + certificate--mx <domain>Test the domain’s MX hosts
-ECiphers per protocol--ip <ip>Pin the node (fix SNI / LB)
--wideAdd key-size / curve / strength columns--file <f> / -iLOne target per line (mass)
-fForward-secrecy ciphers--parallelRun the --file list concurrently
-PServer cipher preference--severity <LVL>Filter CSV/JSON below LEVEL
-hHSTS + HTTP headers--logfile <f>Save a text transcript
-UAll vulnerability checks--jsonfile-pretty <f>Save structured JSON
-cClient simulation--sneaky / --ids-friendlyLower the scan profile
-H -W -O --robotOne named flaw (Heartbleed / SWEET32 / POODLE / ROBOT)--phone-outAllow OCSP/CRL revocation lookups

Cheat sheet#

Scope down to the one answer you need, then group the output and save it. A bare host runs everything except -E and -g.

# Full run, or a single host:port
testssl HOST                              # the whole battery (slow - save it, don't repeat)
testssl https://HOST                      # a URL works too; port defaults to 443
testssl TARGET_IP:8443                    # any host:port for TLS on a custom port

# Fast triage: scope to one question (seconds, not minutes)
testssl -p HOST                           # protocols only: is SSLv3 / TLS 1.0 / 1.1 still on?
testssl -S HOST                           # server defaults + full certificate
testssl -U HOST                           # every named-vulnerability check
testssl -h HOST                           # HSTS + HTTP security headers

# Ciphers, from summary to detail
testssl -E HOST                           # every cipher, grouped per protocol
testssl -E --wide HOST                    # + key-size, curve, and strength columns
testssl -f HOST                           # forward-secrecy ciphers and curves
testssl -P HOST                           # who picks the cipher: server order or client
testssl --rc4 HOST                        # which RC4 ciphers are still offered
testssl -x 3DES HOST                      # test one cipher pattern (name or hexcode)
testssl -c HOST                           # client simulation: which clients still connect

# One named flaw at a time (one advisory, one or two connections)
testssl -H HOST                           # Heartbleed
testssl -W HOST                           # SWEET32 (64-bit block ciphers: 3DES / RC2 / IDEA)
testssl -O HOST                           # POODLE
testssl --robot HOST                      # ROBOT (Bleichenbacher oracle)
# more: -I ccs  -T ticketbleed  -R renegotiation  -C crime  -B breach  -A beast  -L lucky13  -F freak  -J logjam  -D drown

# STARTTLS and mail
testssl -t smtp TARGET_IP:25              # STARTTLS: ftp/smtp/pop3/imap/ldap/nntp/postgres/mysql/...
testssl -t imap TARGET_IP:143             # STARTTLS on IMAP
testssl HOST:465                          # implicit TLS (SMTPS) needs NO -t
testssl --mx DOMAIN                       # every MX host for the domain, STARTTLS on 25

# Pin one node behind a name / load balancer
testssl --ip 10.10.10.5 DOMAIN            # correct SNI (DOMAIN), forced to that node
testssl --ip one DOMAIN                   # just the first address DNS returns

# Save evidence you can diff after a fix
testssl --logfile scans/t.log HOST                               # readable transcript
testssl --jsonfile-pretty scans/t.json HOST                      # structured JSON
testssl --severity HIGH --jsonfile-pretty scans/high.json HOST   # HIGH+ into the file only

# Mass testing and lower profile
testssl --parallel --file targets.txt     # one URI (or full cmd) per line, run concurrently
testssl --ids-friendly HOST               # skip the checks most likely to trip an IDS
testssl --sneaky HOST                     # browser-like User-Agent + referer (not stealthy)
testssl --phone-out HOST                  # allow OCSP/CRL revocation lookups to the CA

Command breakdowns#

Each is one situation, the exact command, and what to expect back.

Run the full battery once and save it#

testssl --logfile scans/full.log --jsonfile-pretty scans/full.json DOMAIN

A bare host runs everything except the per-cipher enumeration and grease checks. It is slow (dozens of handshakes), so capture it to disk once and read the files instead of re-scanning.

Triage fast by scoping to one area#

testssl -p DOMAIN        # protocols, in seconds
testssl -S DOMAIN        # defaults + certificate, in seconds

Scoping is the fast path in 3.x: pick the single question (protocols, cert, vulns, headers) and testssl skips the rest. This replaces the old --fast, which upstream now warns against.

Confirm old protocols are actually disabled#

testssl -p DOMAIN

One yes/no line per version. offered next to SSLv2, SSLv3, TLS 1.0, or TLS 1.1 is a finding on its own; not offered for all of those is the goal.

Read the certificate: names, dates, key, signature#

testssl -S DOMAIN

Subject, SANs, issuer, validity window, key size, and signature algorithm. The SAN list often leaks internal hostnames worth pivoting on.

List exactly which ciphers are offered#

testssl -E --wide DOMAIN

-E walks ciphers per protocol; --wide adds the key-size, curve, and strength columns, so weak suites stand out at a glance.

Run every vulnerability check#

testssl -U DOMAIN

Heartbleed, ROBOT, CCS, CRIME, BREACH, POODLE, SWEET32, BEAST, LUCKY13, FREAK, LOGJAM, DROWN, and more, each actually probed on the wire rather than guessed from a version string.

Test one named flaw without the whole run#

testssl -H DOMAIN        # Heartbleed only
testssl -W DOMAIN        # SWEET32 (64-bit block ciphers: 3DES / RC2 / IDEA)
testssl --robot DOMAIN   # ROBOT (Bleichenbacher oracle)

The single-vuln flags (-H -I -T --robot -R -C -B -O -W -A -L -F -J -D) confirm one advisory in seconds and one or two connections, instead of the full -U sweep.

Scan a STARTTLS service (mail, LDAP, database)#

testssl -t smtp TARGET_IP:25
testssl -t imap TARGET_IP:143
testssl -t ldap TARGET_IP:389

Negotiates the plaintext upgrade first, then runs the TLS checks on the upgraded channel. Without -t, these ports look like “not TLS”. Port 465 is implicit TLS, so scan it with no -t.

Audit all of a domain’s mail servers at once#

testssl --mx DOMAIN

Resolves the MX records and STARTTLS-tests each on port 25, high priority to low: the whole mail edge in one command.

Pin one backend behind a load balancer#

testssl --ip 10.10.10.5 DOMAIN

Sends the correct SNI (DOMAIN) but forces the connection to that node, so a weak member can’t hide behind round-robin DNS. Use --ip one to just take the first address DNS returns.

Check cipher preference and forward secrecy#

testssl -P DOMAIN        # who picks the cipher: server order or client
testssl -f DOMAIN        # the forward-secrecy ciphers and curves on offer

-P shows whether the server enforces its own cipher order (it should); -f lists the FS suites and curves.

See which clients can still connect#

testssl -c DOMAIN

Simulates real clients (browsers, OpenSSL, Java releases) and reports the protocol and cipher each would negotiate, so you know who a hardening change locks out before you ship it.

Check HSTS and HTTP security headers#

testssl -h DOMAIN

HSTS and its max-age, the usual security headers, plus any reverse-proxy or server banner leakage: the HTTP layer riding on the TLS you just audited.

Trim the saved report to just what matters#

testssl --severity HIGH --jsonfile-pretty scans/high.json DOMAIN

--severity filters the file output only, not the console: the JSON/CSV report keeps HIGH+ findings, so it needs a --jsonfile/--csvfile to have any effect.

Lower the profile of a scan#

testssl --ids-friendly --sneaky DOMAIN

--ids-friendly drops the handful of checks most likely to get your IP blocked; --sneaky swaps the testssl User-Agent and referer for browser-like ones. Neither makes a full run quiet.

Mass-test a list of hosts#

testssl --parallel --file targets.txt

One URI (or a full testssl command line) per line; --parallel runs them concurrently. Comments with # are allowed, and nmap greppable output (-oG) works as input too.

Reading the output#

testssl.sh colour-codes every line and closes with an experimental letter grade.

LineMeansAction
green offered (OK) / okHealthyRecord as a passed control
yellow warnContext-dependentLegacy cipher may be acceptable with a reason; verify, then record
red not ok / VULNERABLEFindingConfirm service + version, then document
offered by SSLv2/3, TLS 1.0/1.1Old protocol liveFinding on its own
not offered for old versionsDesired stateGood
  • Protocol table: one row per version; you want not offered for everything below TLS 1.2.
  • Certificate block: name matches host, dates valid, key >= 2048-bit RSA / 256-bit EC, signature SHA-256+ (not SHA-1 or MD5).
  • Rating (experimental): an SSL-Labs-style A+..F grade, a directional summary of what testssl found, not the finding itself. testssl reports what’s negotiated; whether that’s acceptable is a policy call for the service.

Troubleshooting#

ProblemCauseFix
command not foundNot installed / stale PATHReinstall, or run the cloned ./testssl.sh
Oops: Couldn't connectWrong port / service downConfirm the port with Nmap first
Service detected: not TLSSTARTTLS port without -tAdd -t smtp / -t imap / -t ldap
Certificate name mismatchIP target sends no SNIUse DOMAIN, or --ip IP DOMAIN to keep the name
Scan crawls for minutesFull per-cipher enumerationScope with -p / -S / -U; add --connect-timeout
Connection reset mid-runRate limit / WAFSlow down, --ids-friendly, re-confirm scope
Revocation shows as untestedOCSP/CRL lookups off by defaultAdd --phone-out (allows outbound to the CA)
Flags differ from a writeupOptions shift between releasesCheck testssl --version and --help

Gotchas#

  • --fast is deprecated in testssl 3.x. It still parses but prints “not recommended to use anymore,” because its shortcuts skip real probes. Scope the run instead (-p, -S, -U) for a fast, honest answer.
  • A hostname with several A records tests only one node. By default testssl scans the first address it resolves (or --ip one), so a weak member of a round-robin pool hides. Loop --ip over every address to cover the whole set.
  • The bundled OpenSSL sets the ceiling on what a scan can see. Many checks lean on testssl’s own openssl; a cloned checkout ships a patched build under bin/, which is why the clone fingerprints ciphers and detects flaws a stale distro package misses. Prefer the clone when validating a fresh advisory.
  • STARTTLS and implicit TLS want different flags. Port 25/587 is STARTTLS (-t smtp); port 465 is TLS from byte one (no -t). Mixing them gives “not TLS” on 587 or a hang on 465.
  • Don’t report the letter grade as the verdict. The rating is experimental and rolls findings up its own way, so a single grade can mask the one issue that matters. Cite the actual findings, or turn the grade off with --disable-rating.

Pairs with#

nmap finds which ports even speak TLS first (-p 443 --open, or ssl-enum-ciphers for a fast cipher grade). sslscan gives a quick protocol and cipher overview when you don’t need the vulnerability pass, and sslyze scales clean automation across many hosts. Drop to openssl s_client -connect HOST:443 to confirm one odd result by hand at the raw handshake, and jq slices the JSON output for reporting. Reach for sslscan or sslyze when you need speed across a fleet; stay on testssl.sh when you need depth and the named-vuln checks.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop