tribut / homeassistant-docker-venv

Run Home Assistant as non-root using the official docker image
123 stars 16 forks source link

Non-root user for HA under s6-overlay #6

Closed Chris-V closed 4 years ago

Chris-V commented 4 years ago

This totally got out of my head :sweat_smile:. I ran this script for almost 2 weeks now without any issues.

This actually replaces the Home Assistant run script with the whole user check, package install and venv management deals. Using S6 this would be preferable to add these things to /etc/cont-init.d/. The current solution is mostly for simplicity and easier end-user setups.

Since it replaces pretty much everything, maybe we could preserve old config formats in a "0.106 and older" directory and these in a "0.107 and newer"?

I notice that Home Assistant added Jemalloc support recently: https://github.com/home-assistant/core/blob/dev/rootfs/etc/services.d/home-assistant/run#L8. This is not included here as this is not yet in a final release. This will require backporting eventually :(

Fixes #5

fanaticDavid commented 4 years ago

Thank you so much for this script, Chris! Yesterday, I was finally able to update to 0.107.7 (from 0.106.6) using this (not updating to 0.108 just yet).

However, I had to add a little bit of code to make it work in my specific scenario: not only do I avoid running the HA container as root, I also don't connect it to the host network. That creates a problem when trying to use devices (such as Z-Wave and RFLink), but I always managed to work around that with the following snippet in my docker-compose file:

group_add:
  - dialout

When using this new script, that workaround no longer works and the container has no access to my devices. So now I add an environment variable GROUPADD to my docker-compose file:

environment:
  - PUID=${UID}
  - PGID=${GID}
  - GROUPADD=dialout
  - PACKAGES=iputils

And my modified run script looks like this:

#!/usr/bin/with-contenv bashio

USER="homeassistant"
PUID="${PUID:-1000}"
PGID="${PGID:-1000}"
GROUPADD="${GROUPADD:-}"

UMASK="${UMASK:-}"

PACKAGES="${PACKAGES:-}"

VENV_PATH="${VENV:-/var/tmp/homeassistant-venv}"
CONFIG_PATH=/config

#
# Creating user
#

# Some HA commands seem to fail if we don't have an actual user.
# ie: shell_command would return error code 255
bashio::log.info "Creating user $USER with $PUID:$PGID"

deluser "$USER" >/dev/null 2>&1 || true
delgroup "$USER" >/dev/null 2>&1 || true

addgroup -g "$PGID" "$USER"
adduser -G "$USER" -D -H -u "$PUID" "$USER"

if [ -n "${GROUPADD}" ]; then
  bashio::log.info "Adding extra groups: $GROUPADD"
  addgroup "$USER" "$GROUPADD"
fi

#
# Install extra packages
#

if [ -n "${PACKAGES}" ]; then
  bashio::log.info "Installing extra packages: $PACKAGES"
  apk add --quiet --no-progress --no-cache $PACKAGES
fi

#
# Create virtual environment
#

bashio::log.info "Initializing venv in $VENV_PATH"
su "$USER" \
  -c "python3 -m venv --system-site-packages '$VENV_PATH'"

#
# Fix permissions
#

cd "$CONFIG_PATH" || bashio::exit.nok "Can't find config folder: $CONFIG_PATH"
chown -R "$USER:$USER" .

if [ -n "${UMASK}" ]; then
  bashio::log.info "Setting umask: $UMASK"
  umask "$UMASK"
fi

#
# Run homeassistant
#
. 
bashio::log.info "Activating venv"
. "$VENV_PATH/bin/activate"

bashio::log.info "Starting homeassistant"
exec \
  s6-setuidgid "$USER" \
  python3 -m homeassistant --config "$CONFIG_PATH"

