Troubleshooting

Apache ProxyPass to an HTTPS backend: AH00961 failed to enable ssl support

AH00961 means SSLProxyEngine is off, so Apache will not speak TLS to the backend. Tested on httpd 2.4.69 and 2.4.48: the 500, the hidden handshake errors, and each fix.

· 16 min read · How we verify this

Key points

  • AH00961: https: failed to enable ssl support means ProxyPass points at an https:// URL but SSLProxyEngine is off for that virtual host. It is off by default. Apache answers 500 Internal Server Error. Add SSLProxyEngine on to the same <VirtualHost>, or to a <Proxy> section on 2.4.30 and later.
  • On httpd 2.4.49 and later, the line before it reads AH01961: failed to enable ssl support [Hint: if using mod_ssl, see SSLProxyEngine], logged by the core. Up to 2.4.48, mod_ssl logged AH01961: SSL Proxy requested for host:port but not enabled [Hint: SSLProxyEngine]. Old answers quote the old text, so a search for it misses current logs.
  • With the engine on, Apache still checks the backend's certificate name and expiry, even under the default SSLProxyVerify none. A failed check gives the client 500 Proxy Error with Error during SSL Handshake with remote server. In both versions tested, the status was 500, not the 502 the manual names.
  • The reason for the failed check (AH02411 name mismatch, AH02005 CN mismatch, AH02004 expired, AH02003 with wrong version number) is logged at info level. At the default LogLevel warn you never see it. Set LogLevel warn ssl:info to read it.
  • An IP address in the ProxyPass URL means no SNI is sent, and the name check compares the IP with the certificate. ProxyPreserveHost On makes Apache send and check the client's Host instead. Use the backend's real hostname in the URL, and trust its CA with SSLProxyVerify require rather than turning checks off.

AH00961: https: failed to enable ssl support means Apache was told to proxy to an https:// backend, but SSLProxyEngine is off. Apache will not speak TLS to the backend without it, and it is off by default. The client gets 500 Internal Server Error. The fix is one line in the same <VirtualHost> as the ProxyPass:

apache
<VirtualHost *:443>
    SSLProxyEngine on
    ProxyPass        "/" "https://backend.example.internal:8443/"
    ProxyPassReverse "/" "https://backend.example.internal:8443/"
</VirtualHost>

That is often not the end of it. Once the engine is on, Apache checks the backend's certificate name and dates by default. When a check fails, the client gets 500 Proxy Error with the reason Error during SSL Handshake with remote server. The log line that names the real cause is written at info level, so it does not show at the default LogLevel warn.

Here is what each case did on httpd 2.4.69 and 2.4.48. Both versions returned the same status for every row.

What is wrongClient getsSeen at LogLevel warnReal cause, logged at info (ssl:info)Fix
No SSLProxyEngine on500 Internal Server ErrorAH01961 and AH00961(none needed)SSLProxyEngine on
Certificate is for another name500 Proxy ErrorAH00898: Error during SSL Handshake with remote serverAH02411: Peer certificate does not match for hostname backend.testUse a URL host the certificate names
Certificate has expired500 Proxy ErrorAH00898AH02004: Peer certificate is expiredRenew it on the backend
ProxyPass uses an IP address500 Proxy ErrorAH00898AH02411 ... for hostname 10.201.14.2Use the hostname, not the IP
ProxyPreserveHost On500 Proxy ErrorAH00898AH02411 ... for hostname www.example.comCertificate must cover the public name, or turn it off
https:// to a plain HTTP port500 Proxy ErrorAH00898AH02003: SSL Proxy connect failed and wrong version numberhttp://, or the real TLS port
SSLProxyVerify require with no CA file500 Proxy ErrorAH00898AH02276 ... Error (20): unable to get local issuer certificateSSLProxyCACertificateFile

The rest of this page explains each row, and why the log looks different from what most answers quote.

What SSLProxyEngine is, and why it is off#

A reverse proxy makes two connections. One is from the browser to Apache. The other is from Apache to the backend, the app server behind it. Each connection can use TLS or not. SSLEngine on turns on TLS for the first one, where Apache is the server. SSLProxyEngine on turns on TLS for the second one, where Apache is the client.

The two switches are separate. A site served over HTTPS can still proxy to a plain http:// backend, and that is the common setup. TLS termination vs passthrough covers when each leg should use TLS.

The mod_ssl manual gives the default as SSLProxyEngine off. It says: "By default the SSL/TLS Protocol Engine is disabled for proxy both for the main server and all configured virtual hosts." So every ProxyPass to an https:// URL needs SSLProxyEngine on in the same virtual host. The manual's own example puts it inside <VirtualHost>. If you have several virtual hosts, check each one that proxies to HTTPS.

