Protecting Home Assistant with Cloudflare Access and mTLS on Android

There are many different ways to set up Home Assistant so that it is accessible when you're away from home, which have various pros and cons, but I had some extra requirements beyond just securing it with HTTPS/TLS which I haven't found any articles about (in bold):

  1. Does not expose my home IP address
  2. Does not publicly expose my Home Assistant instance (i.e. no publicly-accessible login form)
  3. Secured using TLS/SSL
  4. Accessible from any device
  5. Works with the Home Assistant Android native app
  6. Does not require running a network client "tool" on my phone (e.g. VPN client, Cloudflare WARP client)
  7. Ideally doesn't require a paid subscription to something
  8. Can support Google Assistant integration in future

This blog post, as with most of my posts, serves as a combination of a reminder to my future self on how I got it working, and hopefully also a useful resource for others. As such, this post will be rough-around-the-edges in places.

Things this blog post doesn't cover:

  • Setting up Cloudflare with your domain
  • Setting up Cloudflare Tunnel
  • Configuring Home Assistant to accept reverse-proxied traffic through Cloudflare Tunnel (see Home Assistant docs)

Ways of exposing Home Assistant

Some different ways of setting up Home Assistant so that it's accessible publicly:

  • Paying for Home Assistant Cloud
    • Pro: easiest, supports the maintainers
    • Con: subscription service, leaves your HA instance exposed to the internet
    • Satisfies requirements 1-6, 8 (I think)
  • Forwarding a port on your home router
    • Pro: easy to set up
    • Con: exposing your home IP, and has potential risks from security holes in Home Assistant
    • Satisfies requirements 3-8
  • Using a VPN (incl. Tailscale) to connect back into your home network
    • Pro: secure, no attack surface for your home server
    • Con: you have to install the VPN client wherever you want to use it, and connect/disconnect when needed
    • Satisfies requirements 1-5, 7
  • Hiding your Home Assistant instance behind Cloudflare, using a Cloudflare Tunnel to connect outbound from your home network to Cloudflare.
    • Pro: secure, no direct route of attack to your home server
    • Con: has potential risks from security holes in Home Assistant
    • Satisfies requirements 1, 3-8

Requirement 2 (does not publicly expose my Home Assistant instance to the internet) can be mitigated when using Cloudflare by using Cloudflare Access to require visitors to log in (e.g. via Google, Okta, one-time PIN via email) before they even get to your Home Assistant login form. This is great from a security standpoint, but it does cause issues with the Android app.

Cloudflare Access and the Android app

If you set up Cloudflare Access so that it prompts the user to log in via either an OTP sent via email, or via another identity provider before exposing your Home Assistant login form, this breaks compatibility with the Android app. This is because when you access your Home Assistant domain, e.g. ha.mydomain.co.uk, you will be redirected to your identity provider domain before you get to see your Home Assistant login form (e.g. myname.cloudflareaccess.com). This redirect takes you out of the Home Assistant app and into your browser, and so once you've logged into your identity provider, the Android app never receives the auth token generated as part of the final login step, which it needs to be able to communicate with Home Assistant. As such, we need to find another way of authenticating the Android app which does not require redirecting to another domain.

There have been issues raised on GitHub about this exact issue, but there is no solution for the redirect problem at present.

Just get to the point

What does seem to work is having two subdomains, which both back off to the same Home Assistant instance, one which is for use in a browser, and one which is for use from the Android app.

  • ha.mydomain.co.uk - for use from a browser
    • Uses Cloudflare Access to enforce login via an identity provider
    • Accessible from anywhere, as long as you can log into your identity provider
  • ha-android.mydomain.co.uk - for use from the Android app
    • Uses mTLS (mutual TLS) to require the Android app to present a client TLS certificate to Cloudflare
    • Requires manual setup on each Android device, but only once

You will need two separate Cloudflare Tunnels, one for each domain, both of which should back off to the same origin URL.

I've seen a number of other guides/tutorials online describing how to set up Cloudflare Access, but I haven't seen much explaining how to set up mTLS with Cloudflare, so that's what I'll do next.

Setting up mTLS with Cloudflare and Android

Tl;dr this is how I did it:

  1. Remove Cloudflare Access rules for your ha-android domain
    • Cloudflare Access rules will prompt the user to log in, which we don't want
  2. Generate a client certificate in Cloudflare
    1. Use the "SSL/TLS" pane of Cloudflare to generate and download a client certificate and secret (or generate it yourself offline). Store the certificate as cf.pem and the private key as cf.key
    2. Convert these two files into a pfx file, which is importable in Android, using the following command (source): openssl pkcs12 -export -out cf.pfx -inkey cf.key -in cf.pem
    3. Securely transfer this file to your Android phone
  3. Install client certificate into Android cert store
    1. Go to Settings > Security > Credential Storage, tap Install from storage, then locate the file you transferred in the previous step
    2. Give the certificate a sensible name so that future-you remembers what it is
  4. Create an mTLS rule in Cloudflare WAF to block non-certificate-holders
    1. From the "SSL/TLS" pane in Cloudflare, click "Client certificates", then click the "Create mTLS rule" button. If this isn't visible in Cloudflare, it's because they've moved it to the API Shield section which they are warning about right now ('Nov 2022)
  5. Configure Home Assistant app to use cert
    1. Change the "External URL" in your app settings to use your ha-android domain, then restart the app. You should be prompted to select a client certificate on next start.

That's it. You now have a domain for browser use, and a domain which works in the Android app, without having to start any other VPN apps first.

References

Some posts I found useful while setting this up: