Securing HomeAssistant with client certificates (works with Safari/iOS)

I recently moved from SmartThings to HomeAssistant. One of the things I didn’t have to think about too much with SmartThings was how to authenticate all of my connected devices (laptops, phones, tablets, etc.) with my HA platform. I wanted to find a good balance between security and convenience with HomeAssistant.

HomeAssistant makes it easy to secure your install with a password. Coupled with TLS, this is pretty solid. But there’s just something about the idea of a publically facing page that anyone on the Internet can get to, protected with nothing but a password that made me feel uneasy.

Client certificates are a very robust authentication mechanism that involves installing a digital certificate on each device you wish to grant access to. Each certificate is signed by the server certificate, which is how the server knows that the client is valid.

This feels nicer than HomeAssistant’s built-in security measures to me for a few reasons:

  1. Individual client certificates can be revoked. You don’t have to configure authentication on every device you own if someone loses their phone.
  2. While I highly doubt there are any issues with HomeAssistant, I feel more confident in nginx and openssl.
  3. Unless you add a passphrase to the client certificates (I didn’t), the whole thing is passwordless and still manages to be pretty darn secure.
  4. If I ever became truly paranoid, I could turn on HomeAssistant’s password protection and my HA dashboard would essentially need two authentication factors (the SSL cert + the password).

While I did find this approach more appealing, there are several drawbacks:

  1. It’s way harder to set up. You need to run a bunch of openssl commands, and install a certificate on each device you want to grant access to.
  2. The HomeAssistant web UI requires WebSockets, which seem to not play nicely in combination with client certificates on Safari or iOS devices. My household has iOS users, so this was something I needed to figure out.

I think I managed to get this working. The only disadvantage is that clients are granted access for an hour after successfully authenticating once. The basic approach is to tag authenticated browsers with an access token that’s good for a short period of time, long enough for them to establish a WebSocket connection. I’ll go through the steps in setting this up.

What you need

  1. Install packages:  sudo apt-get install nginx nginx-extras lua5.1 liblua5.1-dev
  2. openssl
  3. luacrypto module, which exposes openssl bindings in lua.

luacrypto was kind of a pain to install. Here’s what I did to get it working with my nginx install. It involved patching (thanks to this very helpful StackOverflow post for the tip):

Setting up a Certificate Authority

There are already good guides on doing this. I recommend this one. In this guide, I’m using the default_CA parameters pre-filled by openssl on my system.

Generate client certificates

I put a script in /usr/local/bin  to make this easier:

You then run this for each device you want to grant access to:

Make sure to supply an export password. Generated certificate files will be placed in /etc/ssl/ca/certs/users .

Get the certificates on the devices

The .p12 file is the one you want. Make sure to not compromise the certificates in the process.

I rsynced the files to my laptop and attached them to a LastPass note, which I could access on my devices. On most devices, you should be able to just open the .p12 file and it’ll do what you want.

On iOS devices, I needed to serve the certificates over HTTPS on a trusted network because they needed to be “opened” by Safari in order to be recognized.

Configure nginx

Here’s my nginx config. You’ll need to substitute your domain and SSL certificate parameters:

If this all worked, you should be able to access from a device with a client certificate installed, but not otherwise. Unfortunately if you’re using iOS or Safari, you’ll probably notice that the page loads, but you get a forever spinny wheel. If you look in the debugger console, you might see messages that look like this:

This is because the browser isn’t sending the client certificate information when trying to connect to the WebSocket and is therefore failing.

Fixing compatibility with iOS/Safari

Safari does actually send client cert info along with the initial request. Nginx has a really cool module that allows you to insert all sorts of fancy logic with lua scripts. I added one that tags browsers supplying a valid client certificate with a cookie granting access for about an hour. This worked really well. Since this is all over HTTPS, and the access tokens are short-lived, I felt pretty comfortable.

