Added homeassistant, more ldap, transmission fixed.
This commit is contained in:
@@ -88,6 +88,7 @@
|
||||
./modules/monitoring.nix
|
||||
eurovote.nixosModules.default
|
||||
./modules/services/eurovote.nix
|
||||
./modules/services/home-assistant.nix
|
||||
] ++ extraModules;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}];
|
||||
};
|
||||
}
|
||||
@@ -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"; }
|
||||
];
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -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
@@ -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} -")
|
||||
|
||||
@@ -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: |-
|
||||
|
||||
Reference in New Issue
Block a user