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.1are legacy,TLS 1.2/1.3are 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/DHEkey 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.
| Flag | Does | Flag | Does |
|---|---|---|---|
-p | Protocols only | -t <proto> | STARTTLS (smtp/imap/ldap/…) |
-S | Server defaults + certificate | --mx <domain> | Test the domain’s MX hosts |
-E | Ciphers per protocol | --ip <ip> | Pin the node (fix SNI / LB) |
--wide | Add key-size / curve / strength columns | --file <f> / -iL | One target per line (mass) |
-f | Forward-secrecy ciphers | --parallel | Run the --file list concurrently |
-P | Server cipher preference | --severity <LVL> | Filter CSV/JSON below LEVEL |
-h | HSTS + HTTP headers | --logfile <f> | Save a text transcript |
-U | All vulnerability checks | --jsonfile-pretty <f> | Save structured JSON |
-c | Client simulation | --sneaky / --ids-friendly | Lower the scan profile |
-H -W -O --robot | One named flaw (Heartbleed / SWEET32 / POODLE / ROBOT) | --phone-out | Allow 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.
| Line | Means | Action |
|---|---|---|
green offered (OK) / ok | Healthy | Record as a passed control |
yellow warn | Context-dependent | Legacy cipher may be acceptable with a reason; verify, then record |
red not ok / VULNERABLE | Finding | Confirm service + version, then document |
offered by SSLv2/3, TLS 1.0/1.1 | Old protocol live | Finding on its own |
not offered for old versions | Desired state | Good |
- Protocol table: one row per version; you want
not offeredfor 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#
| Problem | Cause | Fix |
|---|---|---|
command not found | Not installed / stale PATH | Reinstall, or run the cloned ./testssl.sh |
Oops: Couldn't connect | Wrong port / service down | Confirm the port with Nmap first |
Service detected: not TLS | STARTTLS port without -t | Add -t smtp / -t imap / -t ldap |
| Certificate name mismatch | IP target sends no SNI | Use DOMAIN, or --ip IP DOMAIN to keep the name |
| Scan crawls for minutes | Full per-cipher enumeration | Scope with -p / -S / -U; add --connect-timeout |
| Connection reset mid-run | Rate limit / WAF | Slow down, --ids-friendly, re-confirm scope |
| Revocation shows as untested | OCSP/CRL lookups off by default | Add --phone-out (allows outbound to the CA) |
| Flags differ from a writeup | Options shift between releases | Check testssl --version and --help |
Gotchas#
--fastis 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--ipover 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 underbin/, 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.