diff --git a/CLAUDE.md b/CLAUDE.md index a51e148..2986576 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -367,6 +367,7 @@ Observed and standardized across servers: | esh-pve-nas | 10.0.50.55 | ESH home lab | Proxmox VE hypervisor (storage / media) | `servers/esh-pve-nas/README.md` | | esh-vm-db | 10.0.50.60 | ESH home lab | DB VM — PostgreSQL (paperless-ng) + MongoDB; bare-metal VM, no Docker | `servers/esh-vm-db/README.md` | | esh-ml1 | 10.0.50.80 | ESH home lab | GPU LXC (CT 110 on esh-pve, RTX 2000E Ada 16 GB, host-side NVIDIA driver) — **the fleet's embedding + reranking service** (TEI), sole gateway backend since 2026-09-25 | `servers/esh-ml1/README.md` | +| esh-matter | 10.0.90.20 | ESH home lab (VLAN 90, esh-iot) | LXC (CT 111 on esh-pve), VLAN 90 only — Matter server (matter.js) for Home Assistant; :5580 firewalled to HA; in vzdump (fabric credentials) | `servers/esh-matter/README.md` | | nh3-ml1 | 10.100.50.80 | NH3 | GPU LXC (CT 109 on nh3-pve, RTX 2000E Ada 16 GB, host-side NVIDIA driver) — second embed/rerank (TEI) backend, twin of esh-ml1; not yet behind the gateway (2026-09-25) | `servers/nh3-ml1/README.md` | **Placement rules:** diff --git a/dns/internal.yaml b/dns/internal.yaml index f3fe32c..1ee5e11 100644 --- a/dns/internal.yaml +++ b/dns/internal.yaml @@ -101,6 +101,7 @@ hosts: - {name: esh-pve-nas, site: esh, v4: 10.0.50.55, note: Proxmox hypervisor — storage/media} - {name: esh-vm-db, site: esh, v4: 10.0.50.60, note: PostgreSQL + MongoDB} - {name: esh-ml1, site: esh, v4: 10.0.50.80, note: CT110 on esh-pve — RTX 2000E Ada, embedding + reranking (TEI) + reward seat} + - {name: esh-matter, site: esh, v4: 10.0.90.20, note: CT111 on esh-pve — VLAN 90 (esh-iot) only; Matter server (matter.js) for HA; :5580 open to HA 10.0.50.46 only} - {name: vm-esh-nas, site: esh, v4: 10.0.50.154, note: NAS-adjacent docker host} - {name: esh-filebot, site: esh, v4: 10.0.50.70, note: restic / file-sync VM} - {name: esh-gw, site: esh, v4: 10.0.250.1, note: esh-gw} diff --git a/persistent-memory.md b/persistent-memory.md index c6ca929..36721ff 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -218,6 +218,7 @@ in `servers/pfi-gx10/README.md`. → `persistent-memory.d/2026-09-24-gx10-ac-res ## Recent decisions +- `[2026-09-26]` **esh-matter LIVE: a Matter server (matter.js 1.4.0) on CT 111 @ 10.0.90.20, VLAN 90 only**, for ha-dev (operator-approved, relayed). It is on the IoT link because Matter/Thread IPv6 (Echo ULA + RIO routes) is link-only. Kernel RA with rt_info_max_plen=64, forwarding off; Docker ip-forward/iptables off. The in-CT nftables admits :5580 from HA 10.0.50.46 only and SSH from mgmt ranges only, and accepts everything else so Matter UDP never meets conntrack. The CT is in esh-pve vzdump (fabric creds). Acceptance all green: fdad:: SLAAC; ping6 thermostat; 2 Thread routes learned (Aqara + Echo); ws OK from inside the HA container; refused from 10.0.50.45, nh3-dev and a temp VLAN 90 netns. → `servers/esh-matter/README.md` - `[2026-09-26]` **Embed/rerank LOAD-SHARED across esh-ml1 + nh3-ml1 (Prime).** Second deployments were added for qwen3-embedding and reranker (config) and for reranker-a3-bge-v2-m3 (DB, `/model/new`). Without failover, a dead backend failed 7/40 embeds; `router_settings.enable_weighted_failover: true` (affects only multi-deployment groups) brought it to rerank 80/80 and embed 38/40 at onset, 60/60 sustained. In LiteLLM 1.97 the embedding path does not stamp the failed deployment, so it relies on cooldown. LiteLLM restarted 3× tonight, ~36 s each. - `[2026-09-26]` **Two dataset-foundry utility seats LIVE on nh3-ml1 (brokkr; operator approval relayed):** LFM2.5-VL-3B on llama.cpp `:8030` (gateway `lfm25-vl-3b`, LiteLLM restarted 36 s at 0039) and VibeVoice-ASR-Streaming-1.5B on **audio.cpp** `:8031`, not vibevoice.cpp as specced; the GGUF card names audio.cpp. Controls: VL read a synthetic image exactly, but hallucinates when no image is sent; ASR WER 3/69 on the bundled LibriSpeech clips. libsoxr added (linear resampling misheard a word). First ASR request ~31 s cold. Answered brokkr: the old 27B image seat (10.250.50.54:8015) was retired 09-14; baseline = gateway `image-judge` (Flash-Next). - `[2026-09-26]` **Coder seat STAYS on fv-ml1 (Prime).** The nh3-ml1 copy gave the same quality (teacher-forced true-code logprob diff +0.008 ± 0.019) but ran ~5× slower (64-tok FIM ~1.0 s vs 0.2 s; 63 vs 338 tok/s), and freeing 6.3 GB on fv-ml1 GPU 1 (20 GB spare) bought little. Copy removed; recipe kept in `stacks/coder-seat/`. The RTX 2000E suits embed/rerank/classify, not latency-sensitive generation. Only coder was a candidate: parakeet stays (earlier ruling), voices is a generation seat. diff --git a/playbooks/esh-matter-lxc.yaml b/playbooks/esh-matter-lxc.yaml new file mode 100644 index 0000000..80ad605 --- /dev/null +++ b/playbooks/esh-matter-lxc.yaml @@ -0,0 +1,204 @@ +# esh-matter — a small LXC on esh-pve whose ONLY network leg is VLAN 90 (esh-iot), +# running the Matter server (matter.js, stacks/matter-server) for Home Assistant. +# Requested by ha-dev, operator-approved 2026-09-26. +# +# WHY IT SITS ON VLAN 90: Matter's operational traffic is IPv6, and VLAN 90's only +# IPv6 is the Thread ULA fdad:29e:d492:fd87::/64. It is advertised by an Echo +# border router, not the UDM, and is visible only on that link. The Thread routes +# (RA route-information options from the border routers) are likewise link-only. +# HA core reaches the server over an IPv4 websocket (routed VLAN 50 → 90; the UDM +# policy InternalToIOT already allows it), so HA itself does not change. +# +# WHY AN LXC, NOT A MACVLAN ON esh-docker-vm: it keeps the untrusted IoT leg off the +# host that runs HA and everything else (a VM with a history of wedges). It gets +# its own firewall and its own vzdump backup. +# +# IPv6: kernel RA processing (no NetworkManager/networkd; the Debian 12 template +# uses ifupdown). accept_ra=1 plus accept_ra_rt_info_max_plen=64 learns Thread +# routes. IPv6 forwarding stays OFF (matter.js os_requirements: forwarding +# disables RFC 4191 reachability probing). Docker is told not to touch forwarding +# or iptables; the server runs with host networking, so it needs neither. +# +# FIREWALL (in-CT nftables): 5580 (the websocket and dashboard, UNAUTHENTICATED) is +# accepted only from HA at {{ ha_ip }}, then dropped for everyone else, v4 and v6. +# SSH is accepted only from non-IoT management ranges. Everything else, Matter +# UDP 5540 and mDNS 5353 included, is left alone on purpose. That keeps conntrack +# out of the Matter path, so sleepy-device reports are not dropped by a 120 s UDP +# timeout (matter.js "Stateful firewalls" note). +# +# BACKUP: the CT is added to esh-pve's vzdump job (PBS-ANA, mirrored to PBS-NH3). +# The data dir holds the Matter fabric root credentials; losing it means +# re-commissioning every device. +# +# Run: scripts/elway root@esh-pve --playbook playbooks/esh-matter-lxc.yaml +# Then: scripts/deploy-stack.sh esh-matter matter-server (+ docker compose up -d) + +vars: + ctid: 111 + hostname: esh-matter + ip_cidr: 10.0.90.20/24 + ip_addr: 10.0.90.20 + gateway: 10.0.90.1 + vlan: 90 + rootfs_storage: local-lvm + rootfs_gb: 8 + cores: 2 + memory_mb: 1024 + swap_mb: 512 + startup_order: 30 + template: debian-12-standard_12.12-1_amd64.tar.zst + ha_ip: 10.0.50.46 + # Non-IoT sources allowed to SSH: esh-server, esh-userland, esh-mgmt, NH3, mesh. + ssh_sources: "10.0.50.0/24, 10.0.10.0/24, 10.0.250.0/24, 10.100.0.0/16, 100.64.0.0/10" + infra_ops_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN+1HBwfXrkfTYWdcnWCjLJ6VLAGC87gxH5h5vKaaA3c infra-ops@pfi-fleet" + +steps: + - name: Create CT {{ ctid }} ({{ hostname }}) with its only leg on VLAN {{ vlan }}, IPv6 SLAAC + shell: | + pct create {{ ctid }} local:vztmpl/{{ template }} \ + --hostname {{ hostname }} --unprivileged 1 --features nesting=1,keyctl=1 \ + --cores {{ cores }} --memory {{ memory_mb }} --swap {{ swap_mb }} \ + --rootfs {{ rootfs_storage }}:{{ rootfs_gb }} \ + --net0 name=eth0,bridge=vmbr0,firewall=0,gw={{ gateway }},ip={{ ip_cidr }},ip6=auto,tag={{ vlan }},type=veth \ + --nameserver {{ gateway }} \ + --onboot 1 --startup order={{ startup_order }},up=10 \ + --description "{{ hostname }} — Matter server (matter.js) on VLAN {{ vlan }} for Home Assistant. Built by eshpfi playbooks/esh-matter-lxc.yaml; stack stacks/matter-server. IN vzdump (fabric credentials)." + when: "! pct status {{ ctid }} >/dev/null 2>&1" + + # esh-pve's job names its vmids explicitly. Append ours; leave the rest alone. + - name: Add CT {{ ctid }} to esh-pve's vzdump job + shell: | + set -e + CT={{ ctid }} JOBS="$(pvesh get /cluster/backup --output-format json)" python3 - <<'PY' + import json, os, subprocess + ct = os.environ["CT"] + for j in json.loads(os.environ["JOBS"]): + ids = [x for x in str(j.get("vmid", "")).split(",") if x] + if not ids or ct in ids: + continue + subprocess.run(["pvesh", "set", "/cluster/backup/" + j["id"], "--vmid", ",".join(ids + [ct])], check=True) + print("added", ct, "to", j["id"]) + PY + when: "! grep -E '^\\s+vmid .*\\b{{ ctid }}\\b' /etc/pve/jobs.cfg" + + - name: Start the container + shell: pct start {{ ctid }} && sleep 6 + when: "! pct status {{ ctid }} | grep -q running" + + - name: IPv6 RA processing with Thread route-information options, forwarding off + shell: | + pct exec {{ ctid }} -- bash -s <<'EOF' + set -euo pipefail + cat > /etc/sysctl.d/60-matter-ipv6.conf <<'EOC' + # Matter server — see eshpfi playbooks/esh-matter-lxc.yaml + net.ipv6.conf.all.forwarding = 0 + net.ipv6.conf.eth0.accept_ra = 1 + net.ipv6.conf.eth0.accept_ra_rt_info_max_plen = 64 + EOC + sysctl -q -p /etc/sysctl.d/60-matter-ipv6.conf + EOF + when: "! pct exec {{ ctid }} -- sh -c 'test $(cat /proc/sys/net/ipv6/conf/eth0/accept_ra_rt_info_max_plen) = 64'" + + - name: Base packages + bookworm point upgrade (+ nftables) + shell: | + pct exec {{ ctid }} -- bash -s <<'EOF' + set -euo pipefail + export DEBIAN_FRONTEND=noninteractive + for i in $(seq 1 30); do getent hosts deb.debian.org >/dev/null && break; sleep 1; done + apt-get update -qq + apt-get -y -qq full-upgrade + apt-get install -y -qq --no-install-recommends ca-certificates curl gnupg sudo jq less rsync locales nftables iputils-ping iproute2 + sed -i 's/^# *en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen >/dev/null + EOF + when: "! pct exec {{ ctid }} -- sh -c 'command -v nft && command -v rsync && command -v jq && locale -a | grep -qi en_US.utf8' >/dev/null 2>&1" + + - name: Fleet identities (docker 851, infra-ops 850, vh 1000) + /opt/docker tree + shell: | + pct exec {{ ctid }} -- bash -s <<'EOF' + set -euo pipefail + getent group docker >/dev/null || groupadd -g 851 docker + getent group infra-ops >/dev/null || groupadd -g 850 infra-ops + id infra-ops >/dev/null 2>&1 || useradd -u 850 -g 850 -G docker -m -s /bin/bash infra-ops + chmod 0700 /home/infra-ops + install -d -m 0700 -o infra-ops -g infra-ops /home/infra-ops/.ssh + echo '{{ infra_ops_pubkey }}' > /home/infra-ops/.ssh/authorized_keys + chown infra-ops:infra-ops /home/infra-ops/.ssh/authorized_keys + chmod 0600 /home/infra-ops/.ssh/authorized_keys + echo 'infra-ops ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/infra-ops + chmod 0440 /etc/sudoers.d/infra-ops + id vh >/dev/null 2>&1 || useradd -u 1000 -U -G docker,sudo -m -s /bin/bash vh + chmod 0700 /home/vh + install -d -m 2775 -o root -g docker /opt/docker /opt/docker/compose /opt/docker/conf + EOF + when: "! pct exec {{ ctid }} -- sh -c 'test \"$(id -u infra-ops)\" = 850 && test \"$(getent group docker | cut -d: -f3)\" = 851 && test -d /opt/docker/compose'" + + # Applied BEFORE Docker, so it is already in force when dockerd first starts. + - name: Firewall — 5580 from HA only, SSH from management ranges only + shell: | + pct exec {{ ctid }} -- bash -s <<'EOF' + set -euo pipefail + cat > /etc/nftables.conf <<'EOC' + #!/usr/sbin/nft -f + # esh-matter guard — eshpfi playbooks/esh-matter-lxc.yaml. Policy ACCEPT on + # purpose: only the two ports below are filtered, so Matter UDP and mDNS never + # touch conntrack-based rules. + flush ruleset + table inet matter_guard { + chain input { + type filter hook input priority 0; policy accept; + iif "lo" accept + tcp dport 5580 ip saddr {{ ha_ip }} accept + tcp dport 5580 counter drop + tcp dport 22 ip saddr { {{ ssh_sources }} } accept + tcp dport 22 counter drop + } + } + EOC + systemctl enable -q nftables + systemctl restart nftables + EOF + when: "! pct exec {{ ctid }} -- sh -c 'nft list table inet matter_guard 2>/dev/null | grep -q \"5580 ip saddr {{ ha_ip }} accept\"'" + + - name: docker-ce, hands off forwarding and iptables (host networking only) + shell: | + pct exec {{ ctid }} -- bash -s <<'EOF' + set -euo pipefail + export DEBIAN_FRONTEND=noninteractive + install -d /etc/docker + printf '{\n "ip-forward": false,\n "iptables": false,\n "ip6tables": false\n}\n' > /etc/docker/daemon.json + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" \ + > /etc/apt/sources.list.d/docker.list + apt-get update -qq + apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin + EOF + when: "! pct exec {{ ctid }} -- sh -c 'command -v docker' >/dev/null 2>&1" + + - name: Matter data dir (fabric credentials) owned by the container's uid 1000 + shell: pct exec {{ ctid }} -- install -d -m 0750 -o 1000 -g 1000 /opt/docker/data /opt/docker/data/matter-server + when: "! pct exec {{ ctid }} -- test -d /opt/docker/data/matter-server" + +verify: + - name: Running, onboot, in the vzdump job + shell: "pct status {{ ctid }} | grep -q running && pct config {{ ctid }} | grep -q '^onboot: 1' && grep -Eq '^\\s+vmid .*\\b{{ ctid }}\\b' /etc/pve/jobs.cfg" + changed_when: "false" + + - name: IPv6 — RA route-info on, forwarding off, a SLAAC address on eth0 + shell: | + pct exec {{ ctid }} -- sh -c 'test $(cat /proc/sys/net/ipv6/conf/eth0/accept_ra_rt_info_max_plen) = 64 && test $(cat /proc/sys/net/ipv6/conf/all/forwarding) = 0 && ip -6 addr show dev eth0 scope global | grep -q inet6' + changed_when: "false" + + - name: Firewall loaded and persistent + shell: pct exec {{ ctid }} -- sh -c 'nft list table inet matter_guard | grep -Eq "dport 5580 counter .*drop" && systemctl is-enabled --quiet nftables' + changed_when: "false" + + - name: Docker left forwarding alone + shell: pct exec {{ ctid }} -- sh -c 'docker info >/dev/null && test $(cat /proc/sys/net/ipv6/conf/all/forwarding) = 0' + changed_when: "false" + + - name: Fleet identities are the pinned ids + shell: | + pct exec {{ ctid }} -- sh -c 'test "$(id -u infra-ops)" = 850 && test "$(id -u vh)" = 1000 && test "$(getent group docker | cut -d: -f3)" = 851' + changed_when: "false" diff --git a/servers/esh-matter/README.md b/servers/esh-matter/README.md new file mode 100644 index 0000000..5dfdd74 --- /dev/null +++ b/servers/esh-matter/README.md @@ -0,0 +1,62 @@ +# esh-matter + +LXC **CT 111 on esh-pve** whose **only network leg is VLAN 90 (esh-iot)**. It runs +the **Matter server** (`stacks/matter-server`, matter.js 1.4.0) for Home Assistant. +Built 2026-09-26 for ha-dev (operator-approved) by +[`playbooks/esh-matter-lxc.yaml`](../../playbooks/esh-matter-lxc.yaml). + +| | | +|---|---| +| **IPv4** | `10.0.90.20/24`, gateway `10.0.90.1`. Static, below the UDM's `.40–.250` DHCP pool | +| **IPv6** | SLAAC from the Echo's RA: `fdad:29e:d492:fd87:be24:11ff:fe3d:c6c7` (Thread ULA) + link-local. No global prefix exists on VLAN 90 | +| **DNS** | `esh-matter.esh.internal` (HA uses the IP: its container can't resolve `.internal`) | +| **SSH** | `ssh esh-matter` → `infra-ops@10.0.90.20`, or `pct enter 111` on esh-pve | +| **OS** | Debian 12, unprivileged, `nesting=1,keyctl=1` (Docker), 2 cores / 1 GB / 8 GB | +| **Backup** | **in esh-pve's vzdump job** (PBS-ANA 03:30, synced to PBS-NH3). `/opt/docker/data/matter-server` holds the Matter **fabric root credentials**; losing it means re-commissioning every device | + +## Why it is built this way + +- **It must be on the devices' link.** Matter's operational traffic is IPv6, and + VLAN 90's only IPv6 is the Thread ULA advertised by an Echo border router. + Thread routes arrive as RA route-information options, which are link-only. HA + (VLAN 50) reaches the server over IPv4 `ws://10.0.90.20:5580/ws`, routed by the + UDM (policy `InternalToIOT`), so the HA container did not change. +- **Kernel RA processing, forwarding off.** Set in `/etc/sysctl.d/60-matter-ipv6.conf`: + `accept_ra=1`, `accept_ra_rt_info_max_plen=64`, `all.forwarding=0`. With + forwarding on, RFC 4191 reachability probing stops (matter.js OS requirements). + Docker is configured with `ip-forward/iptables/ip6tables: false`; the server + uses host networking. +- **Firewall (in-CT nftables, `/etc/nftables.conf`):** + - TCP **5580** (websocket + dashboard, **unauthenticated**) is accepted only + from **HA at 10.0.50.46**, then dropped. + - SSH is accepted only from 10.0.50/24, 10.0.10/24, 10.0.250/24, 10.100/16 and + 100.64/10. + - Everything else is accepted on purpose, so Matter UDP and mDNS never meet + conntrack. The matter.js docs warn that a stateful firewall's 120 s UDP + timeout drops sleepy-device reports. + - The PVE firewall is disabled on esh-pve, so this in-CT table is the control. +- **An LXC, not a macvlan on esh-docker-vm:** the untrusted IoT leg stays off the + VM that runs HA and everything else. + +## Acceptance (2026-09-26 ~1305) + +- `ip -6 addr`: link-local + `fdad:29e:d492:fd87:…` ✓. `ping -6` to the Aqara + thermostat `fdad:29e:d492:fd87:56ef:44ff:fea1:7a36`: 3/3 ✓. +- `ip -6 route` learned **two Thread routes by RA**: + - `fd0e:3007:2ebd::/64 via fe80::56ef:44ff:fea1:7a36` (the Aqara W200, Apple + Thread network); + - `fdb6:ee32:f4c2:1::/64 via fe80::5a9a:3eff:fe49:6d36` (the Echo). +- `ws://10.0.90.20:5580/ws` from **inside the homeassistant container**: answered + `server_info` (schema 13, `matter-server/1.4.0 (matter.js/0.17.9)`, fabric 1) ✓. +- **Negative controls:** 5580 is refused from the esh-docker-vm host (10.0.50.45), + from nh3-dev, and from a **VLAN 90 vantage** (a temporary netns on esh-pve at + 10.0.90.39, which can ping the CT). SSH is also refused from that VLAN 90 + vantage. The drop counters incremented. + +## Not yet wired + +- **Monitoring.** Kuma can't probe 5580 (only HA may reach it), and there is no + Beszel agent yet. HA's Matter integration surfaces an outage. +- **UniFi mDNS reflector is ON for esh-iot** (`mdns_enabled`). matter.js warns + mDNS forwarders can hinder Matter. It is left alone because other devices may + rely on it; revisit only if commissioning or discovery misbehaves. diff --git a/servers/esh-matter/ssh-target b/servers/esh-matter/ssh-target new file mode 100644 index 0000000..c354a65 --- /dev/null +++ b/servers/esh-matter/ssh-target @@ -0,0 +1 @@ +infra-ops@10.0.90.20 diff --git a/servers/esh-matter/system-details.txt b/servers/esh-matter/system-details.txt new file mode 100644 index 0000000..d62bbc0 --- /dev/null +++ b/servers/esh-matter/system-details.txt @@ -0,0 +1,160 @@ + +===== HOST ===== + +Hostname: esh-matter.esteban.net +Date: 2026-09-26T20:06:32+00:00 +Uptime: up 4 minutes +OS: Debian GNU/Linux 12 (bookworm) +Kernel: 6.8.12-42-pve +Arch: x86_64 + +===== HARDWARE ===== + +CPU cores: 2 +CPU model: 13th Gen Intel(R) Core(TM) i9-13900H +MemTotal: 1.0 GB +MemAvailable: 0.7 GB + +===== GPUS ===== + +nvidia-smi not present (no NVIDIA GPUs or driver not installed) + +===== FILESYSTEMS (df) ===== + +Filesystem Size Used Avail Use% Mounted on +/dev/mapper/pve-vm--111--disk--0 7.8G 2.0G 5.5G 27% / + +===== PERSISTENT MOUNTS (/etc/fstab, non-comment) ===== + + +===== TARGETED DATA PATHS ===== + +/opt (total: 40K) + total 16 + drwxr-xr-x 4 root root 4096 2026-09-26 20:03 . + drwxr-xr-x 18 root root 4096 2026-09-26 20:02 .. + drwx--x--x 4 root root 4096 2026-09-26 20:03 containerd + drwxrwsr-x 5 root docker 4096 2026-09-26 20:03 docker + +/opt/docker (total: 32K) + total 20 + drwxrwsr-x 5 root docker 4096 2026-09-26 20:03 . + drwxr-xr-x 4 root root 4096 2026-09-26 20:03 .. + drwxrwsr-x 3 root docker 4096 2026-09-26 20:04 compose + drwxrwsr-x 2 root docker 4096 2026-09-26 20:03 conf + drwxr-s--- 3 vh vh 4096 2026-09-26 20:03 data + +/opt/docker/compose (total: 20K) + total 12 + drwxrwsr-x 3 root docker 4096 2026-09-26 20:04 . + drwxrwsr-x 5 root docker 4096 2026-09-26 20:03 .. + drwxr-sr-x 2 infra-ops docker 4096 2026-09-26 20:04 matter-server + +/opt/docker/conf (total: 4.0K) + total 8 + drwxrwsr-x 2 root docker 4096 2026-09-26 20:03 . + drwxrwsr-x 5 root docker 4096 2026-09-26 20:03 .. + +/var/lib/docker (total: 4.0K) + +/srv (total: 4.0K) + total 8 + drwxr-xr-x 2 root root 4096 2025-09-07 15:14 . + drwxr-xr-x 18 root root 4096 2026-09-26 20:02 .. + + +===== DOCKER ===== + +Server: 29.8.1 Client: 29.8.1 + +----- docker info ----- +Containers: 1 (running 1, paused 0, stopped 0) +Images: 1 +Runtimes: map[io.containerd.runc.v2:{{runc [] map[]} map[org.opencontainers.runtime-spec.features:{"ociVersionMin":"1.0.0","ociVersionMax":"1.3.0","hooks":["prestart","createRuntime","createContainer","startContainer","poststart","poststop"],"mountOptions":["async","atime","bind","defaults","dev","diratime","dirsync","exec","iversion","lazytime","loud","mand","noatime","nodev","nodiratime","noexec","noiversion","nolazytime","nomand","norelatime","nostrictatime","nosuid","nosymfollow","private","ratime","rbind","rdev","rdiratime","relatime","remount","rexec","rnoatime","rnodev","rnodiratime","rnoexec","rnorelatime","rnostrictatime","rnosuid","rnosymfollow","ro","rprivate","rrelatime","rro","rrw","rshared","rslave","rstrictatime","rsuid","rsymfollow","runbindable","rw","shared","silent","slave","strictatime","suid","symfollow","sync","tmpcopyup","unbindable"],"linux":{"namespaces":["cgroup","ipc","mount","network","pid","time","user","uts"],"capabilities":["CAP_CHOWN","CAP_DAC_OVERRIDE","CAP_DAC_READ_SEARCH","CAP_FOWNER","CAP_FSETID","CAP_KILL","CAP_SETGID","CAP_SETUID","CAP_SETPCAP","CAP_LINUX_IMMUTABLE","CAP_NET_BIND_SERVICE","CAP_NET_BROADCAST","CAP_NET_ADMIN","CAP_NET_RAW","CAP_IPC_LOCK","CAP_IPC_OWNER","CAP_SYS_MODULE","CAP_SYS_RAWIO","CAP_SYS_CHROOT","CAP_SYS_PTRACE","CAP_SYS_PACCT","CAP_SYS_ADMIN","CAP_SYS_BOOT","CAP_SYS_NICE","CAP_SYS_RESOURCE","CAP_SYS_TIME","CAP_SYS_TTY_CONFIG","CAP_MKNOD","CAP_LEASE","CAP_AUDIT_WRITE","CAP_AUDIT_CONTROL","CAP_SETFCAP","CAP_MAC_OVERRIDE","CAP_MAC_ADMIN","CAP_SYSLOG","CAP_WAKE_ALARM","CAP_BLOCK_SUSPEND","CAP_AUDIT_READ","CAP_PERFMON","CAP_BPF","CAP_CHECKPOINT_RESTORE"],"cgroup":{"v1":true,"v2":true,"systemd":true,"systemdUser":true,"rdma":true},"seccomp":{"enabled":true,"actions":["SCMP_ACT_ALLOW","SCMP_ACT_ERRNO","SCMP_ACT_KILL","SCMP_ACT_KILL_PROCESS","SCMP_ACT_KILL_THREAD","SCMP_ACT_LOG","SCMP_ACT_NOTIFY","SCMP_ACT_TRACE","SCMP_ACT_TRAP"],"operators":["SCMP_CMP_EQ","SCMP_CMP_GE","SCMP_CMP_GT","SCMP_CMP_LE","SCMP_CMP_LT","SCMP_CMP_MASKED_EQ","SCMP_CMP_NE"],"archs":["SCMP_ARCH_AARCH64","SCMP_ARCH_ARM","SCMP_ARCH_LOONGARCH64","SCMP_ARCH_MIPS","SCMP_ARCH_MIPS64","SCMP_ARCH_MIPS64N32","SCMP_ARCH_MIPSEL","SCMP_ARCH_MIPSEL64","SCMP_ARCH_MIPSEL64N32","SCMP_ARCH_PPC","SCMP_ARCH_PPC64","SCMP_ARCH_PPC64LE","SCMP_ARCH_RISCV64","SCMP_ARCH_S390","SCMP_ARCH_S390X","SCMP_ARCH_X32","SCMP_ARCH_X86","SCMP_ARCH_X86_64"],"knownFlags":["SECCOMP_FILTER_FLAG_TSYNC","SECCOMP_FILTER_FLAG_SPEC_ALLOW","SECCOMP_FILTER_FLAG_LOG","SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV"],"supportedFlags":["SECCOMP_FILTER_FLAG_TSYNC","SECCOMP_FILTER_FLAG_SPEC_ALLOW","SECCOMP_FILTER_FLAG_LOG"]},"apparmor":{"enabled":true},"selinux":{"enabled":true},"intelRdt":{"enabled":true,"schemata":true,"monitoring":true},"memoryPolicy":{"modes":["MPOL_BIND","MPOL_DEFAULT","MPOL_INTERLEAVE","MPOL_LOCAL","MPOL_PREFERRED","MPOL_PREFERRED_MANY","MPOL_WEIGHTED_INTERLEAVE"],"flags":["MPOL_F_NUMA_BALANCING","MPOL_F_RELATIVE_NODES","MPOL_F_STATIC_NODES"]},"mountExtensions":{"idmap":{"enabled":true}},"netDevices":{"enabled":true}},"annotations":{"io.github.seccomp.libseccomp.version":"2.5.4","org.opencontainers.runc.checkpoint.enabled":"true","org.opencontainers.runc.commit":"v1.5.1-0-g8f2685a4","org.opencontainers.runc.version":"1.5.1"},"potentiallyUnsafeConfigAnnotations":["bundle","org.systemd.property.","org.criu.config"]}]} runc:{{runc [] map[]} map[org.opencontainers.runtime-spec.features:{"ociVersionMin":"1.0.0","ociVersionMax":"1.3.0","hooks":["prestart","createRuntime","createContainer","startContainer","poststart","poststop"],"mountOptions":["async","atime","bind","defaults","dev","diratime","dirsync","exec","iversion","lazytime","loud","mand","noatime","nodev","nodiratime","noexec","noiversion","nolazytime","nomand","norelatime","nostrictatime","nosuid","nosymfollow","private","ratime","rbind","rdev","rdiratime","relatime","remount","rexec","rnoatime","rnodev","rnodiratime","rnoexec","rnorelatime","rnostrictatime","rnosuid","rnosymfollow","ro","rprivate","rrelatime","rro","rrw","rshared","rslave","rstrictatime","rsuid","rsymfollow","runbindable","rw","shared","silent","slave","strictatime","suid","symfollow","sync","tmpcopyup","unbindable"],"linux":{"namespaces":["cgroup","ipc","mount","network","pid","time","user","uts"],"capabilities":["CAP_CHOWN","CAP_DAC_OVERRIDE","CAP_DAC_READ_SEARCH","CAP_FOWNER","CAP_FSETID","CAP_KILL","CAP_SETGID","CAP_SETUID","CAP_SETPCAP","CAP_LINUX_IMMUTABLE","CAP_NET_BIND_SERVICE","CAP_NET_BROADCAST","CAP_NET_ADMIN","CAP_NET_RAW","CAP_IPC_LOCK","CAP_IPC_OWNER","CAP_SYS_MODULE","CAP_SYS_RAWIO","CAP_SYS_CHROOT","CAP_SYS_PTRACE","CAP_SYS_PACCT","CAP_SYS_ADMIN","CAP_SYS_BOOT","CAP_SYS_NICE","CAP_SYS_RESOURCE","CAP_SYS_TIME","CAP_SYS_TTY_CONFIG","CAP_MKNOD","CAP_LEASE","CAP_AUDIT_WRITE","CAP_AUDIT_CONTROL","CAP_SETFCAP","CAP_MAC_OVERRIDE","CAP_MAC_ADMIN","CAP_SYSLOG","CAP_WAKE_ALARM","CAP_BLOCK_SUSPEND","CAP_AUDIT_READ","CAP_PERFMON","CAP_BPF","CAP_CHECKPOINT_RESTORE"],"cgroup":{"v1":true,"v2":true,"systemd":true,"systemdUser":true,"rdma":true},"seccomp":{"enabled":true,"actions":["SCMP_ACT_ALLOW","SCMP_ACT_ERRNO","SCMP_ACT_KILL","SCMP_ACT_KILL_PROCESS","SCMP_ACT_KILL_THREAD","SCMP_ACT_LOG","SCMP_ACT_NOTIFY","SCMP_ACT_TRACE","SCMP_ACT_TRAP"],"operators":["SCMP_CMP_EQ","SCMP_CMP_GE","SCMP_CMP_GT","SCMP_CMP_LE","SCMP_CMP_LT","SCMP_CMP_MASKED_EQ","SCMP_CMP_NE"],"archs":["SCMP_ARCH_AARCH64","SCMP_ARCH_ARM","SCMP_ARCH_LOONGARCH64","SCMP_ARCH_MIPS","SCMP_ARCH_MIPS64","SCMP_ARCH_MIPS64N32","SCMP_ARCH_MIPSEL","SCMP_ARCH_MIPSEL64","SCMP_ARCH_MIPSEL64N32","SCMP_ARCH_PPC","SCMP_ARCH_PPC64","SCMP_ARCH_PPC64LE","SCMP_ARCH_RISCV64","SCMP_ARCH_S390","SCMP_ARCH_S390X","SCMP_ARCH_X32","SCMP_ARCH_X86","SCMP_ARCH_X86_64"],"knownFlags":["SECCOMP_FILTER_FLAG_TSYNC","SECCOMP_FILTER_FLAG_SPEC_ALLOW","SECCOMP_FILTER_FLAG_LOG","SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV"],"supportedFlags":["SECCOMP_FILTER_FLAG_TSYNC","SECCOMP_FILTER_FLAG_SPEC_ALLOW","SECCOMP_FILTER_FLAG_LOG"]},"apparmor":{"enabled":true},"selinux":{"enabled":true},"intelRdt":{"enabled":true,"schemata":true,"monitoring":true},"memoryPolicy":{"modes":["MPOL_BIND","MPOL_DEFAULT","MPOL_INTERLEAVE","MPOL_LOCAL","MPOL_PREFERRED","MPOL_PREFERRED_MANY","MPOL_WEIGHTED_INTERLEAVE"],"flags":["MPOL_F_NUMA_BALANCING","MPOL_F_RELATIVE_NODES","MPOL_F_STATIC_NODES"]},"mountExtensions":{"idmap":{"enabled":true}},"netDevices":{"enabled":true}},"annotations":{"io.github.seccomp.libseccomp.version":"2.5.4","org.opencontainers.runc.checkpoint.enabled":"true","org.opencontainers.runc.commit":"v1.5.1-0-g8f2685a4","org.opencontainers.runc.version":"1.5.1"},"potentiallyUnsafeConfigAnnotations":["bundle","org.systemd.property.","org.criu.config"]}]}] +Default runtime: runc +Storage driver: overlayfs +Root dir: /var/lib/docker +Server version: 29.8.1 + +----- running containers ----- +NAMES IMAGE STATUS PORTS +matter-server ghcr.io/matter-js/matterjs-server:1.4.0 Up About a minute (healthy) + +----- all containers ----- +NAMES IMAGE STATUS +matter-server ghcr.io/matter-js/matterjs-server:1.4.0 Up About a minute (healthy) + +----- networks ----- +NAME DRIVER SCOPE +bridge bridge local +host host local +none null local + +----- networks (external, non-default — worth knowing for compose external: true) ----- + +----- named volumes ----- +VOLUME NAME DRIVER + +----- compose projects currently running ----- +matter-server + +===== COMPOSE FILES (/opt/docker/compose/) ===== + + +>>> /opt/docker/compose/matter-server/compose.yaml +# matter-server — the Matter controller for Home Assistant, on esh-matter (CT 111 +# on esh-pve, whose ONLY network leg is VLAN 90 / esh-iot, 10.0.90.20). +# Requested by ha-dev, operator-approved 2026-09-26. +# +# matter.js server (matter-js/matterjs-server), the drop-in successor to +# python-matter-server that HA has used since 2026.2. Same websocket API on +# :5580/ws. Pinned: 1.4.0 (stable, 2026-08-07) by digest. +# +# host networking is REQUIRED: Matter is IPv6 link-local and mDNS. The server must +# see eth0's Thread ULA and the border routers' RAs directly. +# +# ⚠ :5580 (websocket + dashboard) is UNAUTHENTICATED. The CT's nftables admits it +# only from HA at 10.0.50.46 (playbooks/esh-matter-lxc.yaml). +# ⚠ /data holds the Matter fabric ROOT CREDENTIALS. Losing it means re-commissioning +# every device. The whole CT is in esh-pve's vzdump job (PBS-ANA → PBS-NH3). Never +# put it in git. +name: matter-server +services: + matter-server: + image: ${MATTER_IMAGE} + container_name: matter-server + restart: unless-stopped + network_mode: host + volumes: + - /opt/docker/data/matter-server:/data + environment: + - STORAGE_PATH=/data + - PRIMARY_INTERFACE=eth0 + - TZ=America/Los_Angeles + - LOG_LEVEL=${MATTER_LOG_LEVEL:-info} + +===== CONFIG LAYOUT (/opt/docker/conf/ — top 200 entries) ===== + +/opt/docker/conf + +===== LISTENING PORTS ===== + +*:22 +*:50427 +*:5580 +127.0.0.1:25 +[::1]:25 + +===== MODEL / HUGGINGFACE CACHES ===== + + +===== DOCKER-ADJACENT SYSTEMD SERVICES ===== + +container-getty@1.service running +container-getty@2.service running +containerd.service running +docker.service running + +===== DONE ===== + +Paste the above back into the chat, or pass a path as argv[1] to save. diff --git a/stacks/matter-server/.env.example b/stacks/matter-server/.env.example new file mode 100644 index 0000000..8fa442e --- /dev/null +++ b/stacks/matter-server/.env.example @@ -0,0 +1,4 @@ +# matter-server tunables (esh-matter). Copy to `.env` on the server. +# matter.js server 1.4.0 (stable, 2026-08-07), pinned by digest. +MATTER_IMAGE=ghcr.io/matter-js/matterjs-server:1.4.0@sha256:54232d0d3e7dff5a54759469d2753399270412b4c30c55b31750a4595e4cb236 +MATTER_LOG_LEVEL=info diff --git a/stacks/matter-server/README.md b/stacks/matter-server/README.md new file mode 100644 index 0000000..cb53ae4 --- /dev/null +++ b/stacks/matter-server/README.md @@ -0,0 +1,18 @@ +# matter-server + +The **Matter controller for Home Assistant**: matter.js server +(`ghcr.io/matter-js/matterjs-server`, **1.4.0**, digest-pinned) on **esh-matter** +(CT 111, VLAN 90 only, `10.0.90.20`), with host networking. HA's `matter` +integration connects to `ws://10.0.90.20:5580/ws`. + +- matter.js server is the drop-in successor to python-matter-server (HA has used + it since 2026.2), with the same websocket API. +- `/data` → `/opt/docker/data/matter-server` (owned by uid 1000, the image's + unprivileged user) holds the **fabric root credentials**. It is covered by the + CT's vzdump backup. Never commit it. +- :5580 is unauthenticated, so the CT firewall admits HA (10.0.50.46) only. +- Host requirements (IPv6 RA with route-info, forwarding off) and acceptance + results: [`servers/esh-matter/README.md`](../../servers/esh-matter/README.md). + +Deploy: `scripts/deploy-stack.sh esh-matter matter-server`, then on the host +`cp -n .env.example .env && docker compose config -q && docker compose up -d`. diff --git a/stacks/matter-server/compose.yaml b/stacks/matter-server/compose.yaml new file mode 100644 index 0000000..ec720e1 --- /dev/null +++ b/stacks/matter-server/compose.yaml @@ -0,0 +1,30 @@ +# matter-server — the Matter controller for Home Assistant, on esh-matter (CT 111 +# on esh-pve, whose ONLY network leg is VLAN 90 / esh-iot, 10.0.90.20). +# Requested by ha-dev, operator-approved 2026-09-26. +# +# matter.js server (matter-js/matterjs-server), the drop-in successor to +# python-matter-server that HA has used since 2026.2. Same websocket API on +# :5580/ws. Pinned: 1.4.0 (stable, 2026-08-07) by digest. +# +# host networking is REQUIRED: Matter is IPv6 link-local and mDNS. The server must +# see eth0's Thread ULA and the border routers' RAs directly. +# +# ⚠ :5580 (websocket + dashboard) is UNAUTHENTICATED. The CT's nftables admits it +# only from HA at 10.0.50.46 (playbooks/esh-matter-lxc.yaml). +# ⚠ /data holds the Matter fabric ROOT CREDENTIALS. Losing it means re-commissioning +# every device. The whole CT is in esh-pve's vzdump job (PBS-ANA → PBS-NH3). Never +# put it in git. +name: matter-server +services: + matter-server: + image: ${MATTER_IMAGE} + container_name: matter-server + restart: unless-stopped + network_mode: host + volumes: + - /opt/docker/data/matter-server:/data + environment: + - STORAGE_PATH=/data + - PRIMARY_INTERFACE=eth0 + - TZ=America/Los_Angeles + - LOG_LEVEL=${MATTER_LOG_LEVEL:-info}