From httpd 2.4.30, the directive is also allowed inside a <Proxy> section. That scopes it to one backend:

apache
<Proxy "https://backend.example.internal:8443/">
    SSLProxyEngine on
</Proxy>
ProxyPass "/" "https://backend.example.internal:8443/"

That form returned 200 on 2.4.69. The 2.4.55 change log entry for PR 66190 notes that before that release, this form logged an error even though "the connection continued to work". If you see a stray error line on an older release with this setup, that is why.

The manual also says not to put SSLProxyEngine in a virtual host used as a forward proxy, where browsers send CONNECT requests. A forward proxy tunnels TLS and does not need it. This page is about the reverse proxy case.

The log lines: AH00961, and AH01961 in two wordings#

With the engine off, proxy_util.c asks for TLS on the new backend connection, gets a refusal, and logs:

text
[proxy:error] AH00961: https: failed to enable ssl support for 10.201.14.2:8443 (backend.test)

It then returns HTTP_INTERNAL_SERVER_ERROR. That is the plain 500 page, "Internal Server Error", not a Proxy Error page. Nothing was sent to the backend. The TCP connection was made, so this is not the 503 of a failed connect described in why Apache returns 503 with AH01114.

The line logged just before AH00961 changed in httpd 2.4.49. Most forum answers quote the old one.

Up to 2.4.48, mod_ssl itself refused, at error level, with a hint:

text
[ssl:error] AH01961: SSL Proxy requested for front:9000 but not enabled [Hint: SSLProxyEngine]

From 2.4.49, the CHANGES file records a refactor. The core gained ap_ssl_bind_outgoing(), which mod_proxy now calls "instead of the previous optional function ssl_engine_set". mod_ssl's refusal moved to debug level, with a new number, AH10272. The core now writes the error, still numbered AH01961 but in new words:

text
[ssl:debug] mod_ssl.c(562): AH10272: SSL Proxy requested for front:9000 but not enabled for us.
[core:error] AH01961:  failed to enable ssl support [Hint: if using mod_ssl, see SSLProxyEngine]
[proxy:error] AH00961: https: failed to enable ssl support for 10.201.14.2:8443 (backend.test)

The first line only appears with LogLevel ssl:debug. At the default level, a 2.4.69 log shows the second and third lines. Note the double space after AH01961: and the new module tag, core. A search for "SSL Proxy requested for" or "not enabled [Hint: SSLProxyEngine]" no longer finds anything that a current server writes. The hint is still there, just worded as "if using mod_ssl, see SSLProxyEngine".

The second wall: certificate checks with SSLProxyVerify none#

Once SSLProxyEngine on is in place, many setups move from a 500 to a different 500. This is where most of the confusion comes from.

SSLProxyVerify controls whether Apache checks that the backend's certificate was signed by a CA it trusts. Its default is none. Many people read that as "Apache accepts any certificate". It does not. Three other checks run on their own, and all three default to on:

  • SSLProxyCheckPeerName compares the host name in the request URL with the certificate. The manual says it matches "one of the CN attribute(s) of the certificate's subject, or ... the subjectAltName extension". Available since 2.4.5.
  • SSLProxyCheckPeerCN is the older check. It compares only the first CN with the host name.
  • SSLProxyCheckPeerExpire checks that the certificate is inside its validity dates.

A CN (common name) is the name field in the certificate's subject. The subjectAltName (SAN) is the list of names a modern certificate is valid for. Browsers use only the SAN. Apache's name check accepts either.

In ssl_engine_io.c, these checks run after the handshake itself has finished. If one fails, Apache closes the connection to the backend and the request fails. So a self-signed backend certificate passes when its name and dates are right: in the test, a certificate from an unknown CA, named backend.test, gave 200 under the defaults. The same CA's certificate for other.test gave 500.

Which status: 500, not 502#

The manual says that if the name or expiry check fails, "a 502 status code (Bad Gateway) is returned". mod_ssl also logs AH01997: SSL handshake failed: sending 502 at info level. But the client got 500 on both 2.4.69 and 2.4.48, with this page:

text
HTTP/1.1 500 Proxy Error
...
Reason: Error during SSL Handshake with remote server

The source explains it. The request cannot be sent, and a note on the connection says the TLS connect failed. In that case proxy_util.c calls ap_proxyerror(r, HTTP_INTERNAL_SERVER_ERROR, "Error during SSL Handshake with remote server"). That is 500, and the status line reads 500 Proxy Error. If you are matching on 502 in monitoring, these failures will slip past. The general meaning of each 5xx code across proxies is in 502 vs 503 vs 504.

