184 lines
6.5 KiB
Nix
184 lines
6.5 KiB
Nix
{ config, lib, pkgs, homeyConfig, ... }:
|
||
|
||
# Home Assistant — home automation hub.
|
||
#
|
||
# Auth model: command_line auth provider → OpenLDAP (primary).
|
||
# The auth script (ha-ldap-auth.py) does a search-bind as the readonly
|
||
# service account, then re-binds as the user to verify the password.
|
||
# A local homeassistant provider is kept as break-glass fallback.
|
||
# No Authelia forward_auth — HA handles its own sessions and tokens.
|
||
#
|
||
# Network: --network=host so mDNS/device-discovery works on the LAN.
|
||
# Caddy proxies https://home.<domain> → http://localhost:8123.
|
||
# LAN devices can also reach HA directly at 192.168.1.100:8123.
|
||
#
|
||
# Volume layout:
|
||
# <dataDir>/home-assistant/config/ → /config (configuration.yaml, automations…)
|
||
#
|
||
# Secrets consumed from sops:
|
||
# openldap/ro_password (shared with openldap/mealie modules)
|
||
|
||
let
|
||
cfg = config.homey.homeAssistant;
|
||
dataDir = config.homey.storage.mountPoint;
|
||
domain = homeyConfig.domain;
|
||
|
||
# ldap3 and pyasn1 are pure Python. Extract their .py source files from
|
||
# the PyPI tarballs so the HA container's Python (any version) can import
|
||
# them. The Nix-compiled site-packages approach fails because everything in
|
||
# there is a symlink into /nix/store, which doesn't exist inside the container.
|
||
ldap3Libs = pkgs.runCommand "ldap3-py-libs" {} ''
|
||
mkdir -p $out
|
||
cd $TMPDIR
|
||
|
||
mkdir ldap3-extract && cd ldap3-extract
|
||
tar -xf ${pkgs.python3Packages.ldap3.src} --strip-components=1
|
||
cp -r ldap3 $out/
|
||
cd $TMPDIR
|
||
|
||
mkdir pyasn1-extract && cd pyasn1-extract
|
||
tar -xf ${pkgs.python3Packages.pyasn1.src} --strip-components=1
|
||
cp -r pyasn1 $out/
|
||
'';
|
||
|
||
# Auth script: shebang uses the container's own Python (/usr/bin/env python3)
|
||
# and prepends the mounted ldap3 source tree to sys.path at runtime.
|
||
haLdapAuth = pkgs.writeTextFile {
|
||
name = "ha-ldap-auth";
|
||
executable = true;
|
||
text = ''
|
||
#!/usr/bin/env python3
|
||
import sys
|
||
sys.path.insert(0, "/auth/site-packages")
|
||
'' + builtins.readFile ./ha-ldap-auth.py;
|
||
};
|
||
|
||
# Placed in the Nix store; copied to /config only on first boot.
|
||
initialConfig = pkgs.writeText "ha-initial-configuration.yaml" ''
|
||
homeassistant:
|
||
auth_providers:
|
||
- type: command_line
|
||
command: /auth/ha-ldap-auth
|
||
args: []
|
||
meta: true
|
||
- type: homeassistant
|
||
|
||
http:
|
||
use_x_forwarded_for: true
|
||
trusted_proxies:
|
||
- 127.0.0.1
|
||
- "::1"
|
||
|
||
mobile_app:
|
||
'';
|
||
in
|
||
{
|
||
options.homey.homeAssistant = {
|
||
enable = lib.mkEnableOption "Home Assistant";
|
||
|
||
image = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "ghcr.io/home-assistant/home-assistant:stable";
|
||
};
|
||
|
||
port = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 8123;
|
||
description = "Port HA listens on (host network, so this is a real host port).";
|
||
};
|
||
};
|
||
|
||
config = lib.mkIf cfg.enable {
|
||
|
||
# openldap/ro_password is also declared by mealie.nix — NixOS merges them.
|
||
sops.secrets."openldap/ro_password" = { owner = "root"; };
|
||
|
||
# -------------------------------------------------------------------------
|
||
# Container
|
||
# -------------------------------------------------------------------------
|
||
virtualisation.oci-containers.containers.home-assistant = {
|
||
image = cfg.image;
|
||
environment = { TZ = homeyConfig.timezone; };
|
||
volumes = [
|
||
"${dataDir}/home-assistant/config:/config"
|
||
"${haLdapAuth}:/auth/ha-ldap-auth:ro"
|
||
"${ldap3Libs}:/auth/site-packages:ro"
|
||
"${config.sops.secrets."openldap/ro_password".path}:/run/secrets/ldap-ro-password:ro"
|
||
];
|
||
# host network: mDNS / Matter / direct LAN access all require this.
|
||
# No ports[] mapping needed — HA binds to 0.0.0.0:8123 on the host.
|
||
extraOptions = [
|
||
"--network=host"
|
||
"--cap-add=NET_ADMIN"
|
||
"--cap-add=NET_RAW"
|
||
];
|
||
};
|
||
|
||
# -------------------------------------------------------------------------
|
||
# Systemd service: ordering + first-boot config init
|
||
# -------------------------------------------------------------------------
|
||
systemd.services."podman-home-assistant" = {
|
||
after = lib.mkAfter [ "mnt-data.mount" ];
|
||
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||
serviceConfig.ExecStartPre = [
|
||
(pkgs.writeShellScript "ha-init-config" ''
|
||
CFG="${dataDir}/home-assistant/config/configuration.yaml"
|
||
if [ ! -f "$CFG" ]; then
|
||
mkdir -p "$(dirname "$CFG")"
|
||
install -m 644 ${initialConfig} "$CFG"
|
||
fi
|
||
'')
|
||
];
|
||
};
|
||
|
||
# Open 8123 in the NixOS firewall for direct LAN access.
|
||
# The HA mobile app connects here directly; Caddy also proxies via this port.
|
||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||
|
||
# -------------------------------------------------------------------------
|
||
# Caddy virtual host — no forward_auth; HA handles its own sessions
|
||
# -------------------------------------------------------------------------
|
||
homey.caddy.virtualHosts = [{
|
||
subdomain = "home";
|
||
port = cfg.port;
|
||
auth = false;
|
||
}];
|
||
|
||
# -------------------------------------------------------------------------
|
||
# Authelia: bypass entirely for home.domain
|
||
# Priority 18 — blanket bypass range (10–19)
|
||
# -------------------------------------------------------------------------
|
||
homey.authelia.accessControlRules = [{
|
||
priority = 18;
|
||
domain = [ "home.${domain}" ];
|
||
policy = "bypass";
|
||
}];
|
||
|
||
# -------------------------------------------------------------------------
|
||
# Storage directories
|
||
# -------------------------------------------------------------------------
|
||
homey.storage.extraDirs = [
|
||
{ path = "home-assistant"; }
|
||
{ path = "home-assistant/config"; mode = "0755"; }
|
||
];
|
||
|
||
# -------------------------------------------------------------------------
|
||
# Backup
|
||
# -------------------------------------------------------------------------
|
||
homey.backup.extraPaths = [ "${dataDir}/home-assistant/config" ];
|
||
# Uncomment to exclude the recorder SQLite DB if it grows too large:
|
||
# homey.backup.extraExcludePaths = [
|
||
# "${dataDir}/home-assistant/config/home-assistant_v2.db"
|
||
# ];
|
||
|
||
# -------------------------------------------------------------------------
|
||
# Uptime Kuma monitor
|
||
# -------------------------------------------------------------------------
|
||
homey.monitoring.monitors = [{
|
||
name = "Home Assistant";
|
||
url = "https://home.${domain}";
|
||
interval = 60;
|
||
}];
|
||
};
|
||
}
|