Added homeassistant, more ldap, transmission fixed.

This commit is contained in:
Aner Zakobar
2026-09-27 09:12:57 +03:00
parent dd30a5935c
commit da020a8dfe
8 changed files with 254 additions and 9 deletions
+1
View File
@@ -88,6 +88,7 @@
./modules/monitoring.nix
eurovote.nixosModules.default
./modules/services/eurovote.nix
./modules/services/home-assistant.nix
] ++ extraModules;
};
+3
View File
@@ -116,6 +116,9 @@
# Eurovision voting app
homey.eurovote.enable = true;
# Home automation
homey.homeAssistant.enable = true;
# Monitoring stack
homey.uptimeKuma.enable = true;
homey.ntfy.enable = true;
+56
View File
@@ -0,0 +1,56 @@
import sys
import json
import os
from ldap3 import Server, Connection, SUBTREE
from ldap3.utils.conv import escape_filter_chars
username = os.environ.get("username", "").strip()
password = os.environ.get("password", "")
if not username or not password:
sys.exit(1)
LDAP_URL = "ldap://127.0.0.1"
LDAP_PORT = 389
BASE_DN = "ou=users,dc=zakobar,dc=com"
RO_DN = "cn=readonly,dc=zakobar,dc=com"
RO_PASS_FILE = "/run/secrets/ldap-ro-password"
try:
with open(RO_PASS_FILE) as f:
ro_password = f.read().strip()
if not ro_password:
sys.exit(1)
server = Server(LDAP_URL, port=LDAP_PORT)
# Step 1: search-bind as readonly to find user DN + display name
ro_conn = Connection(server, user=RO_DN, password=ro_password)
if not ro_conn.bind():
sys.exit(1)
ro_conn.search(
BASE_DN,
f"(uid={escape_filter_chars(username)})",
search_scope=SUBTREE,
attributes=["cn", "uid"],
)
if not ro_conn.entries:
sys.exit(1)
user_dn = ro_conn.entries[0].entry_dn
display_name = str(ro_conn.entries[0].cn)
ro_conn.unbind()
# Step 2: bind as user to verify password
user_conn = Connection(server, user=user_dn, password=password)
if not user_conn.bind():
sys.exit(1)
user_conn.unbind()
print(json.dumps({"name": display_name}))
sys.exit(0)
except Exception:
sys.exit(1)
+183
View File
@@ -0,0 +1,183 @@
{ 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;
}];
};
}
+4 -2
View File
@@ -56,6 +56,7 @@ in
"${dataDir}/media/tvshows:/downloads/tvshows"
"${dataDir}/media/general:/downloads/general"
"${dataDir}/media/complete:/downloads/complete"
"${dataDir}/media/incomplete:/downloads/incomplete"
];
extraOptions = [ "--network=homey" ];
@@ -87,8 +88,9 @@ in
# Storage directories
# -----------------------------------------------------------------------
homey.storage.extraDirs = [
{ path = "transmission"; }
{ path = "transmission/config"; }
{ path = "transmission"; user = "1000"; group = "1000"; mode = "0755"; }
{ path = "transmission/config"; user = "1000"; group = "1000"; mode = "0755"; }
{ path = "media/incomplete"; user = "1000"; group = "1000"; mode = "0755"; }
];
# -----------------------------------------------------------------------
+1 -1
View File
@@ -113,7 +113,7 @@ let
from uptime_kuma_api import UptimeKumaApi, MonitorType
api = UptimeKumaApi(KUMA_URL)
api = UptimeKumaApi(KUMA_URL, timeout=30)
# Initial admin setup via Socket.IO — idempotent (raises if already done, ignore it)
try:
+4 -4
View File
@@ -102,10 +102,10 @@ in
"d ${cfg.mountPoint} 0755 root root -"
# Shared media directories used by both Jellyfin and Transmission.
"d ${cfg.mountPoint}/media 0755 root root -"
"d ${cfg.mountPoint}/media/movies 0755 root root -"
"d ${cfg.mountPoint}/media/tvshows 0755 root root -"
"d ${cfg.mountPoint}/media/general 0755 root root -"
"d ${cfg.mountPoint}/media/complete 0755 root root -"
"d ${cfg.mountPoint}/media/movies 0755 1000 1000 -"
"d ${cfg.mountPoint}/media/tvshows 0755 1000 1000 -"
"d ${cfg.mountPoint}/media/general 0755 1000 1000 -"
"d ${cfg.mountPoint}/media/complete 0755 1000 1000 -"
"d ${cfg.mountPoint}/restic-cache 0700 root root -"
] ++ (map
(d: "d ${cfg.mountPoint}/${d.path} ${d.mode} ${d.user} ${d.group} -")
+2 -2
View File
@@ -54,8 +54,8 @@ sops:
b0RlaHcyNUJSUHRXRlhEUG1NWXBlb28KS5RrYXQL1Joc5I3IJGsa+mfWKtvtpVvc
w9uLPKkQDROeS8dBqWQf4YVqtIDHU81X+P5i0dVr7roDNzhN+3RLXQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-07-29T09:55:26Z"
mac: ENC[AES256_GCM,data:VgiX5YeFYFcanGzBRbnwJGcS1JcguoKU2MaBZOG/m9xI7lIO8BAJU9mwuGOWq2vRCq5lQIiWVlDZmHksyBntrcRlQGF4ByKZO5F76QlXJ4u6DynJ1mIl2CWt6+VIfJolaIoYf1TL3B1dDu9xThSDd3qakm0NU52NqiZoeUv9QNc=,iv:dsvJlzuoVH2xsx3AkIM7sy0GuXqvOe02/xbOVC5O/FU=,tag:YQEUsD4zDWFYBZGcuArKPg==,type:str]
lastmodified: "2026-09-15T19:40:07Z"
mac: ENC[AES256_GCM,data:BXt3np3LPBzMmGDPPPbIEeNGIckC1IWDDo/ha8e5wTyBHAw9o8RN2Q0amMo/tNpH3qeQbAy2y9/ZHNnzU4I/iG2mTI7go/tlR2g3tFioBcL0Fb/1EBOH6P1TMaVNIukuLzTs1AJ/66lrd7eOI+ftUIZOe6GAbjdEBSHU12zmqEg=,iv:TE9/uYu7CxGM4WOxMjMFQvCjDATjowifksEgiF67BdE=,tag:Taqp2p2muqkMEQ81RIeAig==,type:str]
pgp:
- created_at: "2026-07-29T09:53:25Z"
enc: |-