Improving Docker security - non-root configuration

Instructions published on https://www.home-assistant.io/installation/generic-x86-64 explain how to run the container with the “privileged” flag and in root mode. This can be a disaster for security, especially if your setup is internet facing.

To improve security, we will:

  1. Run the container as a low privileged user
  2. Prevent the (user running the) container from acquiring extra privileges
  3. Define granular capabilities for the container if required

I’m using Docker with docker-compose on a Ubuntu server but the same steps can likely be followed on your prefered container orchestration engine and Linux distro.

  1. Run the container as a low privileged user

Create a new user and group on your docker host; add user to the group. If required, add yourself to the group as well to easily edit config. files

sudo adduser hassuser
sudo groupadd -g 8123 hassgroup
sudo usermod -a -G hassgroup hassuser

Change ownership of your HA folder where your config. files are

sudo chown -R hassuser:hassgroup /[path_to_config_folder]/

Change permissions so members of your new group can operate. We will allow members of the group to read, write and execute files in this folder

sudo chmod -R g+rwx /[path_to_config_folder]/

Recreate the container:

  • Follow the steps described by tribut on https://github.com/tribut/homeassistant-docker-venv, kudos to him for creating the script
  • git clone the repo in your HA config. folder
  • Update your docker-compose to re-create the container using your new user and the run override.

hass:
    image: homeassistant/home-assistant:latest
    network_mode: host
    volumes:
    -  /[path_to_config_folder]/:/config
    -  /[path_to_config_folder]/docker/dockerrun/run:/etc/services.d/home-assistant/run
    environment:
    - TZ=[your TZ]
    - PUID=1001
    - PGID=8123
    - UMASK=007
    - PACKAGES=iputils

2. Prevent the (user runing the) container from acquiring extra privileges

For extra security, you can add to your docker-compose file the following options. This will disable container processes from gaining new privileges.

security_opt:  
    - no-new-privileges

3. Define granular capabilities for the container if required

I need to leverage the Bluetooth of my host for some HA integration. Based on your needs, you may need to access other hardware or leverage other capabilities. Because now we are in a more secure state, the low privileged user running the container doesn’t have control over BT. Let’s fix that:

sudo usermod -a -G bluetooth hassuser

Now even if our user is part of the BT group, the container itself cannot administer Bluetooth hardware. You need to research which capabilities are needed based on your use case but to give you an example I was trying to use the ble_monitor custom components https://github.com/custom-components/ble_monitor

