SSL client certificate with reverse proxy - 400 Bad Request - No required SSL certificate was sent

Hi!

I’m trying to establish a client certificate/mutual authentication mechanism with this setup:

Computer (shall require certificate) ↔ xxx.duckdns.org ↔ reverse-proxy (nginx) ↔ server application

The reverse proxy works fine if I do not use a client certificate. But once I set it up and configure it in nginx I can’t solve this error:

400 Bad Request

No required SSL certificate was sent

The nginx error log also shows

client sent no required SSL certificate while reading client request headers, client: …, server: xxx.duckdns.org, request: “GET /service_worker.js HTTP/2.0”, host: “xxx.duckdns.org

The certificate was generated according to this page:

echo "Setting up client-certificate signing CA ..."
openssl genrsa -aes256 -passout pass:${PRIVATE_CA_PASSWORD} -out ca.pass.key 4096
openssl rsa -passin pass:${PRIVATE_CA_PASSWORD} -in ca.pass.key -out ca.key

echo "Make the signing CA valid for 100 years ..."
openssl req -new -x509 -days 36500 -key ca.key -out ca.pem -subj '/CN=Martin'

echo "Creating a client keypair ..."
echo "- RSA key"
openssl genrsa -aes256 -passout pass:${PUBLIC_KEY} -out ${CLIENT_ID}.pass.key 4096
openssl rsa -passin pass:${PUBLIC_KEY} -in ${CLIENT_ID}.pass.key -out ${CLIENT_ID}.key
echo "- CSR"
openssl req -new -passin pass:${PUBLIC_KEY} -key ${CLIENT_ID}.key -out ${CLIENT_ID}.csr -subj '/CN=User'

echo "Signing this key with the CA ..."
# valid: 100 years
openssl x509 -req -days 36500 -in ${CLIENT_ID}.csr -CA ca.pem -CAkey ca.key -set_serial ${CLIENT_SERIAL} -passin pass:${PRIVATE_CA_PASSWORD} -out ${CLIENT_ID}.pem

echo "Generating crt file from pem file ..."
openssl x509 -outform der -in ${CLIENT_ID}.pem -out ${CLIENT_ID}.crt

echo "Bundle client key into pfx file ..."
openssl pkcs12 -export -out ${CLIENT_ID}.pfx -inkey ${CLIENT_ID}.key -in ${CLIENT_ID}.pem -certfile ca.pem -password pass:${EXPORT_PASSWORD}

echo "Checking certificate ..."
openssl verify -CAfile ca.pem user.pem

The certificate check in the last line shows OK in the end.

On the computer I tried out using the ${CLIENT_ID}.pfx and the ${CLIENT_ID}.crt - both without success.

Before trying this, I established the SSL certificate for the proxy according to this page using certbot-auto.

The nginx config looks like this:

map $http_upgrade $connection_upgrade {
	default upgrade;
	''      close;
}

server {
	server_name xxx.duckdns.org;

	listen [::]:80 default_server ipv6only=off;
	return 301 https://$host$request_uri;
}

server {
	server_name xxx.duckdns.org;

	ssl_certificate /etc/letsencrypt/live/xxx.duckdns.org/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/xxx.duckdns.org/privkey.pem;

	ssl_trusted_certificate /etc/letsencrypt/live/xxx.duckdns.org/chain.pem;

	ssl_dhparam /etc/nginx/ssl/dhparams.pem;

	listen [::]:443 ssl http2 default_server ipv6only=off;
	add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
	
	ssl on;
	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_ciphers TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:EECDH+CHACHA20:EECDH+AESGCM:EECDH+AES;
	ssl_prefer_server_ciphers on;
	ssl_session_cache shared:SSL:10m;
	ssl_session_timeout 1d;

	### once I remove these 3 lines everything works but without the client certificate ###
	ssl_client_certificate /etc/nginx/certificates/ca.pem;
	ssl_verify_client on;
	ssl_verify_depth 3;

	# OCSP stapling
	ssl_stapling on;
	ssl_stapling_verify on;

	proxy_buffering off;

	location / {
		# the server application runs on port 30303 of the same machine
		proxy_pass https://127.0.0.1:30303;
		proxy_set_header Host $host;
		proxy_redirect http:// https://;
		proxy_http_version 1.1;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection $connection_upgrade;
	}
}

The nginx error sounds like the client did not even try to send a certificate… the certificate chain does not seem to be established? (no wonder - I did not use any of the files from the SSL proxy certificate generation in the client certificate generation or vise versa.)

How can I invoke this in the given setup? Basically all tutorials I read only state that in nginx I need to use the ssl_client_certificate option… but I could not find information about the usage together with a DNS server and a reverse proxy.

Thanks a lot for hints! :slight_smile:

1 Like

You should verify your client certificate configuration using curl like

curl --cert path/to/client.crt --key path/to/client.key https://xxx.duckdns.org

In any case, it doesn’t matter if you use nginx as reverse proxy or not, nginx is terminating the ssl connection in any case.

Hi!
when using the user.crt file curl gives me
“curl: (58) could not load PEM client certificate, OpenSSL error error:0909006C:PEM routines:get_name:no start line, (no key found, wrong pass phrase, or wrong file format?)”

and when using the user.pem file it basically downloads an empty page with the home assistant blue bar at the top:

Summary
pi@raspberrypi:/home/homeassistant/.homeassistant $ curl --cert ./CertificateAuthCA/user.pem --key ./CertificateAuthCA/user.key https://xxx.duckdns.org
<!DOCTYPE html><html lang="en"><head><link rel="preload" href="/frontend_latest/core.2de630fb.js" as="script" crossorigin="use-credentials"><link rel="preload" href="/static/fonts/roboto/Roboto-Regular.woff2" as="font" crossorigin><link rel="preload" href="/static/fonts/roboto/Roboto-Medium.woff2" as="font" crossorigin><meta charset="utf-8"><link rel="manifest" href="/manifest.json" crossorigin="use-credentials"><link rel="icon" href="/static/icons/favicon.ico"><meta name="viewport" content="width=device-width,user-scalable=no"><style>body{font-family:Roboto,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-weight:400;margin:0;padding:0;height:100vh}</style><title>Home Assistant</title><link rel="apple-touch-icon" sizes="180x180" href="/static/icons/favicon-apple-180x180.png"><link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4"><meta name="apple-itunes-app" content="app-id=1099568401"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"><meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"><meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"><meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"><meta name="msapplication-TileColor" content="#03a9f4ff"><meta name="mobile-web-app-capable" content="yes"><meta name="referrer" content="same-origin"><meta name="theme-color" content="#03A9F4"><style>#ha-init-skeleton::before{display:block;content:"";height:112px;background-color:#03A9F4}</style></head><body><div id="ha-init-skeleton"></div><home-assistant></home-assistant><script>function _ls(e){var t=document.documentElement,s=t.insertBefore(document.createElement("script"),t.lastChild);s.defer=!0,s.src=e}window.Polymer={lazyRegister:!0,useNativeCSSProperties:!0,dom:"shadow",suppressTemplateNotifications:!0,suppressBindingNotifications:!0},"customElements"in window&&"content"in document.createElement("template")||document.write("<script src='/static/polyfills/webcomponents-bundle.js'><\/script>");var isS101=/\s+Version\/10\.1(?:\.\d+)?\s+Safari\//.test(navigator.userAgent)</script><script type="module" crossorigin="use-credentials">import "/frontend_latest/core.2de630fb.js";
      import "/frontend_latest/app.714f6631.js";
      import "/frontend_latest/hass-icons.491db358.js";
      window.customPanelJS = "/frontend_latest/custom-panel.7a021869.js";</script><script nomodule>(function() {
        // // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
        if (!isS101) {
          window.customPanelJS = "/frontend_es5/custom-panel.c5e5b82f.js";
          _ls("/static/polyfills/custom-elements-es5-adapter.js");
          _ls("/frontend_es5/compatibility.a79f1f44.js");
          _ls("/frontend_es5/core.9eab0e85.js");
          _ls("/frontend_es5/app.7201221c.js");
          _ls("/frontend_es5/hass-icons.95da698b.js");
          }
      })();</script></body></html>

So something seems to be odd about your .crt file (probably).
But the .pem file is working, so in fact you’re able to establish a client certificate session with nginx,
thats why you get the hass base html with curl.

So my best guess is that your browser or your os is not aware of the certificates and thats the reason why you cannot connect.

Which OS and browser are you on ? Linux/Mac/Windows FF/Chrome ?

I added the certificates to osx and android devices, and that was a must have to make the connection possible.

I’m using Chrome 79.0.3945.88 on Windows 10. For testing I used the crt file and also the pfx file - and I tested everything at least 10 times now without succes … Windows can successfully install the crt file - I believe curl cannot use crt file format:

Yes, might be. I am no windows guy, but your help output indicates that windows curl does not support .crt.

I am unsure how you do it on windows systems, but in any case you need more than the user.crt.
At least user.crt, user.key and your own CA, otherwise the system wont accept the certificate chain.

On OSX I had to install the the pfx (since it included CA/User CRT and User KEY) and set them to a tusted mode, on android the CA, user.crt and user.key seperately.

Hehe I don’t think it’s a Windows-related problem - instead there does not seem to be a proper certificate chain at all.
I do have these files:

  • ca.key
  • ca.pem
  • user.crt
  • user.csr
  • user.key
  • user.pem
  • user.pfx

But the user.* files are generated completely without taking the letsencrypt files from this tutorial into account: https://www.home-assistant.io/docs/ecosystem/nginx/
Shouldn’t the certificate files be chained properly? As I mentioned I have no idea how to do this :slightly_smiling_face:

Do you have the same or a similar setup in place? Maybe you could share the commands you used to set everything up? :slight_smile:

No, it’s not a windows related problem.

As stated: Your connection works, you verified it by using the curl command. So nginx side everything is ok.

To use client certificates inside chrome you need to import all related certificates into the system keychain.

So I would suggest to import ca.pem and user.pem or user.pfx.

If you have them installed and set them as trusted (since they are self signed, do not know how windows behave, osx does not like them by default) you need to tell chrome to use the certficates.

On OSX i did this by using following command:

defaults write com.google.Chrome AutoSelectCertificateForUrls -array-add -string '{"pattern":"https://[*.]xxxxxx.xx","filter":{"ISSUER":{"O":"xxxxx"}}}'

Hi,
you were right - Chrome did not use the certificate I installed! In fact I never tried with Edge or Firefox - these browsers did not have any problems… so I always thought I did something wrong but in fact everything was correct. To make Chrome work this solved my problem: https://serverfault.com/questions/845766/generating-a-self-signed-cert-with-openssl-that-works-in-chrome-58/870832#870832
Thanks for your help! :slight_smile:

1 Like