Why the log does not say which check failed#

At LogLevel warn, each of these failures leaves the same three error lines:

text
[proxy:error] (20014)Internal error (specific information not available): AH01084: pass request body failed to 10.201.14.2:8444 (backend.test)
[proxy:error] AH00898: Error during SSL Handshake with remote server returned by /
[proxy_http:error] AH01097: pass request body failed to 10.201.14.2:8444 (backend.test) from 10.201.14.4 ()

pass request body failed is misleading. The request was a GET with no body. The handshake failed when Apache first tried to write the request, and the code path that reports it is named for the body.

The line that names the cause is logged by mod_ssl with APLOG_INFO. Turn it on for mod_ssl only:

apache
LogLevel warn ssl:info

Then the same failure shows its cause:

text
[ssl:info] AH02411: SSL Proxy: Peer certificate does not match for hostname backend.test
[ssl:info] AH02004: SSL Proxy: Peer certificate is expired
[ssl:info] AH02005: SSL Proxy: Peer certificate CN mismatch: Certificate CN: other.test Requested hostname: backend.test
[ssl:info] AH02003: SSL Proxy connect failed
[ssl:info] SSL Library Error: error:0A00010B:SSL routines::wrong version number

Each failed request writes one of these. ssl:info also logs a line for every client TLS connection, so turn it off once you have the answer.

AH02411 and AH02005: the name does not match#

AH02411 is the current name check. It names the host Apache compared, which is the clue: it is not always the host you think.

That name comes from proxy_util.c. With ProxyPreserveHost Off (the default), it is the host in the ProxyPass URL. With ProxyPreserveHost On, it is the Host header the client sent. The same name is sent as SNI (Server Name Indication), the field in the TLS hello that tells the backend which site the client wants.

So there are three ways to get AH02411 against a correct certificate:

  1. The URL uses another name. ProxyPass "/" "https://app:8443/" against a certificate for app.internal.example.com. Write the full name in the URL. If DNS does not resolve it from the Apache host, add it to /etc/hosts rather than switching to the IP.
  2. ProxyPreserveHost On. In the test, a client sent Host: www.example.com. The backend saw SNI www.example.com, and Apache logged AH02411 ... for hostname www.example.com. The manual says ProxyPreserveHost "should normally be turned Off". If the backend needs the public Host, its certificate must also cover the public name.
  3. The URL uses an IP address. See the next section.

AH02005 is the legacy CN check. Since 2.4.21 it runs only when you set SSLProxyCheckPeerName off together with SSLProxyCheckPeerCN on. It compares the first CN only and ignores the SAN. In the test, it failed for a certificate whose CN was other.test, with the requested name backend.test. If you see AH02005, someone set that combination on purpose, often from an old guide.

AH02004: the backend's certificate has expired#

AH02004: SSL Proxy: Peer certificate is expired covers both ends of the dates. The code fails the check if the current time is before the certificate's start date or after its end date. In the test, a certificate that expired in 2021 failed this way on both versions, with the usual 500.

The fix is to renew the certificate on the backend. Internal backends are where this bites, because nobody watches their dates. SSLProxyCheckPeerExpire off made the same request return 200 in the test. Use it only to get a site back while the new certificate is made, then remove it.

No SNI when the backend is an IP address#

The SNI code in ssl_engine_io.c skips any name that parses as an IP address. The comment quotes RFC 6066: "Literal IPv4 and IPv6 addresses are not permitted". With ProxyPass "/" "https://10.201.14.2:8443/", the test backend received a TLS hello with no SNI at all. With a name in the URL, it received the name.

Two things then go wrong:

  • The name check compares the IP. The certificate named backend.test, so 2.4.69 logged AH02411 ... for hostname 10.201.14.2 and returned 500. A certificate that lists that IP address in its SAN may pass, but that case was not tested here.
  • A backend that hosts several sites cannot pick one. It serves its default certificate, or refuses the handshake. When the backend is itself Apache 2.4.64 or later, a missing SNI with a Host that selects a non-default site gives 421 Misdirected Request. That case is in Apache 421 Misdirected Request behind a proxy.

Use the hostname in the URL. Apache then sends it as SNI and checks the certificate against it.

Turning the checks off: since 2.4.21, either directive turns off both#

Most answers to this error end with:

apache
SSLProxyEngine on
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLProxyCheckPeerExpire off

Before 2.4.21, you needed both name lines. The manual explains that in 2.4.5 to 2.4.20, "both directives must be set to off to completely avoid remote server certificate name validation". Since 2.4.21, "all configurations which disable either one of the SSLProxyCheckPeerName or SSLProxyCheckPeerCN options will suppress all remote server certificate name validation."

