Client Certificate Authentication - No Encryption!

This is an unusual requirement which is far from the correct way to do this, however, this is a system which I’ve just taken on and we need to get this working in this way until we can do it better next year.

There’s an API endpoint, secured with TLS, but also secured by the presentation of a client certificate. The unusual part here is the certificate must have a given string for the organisation name and common name. There is no other purpose for this certificate; not for encryption or any other purpose.

The frontend bind line has verify none as we don’t (apparently) care where the cert has come from.

bind    *:443  ssl crt-list /etc/haproxy/certs/certlist.txt ca-file /etc/haproxy/certs/internal/RootAuthority.pem verify none

Just trying to log either the Common Name or Organisation Name doesn’t work using this line in the frontend: (Edit: it just logs “”)

log-format "%ci %{+Q}[ssl_c_s_dn(c)]"

I’ve looked at Lua to try and get this information, but I’ve no experience with Lua and feel this should be possible through the existing configuration language.

If anyone can get both of these certificate fields into variables that would really help.

verify none means the client certificate is not even requested, which is not possible if you want to access client certificate data.

You need verify optional so that the client is actually able to use its client certificate. Then you should be able to access those fields.

Doing that results in this entry in the log: SSL client certificate not trusted

How would you overcome the untrusted nature of the certificate to be able to read the certificate fields?

You will need ca-ignore-err all on the bind line. Or crt-ignore-err all, not sure, one of the two.

That’s cleared the SSL trust problem, however I’m still not able to pickup the certificate information. Additional fields have been added to the log entry in case there was some anomaly reading the subject line.

log-format "%ci CERTIFICATE INFORMATION: %{+Q}[ssl_c_s_dn] / %{+Q}[ssl_c_notbefore] / %{+Q}[ssl_c_notafter] / %{+Q}[ssl_c_serial]"

This results in the log file entry:

Dec 13 09:45:48 localhost haproxy[255615]: 192.168.1.1 CERTIFICATE INFORMATION: "" / "" / "" / ""

Any thoughts why the certificate information isn’t being picked up?

If you want additional help you need to post the entire configuration and the haproxy version.

HAProxy version 2.8.4-a4ebf9d, released 2023/11/17

#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
        log         127.0.0.1:514 local2
        chroot      /var/lib/haproxy
        pidfile     /var/run/haproxy.pid
        maxconn     4000
        user        haproxy
        group       haproxy
        daemon
        crt-base    /etc/haproxy/certs

        # turn on stats unix socket
        stats socket /var/lib/haproxy/stats

        # utilize system-wide crypto-policies
        ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
        ssl-default-bind-ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
        mode                    http
        log                     global
        option                  httplog
        option                  dontlognull
        option http-server-close
        option forwardfor       except 127.0.0.0/8
        option                  redispatch
        retries                 3
        timeout http-request    10s
        timeout queue           1m
        timeout connect         10s
        timeout client          1m
        timeout server          1m
        timeout http-keep-alive 10s
        timeout check           10s
        maxconn                 3000


#---------------------------------------------------------------------
# Frontend all-port-80
#---------------------------------------------------------------------
frontend all-port-80
        bind *:80
        option http-server-close
        http-request redirect scheme https code 301


#---------------------------------------------------------------------
# Frontend all-ip-443
#---------------------------------------------------------------------
frontend all-ip-443
        bind    *:443  ssl crt-list /etc/haproxy/certs/certlist.txt ca-file /etc/haproxy/certs/internal/RootAuthority.pem verify none ca-ignore-err all crt-ignore-err all
        option  http-keep-alive


        # Log Client Certificate Inormation
        log-format "%ci CERTIFICATE INFORMATION: %{+Q}[ssl_c_s_dn] / %{+Q}[ssl_c_notbefore] / %{+Q}[ssl_c_notafter] / %{+Q}[ssl_c_serial]"


        # Host based ACLs
        acl     acl_haproxyhost             	hdr(host) -i haproxyhost.dif.domain.com
        acl     acl_password.domain.com         hdr(host) -i password.domain.com

        acl     acl_uat-im1.ct1.domain.com      hdr(host) -i uat-im1.ct1.domain.com
        acl     acl_uat-im2.ct1.domain.com      hdr(host) -i uat-im2.ct1.domain.com
        acl     acl_uat-api.ct1.domain.com      hdr(host) -i uat-api.ct1.domain.com


        # Backend Selection
        use_backend haproxyhost             	if acl_haproxyhost
        use_backend password.domain.com         if acl_password.domain.com

        use_backend uat-im1.ct1.domain.com      if acl_uat-im1.ct1.domain.com
        use_backend uat-im2.ct1.domain.com      if acl_uat-im2.ct1.domain.com
        use_backend uat-api.ct1.domain.com      if acl_uat-api.ct1.domain.com



#---------------------------------------------------------------------
# Backend haproxyhost
#---------------------------------------------------------------------
backend haproxyhost
        server haproxyhost 127.0.0.1:81

