Accessing HA behind NGINX and client cert auth from Chrome (Android/Windows)

Just got into Home Assistant, and although I’m excited about the possibilities, I’d like to lock down remote access and am very close to making it all work perfectly. Any help would be very appreciated

I’m currently trying to set up remote access from my Android phone and a few Windows machines on Chrome/Chromium-based browsers, where each device has its own SSL client certificate while the server uses a SSL cert from Let’sEncrypt. I can actually remotely access HA perfectly after installing a client cert on any device, but whenever I completely close/kill the Chrome app on either platform, I’m unable to reconnect to HA. More details below to help triage the issue, but does anybody have this setup working, and could they share their setup and config?

Some details about my setup:

  • Home Assistant 0.57.3 on Raspbian on a Pi 3 with a password
  • NGINX reverse proxy, config file supplied below
    • ssl_certificate and ssl_certificate_key fields are from LetsEncrypt
    • ssl_client_certificate field (the CA, as I understand it) properly generated, mostly based on this guide.
    • Using client certs (.p12 extensions) on each device, generated by mostly following that same guide
  • Running the latest version of Chrome and the Samsung Browser on Android and on various Win10 machines
    • Installed client certificate through the system settings
    • Upon accessing the site for the first time, I am prompted to select a cert to use, and upon selecting my client cert, I gain access to HA
    • Killing the browser from the multi-tasking view, reopening Chrome, and refreshing the site results in either the HA “Connecting” screen if I logged into HA previously or a general “ERR_FAILED” page from the browser if I installed the cert but didn’t log in. Not sure this actually matters.
    • The app must be killed. Simply pressing back to the home screen on Android is not enough to reproduce the issue.

Long story short, I used Chrome dev tools to see an error come from WebSockets while I reproduced the issue: “Websocket connection to ‘wss:///api/websocket’ failed: Websocket opening handshake was canceled” from a script called core-.js. I’m pretty sure this error comes from something called a service worker when it tries to create a WebSocket object, but I could be wrong. This same error comes up for both Windows and Android in Chrome when I reproduce the issue.

Clearing all cookies and site data for works around the issue, as I’m prompted for a certificate when I try again. Firefox and Edge on Windows also get around the issue by just prompting for a cert every time I restart the browser and load the site. I really want this to work in Chrome though so I can use html5 notifications on my desktop and phone.

My suspicion is that the problem is with WebSockets themselves rather than a NGINX misconfiguration, but I’ll admit, I don’t fully understand exactly what each line in the config file does. More specifically, I’m wondering if maybe WebSockets is not properly closing the SSL connection because of the surprise kill? I don’t know, I’m just speculating at this point.

NGINX config below, for reference. Things in are from me.

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

# http->https
server {
    server_name <my-site>;

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

# Home Assistant with client certs
server {
    server_name <my-site>;

    ssl_certificate /etc/letsencrypt/live/<my-site>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<my-site>/privkey.pem;

    ssl_dhparam <path-to-dhparams>/dhparams.pem;

    ssl_client_certificate <path-to-ca>/ca.crt;
    ssl_crl <path-to-ca>/ca.crl;
    ssl_verify_client optional;
    #ssl_session_timeout 5m; # Note: this doesn't have any effect on the issue

    listen [::]:443 default_server ipv6only=off; # if your nginx version is >= 1.9.5 you can also add the "http2" fl\
ag here
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    ssl on;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    proxy_buffering off;

    location / {
        # SSL client verification is optional in general but not for the front end, so I can use other apps without client certificates.
        if ($ssl_client_verify != SUCCESS) {
            return 403;
        }
        proxy_pass http://localhost:<ha_port>;
        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 X-Forward-Proto https;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }

    location /api/websocket {
        proxy_pass http://localhost:<ha_port>/api/websocket;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Not sure if it’s related or not, but this behavior seems awfully similar to the 401 WebSocket issue for nginx basic auth…

Does anybody else use client-side certificates and can you share your setup/config?

For anybody who comes across this post in the future…

I’ve tried to figure out how to properly debug this for days without success, but granted, my specialty isn’t web development. I ended up switching to basic auth, and now I’m hitting the same 401 issue described in the link above on my Android device consistently and sometimes on my Windows devices in Chrome. Will update this thread if I figure out the proper solution at some point…

1 Like

I tried using custom HTML to resolve the problem, notably by disabling the service worker, but I couldn’t get it to work. Probably someone more familiar with web programming could figure it out.

I ended up switching to Oauth2 using bitly/oauth2_proxy behind nginx and the auth_request directive, and that worked just fine without any extra hacks. The only parameters oauth2_proxy needed from me were upstream, email_domain/authorized_emails_file, client_id, and client_secret. I also added cookie_secret and cookie_refresh, but I don’t think those are strictly required.

I’ve been running into this issue for a while now. Up until a few releases ago, I was able to access Home Assistant through an NGINX reverse proxy with client certificate authentication just fine. That is with Google Chrome on an Android smarphone. But lately, it keeps on giving me “ERR_FAILED” after a while (the duration of which varies) eventhough it keeps working just fine in Firefox on my MacBook Pro.

I can only fix it temporarily by unregistering the service worker via chrome://serviceworker-internals. After unregistering it, I can reload the page and it will ask me again which certificate to use.

1 Like

Many thanks for that solution, also had this mysteriously happen to me in a similar setup this morning.

(sorry for the necro thread, but this is exactly the right topic)

I think the issue is that the browser tries to download /service_worker.js every so often (I’m guessing based on cache rules?). This is the error I see in console if I refresh in exactly the right way:

https://chromium.googlesource.com/chromium/src/+/d0dfdbbae1b2356c56e566d5880796af8cf246c1/content/browser/service_worker/service_worker_write_to_cache_job.cc#37

I think this is the service worker code for Chrome. It looks like it just flat-out doesn’t support client TLS. (see this in the same source).

This leads me to believe that this error would go away if client cert auth was turned off for /service_worker.js, but I don’t think that’s something nginx supports. At least not without putting most of the routes in the default server.

To temporarily fix the ERR_FAILED error, the nicest way I’ve found to do this is:

  1. Open the dev console (Cmd + Alt + J) on Mac
  2. Force refresh (Cmd + Shift + R)

For mobile, clearing data from the pinned app also does the trick.

Think force refresh … force refreshes harder when the dev console open.

My guess is that the only mostly clean way to solve this upstream of HASS is to handle SSL termination with something other than nginx. Apparently apache supports route-based client auth (not that I have any interest in using apache).

Maybe there’s a way to get HASS to behave differently as well, though. Anyone have ideas?