If a compliance scan flags “TLS 1.0 enabled” or “weak cipher suites supported” on an Apache vhost, the finding is almost always real. Apache’s default SSLProtocol in the 2.4 branch is all -SSLv3, so TLS 1.0 and TLS 1.1 are offered unless someone explicitly removed them. A vhost that was correct three years ago may still be accepting protocol versions that PCI DSS and modern browser policy treat as deprecated.

This is an audit and lockdown procedure, not an incident runbook: measure what the server offers, measure what clients actually negotiate, remove the deprecated surface, and verify it stays removed. The exposure is quiet. Nothing in the error log complains, traffic flows normally, and the only symptom is %{SSL_PROTOCOL}x showing TLSv1 connections you did not know you had, or an external scanner showing cipher suites you thought were gone.

What this audit captures

Three separate questions, and conflating them is how lockdowns break production:

  1. What does the server offer? The protocol versions and cipher suites in the handshake, per vhost. Answered by scanning the listener.
  2. What do clients actually negotiate? The real population of SSL_PROTOCOL and SSL_CIPHER values across your traffic. This is a log-analysis question, and it is the only safe basis for deciding when you can disable TLS 1.0/1.1 without cutting off paying clients.
  3. Adjacent legacy surface. TRACE method support (Cross-Site Tracing), TLS compression, and session ticket handling. These ride along on the same audit because they live in the same directives and the same scanner findings.

Prerequisites

  • Root or sudo on the Apache host, and permission to reload Apache (apachectl graceful). Protocol changes take effect only on reload or restart.
  • A second host (or your workstation) on the network path to run scans from. Scanning localhost is fine for a first pass but misses anything a load balancer or TLS-terminating proxy in front of Apache changes. If an LB terminates TLS, the exposure is on the LB, not Apache, and this procedure applies to the LB config instead.
  • Apache 2.4.x. Check httpd -v and openssl version together, because protocol support is a property of both. TLS 1.3 requires Apache 2.4.37 or later built against OpenSSL 1.1.1+.
  • Knowledge of which vhosts share an IP:port pair. Name-based vhosts complicate per-vhost protocol policy (see pitfalls).

Procedure

1. Scan what the server offers

Run ssl-enum-ciphers against each public vhost. This answers “what does the server offer” without touching config:

# Enumerate protocol versions and cipher suites per vhost
nmap --script ssl-enum-ciphers -p 443 www.example.com

# Force SNI if the vhost is name-based and the scan hits the wrong cert
nmap --script ssl-enum-ciphers --script-args tls.servername=www.example.com -p 443 203.0.113.10

Read the output for two things: any TLSv1.0 or TLSv1.1 section that lists ciphers (meaning the protocol is accepted), and cipher entries flagged weak (RC4, 3DES, export-grade, NULL). Older nmap versions can miss TLS 1.3 entirely, so a missing TLSv1.3 section does not prove TLS 1.3 is off; confirm with openssl.

Spot-check each protocol version directly:

# Each of these should FAIL after lockdown. Before lockdown, note which succeed.
openssl s_client -connect www.example.com:443 -tls1   </dev/null 2>&1 | grep -E "Protocol|Cipher"
openssl s_client -connect www.example.com:443 -tls1_1 </dev/null 2>&1 | grep -E "Protocol|Cipher"
openssl s_client -connect www.example.com:443 -tls1_2 </dev/null 2>&1 | grep -E "Protocol|Cipher"

A successful handshake on -tls1 or -tls1_1 confirms the finding. Two caveats:

  • On an OpenSSL 3.x client, -tls1 and -tls1_1 can fail with “no protocols available” because the client’s default security level blocks those versions regardless of what the server offers. Treat the nmap scan as the authority, or test with -cipher 'DEFAULT@SECLEVEL=0' on the client side.
  • You are testing the default vhost on that IP:port unless s_client sends the right SNI. Pass -servername www.example.com explicitly on multi-vhost IPs.

2. Log what clients actually negotiate

Add a dedicated SSL request log using mod_ssl’s environment variables:

# In server config or vhost context
LogFormat "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %>s %b" ssl_request
CustomLog logs/ssl_request_log ssl_request

Reload and let it collect. How long depends on traffic shape: a day captures business-hours clients, a week captures weekly batch jobs and cron-driven API consumers. Legacy TLS clients are often not browsers. They are old Java runtimes, embedded devices, partner integrations, and monitoring probes, and they connect on their own schedule.

3. Tally the negotiated protocols

# Distribution of negotiated protocol versions ($4 is SSL_PROTOCOL; %t splits into two fields)
awk '{print $4}' logs/ssl_request_log | sort | uniq -c | sort -rn

# Who is still on TLS 1.0 or 1.1 ($3 is the client), and what path they request ($7)
awk '$4 ~ /^TLSv1(\.1)?$/ {print $3, $7}' logs/ssl_request_log | sort | uniq -c | sort -rn | head -30

The first command sizes the problem. The second builds the migration list. Three outcomes:

  • Zero TLS 1.0/1.1 connections. Lockdown is safe. Proceed.
  • A small, identifiable set. Old scanner, one partner endpoint, an internal probe. Fix or exception-list those clients, then lock down.
  • Material volume. Disabling immediately will break clients. You now have a data-backed remediation project instead of a guess, which is the point of the log.

4. Lock down protocol versions

In the vhost (or server-wide) SSL config:

# TLS 1.2 and 1.3 only
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

Prefer the all -... form over enumerating -all +TLSv1.2 +TLSv1.3: the subtractive form does not silently exclude protocol versions added in the future. Whichever form you use, run configtest before reloading:

apachectl configtest && apachectl graceful

