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

Steps

  1. 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; done
    
    tls1: refused
    tls1_1: refused
    tls1_2: offered
    tls1_3: offered

    Two refusals at the bottom and two offers at the top is the shape you want.

  2. 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; done
    
    ECDHE+AESGCM: accepted
    kRSA: accepted
    aNULL: refused

    kRSA: accepted means RSA key exchange is still available, so sessions recorded today can be read by anyone who later obtains the private key.

  3. 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 2592000
    
    subject=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 2592000 exits non-zero when the certificate expires inside 30 days, so this line works as a CI gate.

  4. 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

Sign: Every version comes back as refused and the listener is recorded as hardened.Cause: Without the @SECLEVEL=0 suffix, OpenSSL 3 declines to offer TLS 1.0 and 1.1 locally, so the loop measures your own client. The negative test needs the control run described on the TLS 1.0 page.
Sign: The domain passes and an internal client still negotiates an old version.Cause: The sweep covered one host and port. Admin consoles, message brokers and a second load balancer hold separate TLS profiles that are rarely updated together. Repeat the loop for each listener.
Sign: A hosted grading service reports A and the sweep finds a weak family.Cause: Grading services score the public edge. When the edge terminates TLS and re-encrypts to an origin with an older profile, the grade describes the edge only. Run the sweep against the origin address as well.

Thresholds

TLS 1.2 is the minimum acceptable version Source: RFC 8996 deprecates TLS 1.0 and TLS 1.1, March 2021
HSTS max-age of 31536000 seconds, one year, is the value required for preload Source: hstspreload.org submission requirements

What to check next

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.

intermediate8 minpublished updated Maks Verny