The test matched that. Against a certificate for the wrong name, SSLProxyCheckPeerCN off alone gave 200, and SSLProxyCheckPeerName off alone gave 200. Only the pair SSLProxyCheckPeerName off plus SSLProxyCheckPeerCN on still checked, using the legacy CN rule. The code shows the same logic: the name check runs only if PeerName is not off, and either PeerCN is not off or PeerName is explicitly on.

The block above does work. It also means Apache will talk to anything that answers on that address and port, with any certificate. Inside one host or one private network, that may be a choice you can defend. Across any network you do not control, it removes the reason to use TLS on that leg.

The better fix: trust the backend's CA#

If the backend uses a private or self-signed CA, give Apache that CA and require it:

apache
SSLProxyEngine on
SSLProxyVerify require
SSLProxyCACertificateFile "/etc/httpd/certs/internal-ca.pem"
ProxyPass "/" "https://backend.example.internal:8443/"

The manual describes SSLProxyCACertificateFile as "the all-in-one file where you can assemble the Certificates of Certification Authorities (CA) whose remote servers you deal with". With the test CA in that file, the request returned 200. The name and expiry checks stay on, so the certificate must also match the URL host.

Without the CA file, SSLProxyVerify require still starts. At startup 2.4.69 logged AH01897: Init: Oops, you want to request client authentication, but no CAs are known for verification!? as a warning. Each request then failed with 500. At info level, it logged AH02276: Certificate Verification: Error (20): unable to get local issuer certificate. Error 20 means Apache could not find the CA that signed the backend's certificate. nginx hits the same error number for the same reason, as shown in why nginx fails the TLS handshake to an HTTPS upstream.

AH02003 and wrong version number: the port is not TLS#

AH02003: SSL Proxy connect failed means the TLS handshake itself failed, before any certificate check. The SSL Library Error line under it says why. wrong version number means the bytes that came back were not TLS. The usual cause is https:// in the ProxyPass URL for a port that speaks plain HTTP. In the test, OpenSSL 3.5 wrote it as error:0A00010B:SSL routines::wrong version number, and OpenSSL 1.1.1 in the 2.4.48 image as error:1408F10B:SSL routines:ssl3_get_record:wrong version number. Change the scheme to http://, or point at the backend's TLS port.

How the cases were reproduced#

Apache ran from the official httpd:2.4.69 image (OpenSSL 3.5.7) and the httpd:2.4.48 image (OpenSSL 1.1.1d), each with the event MPM and only the modules needed: mod_ssl, mod_proxy and mod_proxy_http. Each case had its own <VirtualHost> on its own port. The backend was a Python 3.13 HTTPS server in a second container on the same Docker network, reachable as backend.test. It logged the SNI of every handshake. Its certificates came from one test CA. One was for backend.test and one for other.test. A third was for backend.test but expired in 2021. The last had a CN of backend.test and no SAN, and it passed under the defaults. Each case was run once at LogLevel warn, then again at LogLevel info ssl:debug, with curl as the client.

Worked case: from 500 to 500 to 200#

Take an Apache front end on a current 2.4 release. It must proxy to an internal API at https://10.0.0.5:8443/. The API's certificate is self-signed, with CN and SAN api.internal. The first config is:

apache
<VirtualHost *:443>
    SSLEngine on
    # site certificate lines here
    ProxyPass "/api/" "https://10.0.0.5:8443/"
</VirtualHost>

Walk it through with the rules above.

  1. A request to /api/ gets a plain 500. The error log shows AH01961: failed to enable ssl support [Hint: if using mod_ssl, see SSLProxyEngine] and then AH00961. SSLEngine on covers only the browser side. Add SSLProxyEngine on.
  2. The next request gets 500 Proxy Error with Error during SSL Handshake with remote server. The log at warn shows AH01084, AH00898 and AH01097, none of which names a cause.
  3. Add LogLevel warn ssl:info and repeat the request. The log would now show AH02411: SSL Proxy: Peer certificate does not match for hostname 10.0.0.5. The certificate says api.internal, and the URL says an IP. Apache also sent no SNI.
  4. Put 10.0.0.5 api.internal in /etc/hosts on the Apache host. Change the line to ProxyPass "/api/" "https://api.internal:8443/". The name check now compares api.internal with api.internal, so it passes, and the backend receives SNI.
  5. The certificate is self-signed, so add its file as SSLProxyCACertificateFile with SSLProxyVerify require. Now Apache refuses any other certificate on that port, too. Remove the ssl:info override.