I don't know if what I did could've been done any better (as I'm not a developer), but it works for me. It probably wouldn't handle adding multiple groups this way, but I don't have any need for that right now. I'm also not sure if there would be any point in adding this to the PR as I'm aware it's a very specific use case, but I figured I'd share anyway.

Thanks again for your awesome work, Chris! 🙇

Nephiel commented 4 years ago

I can't get it to work, looks like it doesn't find python3:

[s6-init] making user provided files available at /var/run/s6/etc...exited 0.
[s6-init] ensuring user provided files have correct perms...exited 0.
[fix-attrs.d] applying ownership & permissions fixes...
[fix-attrs.d] done.
[cont-init.d] executing container initialization scripts...
[cont-init.d] udev.sh: executing... 
starting version 3.2.9
[19:14:43] INFO: Update udev information
[cont-init.d] udev.sh: exited 0.
[cont-init.d] done.
[services.d] starting services
Running as 0:0
Initializing venv in /var/tmp/venv
./run: line 16: python3: not found
[services.d] done.
[cont-finish.d] executing container finish scripts...
[cont-finish.d] done.
[s6-finish] waiting for services.
[s6-finish] sending all processes the TERM signal.
[s6-finish] sending all processes the KILL signal and exiting.
Nephiel commented 4 years ago

Nevermind, it turns out on my setup python3 binary is in /usr/local/bin/ but the PATH env var at that point is /usr/bin:/usr/sbin:/bin:/sbin, I had to edit run adding the full path to python3.

Chris-V commented 4 years ago

@fanaticDavid I also run HA in a non-host network but I added the HA group to my Z-Wave device with a udev rules.

Extra groups are a good idea though. Would be more flexible with group IDs though. I will see what I can do.

@Nephiel That's odd. What architecture are you on? Is that HA 0.108? I didn't the newest release yet.

Nephiel commented 4 years ago

@Chris-V I am using latest homeassistant-aarch64 stable (0.108) on a RPi 3B+ running Devuan. After adding the full path to python3 in the run script, it runs as a non-root user.

tsheeran1 commented 4 years ago

@Chris-V I greatly appreciate this work (also @fanaticDavid ). I'm having trouble getting it to work correctly.

(Note: Please accept my apologies, and advise if I should have addressed this in a ticket rather than a pull comment... I'm pretty new at this stuff)

I'm using docker-compose, and @fanaticDavid's version of run.

The container comes up, and when I check the logs I see the following:

2020-04-10 17:54:19 INFO (MainThread) [homeassistant.setup] Setting up zwave
2020-04-10 17:54:19 INFO (MainThread) [homeassistant.setup] Setup of domain zwave took 0.0 seconds.
2020-04-10 17:54:21 INFO (MainThread) [homeassistant.components.zwave] Z-Wave USB path is /dev/ttyACM0
2020-04-10 17:54:21 ERROR (MainThread) [homeassistant.config_entries] Error setting up entry Z-Wave (import from configuration.yaml) for zwave
Traceback (most recent call last):
   File "/usr/src/homeassistant/homeassistant/config_entries.py", line 216, in async_setup                                
      hass, self
   File "/usr/src/homeassistant/homeassistant/components/zwave/__init__.py", line 369, in async_setup_entry
      config_path=config.get(CONF_CONFIG_PATH),
   File "/usr/local/lib/python3.7/site-packages/openzwave/option.py", line 82, in __init__                                
      libopenzwave.PyOptions.__init__(self, config_path=config_path, user_path=user_path, cmd_line=cmd_line)
   File "src-lib/libopenzwave/libopenzwave.pyx", line 703, in libopenzwave.PyOptions.__init__
libopenzwave.LibZWaveException: "LibOpenZwave Generic Exception : Can't write in user directory /config" 

which appears to indicate homeassistant can't write to the /confg directory in the container.

I've tried a number of things with directory permissions including setting chmod 777 on the config directory. From the host file system I show the following (name of my host directory is ha_config):

homeassistant@rpi:~ $ ls -ld ha_config
drwxrwxrwx 1 homeassistant homeassistant 608 Apr 10 18:22 ha_config  

From inside the container:

bash-5.0# ls -ld config
drwxrwxrwx    1 homeassi homeassi       608 Apr 10 18:22 config 

bash-5.0# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
bash-5.0# su homeassistant
/config $ id
uid=1001(homeassistant) gid=1001(homeassistant) groups=20(dialout),1001(homeassistant),1001(homeassistant)
/config $ ps -a
PID   USER     TIME  COMMAND                                                                                             1 root      0:00 s6-svscan -t0 /var/run/s6/services
31 root      0:00 s6-supervise s6-fdholderd
178 root      0:00 udevd --daemon
193 root      0:00 s6-supervise home-assistant
196 homeassi  1:33 python3 -m homeassistant --config /config
387 root      0:00 /bin/bash
394 homeassi  0:00 ash
396 homeassi  0:00 ps -a   

which seems to show the permissions on config matching both host and container, the container running as root overall but the homeassistant process running as homeassistant:homeassistant

for completeness my docker-compose.yml file is:

version: '3'
services:
  homeassistant:
    container_name: ha107-0
    image: homeassistant/raspberrypi3-homeassistant:0.107.0
    ports:
      - 8123:8123
    restart: "no"
    network_mode: host
    volumes:
      - "/home/homeassistant/ha_config:/config:rw"
      - "/etc/letsencrypt:/cert:ro"
      - "/etc/localtime:/etc/localtime:ro"
      - "/home/homeassistant/ha_config/docker/run:/etc/services.d/home-assistant/run"
    devices:
      - /dev/ttyACM0:/dev/ttyACM0
    environment:
      - TZ=America/Detroit
      - PUID=${UID}
      - PGID=${GID}
      - GROUPADD=dialout
      - PACKAGES=iputils

and my run file is (added a few extra log outputs):


USER="homeassistant"
PUID="${PUID:-1000}"
PGID="${PGID:-1000}"
GROUPADD="${GROUPADD:-}"

UMASK="${UMASK:-}"

PACKAGES="${PACKAGES:-}"

VENV_PATH="${VENV:-/var/tmp/homeassistant-venv}"
CONFIG_PATH=/config

#
# Creating user
#

# Some HA commands seem to fail if we don't have an actual user.
# ie: shell_command would return error code 255
bashio::log.info "Creating user $USER with $PUID:$PGID"

deluser "$USER" >/dev/null 2>&1 || true
delgroup "$USER" >/dev/null 2>&1 || true

addgroup -g "$PGID" "$USER"
adduser -G "$USER" -D -H -u "$PUID" "$USER"

if [ -n "${GROUPADD}" ]; then
  bashio::log.info "Adding extra groups: $GROUPADD"
  addgroup "$USER" "$GROUPADD"
fi

bashio::log.info "Current user $(id)"

#
# Install extra packages
#

if [ -n "${PACKAGES}" ]; then
  bashio::log.info "Installing extra packages: $PACKAGES"
  apk add --quiet --no-progress --no-cache $PACKAGES
fi

#
# Create virtual environment
#

bashio::log.info "Initializing venv in $VENV_PATH"
su "$USER" \
  -c "python3 -m venv --system-site-packages '$VENV_PATH'"

#
# Fix permissions
#

bashio::log.info "changing $CONFIG_PATH owner to $USER:$USER"
cd "$CONFIG_PATH" || bashio::exit.nok "Can't find config folder: $CONFIG_PATH"
chown -R "$USER:$USER" .
bashio::log.info "$(ls -la)"

if [ -n "${UMASK}" ]; then
  bashio::log.info "Setting umask: $UMASK"
  umask "$UMASK"
fi

#
# Run homeassistant
#

bashio::log.info "Activating venv at $VENV_PATH"
. "$VENV_PATH/bin/activate"

bashio::log.info "Starting homeassistant as $USER:$USER"
exec \
  s6-setuidgid "$USER" \
  python3 -m homeassistant --config "$CONFIG_PATH"

Thanks again for all your work. I would not even have got this far without you. Any advice would be greatly appreciated...

Chris-V commented 4 years ago

I added the LD_PRELOAD variable from 0.108 and am not seeing any adverse effects so far.

@fanaticDavid I ported the code you sent me, but I changed it to supply GIDs instead of group names. This would allow you to create non built-in groups. Multiple extra GIDs are supported. It worked in my dev environment and I didn't test it in my live environment. Let me know what you think: -e EXTRA_GID=20

@Nephiel I added full paths to the exec line. Maybe that's a variation of the rpi3 image. Still that's odd.

Chris-V commented 4 years ago

@tsheeran1 chown is executed against /config to ensure HA will be able to write. Maybe dialout (20) is not working on your system to give permission to your usb stick?

I use these udev rules on my host system to give HA access to the stick (HUSBZB-1) without fiddling with extra groups and such. This has the added benefit of creating a /dev/zwave device that is easier to remember.

/etc/udev/rules.d/99-hubz.rules

SUBSYSTEM=="tty", ATTRS{interface}=="HubZ Z-Wave Com Port", SYMLINK+="zwave", GROUP="homeassistant", MODE="0660"
SUBSYSTEM=="tty", ATTRS{interface}=="HubZ ZigBee Com Port", SYMLINK+="zigbee", GROUP="homeassistant", MODE="0660"
johnluetke commented 4 years ago

@Chris-V I got an error on container start up for not-specifying EXTRA_GID in my docker compose. the error both occurred when the EXTRA_GID was omitted, and also when the variable was set to empty EXTRA_GID=.

I was able to get around it by specifying the same value as in PGID

Examples:

Start up error:

environment:
      - PUID=5003
      - PGID=5001
      - UMASK=007

Start up error

environment:
      - PUID=5003
      - PGID=5001
      - UMASK=007
      - EXTRA_GID=

Works:

environment:
      - PUID=5003
      - PGID=5001
      - UMASK=007
      - EXTRA_GID=5001
Chris-V commented 4 years ago

@johnluetke Fixed now. Sorry about that :)

johnluetke commented 4 years ago

No worries, seems i did the pull right as you first added it :-)

