Only TLS in the backend

Hello community,
I use HomeAssistant for control and monitoring. This also involves reading a Kostal KSEM via Modbus. I would now like to address the KSEM via TLS, but since HomeAssistant cannot do this at this point, I have installed HAProxy in a container on Proxmox. The unencrypted query via HAProxy works. Now I would like to configure HAProxy so that the connection to the backend is encrypted. Unfortunately, I have not been able to find any information on how I can/must configure this.

Here is my configuration and also the output from haproxy -vv

Best regards Christopher

haproxy -vv
HAProxy version 2.6.12-1+deb12u2 2025/04/29 - https://haproxy.org/
Status: long-term supported branch - will stop receiving fixes around Q2 2027.
Known bugs: http://www.haproxy.org/bugs/bugs-2.6.12.html
Running on: Linux 6.8.12-14-pve #1 SMP PREEMPT_DYNAMIC PMX 6.8.12-14 (2025-08-26T22:25Z) x86_64
Build options :
  TARGET  = linux-glibc
  CPU     = generic
  CC      = cc
  CFLAGS  = -O2 -g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -Wall -Wextra -Wundef -Wdeclaration-after-statement -Wfatal-errors -Wtype-limits -Wshift-negative-value -Wshift-overflow=2 -Wduplicated-cond -Wnull-dereference -fwrapv -Wno-address-of-packed-member -Wno-unused-label -Wno-sign-compare -Wno-unused-parameter -Wno-clobbered -Wno-missing-field-initializers -Wno-cast-function-type -Wno-string-plus-int -Wno-atomic-alignment
  OPTIONS = USE_PCRE2=1 USE_PCRE2_JIT=1 USE_OPENSSL=1 USE_LUA=1 USE_SLZ=1 USE_SYSTEMD=1 USE_OT=1 USE_PROMEX=1
  DEBUG   = -DDEBUG_STRICT -DDEBUG_MEMORY_POOLS

Feature list : -51DEGREES +ACCEPT4 +BACKTRACE -CLOSEFROM +CPU_AFFINITY +CRYPT_H -DEVICEATLAS +DL -ENGINE +EPOLL -EVPORTS +GETADDRINFO -KQUEUE +LIBCRYPT +LINUX_SPLICE +LINUX_TPROXY +LUA -MEMORY_PROFILING +NETFILTER +NS -OBSOLETE_LINKER +OPENSSL +OT -PCRE +PCRE2 +PCRE2_JIT -PCRE_JIT +POLL +PRCTL -PROCCTL +PROMEX -QUIC +RT +SLZ -STATIC_PCRE -STATIC_PCRE2 +SYSTEMD +TFO +THREAD +THREAD_DUMP +TPROXY -WURFL -ZLIB

Default settings :
  bufsize = 16384, maxrewrite = 1024, maxpollevents = 200

Built with multi-threading support (MAX_THREADS=64, default=2).
Built with OpenSSL version : OpenSSL 3.0.16 11 Feb 2025
Running on OpenSSL version : OpenSSL 3.0.17 1 Jul 2025
OpenSSL library supports TLS extensions : yes
OpenSSL library supports SNI : yes
OpenSSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
OpenSSL providers loaded : default
Built with Lua version : Lua 5.3.6
Built with the Prometheus exporter as a service
Built with network namespace support.
Built with OpenTracing support.
Support for malloc_trim() is enabled.
Built with libslz for stateless compression.
Compression algorithms supported : identity("identity"), deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
Built with transparent proxy support using: IP_TRANSPARENT IPV6_TRANSPARENT IP_FREEBIND
Built with PCRE2 version : 10.42 2022-12-11
PCRE2 library supports JIT : yes
Encrypted password support via crypt(3): yes
Built with gcc compiler version 12.2.0

Available polling systems :
      epoll : pref=300,  test result OK
       poll : pref=200,  test result OK
     select : pref=150,  test result OK