Nothing in that path needed SSLProxyCheckPeerName off. The two errors were a missing switch and an IP in the URL.

Where this answer stops applying#

This page covers mod_proxy_http with mod_ssl on httpd 2.4, tested on 2.4.69 and 2.4.48 in Docker. Three version boundaries were not tested release by release: 2.4.21 for the name directives, 2.4.30 for <Proxy> context and 2.4.49 for the new AH01961 text. They come from the manual, the CHANGES file and the tagged source. Linux distributions patch httpd, so an older distribution build may differ. The status codes were read from the client on both versions. Other releases were not checked for the 500. Client certificates sent to the backend (SSLProxyMachineCertificateFile), backend CRLs, SSLProxyProtocol and cipher mismatches, and HTTP/2 to the backend through mod_proxy_http2 are not covered. Since 2.4.49, the CHANGES file says another SSL module can secure proxy connections in place of mod_ssl. Such a module has its own directives, and they are not covered here. If the handshake works and you get 502 later, see why Apache logs AH01102 instead.

Frequently asked questions#

What does AH00961 failed to enable ssl support mean?#

Apache was asked to proxy to an https:// URL, but no SSL module would take the outgoing connection. With mod_ssl, that means SSLProxyEngine is off for that virtual host. It is off by default. Add SSLProxyEngine on next to the ProxyPass.

Why does the log say AH01961 failed to enable ssl support with no host name?#

That is the current wording, logged by the core since httpd 2.4.49. Up to 2.4.48, mod_ssl logged AH01961: SSL Proxy requested for host:port but not enabled [Hint: SSLProxyEngine]. The cause and the fix are the same.

Where is SSLProxyEngine on supposed to go?#

In the same <VirtualHost> as the ProxyPass. On 2.4.30 and later it can also go in a <Proxy> section for one backend URL. SSLEngine on does not turn it on.

What does Error during SSL Handshake with remote server mean?#

The TLS connection to the backend failed, or one of Apache's certificate checks rejected it. The client gets 500 Proxy Error. The specific reason is logged at info level, so set LogLevel warn ssl:info and look for AH02411, AH02004, AH02005 or AH02003.

Does SSLProxyVerify none turn off all certificate checks?#

No. It only skips checking the CA. The name and expiry checks (SSLProxyCheckPeerName, SSLProxyCheckPeerCN, SSLProxyCheckPeerExpire) are on by default and still run. A self-signed certificate passes only if its name matches the URL host and its dates are valid.

Do I need both SSLProxyCheckPeerName off and SSLProxyCheckPeerCN off?#

Not on 2.4.21 or later. Turning off either one turns off all name checks. The exception is SSLProxyCheckPeerName off with SSLProxyCheckPeerCN on, which keeps the legacy check of the first CN.

Why does ProxyPass to an IP address fail with an HTTPS backend?#

Apache sends no SNI for an IP address, and it checks the certificate against the IP. A certificate that lists only host names fails that check with AH02411. Use the backend's hostname in the URL, adding it to /etc/hosts if DNS does not have it.

Primary sources#

Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.

  1. Apache HTTP Server 2.4, mod_ssl (SSLProxyEngine, SSLProxyVerify, SSLProxyCACertificateFile, SSLProxyCheckPeerName, SSLProxyCheckPeerCN, SSLProxyCheckPeerExpire)
  2. Apache HTTP Server 2.4, mod_proxy (ProxyPreserveHost)
  3. httpd 2.4.x source, modules/ssl/mod_ssl.c (ssl_hook_ssl_bind_outgoing, AH10272 at debug)
  4. httpd 2.4.48 source, modules/ssl/mod_ssl.c (AH01961 "SSL Proxy requested for %s but not enabled [Hint: SSLProxyEngine]" at error)
  5. httpd 2.4.x source, server/ssl.c (ap_ssl_bind_outgoing, AH01961 "failed to enable ssl support" at error)
  6. httpd 2.4.x source, modules/proxy/proxy_util.c (AH00961, ssl_hostname from ProxyPreserveHost, "Error during SSL Handshake with remote server" as HTTP_INTERNAL_SERVER_ERROR)
  7. httpd 2.4.x source, modules/ssl/ssl_engine_io.c (SNI skipped for IP addresses, AH02003, AH02004, AH02005, AH02411 at info)
  8. httpd 2.4.x CHANGES (2.4.49 ap_ssl_bind_outgoing; 2.4.21 SSLProxyCheckPeerName and SSLProxyCheckPeerCN; 2.4.55 PR 66190)

Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.

More in troubleshooting proxies#