fanaticDavid commented 4 years ago

@Chris-V Thank you so much for integrating extra GIDs in your script, and especially for improving upon it. I just tested the latest version of the script with HA 0.107.7 and everything seems to have started up just fine 👍

FuzzyMistborn commented 4 years ago

@tsheeran1 chown is executed against /config to ensure HA will be able to write. Maybe dialout (20) is not working on your system to give permission to your usb stick?

I use these udev rules on my host system to give HA access to the stick (HUSBZB-1) without fiddling with extra groups and such. This has the added benefit of creating a /dev/zwave device that is easier to remember.

/etc/udev/rules.d/99-hubz.rules

SUBSYSTEM=="tty", ATTRS{interface}=="HubZ Z-Wave Com Port", SYMLINK+="zwave", GROUP="homeassistant", MODE="0660"
SUBSYSTEM=="tty", ATTRS{interface}=="HubZ ZigBee Com Port", SYMLINK+="zigbee", GROUP="homeassistant", MODE="0660"

I'm trying to do this in my udev rule for WyzeSense and it's not working. No matter what I put as group/owner on reboot /dev/wyzesense is owned by root:root. Any ideas?

Here's my 99-wyze.rules text: SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="e024", SYMLINK+="wyzesense", OWNER="fuzzy", GROUP="fuzzy", MODE="0660"

