Inspect and tighten a TLS service
Task
Stand up a TLS service, inspect it from the outside the way an auditor would, find the three failures that pass in a browser and fail everywhere else, and tighten the protocol and cipher configuration. Then confirm an old client is correctly refused.
Steps
- Generate a self-signed certificate and configure nginx to use it. Confirm with
openssl s_client -connect localhost:443that the connection is encrypted, and state what the self-signed certificate does NOT provide. - Create the incomplete-chain failure deliberately: install a leaf certificate without its intermediate. Show that a browser you have visited the CA from works, while
curlreports "unable to get local issuer certificate". - Create the SAN failure: issue a certificate with the hostname only in the Common Name, not the SAN. Browsers reject it (Chrome since version 58) and so does Go (since 1.15), while
openssl x509 -textstill shows the expected name. Now try your command-line tools and notice they do NOT reject it:openssl s_client -verify_hostname, curl and Python'ssslall still fall back to the Common Name when there is no SAN. That is the trap -- the certificate passes every check you can run from a shell and fails for your users. To see the failure from a shell, switch the fallback off:python3 -c "import ssl,socket; c=ssl.create_default_context(cafile='lab.crt'); c.hostname_checks_common_name=False; c.wrap_socket(socket.create_connection(('localhost',443)), server_hostname='localhost')"raises "Hostname mismatch". - Create the key-mismatch failure: install a certificate alongside the wrong private key. Confirm nginx refuses to start, and diagnose it by comparing the two modulus hashes.
- Tighten the configuration to TLS 1.2 and 1.3 only, with a modern cipher list. Confirm the SERVER now refuses TLS 1.1 -- which takes more care than it looks. A modern OpenSSL client will not offer TLS 1.1 at its default security level, so a plain
openssl s_client -tls1_1fails with "no protocols available" against ANY server, hardened or not: the refusal is the client's own. Let the client try, and read who said no:openssl s_client -connect localhost:443 -tls1_1 -cipher 'DEFAULT:@SECLEVEL=0'. A hardened server answersalert protocol version; a server that still accepts 1.1 completes the handshake. Run it once BEFORE step 5 to see the second, then after to see the first. - Enumerate what the server actually accepts with
nmap --script ssl-enum-ciphers -p 443 localhostand confirm no weak suites remain. - On RHEL, set this system-wide with
update-crypto-policies --set FUTUREand observe that it affects every TLS service at once.
Verify
echo | openssl s_client -connect localhost:443 2>/dev/null | openssl x509 -noout -dates
openssl s_client -connect localhost:443 -tls1_1 -cipher 'DEFAULT:@SECLEVEL=0' </dev/null 2>&1 | grep -qi 'alert protocol version' && echo "server refuses TLS 1.1"
openssl x509 -noout -modulus -in /etc/pki/tls/certs/lab.crt | openssl md5
openssl rsa -noout -modulus -in /etc/pki/tls/private/lab.key | openssl md5
nmap --script ssl-enum-ciphers -p 443 localhost 2>/dev/null | grep -ci 'sslv3\|tlsv1.0\|tlsv1.1' # 0
The two modulus hashes must match, and the SERVER must refuse the old-protocol connection -- alert protocol version is the server speaking; "no protocols available" is only your own client, and proves nothing about the server. The nmap count of weak protocols must be zero. Steps 2 to 4 are the cases an auditor meets weekly, and they do not all fail in the same place: the incomplete chain passes a browser and fails curl, while the CN-only certificate does the opposite -- it passes curl and fails a browser. Test with more than one kind of client.
Notes
The incomplete chain is the nastiest of the three because the obvious test -- open it in a browser -- passes, since browsers cache intermediates from previous sites. curl and every server-to-server call do not. Always test with something other than a browser.
This is an independent study companion for CompTIA Linux+ and is not produced by or endorsed by CompTIA.