Repository: traefik/traefik (language: go)

Issue:
TLS errors only shown in debug mode
### Welcome!

- [x] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [x] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

My report seems the same as https://github.com/traefik/traefik/issues/9768.

It looks like some TLS errors are not shown unless debug level logs are enabled with `--log.level=DEBUG`. In our case we attempted to proxy to a downstream service that uses a self-signed TLS cert, and noticed requests return a 500 error to the client and nothing in the logs to indicate why. 

```
curl -H "Host: example.domain" <random>.elb.eu-west-2.amazonaws.com/test -i
HTTP/1.1 500 Internal Server Error
```

When setting the log level to debug it becomes clear:

```
2024-11-20T16:01:41Z DBG github.com/traefik/traefik/v3/pkg/proxy/httputil/proxy.go:113 > 500 Internal Server Error error="tls: failed to verify certificate: x509: cannot validate certificate for 10.100.1.100 because it doesn't contain any IP SANs"
```

This can be fixed in our case by configuring a `ServersTransport` resource referencing a secret created by cert-manager and specifying the `serverName`, and adding the `traefik.ingress.kubernetes.io/service.serverstransport` annotation on the service to use it.

```
---
apiVersion: v1
kind: Service
metadata:
  name: downstream-service
  annotations:
    traefik.ingress.kubernetes.io/service.serverstransport: default-traefik-test@kubernetescrd
spec:
  ports:
  - targetPort: 5000
    port: 443
    name: nginx-https
  selector:
    app: downstream-service
---
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
  name: traefik-test
spec:
  serverName: downstream-service.default.svc.cluster.local
  rootCAsSecrets:
    - myca
---
apiVersion: v1
kind: Secret
metadata:
  name: myca
data:
  ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
```



### What did you see instead?

No errors present in the log unless debug logs enabled.

### What version of Traefik are you using?

3.2.0

### What is your environment & configuration?

The rest of our set up looks like this:

```
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: downstream-service
spec:
  commonName: downstream-service.default.svc.cluster.local
  dnsNames:
  - downstream-service.default.svc.cluster.local
  duration: 720h
  renewBefore: 360h
  issuerRef:
    kind: ClusterIssuer
    name: internal-issuer
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    rotationPolicy: Always
    size: 2048
  secretName: myca
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test
spec:
  ingressClassName: traefik
  rules:
    - host: example.domain
      http:
        paths:
          - path: /test
            pathType: Prefix
            backend:
              service:
                name: downstream-service
                port:
                  number: 443
```

### If applicable, please paste the log output in DEBUG level

```
2024-11-20T16:01:41Z DBG github.com/traefik/traefik/v3/pkg/proxy/httputil/proxy.go:113 > 500 Internal Server Error error="tls: failed to verify certificate: x509: cannot validate certificate for 10.100.1.100 because it doesn't contain any IP SANs"
```


Discussion / hints:
Hi @nmengin, thanks for the update in #11579 -- since that issue was closed going to comment here.

as a resolution do you think it is worth to change the route error handler log level to INFO? This would surface the entire suite of route related issues, including the TLS errors.

Fix patch:
diff --git a/pkg/server/service/proxy.go b/pkg/server/service/proxy.go
index 8f3c0a418a..ca5f4150ff 100644
--- a/pkg/server/service/proxy.go
+++ b/pkg/server/service/proxy.go
@@ -2,6 +2,7 @@ package service
 
 import (
 	"context"
+	"crypto/tls"
 	"errors"
 	"fmt"
 	"io"
@@ -91,8 +92,9 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar
 		BufferPool:    bufferPool,
 		ErrorLog:      errorLogger,
 		ErrorHandler: func(w http.ResponseWriter, request *http.Request, err error) {
-			statusCode := http.StatusInternalServerError
+			logger := log.FromContext(request.Context())
 
+			statusCode := http.StatusInternalServerError
 			switch {
 			case errors.Is(err, io.EOF):
 				statusCode = http.StatusBadGateway
@@ -109,7 +111,13 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar
 				}
 			}
 
-			log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err)
+			// Log the error with error level if it is a TLS error related to configuration.
+			if isTLSConfigError(err) {
+				logger.Errorf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err)
+			} else {
+				logger.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err)
+			}
+
 			w.WriteHeader(statusCode)
 			_, werr := w.Write([]byte(statusText(statusCode)))
 			if werr != nil {
@@ -121,6 +129,22 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar
 	return proxy, nil
 }
 
+// isTLSError returns true if the error is a TLS error which is related to configuration.
+// We assume that if the error is a tls.RecordHeaderError or a tls.CertificateVerificationError,
+// it is related to configuration, because the client should not send a TLS request to a non-TLS server,
+// and the client configuration should allow to verify the server certificate.
+func isTLSConfigError(err error) bool {
+	// tls.RecordHeaderError is returned when the client sends a TLS request to a non-TLS server.
+	var recordHeaderErr tls.RecordHeaderError
+	if errors.As(err, &recordHeaderErr) {
+		return true
+	}
+
+	// tls.CertificateVerificationError is returned when the server certificate cannot be verified.
+	var certVerificationErr *tls.CertificateVerificationError
+	return errors.As(err, &certVerificationErr)
+}
+
 func isWebSocketUpgrade(req *http.Request) bool {
 	if !httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") {
 		return false


Test patch:
diff --git a/pkg/server/service/proxy_test.go b/pkg/server/service/proxy_test.go
index 40ca69d67b..cae8123807 100644
--- a/pkg/server/service/proxy_test.go
+++ b/pkg/server/service/proxy_test.go
@@ -1,12 +1,15 @@
 package service
 
 import (
+	"crypto/tls"
+	"errors"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/require"
 	"github.com/traefik/traefik/v2/pkg/testhelpers"
 )
 
@@ -35,3 +38,46 @@ func BenchmarkProxy(b *testing.B) {
 		handler.ServeHTTP(w, req)
 	}
 }
+
+func TestIsTLSConfigError(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		err      error
+		expected bool
+	}{
+		{
+			desc: "nil",
+		},
+		{
+			desc: "TLS ECHRejectionError",
+			err:  &tls.ECHRejectionError{},
+		},
+		{
+			desc: "TLS AlertError",
+			err:  tls.AlertError(0),
+		},
+		{
+			desc: "Random error",
+			err:  errors.New("random error"),
+		},
+		{
+			desc:     "TLS RecordHeaderError",
+			err:      tls.RecordHeaderError{},
+			expected: true,
+		},
+		{
+			desc:     "TLS CertificateVerificationError",
+			err:      &tls.CertificateVerificationError{},
+			expected: true,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			actual := isTLSConfigError(test.err)
+			require.Equal(t, test.expected, actual)
+		})
+	}
+}