Potential cipher mismatch when using reverse_proxy directive with old internal server

1. The problem I’m having:

Hello,

I’m trying to reverse proxy to an internal application that we can’t adjust because we no longer have the source code but I’d like to make it “more secure”.

I ran an sslscan against this internal admin application and got a list of supported ciphers:

PORT    STATE SERVICE
443/tcp open  https
| ssl-enum-ciphers:
|   TLSv1.2:
|     ciphers:
|       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       Forward Secrecy not supported by any cipher
|_  least strength: A

In addition this service is configured with a certificate that has a fair few years left on it, but no longer accurate for the DNS record routing to the service, so using tls_insecure_skip_verify. I have no capabilities to change this TLS configuration, ciphers nor common name so trying to use caddy to front it.

When I configure this upstream as a reverse_proxy target in caddy I get a handshake failure as seen in the log below:

2. Error messages and/or full log output:

2024/12/08 19:43:01.012 DEBUG   tls.handshake   default certificate selection results   {"identifier": "localhost", "subjects": ["localhost"], "managed": true, "issuer_key": "local", "hash": "c1d216bb54aa7e632da630b0921dc5ebc7f3a0c577da261791e5901c32b9442b"}
2024/12/08 19:43:01.012 DEBUG   tls.handshake   matched certificate in cache    {"remote_ip": "::1", "remote_port": "33260", "subjects": ["localhost"], "managed": true, "expiration": "2024/12/09 07:42:56.000", "hash": "c1d216bb54aa7e632da630b0921dc5ebc7f3a0c577da261791e5901c32b9442b"}
2024/12/08 19:43:01.017 DEBUG   http.handlers.reverse_proxy     selected upstream       {"dial": "admin.localdomain:443", "total_upstreams": 1}
2024/12/08 19:43:01.022 DEBUG   http.handlers.reverse_proxy     upstream roundtrip      {"upstream": "admin.localdomain:443", "duration": 0.005285108, "request": {"remote_ip": "::1", "remote_port": "33260", "client_ip": "::1", "proto": "HTTP/2.0", "method": "GET", "host": "localhost", "uri": "/", "headers": {"Accept": ["*/*"], "X-Forwarded-For": ["::1"], "X-Forwarded-Proto": ["https"], "X-Forwarded-Host": ["localhost"], "User-Agent": ["curl/8.5.0"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost"}}, "error": "remote error: tls: handshake failure"}
2024/12/08 19:43:01.022 ERROR   http.log.error  remote error: tls: handshake failure    {"request": {"remote_ip": "::1", "remote_port": "33260", "client_ip": "::1", "proto": "HTTP/2.0", "method": "GET", "host": "localhost", "uri": "/", "headers": {"User-Agent": ["curl/8.5.0"], "Accept": ["*/*"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost"}}, "duration": 0.005529867, "status": 502, "err_id": "kcjc1kqit", "err_trace": "reverseproxy.statusError (reverseproxy.go:1269)"}

I am guessing it’s a cipher mismatch but I can’t be sure!

3. Caddy version:

Caddy 2.8.4

4. How I installed and ran Caddy:

Official Docker image (caddy:2.8.4)

a. System environment:

Docker

b. Command:

docker run --rm --network=host docker:2.8.4

c. Service/unit/compose file:

n/a

d. My complete Caddy config:

{
	log {
		output stderr
		level DEBUG
		format console
	}
}

localhost:443 {
	tls internal
	reverse_proxy https://admin.localdomain {
		transport http {
			tls_insecure_skip_verify
		}
		header_up Host admin.localdomain
	}
}

5. Links to relevant resources:

tcpdump/wireshark view of the TLS ClientHello initiated by Caddy when a request lands on the frontend via curl.

Checked connecting to the upstream directly with curl and it works albeit needing the -k flag as you’d expect with the SNI mismatch.

Figured I’d check which client ciphers my curl client supports and got the following:

curl -s https://www.howsmyssl.com/a/check | jq .
{
  "given_cipher_suites": [
    "TLS_CHACHA20_POLY1305_SHA256",
    "TLS_AES_256_GCM_SHA384",
    "TLS_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
    "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
    "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
    "TLS_GOST2012256-GOST89-GOST89",
    "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256",
    "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA",
    "TLS_GOST2001-GOST89-GOST89",
    "TLS_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_256_CBC_SHA256",
    "TLS_RSA_WITH_AES_256_CBC_SHA",
    "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256",
    "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
    "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
    "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
    "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
    "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA",
    "TLS_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_RSA_WITH_AES_128_CBC_SHA256",
    "TLS_RSA_WITH_AES_128_CBC_SHA",
    "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256",
    "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA",
    "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
    "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
    "TLS_RSA_WITH_RC4_128_SHA",
    "TLS_RSA_WITH_RC4_128_MD5",
    "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
    "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
    "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
    "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
    "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
  ],
  "ephemeral_keys_supported": true,
  "session_ticket_supported": false,
  "tls_compression_supported": false,
  "unknown_cipher_suite_supported": false,
  "beast_vuln": false,
  "able_to_detect_n_minus_one_splitting": false,
  "insecure_cipher_suites": {
    "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": [
      "uses RC4 which has insecure biases in its output"
    ],
    "TLS_ECDHE_RSA_WITH_RC4_128_SHA": [
      "uses RC4 which has insecure biases in its output"
    ],
    "TLS_RSA_WITH_RC4_128_MD5": [
      "uses RC4 which has insecure biases in its output"
    ],
    "TLS_RSA_WITH_RC4_128_SHA": [
      "uses RC4 which has insecure biases in its output"
    ]
  },
  "tls_version": "TLS 1.3",
  "rating": "Bad"
}

Notably, all 3 reported in the sslscan result are present in the curl request.

Setting the same cipher check as the upstream and checking what caddy offers up as ciphers:

curl https://localhost/a/check -s -k | jq .
{
  "given_cipher_suites": [
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
    "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
    "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
    "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
    "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
    "TLS_AES_128_GCM_SHA256",
    "TLS_AES_256_GCM_SHA384",
    "TLS_CHACHA20_POLY1305_SHA256"
  ],
  "ephemeral_keys_supported": true,
  "session_ticket_supported": false,
  "tls_compression_supported": false,
  "unknown_cipher_suite_supported": false,
  "beast_vuln": false,
  "able_to_detect_n_minus_one_splitting": false,
  "insecure_cipher_suites": {},
  "tls_version": "TLS 1.3",
  "rating": "Probably Okay"
}

So I guess my question is, can I configure Caddy to use insecure older ciphers or am I out of luck and need to use nginx?

Edit:
Finally trying nginx:1.27.3 via docker.

curl http://localhost/a/check -s | jq .
{
  "given_cipher_suites": [
    "TLS_AES_256_GCM_SHA384",
    "TLS_CHACHA20_POLY1305_SHA256",
    "TLS_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
    "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
    "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
    "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
    "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
    "TLS_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_RSA_WITH_AES_256_CBC_SHA256",
    "TLS_RSA_WITH_AES_128_CBC_SHA256",
    "TLS_RSA_WITH_AES_256_CBC_SHA",
    "TLS_RSA_WITH_AES_128_CBC_SHA",
    "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
  ],
  "ephemeral_keys_supported": true,
  "session_ticket_supported": true,
  "tls_compression_supported": false,
  "unknown_cipher_suite_supported": false,
  "beast_vuln": false,
  "able_to_detect_n_minus_one_splitting": false,
  "insecure_cipher_suites": {},
  "tls_version": "TLS 1.3",
  "rating": "Probably Okay"
}

Sorry for the delay, was taking some time off from the forums.

No, Caddy does not allow you to use insecure ciphers. Only secure ciphers are supported.