The easiest way I could think of to create a cookie that was valid for a limited time was to use an HMAC. Basically I “sign” a hash of the client’s certificate along with an expiry timestamp. The certificate hash, expiration timestamp, and HMAC are all stored in cookies. Nginx can then validate that the expiration timestamp is in the future, and that the HMAC signature matches what’s expected.

You’ll notice the commented-out line in the nginx config above. Uncomment it:

And add the script:


14 thoughts on “Securing HomeAssistant with client certificates (works with Safari/iOS)

  1. Any idea how to get this working on a Synology DS1515+? There’s no lua or even make to compile lua installed.

    • I have no experience with that system, but this particular technique definitely requires the Lua plugin.

  2. I spent quite some time figuring out why your script wasn’t working properly. I always got errors like:

    WebSocket network error: The operation couldn’t be completed. (OSStatus error -9807.)

    The solution is quite “simple” tough.. iOS is much, much more picky about certificates and stuff than Safari on a Mac is.

    On a Mac it seems to be enough to just install and trust the (self created) Root CA, but on iOS you really need the whole chain from Root CA -> Intermediate CA -> Website. If you’re testing this locally, you have actually to create a certificate with the Common Name or whatever your local IP for testing is! Otherwise Safari will prompt you with something like »really open this website?«, then the website loads, you think »Everything is loading, so why is the web socket not working?«.

    So, solution: Provide the whole chain of certificates via a Profile (create one with Apple Configurator 2), install it on your device and then explicitly trust the Root CA (because installing the Profile doesn’t trust the certificates yet).

    • I didn’t need an intermediate certificate. But the CN in the cert is for the appropriate URL. I assume that’s necessary for Safari to know when it’s appropriate to use a particular certificate.

      I did need to trust the cert, though. On OS X I was able to do that in the keychain manager. On iOS opening the p12 lead to a series of prompts that resulted in the certificate being trusted, I think.

      The biggest problem I’ve run into that I’ve not exactly solved is that HomeAssistant started using ServiceWorkers about 6 months ago (I think?). It tries to download /service_worker.js every so often, and it Chrome doesn’t seem to support client certs in this context.

  3. Is the iOS app using a WebView? If so it might perhaps share client certs with Safari. Perhaps the workaround using the lua script leaves any Safari-based WebView to access HA during the one hour window?

  4. Hi Chris,

    sounds good but unfortunately seems not to work as I would expect on my side.

    I mean, setting ssl_verify_client as ‘optional’ I suspect client authentication is just skipped by nginx and in fact I am able to see HA page from iOS Safari even commenting out the lua script.

    Instead forcing ssl_verify_client to ‘on’ I cannot access to that page even with the lua script enabled.

    Any thoughts?



    • The LUA script is what’s doing the enforcing. It enforces that either the browser provides client cert details, or that a valid auth cookie is in place. So having ssl_verify_client to optional while commenting out the LUA script should have the behavior you’re seeing.

      Having it set to “on” will force every request to provide client cert details, which is exactly what the LUA script is trying to get around.

    • Got it,
      performing some further testing I confirm that it works as you are describing.

      Thank for sharing all this info and the script code!

    • I’m not really familiar with the native iOS app. It’s not impossible that it works, but I sort of doubt it does.

    • So surprisingly this worked out of the box in the iOS app for me. In the recent iOS11 beta I’ve been having some issues, but I’m not sure if that’s specific to me upgrading the OS or the version of Caddy that I use (a replacement for nginx).

    • You’re saying that client certs out of the box works with the HASS iOS app? The issue I observed was that Safari (OS X and iOS versions) didn’t send client cert details when making web socket requests, which I think would only be fixed by a software update on Apple’s side.

  5. This was super-informative, and exactly what I was looking to do! Thanks for the great write up and step-by-step instructions. The worst part was luacrypto, but your patch even made that a breeze. Thanks again!

Leave a Reply

Your email address will not be published. Required fields are marked *