Add the following to your docker-compose, and be aware of security implications (your container can now administer more networking stuff :

cap_add:
    - NET_RAW
    - NET_ADMIN

Lastly, update the /[path_to_config_folder]/docker/dockerrun/run to set the capabilities on the python version run into the container - there is probably a better way to do that so it’s future proof based on Python’s version, but you get the idea:

setcap 'cap_net_raw,cap_net_admin+eip' `readlink -f \`which python3.9\``

The next step to improve security will be to stop running the container in host mode, I still need to figure this one out. If you think anything can be improved in this post from a security perspective please let me know, happy to update it… I never used docker and barely used Linux before using HA so pls don’t be too harsh if you find mistakes.

(also posted on reddit on /r/homeassistant/)

8 Likes

FYI: The linuxserver/homeassistant image does this by default.

4 Likes

You’re right but the linuxserver container registry is an intermediary between you and the official HA registry, which could be an extra single point of compromise. I chose to trust the official HA distribution on DockerHub but if you’re comfortable with linuxserver it’s a convenient solution

I do trust the linuxserver team and been using their images for a while now. Never have been a fan of running docker containers with full permission. Short of doing all the work myself, linuxserver is usually right on the money

3 Likes

That will result in a number of integrations no longer working.

Only broadcasts won’t work, so you basically only loose auto-discovery (not such a big deal).
There might be other specific cases, but it won’t be an issue for 99% of the integrations.

1 Like

Specifically UPnP devices and zeroconf/mDNS devices, among other things

I run my HA container in bridge mode and it’s fine.

Because of the HomeKit integration, I need another mdns repeater solution to mirror the traffic (this works but there are probably other ones: GitHub - raetha/mdns_repeater-docker: Allow docker containers in virtual networks to send/receive mdns broadcast messages).

Here is my docker-compose extract if it can help someone:

networks:
  default:
    ipam:
      config:
        - subnet: 172.20.0.0/16

  hass:
    image: homeassistant/home-assistant:latest
    container_name: "hass"
    restart: unless-stopped
    depends_on:
      - "mqtt"
    volumes:
      - /home/thibaut/home-assistant:/config
      - /home/thibaut/home-assistant/docker/dockerrun/run:/etc/services.d/home-assistant/run
    environment:
      - TZ=America/Toronto
      - PUID=1001
      - PGID=8123
      - UMASK=007
      - PACKAGES=iputils
    cap_drop: 
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - FSETID
      - FOWNER
      - SETGID
      - SETUID
      - SYS_CHROOT
      - KILL
      - NET_RAW
      - NET_ADMIN
    security_opt:  
      - no-new-privileges
    ports:
      - 8123:8123
      - 21063:21063 # HomeKit
    healthcheck:
      test: curl --fail http://0.0.0.0:8123/auth/providers || exit 1
      interval: 90s
      retries: 5
      start_period: 5s
      timeout: 15s
    networks:
      transmission:
      default:
        ipv4_address: 172.20.0.5 # static IP required to receive mDNS traffic
2 Likes

Hello @thi_baut,
Thank you very much for your post since it definitely allowed me to run my containerized HA in non-privileged mode with all the needed resources.
I just want to add what I’ve discovered (after 1 day troubleshooting all the steps) since I think it will ease future configurations attempts.
First of all: context.
I used to run HA (hard)core in it’s venv on a raspbian PI but I finally got sick of all of the dependency updates pain so, on HA core 2023.6.1 I’ve decided to switch to docker (total newbie here).
My HA on venv was running with a dedicated non-root user and the needed privileges where just allowed through typical group assignment. In my setup this was:

  • user:
    • “homeassistant”
  • primary group:
    • “homeassistant”
  • additional groups:
    • “dialout” (needed to access the serial on the rpi - likely needed for the default rpi BT radio)
    • “gpio” (needed to acess the raw gpio)
    • “bluetooth” (needed to access bluetooth?)
    • “motion” (needed to access a shared folder on the same host where my motion server instance saves recordings)

No special privileges (setcap) or other where needed to run this on venv and I was able to manage gpio, discover through broadcast and lately connect to the rpi BT radio

Special mention on BT: I don’t have any BT paired devices so I don’t really know what is working or what not on this side: just, my HA sees the rPi BT own adapter and looks like ready to work

Migrating to a non-privileged docker container was a classical little-big fight with things you really don’t know.
In the end what worked was:

  • install the (awesome I’d say) boot script GitHub - tribut/homeassistant-docker-venv: Run Home Assistant as non-root using the official docker image
  • follow yours (and @tribut) instructions
  • skip the step about editing the docker/run script in order to ‘setcap’ the python executable (which is a little pain since you have to spread your settings in more places) and focus on the compose.yml configuration to pass in the needed informations through EXTRA_GID (this variable is not ‘officially’ documented but it is used by the script to build the secondary groups for your running user):
  • add the EXTRA_GID variable to the ‘environment’ section filling in the id(s) of your secondary groups needed to grant permissions to the HA user
  • voila’ HA in non-privileged container can access gpio, a shared host folder where I put my motion server recorders, bluetooth (as I said it just sees the rPi BT radio…no clue if it’s fully working since I don’t have BT devices paired so far)
  • granting privileges through groups is not all: see my sample compose.yml for a complete setup but, for BT it was an hard hunt surfing the web with a lot of different issues/tricks: really confusing. What really worked in the end was to mount the host dbus into the container in the ‘volumes’ section

This is my compose.yml

version: '3'
services:
  homeassistant:
    container_name: homeassistant
    image: "ghcr.io/home-assistant/home-assistant:stable"
    volumes:
      - /home/homeassistant/.homeassistant:/config
      - /etc/localtime:/etc/localtime:ro
      - /run/dbus:/run/dbus
      - /home/motion/data:/home/motion/data
      - /home/homeassistant/docker/run:/etc/services.d/home-assistant/run
    restart: unless-stopped
    network_mode: host
    environment:
      - PUID=999
      - PGID=995
      - EXTRA_GID=997 114 112 20
      - UMASK=007
      - PACKAGES=iputils
    devices:
      - /dev/mem:/dev/mem
      - /dev/gpiomem:/dev/gpiomem
    security_opt:
      - no-new-privileges
    # cap_add:
    #  - NET_RAW
    #  - NET_ADMIN
    #  - SYS_RAWIO

groups and user ids for your ‘homeassistant’ user are easily retrieved on the host with:

id homeassistant

\home\motion\data as previously said is the directory where my motion server saves recordings on my rPi. In my HA I can access these as media by ‘mounting’ the media source path into HA and granting the homeassistant user the correct ‘read/write’ permissions by adding the user to the ‘motion’ group which owns the recordings. I’ve reported this as an example to easily mount host folders into the container and grant the correct permissions (even though this is already widely documented)

Final thoughts:
the cap_add in compose.yml is commented right now in order to see if they’re needed or not and it looks like not (I guess they’re being made useless by setting the EXTRA_GIDs that carry the needed privileges). I’m not totally sure since to be honest I don’t know if these settings need to be ‘refreshed’ in the container by totally rebuilding it. I still havent since I’m a bit sick of these continuos fix and try with almost no clue so I’ve just left them commented out for future reference in case :wink:
Also, I’m not totally sure the /dev/gpiomem mapping under devices is really needed. I’ve put that during testing when I wasn’t able to access the gpio but it was no game-changer. The /dev/mem instead looks like being needed since python rpi gpio uses that for sure (it was complaining being not able to read from that device. I’m using GitHub - thecode/ha-rpi_gpio: Home Assistant Raspberry Pi GPIO Integration to manage my rPi’s gpio)

2 Likes

Awesome thanks for the detailed steps, I didn’t know about the EXTRA_GID

Thanks for this tip, I decided to rely on the linuxserver images to keep this simple. It is a lovely bonus that they provide easy access to the Hass version number so that I can have my update script add that to the logs. It was so easy to make the switch and the instructions from linuxserver are simple and clear.