How to test SSL configuration
Four facts decide the verdict: which protocol versions the listener offers, which cipher families it accepts, whether the certificate validates and when it expires, and whether HSTS is set. The version loop in step 1 answers the first and takes four connections. The rest follow from it.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
This is the sweep a tester runs on a release candidate and again after any infrastructure change that touches TLS termination. It is deliberately short, because a long report nobody reads is how an expired certificate reaches production on a Saturday. Each line below has a separate page with the failure modes and the controls, so the sweep stays at four commands and hands off when something looks wrong.
Run it per listener. A load balancer, an admin port on 8443 and a second region each carry their own TLS profile, and they drift apart between deployments.
Prerequisites
- OpenSSL 1.1.1 or later, and curl 7.52 or later. Both ship on Linux and macOS and install on Windows with winget.
- The
@SECLEVEL=0suffix in step 1. Without it, OpenSSL 3 refuses to offer TLS 1.0 and 1.1 and the loop reports every server as clean. The behaviour is described in SSL_CTX_set_security_level. - A list of every TLS listener in scope, host and port. The sweep is per listener, not per domain.
Steps
- Step 1.
Offer each protocol version on its own and record what the server does with it.
for v in tls1 tls1_1 tls1_2 tls1_3; do if openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com -$v -cipher 'DEFAULT@SECLEVEL=0' </dev/null >/dev/null 2>&1; then echo "$v: offered"; else echo "$v: refused"; fi; donetls1: refused tls1_1: refused tls1_2: offered tls1_3: offeredTwo refusals at the bottom and two offers at the top is the shape you want.
- Step 2.
Probe the cipher families that change the verdict, one connection each.
for fam in "ECDHE+AESGCM" "kRSA" "aNULL"; do if openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com -tls1_2 -cipher "$fam@SECLEVEL=0" </dev/null >/dev/null 2>&1; then echo "$fam: accepted"; else echo "$fam: refused"; fi; doneECDHE+AESGCM: accepted kRSA: accepted aNULL: refusedkRSA: acceptedmeans RSA key exchange is still available, so sessions recorded today can be read by anyone who later obtains the private key. - Step 3.
Read the certificate the listener presents, and ask whether it survives the next 30 days.
openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -checkend 2592000subject=CN = www.cloudflare.com issuer=C = US, O = Let's Encrypt, CN = YE2 notBefore=Sep 3 07:35:04 2026 GMT notAfter=Dec 2 07:35:03 2026 GMT Certificate will not expire-checkend 2592000exits non-zero when the certificate expires inside 30 days, so this line works as a CI gate. - Step 4.
Confirm the browser is told to stay on HTTPS.
curl -sI https://www.cloudflare.com/ | grep -i 'strict-transport-security'strict-transport-security: max-age=31536000; includeSubDomains
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| tls1: offered or tls1_1: offered | A deprecated version is live | Block the release. RFC 8996 forbids both. |
| tls1_3: refused | The listener stops at TLS 1.2 | Not a defect on its own. Raise it if the target profile says 1.3. |
| kRSA: accepted | Key exchange without forward secrecy | Remove the non-ECDHE suites from the profile. |
| aNULL: accepted | Unauthenticated suites are on | Stop and treat as a production incident. |
| Certificate will expire | Under 30 days of validity left | Check renewal automation before it becomes an outage. |
| No strict-transport-security | The first request can go over HTTP | Add the header once the redirect chain is confirmed. |
Common mistakes
Thresholds
What to check next
- How to check TLS version of a website: what a normal client negotiates, as opposed to the offered set above.
- How to check if TLS 1.0 is disabled: the control run that makes step 1 evidence rather than a guess.
- How to check cipher suites of a server: how to read the family probe and rank what it finds.
- How to check the TLS handshake: where to look when one of these commands fails without a clear reason.
- How to check SSL certificate expiry: turning the
-checkendline into an alert that fires early enough.
FAQ
How to test TLS configuration with nmap?
nmap --script ssl-enum-ciphers -p 443 host enumerates versions and suites and grades each one. It opens many connections, so keep it for hosts you own. The loops above answer the same questions with seven connections in total.
Does an A grade from a hosted scanner cover this?
It covers what the scanner can reach from the internet. Internal listeners, origin servers behind a CDN and non-standard ports are outside its view, and those are where an old profile survives longest.
Which of these belongs in CI?
Step 3. -checkend returns a non-zero exit code, so a pipeline step fails on its own without any parsing. The version loop is better run on a schedule, since its output changes only when infrastructure changes.
Should SSL 3.0 be tested too?
Modern OpenSSL builds drop SSLv3 entirely, so the probe cannot be sent. If a listener is old enough to worry about, capture a handshake with a packet trace instead of trying to force the version from a current client.
Verified
Verified by Maks Vernyopenssl 3.1.1curl 8.21.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: tls-certificate TLS versions offered, certificate chain, expiry, SANs
- All security headers and tls checks
intermediate8 minpublished updated Maks Verny