Total: 3 (3 usable), will use epoll.

Available multiplexer protocols :
(protocols marked as <default> cannot be specified using 'proto' keyword)
         h2 : mode=HTTP  side=FE|BE  mux=H2    flags=HTX|HOL_RISK|NO_UPG
       fcgi : mode=HTTP  side=BE     mux=FCGI  flags=HTX|HOL_RISK|NO_UPG
  <default> : mode=HTTP  side=FE|BE  mux=H1    flags=HTX
         h1 : mode=HTTP  side=FE|BE  mux=H1    flags=HTX|NO_UPG
  <default> : mode=TCP   side=FE|BE  mux=PASS  flags=
       none : mode=TCP   side=FE|BE  mux=PASS  flags=NO_UPG

Available services : prometheus-exporter
Available filters :
        [CACHE] cache
        [COMP] compression
        [FCGI] fcgi-app
        [  OT] opentracing
        [SPOE] spoe
        [TRACE] trace

cat /etc/haproxy/haproxy.cfg
global
        log /dev/log    local0
        log /dev/log    local1 notice
        chroot /var/lib/haproxy
        stats socket /run/haproxy/admin.sock mode 660 level admin
        stats timeout 30s
        user haproxy
        group haproxy
        daemon

        # Default SSL material locations
        ca-base /etc/ssl/certs
        crt-base /etc/ssl/private

        # See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
        ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
        ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
        ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

defaults
        log     global
        mode    tcp
        option  tcplog
        option  dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
        errorfile 400 /etc/haproxy/errors/400.http
        errorfile 403 /etc/haproxy/errors/403.http
        errorfile 408 /etc/haproxy/errors/408.http
        errorfile 500 /etc/haproxy/errors/500.http
        errorfile 502 /etc/haproxy/errors/502.http
        errorfile 503 /etc/haproxy/errors/503.http
        errorfile 504 /etc/haproxy/errors/504.http



backend modbus
    mode tcp
#    server modbus 192.168.0.101:502 check
    option ssl-hello-chk
    server ksem 192.168.0.101:802 check
#ssl ca-file /etc/ssl/certs/ksem.pem
#    http-request set-header Host haproxy
#    http-request set-header X-Proto https


#    server modbus 192.168.0.101:802 check ca-file ksem.pem


frontend haproxymodbus
    mode tcp

    # bind all incoming traffic on port 802 for ipv6 and ipv4
    bind :::802
    bind :802

    # instructs HAProxy to wait five seconds before closing the connection unless its a ssl connection
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }

    default_backend modbus

ssl verify none are the arguments to encrypt the traffic without checking the SSL certificate.

Unless you are talking to multiple modbus controllers and want to failover between them, you do not want any of the health check configurations interfering.

backend modbus
 mode tcp
 server modbus 192.168.0.101:802 ssl verify none

If you need to verify the certificate served by the controller because the path between haproxy and the controller is insecure, then specify the ca certificate with ca-file and drop verify none.

Thank you for your support.
Unfortunately, it still doesn’t work.
How can I trace what haproxy is actually doing?

This is my current configuration.

backend modbus
    mode tcp
#    server modbus 192.168.0.101:502 check
    option ssl-hello-chk
    server ksem 192.168.0.101:802 ssl ca-file /etc/ssl/certs/ksem.pem

Stop haproxy and run it manually with debug mode enabled.

haproxy -f /path/to/file -d

Your current configuration is not what I suggested. Start from the basics I suggested before adding other features.

Also you can try to curl against your server, to see what is happening there:

curl -vvvk https://192.168.0.101:802
curl -vvv --cacert /etc/ssl/certs/ksem.pem https://192.168.0.101:802

Even though there is no HTTP layer here we can still see what happens with the TLS handshake, which can be useful. You can also use an equivalent openssl s_client command.

Thank you.

The configuration now looks as follows

backend modbus
    mode tcp