5. Review cipher policy

If your step 1 scan showed only strong ciphers under TLS 1.2, do not churn SSLCipherSuite without cause; the default tracks the linked OpenSSL, and aNULL, eNULL, and export ciphers have been hard-disabled since Apache 2.4.7. If you need an explicit policy, generate one from a maintained reference such as the Mozilla SSL Configuration Generator rather than hand-writing a cipher string from an old blog post.

Two directives matter regardless of the cipher list:

# Server picks the cipher, not the client (off by default)
SSLHonorCipherOrder on

# TLS compression stays off; enabling it opens the CRIME attack
SSLCompression off

TLS 1.3 ciphers are a separate namespace. On Apache before 2.4.43 you cannot and do not need to configure them; OpenSSL’s built-in TLS 1.3 cipher list applies. On 2.4.43 and later, SSLCipherSuite TLSv1.3 ... sets them separately. Do not paste a TLS 1.2 cipher string into the TLS 1.3 slot; the names are different.

6. Disable TRACE

While you are in the config, close the adjacent finding. TRACE should not be on in production:

# Server config context only; this does not work in .htaccess
TraceEnable Off

Verifying the lockdown

Repeat the step 1 scan and step 2 logging after the reload. Verification is the same tooling as detection:

# These handshakes must now fail (mind the OpenSSL 3.x client caveat above)
openssl s_client -connect www.example.com:443 -tls1   </dev/null 2>&1 | tail -3
openssl s_client -connect www.example.com:443 -tls1_1 </dev/null 2>&1 | tail -3

# TRACE must now be rejected (expect 405)
curl -s -o /dev/null -w "%{http_code}\n" -X TRACE https://www.example.com/

# Confirm the reload actually applied (path is /var/log/httpd on RHEL-family)
grep -E "resuming normal operations|syntax error" /var/log/apache2/error.log | tail -5

The error log check matters more than it looks: if a graceful reload silently failed, the old config with the old SSLProtocol is still running, and your scan results will be confusing. A failed graceful leaves the previous configuration live while everyone believes the new one loaded.

Keep the ssl_request_log in place after lockdown. Its ongoing value is drift detection: the day TLSv1 reappears in the tally, either a config management run reverted your change or a new vhost was added without the hardened protocol line.

Common pitfalls

  • Per-vhost SSLProtocol on shared IPs. Before Apache 2.4.42 built against OpenSSL 1.1.1, SSLProtocol was effectively global per IP:port: the first vhost’s setting applied to every name-based vhost on that listener. On 2.4.42+ with OpenSSL 1.1.1+, SNI allows per-vhost protocol policy. If you are on an older build, setting SSLProtocol inside one vhost and not another gives you false confidence. Scan every vhost on the IP.
  • Non-SNI clients hit the default vhost. Clients that do not send SNI land on the first vhost for the IP:port. Harden the default vhost first; it is the one scanners and ancient clients actually negotiate with.
  • Scanning through a load balancer. If something else terminates TLS, your nmap scan measures the LB’s policy, not Apache’s. Locking down Apache while the LB still offers TLS 1.0 accomplishes nothing, and vice versa.
  • Killing a client population you never logged. Skipping step 2 because “nobody uses TLS 1.0 anymore” is how an embedded device fleet or a partner’s Java 6 integration becomes your outage. The log is the evidence; collect it first.
  • -all without re-enabling anything. SSLProtocol -all alone disables everything including future versions. If you use the explicit form, it is -all +TLSv1.2 +TLSv1.3.
  • Leaving TLS 1.3 “unverified” because nmap did not show it. Older nmap builds miss TLS 1.3. Confirm with openssl s_client -tls1_3 before concluding anything.

Signals to monitor

SignalWhy it mattersWarning sign
%{SSL_PROTOCOL}x distribution in SSL request logOnly direct measurement of negotiated protocol versionsAny TLSv1/TLSv1.1 after lockdown; any growth before it
%{SSL_CIPHER}x distributionShows weak-cipher negotiation that version counts hideNULL, RC4, 3DES, or export ciphers appearing
Periodic external ssl-enum-ciphers scanDetects config drift, new vhosts, LB changesTLS 1.0/1.1 sections reappearing in scan output
TRACE requests in access logXST probing; TRACE should be disabledAny TRACE in production; any 2xx response to TRACE
Error log after reloadsConfirms config actually appliedReload attempted without “resuming normal operations” following
TLS handshake CPU costFull handshakes are the most CPU-intensive thing Apache does; protocol changes shift resumption and handshake mixCPU climb on :443 connection rate after config changes

How Netdata helps

  • Netdata’s Apache collector tracks request rate, worker utilization, and connection counts per second, so you can see immediately whether a protocol lockdown changed traffic shape. A sudden drop in completed requests after apachectl graceful is the signature of having cut off a real client population.
  • Error-rate charts correlated against the reload timestamp distinguish “clients failing TLS negotiation” from unrelated 5xx, because TLS failures never reach the access log as requests; the visible symptom is missing throughput, not errors.
  • Per-second CPU on the Apache host lets you confirm that handshake cost did not shift unexpectedly after cipher or protocol changes, especially on hosts without HTTP/2 where each connection pays a full handshake.
  • Web log parsing of the %{SSL_PROTOCOL}x field turns the tally commands in this article into a continuous time series, which makes drift detection automatic instead of a quarterly audit.
  • Alerting on access-log gaps and throughput drops catches the dangerous failure mode of this work: Apache up, port open, and a slice of clients silently unable to complete a handshake.

Netdata’s Apache HTTP Server monitoring with Netdata brings these signals together with per-second metrics and ML anomaly detection.