#---------------------------------------------------------------------
# Backend password.domain.com
#---------------------------------------------------------------------
backend password.domain.com
        option httpchk
        http-check send hdr host pwdserver.dif.domain.com
        http-check expect status 400
        http-request set-header host pwdserver.dif.domain.com

        server pwdserver.dif 172.20.32.16:443 check ssl verify none

#---------------------------------------------------------------------
# Backend uat-im1.ct1.domain.com
#---------------------------------------------------------------------
backend uat-im1.ct1.domain.com
        option httpchk
        http-check send hdr host uat-im1.ct1.domain.com
        http-request set-header Host uat-im1.ct1.domain.com

        server ct1-web-server.ct1 172.22.43.1:443 check ssl verify none

#---------------------------------------------------------------------
# Backend uat-im2.ct1.domain.com
#---------------------------------------------------------------------
backend uat-im2.ct1.domain.com
        option httpchk
        http-check send hdr host uat-im2.ct1.domain.com
        http-request set-header Host uat-im2.ct1.domain.com

        server ct1-web-server.ct1 172.22.43.1:443 check ssl verify none


#---------------------------------------------------------------------
# Backend uat-api.ct1.domain.com
#---------------------------------------------------------------------
backend uat-api.ct1.domain.com
        option httpchk GET /swagger
        http-check send hdr host im2_uat-webapi.local
        http-request set-header Host im2_uat-webapi.local

        server ct1.api-server.ct1 172.22.43.32:80 check


#---------------------------------------------------------------------
# Stastics Page Config
#---------------------------------------------------------------------
listen stats
    bind  *:81               # Bind stats to port 81
    log   global             # Enable Logging
    stats enable             # enable statistics reports
#    stats hide-version       # Hide the version of HAProxy
    stats refresh 30s        # HAProxy refresh time
    stats show-node          # Shows the hostname of the node
    stats uri /stats         # Statistics URL

Like I said, verify needs to be optional instead of none.

Your configuration works, but you cannot refer to ssl_c_serial, this returns a binary, not a string, so you need to add a conversation to string (adding ,hex):

log-format "%ci CERTIFICATE INFORMATION: %{+Q}[ssl_c_s_dn] / %{+Q}[ssl_c_notbefore] / %{+Q}[ssl_c_notafter] / %{+Q}[ssl_c_serial,hex]"

Logging looks like:

127.0.0.1 CERTIFICATE INFORMATION: "/CN=ubuntuvm.local/O=LT-Org SpA/C=IT/ST=MI/L=City" / "150102161510Z" / "160102161510Z" / "97B6A242E673100B"

Apologies, you did say to make verify optional.

It is now partially working, if certificate A is sent then the log outputs the certificate information:

Dec 13 16:15:47 localhost haproxy[267501]: 10.149.208.254 CERTIFICATE INFORMATION: "/O=Saturday Sports/CN=Dicky Davis" / "241211095145Z" / "251211101145Z"

Any other certificate just has blank values again for the certificate fields.

Several certificates have been created and none of them are logged, except the one above. The certificates have been created in the same way and I’m at a loss to understand if there’s a problem with the certificate or HAProxy.

The certificates have been created using the below ini file and certreq.exe on Windows :

[Version]
Signature = "$Windows NT$"

[NewRequest]
Subject = "CN=Bob Wilson,O=Saturday Sports"
KeySpec = 1
KeyLength = 2048
HashAlgorithm = sha256
Exportable = TRUE
KeyUsage = 0xa0
MachineKeySet = FALSE
ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
RequestType = Cert

[Extensions]
2.5.29.19 = "{text}"

Added %{+Q}[ssl_c_used] to the log-format line and it returns “1” with Certificate A and “0” for all other certificates. It looks like for some reason the other certificates are not being sent with the Invoke-WebRequest PS comandlet.

To try and come at this form another angle, another certificate was created using openssl but this hasn’t worked.

openssl req -x509 -newkey rsa:2048 -keyout hugo_raddon.key -out hugo_raddon.crt -days 365 -nodes -subj "/CN=Hugo Raddon/O=Some Company" -addext "keyUsage=digitalSignature" -addext "extendedKeyUsage=clientAuth"
openssl pkcs12 -export -out hugo_raddon.pfx -inkey hugo_raddon.key -in hugo_raddon.crt -certfile hugo_raddon.crt -passout pass:yourpassword

OpenSSL has also been used to dump the details of the working and non-working certificates to text files so a diff could be done. Aside from the expected differences nothing stood out or would explain why the non-working certificate was different.

I’m thinking this is more of a certificate/request issue rather than HAProxy as this stage but haven’t enough evidence to rule anything out just yet.

You can try to enforce client certificates by turning verify to required instead of optional, this may make client cert issues more clear.

The only question I can ask is whether both working and non working certificates are issued from the same exact private CA referenced in /etc/haproxy/certs/internal/RootAuthority.pem, but haproxy should not verify them either way.

It comes as no surprise that Windows was doing something screwy with creating the certificates. Ended up recreating some certificate on Linux and used curl to send them to HAProxy. Everything worked as expected.

Many thanks for your help with this.

1 Like