#    server modbus 192.168.0.101:502 check
    option ssl-hello-chk
    server ksem 192.168.0.101:802 ssl verify none

Calling “sudo curl -vvvk https://192.168.0.101:802” produces the following output:

christopheradm@HAProxy:~$ sudo curl -vvvk https://192.168.0.101:802
*   Trying 192.168.0.101:802...
* Connected to 192.168.0.101 (192.168.0.101) port 802 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
*  subject: CN=77529471; OU=kostal
*  start date: Jan 26 07:16:13 2023 GMT
*  expire date: Jan 21 07:16:13 2043 GMT
*  issuer: CN=TQ Tenant CA for kostal
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* using HTTP/1.x
> GET / HTTP/1.1
> Host: 192.168.0.101:802
> User-Agent: curl/7.88.1
> Accept: */*
>
* TLSv1.3 (IN), TLS alert, bad certificate (554):
* OpenSSL SSL_read: OpenSSL/3.0.17: error:0A000412:SSL routines::sslv3 alert bad certificate, errno 0
* Closing connection 0
curl: (56) OpenSSL SSL_read: OpenSSL/3.0.17: error:0A000412:SSL routines::sslv3 alert bad certificate, errno 0

And the calling “sudo curl -vvv --cacert /etc/ssl/certs/ksem.pem https://192.168.0.101:802” produces the following output:

christopheradm@HAProxy:~$ sudo curl -vvv --cacert /etc/ssl/certs/ksem.pem https://192.168.0.101:802
*   Trying 192.168.0.101:802...
* Connected to 192.168.0.101 (192.168.0.101) port 802 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ksem.pem
*  CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection 0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.


I can see that it is a certificate error, but I cannot interpret the message.

This is how the certificate was created.

openssl req -newkey rsa:2048 -nodes -x509 -days 365 -keyout ksem.key -out ksem.crt -subj "/CN=$HAPROXY_SSL_SERVER_CERT_CN"
cat ksem.crt ksem.key >> /etc/ssl/certs/ksem.pem

Another note from the KSEM documentation

‘If a connection is initiated to a remote station that has a self-signed certificate, this is recognised by the device and must be actively accepted by the user.’

However, no certificate appears in KSEM that I can approve.

Greetings Christopher

This creates more questions than it answers.

What is it that you want to accomplish with the certificate you created?

Do you want to configure mutual TLS between the controller and haproxy, so that haproxy is using the certificate as a client certificate?

If mutual TLS is what you need to configure, than add --cert client.crt --key client.key to your curl calls and crt to your backend server configuration in haproxy.

You do not seem to have uploaded the certificate to this backend, because the certificate that curl is showing is one that has a 10 year expiration.

It also possible that this build-in certificate of this device is rejected by your openssl version for some reason, that is what sslv3 alert bad certificate possibly suggest.

I don’t see how KSEM would initiate a connection to haproxy. It is the other way around, haproxy initiated a connection to KSEM, so so this is probably irrelevant for your use case?

I think he’s hitting the wrong cert too, or the wrong server, or there’s SNI somewhere. It was created in 2023 yet from his commands it’s likely he created it with openssl not that long ago.

The CN is weird in the openssl command, there’s a $ in front of HAPROXY_SSL_SERVER_CERT_CN. Was it a variable at the time ?

-subj "/CN=$HAPROXY_SSL_SERVER_CERT_CN"

Yet the CN we see from the curl output is

*  subject: CN=77529471; OU=kostal

You don’t seem to have the right certificate bound to your backend, HOWEVER this shouldn’t be a big deal as “ssl verify none” is active. From the curl output however, something is broken at a TLS level.

Hello community,

Since the manufacturer has not even responded to the question of whether Modbus TCP works on the KSEM without a license, I will abandon this approach and most likely purchase a wallbox from another manufacturer that directly supports Modbus TCP.

Nevertheless, thank you very much for your support.

Best regards, Christopher

1 Like