diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix index 0590f54ae60e..1985f1308811 100644 --- a/nixos/modules/services/misc/home-assistant.nix +++ b/nixos/modules/services/misc/home-assistant.nix @@ -245,22 +245,85 @@ in { rm -f "${cfg.configDir}/ui-lovelace.yaml" ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml" ''); - serviceConfig = { - ExecStart = "${package}/bin/hass --config '${cfg.configDir}'"; + serviceConfig = let + # List of capabilities to equip home-assistant with, depending on configured components + capabilities = [ + # Empty string first, so we will never accidentally have an empty capability bounding set + # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115 + "" + ] ++ (unique (optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [ + # Required for interaction with hci devices and bluetooth sockets + # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs + "CAP_NET_ADMIN" + "CAP_NET_RAW" + ] ++ lib.optionals (useComponent "emulated_hue") [ + # Alexa looks for the service on port 80 + # https://www.home-assistant.io/integrations/emulated_hue + "CAP_NET_BIND_SERVICE" + ] ++ lib.optionals (useComponent "nmap_tracker") [ + # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities + "CAP_NET_ADMIN" + "CAP_NET_BIND_SERVICE" + "CAP_NET_RAW" + ])); + in { + ExecStart = "${package}/bin/hass --runner --config '${cfg.configDir}'"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; User = "hass"; Group = "hass"; Restart = "on-failure"; + RestartForceExitStatus = "100"; + SuccessExitStatus = "100"; + KillSignal = "SIGINT"; + + # Hardening + AmbientCapabilities = capabilities; + CapabilityBoundingSet = capabilities; + DeviceAllow = [ + "char-ttyACM rw" + "char-ttyAMA rw" + "char-ttyUSB rw" + ]; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateTmp = true; + PrivateUsers = false; # prevents gaining capabilities in the host namespace + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; ProtectSystem = "strict"; + RemoveIPC = true; ReadWritePaths = let + # Allow rw access to explicitly configured paths cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ]; value = attrByPath cfgPath [] cfg; allowPaths = if isList value then value else singleton value; in [ "${cfg.configDir}" ] ++ allowPaths; - KillSignal = "SIGINT"; - PrivateTmp = true; - RemoveIPC = true; - AmbientCapabilities = "cap_net_raw,cap_net_admin+eip"; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ] ++ optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [ + "AF_BLUETOOTH" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SupplementaryGroups = [ "dialout" ]; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; }; path = [ "/run/wrappers" # needed for ping @@ -278,7 +341,6 @@ in { home = cfg.configDir; createHome = true; group = "hass"; - extraGroups = [ "dialout" ]; uid = config.ids.uids.hass; }; diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix index 3b7295324a18..c75dd248ecb3 100644 --- a/nixos/tests/home-assistant.nix +++ b/nixos/tests/home-assistant.nix @@ -1,4 +1,4 @@ -import ./make-test-python.nix ({ pkgs, ... }: +import ./make-test-python.nix ({ pkgs, lib, ... }: let configDir = "/var/lib/foobar"; @@ -6,9 +6,7 @@ let mqttPassword = "secret"; in { name = "home-assistant"; - meta = with pkgs.lib; { - maintainers = with maintainers; [ dotlambda ]; - }; + meta.maintainers = lib.teams.home-assistant.members; nodes.hass = { pkgs, ... }: { environment.systemPackages = with pkgs; [ mosquitto ]; @@ -47,6 +45,10 @@ in { payload_on = "let_there_be_light"; payload_off = "off"; }]; + emulated_hue = { + host_ip = "127.0.0.1"; + listen_port = 80; + }; logger = { default = "info"; logs."homeassistant.components.mqtt" = "debug"; @@ -82,6 +84,9 @@ in { hass.succeed( "mosquitto_pub -V mqttv5 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -m let_there_be_light" ) + with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"): + hass.wait_for_open_port(80) + hass.succeed("curl --fail http://localhost:80/description.xml") with subtest("Print log to ease debugging"): output_log = hass.succeed("cat ${configDir}/home-assistant.log") print("\n### home-assistant.log ###\n") @@ -93,5 +98,8 @@ in { # example line: 2020-06-20 10:01:32 DEBUG (MainThread) [homeassistant.components.mqtt] Received message on home-assistant/test: b'let_there_be_light' with subtest("Check we received the mosquitto message"): assert "let_there_be_light" in output_log + + with subtest("Check systemd unit hardening"): + hass.log(hass.succeed("systemd-analyze security home-assistant.service")) ''; })