More Yak Shaving: Moving to nftables to secure Home Assistant
When I setup Home Assistant last year one of my niggles was that it wanted an entire subdomain, rather than being able to live under a subdirectory. I had a desire to stick various things behind a single SSL host on my home network (my UniFi controller is the other main one), rather than having to mess about with either SSL proxies in every container running a service, or a bunch of separate host names (in particular one for the backend and one for the SSL certificate, for each service) in order to proxy in a single host.
I’ve recently done some reorganisation of my network, including building a new house server (which I’ll get round to posting about eventually) and decided to rethink the whole SSL access thing. As a starting point I had:
- Services living in their own containers
- Another container already running Apache, with SSL enabled + a valid external Let’s Encrypt certificate
And I wanted:
- SSL access to various services on the local network
- Not to have to run multiple copies of Apache (or any other TLS proxy)
- Valid SSL certs that would validate correctly on browsers without kludges
- Not to have to have things like
hass-host
as the front end name andhass-backend-host
as the actual container name.
It dawned on me that all access to the services was already being directed through the server itself, so there was a natural redirection point. I hatched a plan to do a port level redirect there, sending all HTTPS traffic to the service containers to the container running Apache. It would then be possible to limit access to the services (e.g. port 8123 for Home Assistant) to the Apache host, tightening up access, and the actual SSL certificate would have the service name in it.
First step was to figure out how to do the appropriate redirection. I was reasonably sure this would involve some sort of DNAT in iptables
, but I couldn’t find a clear indication that it was possible (there was a lot of discussion about how you also ended up needing SNAT, and I needed multiple redirections to 443 on the Apache container, so that wasn’t going to fly). Having now solved the problem I think iptables
could have done it just fine, but I ended up being steered down the nftables route. This is long overdue; it’s been available since Linux 3.13 but lacking a good reason to move beyond iptables
I hadn’t yet done so (in the same way I clung to ipfwadm
and ipchains
until I had to move).
There’s a neat tool, iptables-restore-translate
, which can take the output of iptables-save
and provide a simple translation to nftables
. That was a good start, but what was neater was moving to the inet
filter instead of ip
which then mean I could write one set of rules which applied to both IPv4 and IPv6 services. No need for rule duplication! The ability to write a single configuration file was nicer than the sh
script I had to configure iptables
as well. I expect to be able to write a cleaner set of rules as I learn more, and although it’s not relevant for the traffic levels I’m shifting I understand the rule parsing is generally more efficient if written properly.Finally there’s an nftables
systemd
service in Debian, so systemctl enable nftables
turned on processing of /etc/nftables.conf
on restart rather than futzing with a pre-up
in /etc/network/interfaces
.
With all the existing config moved over the actual redirection was easy. I added the following block to the end of nftables.conf
(I had no NAT previously in place), which redirects HTTPS traffic directed at 192.168.2.3
towards 192.168.2.2
instead.
nftables dnat configuration
table ip nat {
chain prerouting {
type nat hook prerouting priority 0
# Redirect incoming HTTPS to Home Assistant to Apache proxy
iif "enp24s0" ip daddr 192.168.2.3 tcp dport https \
dnat 192.168.2.2
}
chain postrouting {
type nat hook postrouting priority 100
}
}
I think the key here is I can guarantee that any traffic coming back from the Apache proxy is going to pass through the host doing the DNAT; each container has a point-to-point link configured rather than living on a network bridge. If there was a possibility traffic from the proxy could go direct to the requesting host (e.g. they were on a shared LAN) then you’d need to do SNAT as well so the proxy would return the traffic to the NAT host which would then redirect to the requesting host.
Apache was then configured as a reverse proxy, with my actual config ending up as follows. For now I’ve restricted access to within my house; I’m still weighing up the pros and cons of exposing access externally without the need for a tunnel. The domain I used on my internal network is a proper registered thing, so although I don’t expose any IP addresses externally I’m able to use Mythic Beasts’ DNS validation instructions and have a valid cert.
Apache proxy config for Home Assistant
<VirtualHost *:443>
ServerName hass-host
ProxyPreserveHost On
ProxyRequests off
RewriteEngine on
# Anything under /local/ we serve, otherwise proxy to Home Assistant
RewriteCond %{REQUEST_URI} '/local/.*'
RewriteRule .* - [L]
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://hass-host:8123/$1 [P,L]
ProxyPassReverse /api/websocket ws://hass-host:8123/api/websocket
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) http://hass-host:8123/$1 [P,L]
ProxyPassReverse / http://hass-host:8123/
SSLEngine on
SSLCertificateFile /etc/ssl/le.crt
SSLCertificateKeyFile /etc/ssl/private/le.key
SSLCertificateChainFile /etc/ssl/lets-encrypt-x3-cross-signed.crt
# Static files can be hosted here instead of via Home Assistant
Alias /local/ /srv/www/hass-host/
<Directory /srv/www/hass-host/>
Options -Indexes
</Directory>
# Only allow access from inside the house
ErrorDocument 403 "Not for you."
<Location />
Order Deny,Allow
Deny from all
Allow from 192.168.1.0/24
</Location>
</VirtualHost>
I’ve done the same for my UniFi controller; the DNAT works exactly the same, while the Apache reverse proxy config is slightly different - a change in some of the paths and config to ignore the fact there’s no valid SSL cert on the controller interface.
Apache proxy config for Unifi Controller
<VirtualHost *:443>
ServerName unifi-host
ProxyPreserveHost On
ProxyRequests off
SSLProxyEngine on
SSLProxyVerify off
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLProxyCheckPeerExpire off
AllowEncodedSlashes NoDecode
ProxyPass /wss/ wss://unifi-host:8443/wss/
ProxyPassReverse /wss/ wss://unifi-host:8443/wss/
ProxyPass / https://unifi-host:8443/
ProxyPassReverse / https://unifi-host:8443/
SSLEngine on
SSLCertificateFile /etc/ssl/le.crt
SSLCertificateKeyFile /etc/ssl/private/le.key
SSLCertificateChainFile /etc/ssl/lets-encrypt-x3-cross-signed.crt
# Only allow access from inside the house
ErrorDocument 403 "Not for you."
<Location />
Order Deny,Allow
Deny from all
Allow from 192.168.1.0/24
</Location>
</VirtualHost>
(worth pointing out that one of my other Home Assistant niggles has also been fixed - there’s now the ability to setup multiple users and separate out API access to OAuth, rather than a single password providing full access. It still needs more work in terms of ACLs for users, but that’s a bigger piece of work.)