tsheeran1 commented 4 years ago

I'm wondering if the underlying issues have been fixed. I'm now able to run from homeassistant/raspberrypi3-homeassistant:109.3 with no mods and a very minimal docker-compose.yml:

version: '3'
services:
  homeassistant:
    container_name: ha109-3
    image: homeassistant/raspberrypi3-homeassistant:0.109.3
    restart: "no"
    network_mode: host
    volumes:
      - "/home/homeassistant/config2:/config:rw"
      - "/home/homeassistant/dehydrated/certs/<redacted>.duckdns.org:/certs:ro"
    devices:
      - /dev/ttyACM0:/dev/ttyACM0
    environment:
      - TZ=America/Detroit

To get to this point, I wound up doing a clean install of raspbian buster (along with the required packages, eg. docker-ce, etc). Then tried a clean (unconfigured) run of the :stable image, which worked, and then copied all my old config files to the new /config directory.

Again, this may be old news and I have no idea why this approach worked. I suppose I could go back to my legacy rpi config and try the new docker-compose, but its working and I'm not that curious.

Chris-V commented 4 years ago

@tsheeran1 What underlying issues? Home-Assistant's image is still running as root. This is what we are "fixing" here.

Your example is running as root.

slamp commented 4 years ago

I also test the new file 'run' with home assistant 0.110.4. It is working correctly using docker-compose on a raspberry pi 4 running on Arch linux. Can we merge this PR ?

tribut commented 4 years ago

Thanks so much @Chris-V for taking the lead on this! Excellent work!

Sorry it took me so long to try this out myself, the last few weeks have been challenging as I assume they have been for many of you, too. Hope everyone is healthy and well :heart: