I’ve searched for more information but could not find anyhing according to a local firewall on HA. Is there any reason why there was never a local firewall inplemented into HA? I mean, I am aware that I can separate my network into different subnets and VLANs and then use my cental router/firewall to controll traffic between them, but a critical device like HA should have the possibility to enable a local firewall in my opinion. There sometimes are reasons having specific devices on the same subnet as HA and therefore it would make sense to have the possibility to only allow the according services locally via HA firewall. And people who don’t want/need it could disable it completely anytime.
But -as always- there may be some good reasons why there is no firewall implemented.
I’m just curious and want to be constructive, no trying to troll here
I know @agners is mostly the man here who could answer this question, so if he is willing to take his time to answer this, that would be very appreciated.
Thanks!
Local, personal (or what I would call it desktop) firewalls are mostly useful on general purpose operating system, where it is unknown what type of software is running on, or the software which runs on is not fully trustworthy.
Home Assistant OS is designed as an appliance, where we largely know what software is run on. There is no random port open, it is Home Assistant frontend on 8123 and the observer on 4357. Furthermore, add-on are containers, which are in a separated network (this is a feature of the underlying container technology implemented by Docker). Docker than uses iptables, which is a firewall, to setup what ports are publicly accessible. An add-on needs to explicitly tell the system through it’s config what port should be open to the public.
So in a way, there is some firewall functionally already built-in, but it’s largely transparent.
That said, the system could be extended with more (default) rules to increase security still. But so far this hasn’t really come up as a necessary or a problem.
Fair enough, thanks a bunch for your response. I did not know about add-ons being abstracted by Docker and therefore iptables do come in.
Well, security most always unfortunately is not a concern, until it hits in. So personally I would always appreciate to increase security wherever it is possible. You surely know better than me where there are potentially thing to improve in HA, maybe something to keep in mind for the future.
Thanks again man, your answer is very appreciated!
Hello!
I have just installed HAOS and I’m disappointed by the absence of configurability on that regard, for a tool that prides in having the data be rather private. That is a security concern for me, considering how much data Home Assistant has (location…).
My issue specifically is that add-on containers are always exposed to 0.0.0.0
, even if they are actually only needed by HomeAssistant itself, and that HomeAssistant seems to expose a lot of ports which I doubt are all regularly necessary.
nmap
reports no less than 11 exposed ports on a default install with only Mosquitto & SSH added.
I would expect that number to be something like 3, for HTTP(s), SSH, and maybe some service discovery.
Look at Mosquitto for example. Installing the add-on opens an unencrypted MQTT server port on whatever local network the Home Assistant controller is running on, but I really only need it to be accessible to zigbee2mqtt
and Home Assistant, both of which are running on the same device.
This is bad security practice simply because it unnecessarily increases the attack surface of the system: assuming somewhat untrusted or potentially compromised devices may connect to the local network HAOS is running on, the attack surface is now not only the Home Assistant HTTP server, but also MQTT, hassio-observer, rpcbind, llmnr, as well as ports 18555, 40000 and 40567 which I don’t even know what they are, and any other random addon’s internal services. This is unreasonably hard to audit. There are released vulnerabilities on tons of services very regularly, and compromising any of them gives access to the entirety of HA because every addon gets API credentials. For example Mosquitto itself has quite the collection of security vulnerabilities.
A very basic yet very effective countermeasure is to not expose services that don’t need to be exposed, the idea being that even if they are vulnerable if you can’t access them you can’t exploit them.
My understanding is that currently, when HAOS reads the configuration file related to ports, it always binds to 0.0.0.0:port
and :::port
(ipv6), in turn leaking the plugin’s interface on any network the device is connected to. (This is what docker ps
says.)
It looks like this could be fixed reasonably easily by either:
- Adding a new global option for add-ons (similar to “Start on boot” and “Watchdog”) called “local only” that would make it so that it only binds addons ports to
127.0.0.1
,::1
and thehassio
docker network (172.30.32.1/23
). (Alternatively this could also be made configurable per port.) - Being able to firewall, with e.g. a firewall addon, and opt-in exposed ports for physical interfaces at the system level. (e.g. only 22 and 443 can leak through ethernet and wifi interfaces)
WDYT? Has anyone worked on making such a thing work with HAOS so far?
Thanks,
Ok so I did spend a few hours working on this today, and bearing in mind that my configuration is:
- SSH addon (port 22)
- NGINX addon for HTTPS (port 443)
- Wireguard addon (UDP 51820)
- HAOS debug server enabled (port 22222)
- I want SSDP (UPnP) to work so that HA can read my router’s state
and I don’t need any other port to be open on the local network (I can just connect securely via the wireguard IP if I need access to any other port directly),
I eventually set up the following iptables rules (please don’t set that if you’re using the default configuration without SSH credentials nor working port 443 proxy because you’d lose all access):
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -i docker0 -j ACCEPT
iptables -A INPUT -i hassio -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 22222 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
iptables -A INPUT -p udp --dport 1900 -j ACCEPT
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -j REJECT
iptables -A DOCKER-USER -i lo -j RETURN
iptables -A DOCKER-USER -i docker0 -j RETURN
iptables -A DOCKER-USER -i hassio -j RETURN
iptables -A DOCKER-USER -p tcp --dport 22 -j RETURN
iptables -A DOCKER-USER -p tcp --dport 22222 -j RETURN
iptables -A DOCKER-USER -p tcp --dport 443 -j RETURN
iptables -A DOCKER-USER -p udp --dport 51820 -j RETURN
iptables -A DOCKER-USER -p udp --dport 1900 -j RETURN
iptables -A DOCKER-USER -p icmp -j RETURN
iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
iptables -A DOCKER-USER -j REJECT
iptables -D DOCKER-USER -j RETURN
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -i docker0 -j ACCEPT
ip6tables -A INPUT -i hassio -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22222 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -p udp --dport 51820 -j ACCEPT
ip6tables -A INPUT -p udp --dport 1900 -j ACCEPT
ip6tables -A INPUT -p ipv6-icmp -j ACCEPT
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -j REJECT
ip6tables -A DOCKER-USER -i lo -j RETURN
ip6tables -A DOCKER-USER -i docker0 -j RETURN
ip6tables -A DOCKER-USER -i hassio -j RETURN
ip6tables -A DOCKER-USER -p tcp --dport 22 -j RETURN
ip6tables -A DOCKER-USER -p tcp --dport 22222 -j RETURN
ip6tables -A DOCKER-USER -p tcp --dport 443 -j RETURN
ip6tables -A DOCKER-USER -p udp --dport 51820 -j RETURN
ip6tables -A DOCKER-USER -p udp --dport 1900 -j RETURN
ip6tables -A DOCKER-USER -p ipv6-icmp -j RETURN
ip6tables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
ip6tables -A DOCKER-USER -j REJECT
ip6tables -D DOCKER-USER -j RETURN
Note that:
- It is necessary to set both INPUT and DOCKER rules, otherwise rules that go through forwarding to docker bypass the INPUT rules and don’t get filtered out
- It is necessary to set the same rules with ip6tables, otherwise while ipv4 traffic will be filtered by iptables, ipv6 traffic would still be open
- on ipv6, icmp is not called icmp but it’s called ipv6-icmp. It is very necessary to have it somewhat open for ipv6 traffic to work (unlike ipv4 that can work without and its icmp is essentially used for pings).
- iptables rules do not persist across reboots
It is possible to set these rules via the SSH Addon, since the iptables rules are shared with the host. (At least for me with Protection mode disabled - I don’t know how that would look if it wasn’t).
The question for me now is how do I make it so that these rules are setup before all the docker containers are started, so that there isn’t a short period during boot where everything is accessible before the firewall rules are setup.
I could access the host via the Host debug SSH server, however there almost all of the filesystem is very read-only, so I can’t set the files to set this up on boot before the HA services start:
> mount
/dev/mmcblk0p3 on / type erofs (ro,relatime,user_xattr,acl,cache_strategy=readaround)
devtmpfs on /dev type devtmpfs (rw,relatime,size=781968k,nr_inodes=195492,mode=755)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,nodev,size=392352k,nr_inodes=819200,mode=755)
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
pstore on /sys/fs/pstore type pstore (rw,nosuid,nodev,noexec,relatime)
bpf on /sys/fs/bpf type bpf (rw,nosuid,nodev,noexec,relatime,mode=700)
tmpfs on /etc/machine-id type tmpfs (ro,size=392352k,nr_inodes=819200,mode=755)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,nosuid,nodev,relatime,pagesize=2M)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,nosuid,nodev,noexec,relatime)
configfs on /sys/kernel/config type configfs (rw,nosuid,nodev,noexec,relatime)
/dev/mmcblk0p1 on /mnt/boot type vfat (rw,relatime,sync,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)
/dev/mmcblk0p7 on /mnt/overlay type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/dropbear type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/modprobe.d type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/modules-load.d type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/udev/rules.d type ext4 (rw,relatime)
/dev/mmcblk0p7 on /root/.docker type ext4 (rw,relatime)
/dev/mmcblk0p7 on /root/.ssh type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/NetworkManager/system-connections type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/hostname type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/hosts type ext4 (rw,relatime)
/dev/mmcblk0p7 on /etc/systemd/timesyncd.conf type ext4 (rw,relatime)
/dev/mmcblk0p8 on /mnt/data type ext4 (rw,relatime,commit=30)
/dev/zram2 on /tmp type ext4 (rw,nosuid,nodev,nobarrier)
tmpfs on /var type tmpfs (rw,nosuid,nodev,relatime,nr_inodes=1048576)
/dev/mmcblk0p7 on /var/lib/NetworkManager type ext4 (rw,relatime)
/dev/mmcblk0p7 on /var/lib/bluetooth type ext4 (rw,relatime)
/dev/mmcblk0p8 on /var/lib/docker type ext4 (rw,relatime,commit=30)
/dev/mmcblk0p7 on /var/lib/systemd type ext4 (rw,relatime)
/dev/mmcblk0p8 on /var/log/journal type ext4 (rw,relatime,commit=30)
Side note: I’d also like to be able to run the workaround command for HAOS reverse DNS takes 7.5s before it fails, making lots of things painfully slow · Issue #3768 · home-assistant/operating-system · GitHub on boot at the same time. It’s less critical that it runs early, in fact it needs to run after the corresponding systemd service is up, but it needs to run on the host, unlike iptables that could theoritically run in the SSH addon if not for the “firewall should be setup before services are started” issue.
Until I find a way to solve this I’m setting up the rules as a startup script of the SSH addon, which is better than nothing.
#!/usr/bin/env python3
import subprocess
# The -m ... and --reject-with statements are just here so that we match the output of iptables -S
# but they are not necessary when otherwise creating the rules, as these are default values.
ipv4_rules = [
"-A INPUT -i lo -j ACCEPT",
"-A INPUT -i docker0 -j ACCEPT",
"-A INPUT -i hassio -j ACCEPT",
"-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT", # ssh addon
"-A INPUT -p tcp -m tcp --dport 22222 -j ACCEPT", # ssh HAOS host
"-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT", # https
"-A INPUT -p udp -m udp --dport 51820 -j ACCEPT", # wireguard
"-A INPUT -p udp -m udp --dport 1900 -j ACCEPT", # SSDP (UPnP)
"-A INPUT -p icmp -j ACCEPT", # ping
"-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT",
"-A INPUT -j REJECT --reject-with icmp-port-unreachable",
]
def run_command(command: str) -> str:
print(f"> {command}")
return subprocess.run(
command, capture_output=True, text=True, check=True, shell=True
).stdout
def lines(command: str) -> set[str]:
command_output = run_command(command)
print(f"{command_output}")
lines = command_output.splitlines()
lines = (line.strip() for line in lines if len(line.strip()) > 0)
return set(lines)
print("Setting up firewall rules")
existing_ipv4_rules = lines("iptables -S")
existing_ipv6_rules = lines("ip6tables -S")
# Create all rules
for ipv4_rule in ipv4_rules:
if ipv4_rule not in existing_ipv4_rules:
run_command(f"iptables {ipv4_rule}")
ipv4_docker_rule = ipv4_rule.replace("INPUT", "DOCKER-USER").replace(
"ACCEPT", "RETURN"
)
if ipv4_docker_rule not in existing_ipv4_rules:
run_command(f"iptables {ipv4_docker_rule}")
ipv6_rule = ipv4_rule.replace("-p icmp", "-p ipv6-icmp").replace(
"icmp-port-unreachable", "icmp6-port-unreachable"
)
if ipv6_rule not in existing_ipv6_rules:
run_command(f"ip6tables {ipv6_rule}")
ipv6_docker_rule = ipv6_rule.replace("INPUT", "DOCKER-USER").replace(
"ACCEPT", "RETURN"
)
if ipv6_docker_rule not in existing_ipv6_rules:
run_command(f"ip6tables {ipv6_docker_rule}")
# We can now clear the default rules that allow all docker forwarding
if "-A DOCKER-USER -j RETURN" in existing_ipv4_rules:
run_command("iptables -D DOCKER-USER -j RETURN")
if "-A DOCKER-USER -j RETURN" in existing_ipv6_rules:
run_command("ip6tables -D DOCKER-USER -j RETURN")
(This requires adding iptables & ip6tables to list of packages to install in the SSH addon.)
I might have a different use case for local firewall. Home network with some devices (192.168.2.x) accessing HA (docker), and other devices (192.168.3.x) which should not access HA. The LAN subnet is 192.168.2.0/23.
The ability to easily install ufw or similar on the HA host, and go “ufw deny from 192.168.3.0 …” would be great.
I’m sure if I had a better router there would be options there, but alas I do not.
And maybe an attacker could set static IP in 192.168.2 instead of DHCP, but at least then I’d know someone’s attacking.
Great topic. Firewall is an important thing for me. I am currently making an yggdrasil mesh network addon for HAOS (to access my instance remotely through it) and I wish to block all ipv6s except a few trusted on a specific interface. Firewall would be great for security.
I believe this is a needed feature (along with the improved VLAN handling - like f.ex. removing them ). It could be either firewall rules or fine-grain control on which interfaces the forwarded ports for services are exposed to (instead of 0.0.0.0:x).
I’m setting up a HA OS (so far very basic). I have different networks for IOT devices (VLANs). Some integrations require direct VLAN access (for multicast/broadcast) - so I’m connecting the HA to the VLAN directly. This exposes all the services towards this subnet. There is no firewalling as it is all L2 traffic (direct VLAN access - no router/fw involved) - and it all happens from the theoretically high-risk isolated network (entire purpose of sandboxing IOT devices). Every IOT device on the VLANs that I connect HA to gets unrestricted access to all exposed services - not very desirable.
In a perfect world I’d like to very much close all the ports on the VLAN side of things (unless really, really necessary). I can live with the main NIC being ‘talkative’ as this one sits in the network of its own and traffic to it is in fact externally firewalled.