From d04e5bcc105d2283eddeb171269e44de1a40cfaf Mon Sep 17 00:00:00 2001 From: Rufus King Date: Sun, 26 Jul 2026 22:12:38 -0400 Subject: [PATCH] describe what changed --- email_template_style.md | 146 +++++ firewall_policies.md | 178 ++++++ network_index.md | 198 +++++++ onlyoffice_stock_tracking.md | 153 +++++ server_homeassistant.md | 305 ++++++++++ server_nas08.md | 1018 ++++++++++++++++++++++++++++++++++ server_nas16.md | 346 ++++++++++++ server_plex32.md | 468 ++++++++++++++++ server_ucg.md | 120 ++++ vlan10_management.md | 67 +++ vlan1_infrastructure.md | 79 +++ vlan20_trusted.md | 74 +++ vlan30_iot.md | 113 ++++ vlan40_guest.md | 75 +++ vlan50_lab.md | 86 +++ 15 files changed, 3426 insertions(+) create mode 100644 email_template_style.md create mode 100644 firewall_policies.md create mode 100644 network_index.md create mode 100644 onlyoffice_stock_tracking.md create mode 100644 server_homeassistant.md create mode 100644 server_nas08.md create mode 100644 server_nas16.md create mode 100644 server_plex32.md create mode 100644 server_ucg.md create mode 100644 vlan10_management.md create mode 100644 vlan1_infrastructure.md create mode 100644 vlan20_trusted.md create mode 100644 vlan30_iot.md create mode 100644 vlan40_guest.md create mode 100644 vlan50_lab.md diff --git a/email_template_style.md b/email_template_style.md new file mode 100644 index 0000000..770dcd2 --- /dev/null +++ b/email_template_style.md @@ -0,0 +1,146 @@ +# KingDezigns Email Template Style Guide + +Last Updated: 2026-05-26 + +--- + +## Overview +All automated HTML emails in the KingDezigns infrastructure follow a consistent visual style. +Reference this document when building new notification emails for any service. + +--- + +## Design Principles +- Dark header (`#0f172a`) with white text โ€” always includes hostname and timestamp +- Color-coded status banner below header (green / amber / red) +- White card body with `border-radius:10px`, `box-shadow`, `border:1px solid #e5e7eb` +- Stat/metric values displayed as large bold figures in `#f9fafb` pill cards +- Monospace `pre` blocks for raw output โ€” dark background `#1e293b`, light text `#e2e8f0` +- Dark footer (`#0f172a`) with "KingDezigns Infrastructure" branding +- Max width: 720px, centered, outer background `#f1f5f9` +- Font stack: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` + +--- + +## Severity Colors + +| Severity | Background | Text | Use When | +|----------|-----------|-------|----------| +| HEALTHY / OK | `#1a7f4b` | white | All clear | +| WARNING | `#b45309` | white | Attention needed | +| CRITICAL | `#b91c1c` | white | Immediate action required | + +Row background tints: +- OK: `#f0fdf4` +- WARNING: `#fffbeb` +- CRITICAL: `#fff1f2` + +--- + +## Header Block +```html + + + + +
+
SERVICE NAME
+
ICON HOSTNAME
+
TIMESTAMP | CONTEXT INFO
+
+
STATUS BADGE
+
+ +``` + +--- + +## Status Banner Block +```html + + ICON  STATUS MESSAGE + +``` + +--- + +## Stat Card Row +```html + +
LABEL
+
VALUE
+ +``` + +--- + +## Info/Detail Block +```html +
+ LABEL + VALUE +
+``` + +--- + +## Error/Warning Block +```html +
+
โš  TITLE
+
CONTENT
+
+``` + +--- + +## Monospace Raw Output Block +```html +
RAW OUTPUT
+``` + +--- + +## Summary Table Block +```html +
+
๐Ÿ“‹ Report Summary
+ + + + + +
LABELVALUE
+
+``` + +--- + +## Footer Block +```html + + SERVICE NAME  |  HOSTNAME  |  KingDezigns Infrastructure + +``` + +--- + +## SMTP Configuration (Zoho) +- Server: `smtppro.zoho.com` +- Port: `465` +- Encryption: `tls` / `ssltls` +- Auth: `login` +- Sender: `rufus.king@kingdezigns.com` +- Sender Name: `Home Assistant` +- Recipient: `rufus.king@kingdezigns.com` +- Credentials: stored in HA `secrets.yaml` as `kd_username` / `kd_password` + +--- + +## Reference Implementation +See `nas08_zfs_report.sh` for the full reference implementation of this style. +All future email notifications should match this visual language. + +--- + +# โœ”๏ธ End of File diff --git a/firewall_policies.md b/firewall_policies.md new file mode 100644 index 0000000..c03961e --- /dev/null +++ b/firewall_policies.md @@ -0,0 +1,178 @@ +# Firewall Policies โ€” KingDezigns Network +Last Updated: 2026-05-24 + +This document contains the **complete firewall policy set** for all VLANs in the KingDezigns network. +Rules are grouped by VLAN and presented in a clean, AIโ€‘friendly table format. + +--- + +# ๐ŸŒ Global Firewall Rules (Affect All VLANs) + +| Rule ID | Name | Action | Protocol | Source | Destination | Port | Notes | +|--------|-------|---------|----------|---------|--------------|--------|--------| +| 20000 | Allow Established / Related | Accept | All | Any | Any | Any | Required for normal TCP/UDP flows | +| 20001 | DNS โ†’ Piโ€‘hole | Accept | TCP/UDP | All VLANs | Piโ€‘hole (192.168.150.35) | 53 | All VLANs use Piโ€‘hole DNS | + +--- + +# ๐ŸŒ External Access Architecture + +All inbound internet traffic enters the network through a single ingress point: + +``` +Internet โ†’ NPM on HAS (192.168.150.30:80/443) โ†’ Backend Services (NAS08/NAS16) +``` + +**Nginx Proxy Manager (NPM)** runs as a Home Assistant add-on on HAS and terminates all external HTTPS connections. +It forwards requests to backend servers inside VLAN 50 using internal IPs. + +Because all proxied traffic originates from HAS (192.168.150.30), backend servers are configured to extract real client IPs from the `X-Forwarded-For` header: + +- **nginx on NAS16** โ€” `/etc/nginx/conf.d/real_ip.conf` sets `set_real_ip_from 192.168.150.30` +- **Apache on NAS16** โ€” `/etc/apache2/conf-available/real_ip.conf` sets `RemoteIPTrustedProxy 192.168.150.30` +- **Fail2Ban on NAS16** โ€” `ignoreip` includes `192.168.150.0/24` to prevent HAS from being banned + +This ensures real attacker IPs are logged and banned, not the proxy IP. + +--- + +# ๐Ÿ”ง VLAN 1 โ€” Infrastructure (192.168.100.0/24) + +## Inbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20008 | Drop | VLAN 30 (IoT) | VLAN 1 | IoT cannot reach Infrastructure | +| 20009 | Drop | VLAN 50 (Lab) | VLAN 1 | Lab cannot reach Infrastructure | + +## Outbound Rules +Outbound access for VLAN 1 is handled by UCG Max auto-generated rules (60000-series), allowing Infrastructure to reach all internal VLANs. + +--- + +# ๐Ÿ”ง VLAN 10 โ€” Management (192.168.110.0/24) + +## Inbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20002 | Accept | VLAN 10 | All VLANs | Management can reach all internal VLANs | +| 20003 | Accept | VLAN 20 | VLAN 10 | Trusted โ†’ All VLANs Except Trusted | + +## Outbound Rules +*(No explicit outbound rules โ€” UCG Max 60000-series and default policy applies.)* + +--- + +# ๐Ÿ”ง VLAN 20 โ€” Trusted Devices (192.168.120.0/24) + +## Inbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20007 | Drop | VLAN 30 (IoT) | VLAN 20 | IoT cannot reach Trusted | +| 20010 | Drop | VLAN 50 (Lab) | VLAN 20 | Lab cannot reach Trusted | + +## Outbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20003 | Accept | VLAN 20 | All VLANs Except 20 | Trusted can reach all other VLANs | + +--- + +# ๐Ÿ”ง VLAN 30 โ€” IoT (192.168.130.0/23) + +## Inbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20005 | Accept | Home Assistant (192.168.150.30) | VLAN 30 | HA โ†’ IoT Control | +| 20011 | Drop | VLAN 50 (Lab) | VLAN 30 | Lab cannot reach IoT | + +## Outbound Rules +| Rule ID | Action | Source | Destination | Port | Notes | +|--------|---------|---------|--------------|--------|--------| +| 20004 | Accept | VLAN 30 | 192.168.150.30 | 8123/TCP/UDP | IoT โ†’ Home Assistant | +| 20006 | Accept | VLAN 30 | 192.168.100.1 | 5540/TCP | IoT โ†’ UCG Max Matter controller | +| 20007 | Drop | VLAN 30 | VLAN 20 | Any | IoT cannot reach Trusted | +| 20008 | Drop | VLAN 30 | VLAN 1 | Any | IoT cannot reach Infrastructure | + +--- + +# ๐Ÿ”ง VLAN 40 โ€” Guest (192.168.140.0/24) + +## Inbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20012 | Drop | VLAN 50 (Lab) | VLAN 40 | Lab cannot reach Guest | + +## Outbound Rules +*(No explicit outbound rules โ€” default policy applies.)* + +--- + +# ๐Ÿ”ง VLAN 50 โ€” Lab / Servers (192.168.150.0/24) + +## Inbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20004 | Accept | VLAN 30 | 192.168.150.30 | IoT โ†’ Home Assistant (port 8123) | + +## Outbound Rules +| Rule ID | Action | Source | Destination | Notes | +|--------|---------|---------|--------------|--------| +| 20009 | Drop | VLAN 50 | VLAN 1 | Lab cannot reach Infrastructure | +| 20010 | Drop | VLAN 50 | VLAN 20 | Lab cannot reach Trusted | +| 20011 | Drop | VLAN 50 | VLAN 30 | Lab cannot reach IoT (except HA exception below) | +| 20012 | Drop | VLAN 50 | VLAN 40 | Lab cannot reach Guest | +| 20013 | Drop | VLAN 50 | All VLANs Except 50 | Global Final Drop | + +## VLAN 50 Exceptions +| Rule ID | Action | Source | Destination | Port | Notes | +|--------|---------|---------|--------------|--------|--------| +| 20005 | Accept | 192.168.150.30 (HAS) | VLAN 30 | Any | Home Assistant โ†’ IoT control | +| โ€” | Accept | 192.168.150.30 (NPM) | 192.168.150.35 | Various | NPM proxy โ†’ NAS08 services | +| โ€” | Accept | 192.168.150.30 (NPM) | 192.168.150.40 | Various | NPM proxy โ†’ NAS16 services | + +--- + +# ๐Ÿ”ง UCG Max Auto-Generated Rules (60000-series) + +These rules are automatically created by the UCG Max when networks are provisioned. +They allow intra-VLAN and general outbound traffic per subnet. Rule order places these after all 20000-series rules. + +| Rule ID | Direction | Action | Source | Destination | Notes | +|--------|-----------|--------|--------|-------------|-------| +| 60001 | LAN In | Accept | 192.168.120.0/24 | Any | VLAN 20 inbound traffic | +| 60002 | LAN In | Accept | 192.168.130.0/23 | Any | VLAN 30 inbound traffic | +| 60003 | LAN In | Accept | 192.168.150.0/24 | Any | VLAN 50 inbound traffic | +| 60004 | LAN In | Accept | 192.168.100.0/24 | Any | VLAN 1 inbound traffic | +| 60005 | LAN In | Accept | 192.168.110.0/24 | Any | VLAN 10 inbound traffic | +| 60001 | LAN Out | Accept | Any | 192.168.120.0/24 | VLAN 20 outbound traffic | +| 60002 | LAN Out | Accept | Any | 192.168.130.0/23 | VLAN 30 outbound traffic | +| 60003 | LAN Out | Accept | Any | 192.168.150.0/24 | VLAN 50 outbound traffic | +| 60004 | LAN Out | Accept | Any | 192.168.100.0/24 | VLAN 1 outbound traffic | +| 60005 | LAN Out | Accept | Any | 192.168.110.0/24 | VLAN 10 outbound traffic | + +> Note: 20000-series rules execute first. The 60000-series rules handle remaining traffic including VLAN 1 (Infrastructure) outbound access to all VLANs. + +--- + +# ๐Ÿง  Summary for AI Systems + +- All VLANs โ†’ Piโ€‘hole DNS allowed +- All VLANs โ†’ Established/Related allowed +- VLAN 1 outbound access to all VLANs handled by UCG Max 60000-series rules +- VLAN 10 (Management) can reach all VLANs (Rule 20002) +- VLAN 20 can reach all VLANs except itself +- VLAN 30 can only reach Home Assistant (8123), UCG Max Matter controller (5540), and DNS +- VLAN 30 client isolation was previously enabled โ€” removed to allow Matter/Thread device pairing +- VLAN 40 is isolated except for DNS +- VLAN 50 cannot initiate to any VLAN except IoT (HA exception) and intra-VLAN 50 traffic +- External internet traffic enters exclusively via NPM on HAS (192.168.150.30) +- Real client IPs are forwarded via X-Forwarded-For from NPM to all backend servers +- Fail2Ban on NAS16 ignores 192.168.150.0/24 to prevent proxy IP banning +- Matter/Thread pairing requires IoT โ†’ UCG Max (192.168.100.1:5540/TCP) โ€” Rule 20006 +- Aqara M100 Thread Border Router has static IP reservation at 192.168.130.30 (VLAN 30) + +This file is the **single source of truth** for firewall logic. + +--- + +# โœ”๏ธ End of File diff --git a/network_index.md b/network_index.md new file mode 100644 index 0000000..2761e9e --- /dev/null +++ b/network_index.md @@ -0,0 +1,198 @@ +# KingDezigns Network Documentation Index +Last Updated: 2026-07-25 + +This index provides a structured overview of all VLANs, servers, and firewall policies in the KingDezigns network. +Each section links to its own dedicated Markdown file for clarity and modularity. + +--- + +# ๐Ÿ“ VLAN Documentation + +## ๐Ÿ”ง Core VLANs +- [VLAN 1 โ€” Infrastructure](vlan1_infrastructure.md) +- [VLAN 10 โ€” Management](vlan10_management.md) + +## ๐Ÿง‘โ€๐Ÿ’ป Trusted User VLANs +- [VLAN 20 โ€” Trusted Devices](vlan20_trusted.md) + +## ๐Ÿ  Smart Home & Guest VLANs +- [VLAN 30 โ€” IoT](vlan30_iot.md) +- [VLAN 40 โ€” Guest](vlan40_guest.md) + +## ๐Ÿงช Server & Lab VLAN +- [VLAN 50 โ€” Lab / Servers](vlan50_lab.md) + +--- + +# ๐Ÿ–ฅ๏ธ Server Documentation + +## Core Services +- [Home Assistant Server](server_homeassistant.md) +- [NAS08 โ€” Pi-hole, Nextcloud (+ ONLYOFFICE/Collabora), Vaultwarden, STOCKPROXY](server_nas08.md) +- [NAS16 โ€” LAMP Stack, MariaDB, Webmin](server_nas16.md) +- [PLEX32 โ€” Plex Media Server + Tautulli](server_plex32.md) + +## Network Hardware +- [UCG Gateway Max](server_ucg.md) + +--- + +# ๐Ÿ”ฅ Firewall Policies + +- [Firewall Policies Overview](firewall_policies.md) + +--- + +# ๐Ÿ›ก๏ธ Security Stack + +## Intrusion Prevention +- **CrowdSec** โ€” installed on HAS as a native HA add-on + - Agent reads NPM, HA, and SSH logs via journald + - 60 active detection scenarios + - Community blocklist updates every 2 hours + - Firewall Bouncer enforces bans at nftables level + - Email alerts on every ban decision + - All LAN VLANs whitelisted โ€” internal IPs can never be banned + - See [Home Assistant Server](server_homeassistant.md) for full configuration details + +## Perimeter Defense (per server) +- **Fail2Ban** โ€” NAS16 (active), PLEX32 (active), NAS08 (recommended) + - `ignoreip` includes `192.168.150.0/24` to prevent HAS proxy IP from being banned + - Real IP forwarding configured on NAS16 (nginx + Apache) and NAS08 + +## DNS-level Protection +- **Pi-hole** โ€” NAS08 (192.168.150.35) + - Blocks ads, trackers, and malicious domains for all VLANs + - All VLANs forced to use Pi-hole via global firewall rule 20001 + - **Local DNS overrides required** for any internally-hosted service accessed by its public hostname from within the LAN (e.g. `cloud.kingdezigns.com` โ†’ `192.168.150.30`), to avoid NAT hairpin failures. See `server_nas08.md` ONLYOFFICE section for a documented example and fix. + +--- + +# ๐ŸŒ Network Architecture Overview + +This documentation set is designed around: + +- Clear segmentation +- Explicit inter-VLAN access +- Isolation of untrusted devices +- Preservation of Home Assistant functionality +- AI-friendly structure and formatting +- Layered security โ€” DNS filtering, intrusion prevention, firewall enforcement + +## Security Layer Model +``` +Internet + โ†“ +UCG Max โ€” firewall, VLAN segmentation, geo-blocking (future) + โ†“ +CrowdSec Firewall Bouncer โ€” nftables ban enforcement + โ†“ +NPM on HAS โ€” single ingress, SSL termination + โ†“ +CrowdSec Agent โ€” log analysis, attack detection + โ†“ +Backend Services (NAS08, NAS16, PLEX32) + โ†“ +Fail2Ban โ€” last-line brute force protection + โ†‘ +Pi-hole โ€” DNS-level ad/tracker/malware blocking (all VLANs) +``` + +--- + +# ๐Ÿ”‘ SSH Key Authentication Map + +SSH key authentication is configured between the following machines โ€” no passwords required: + +| From | To | Purpose | +|------|----|---------| +| KingDezigns001 | PLEX32 (192.168.150.45) | Remote launcher script | +| KingDezigns001 | NAS08 (192.168.150.35) | Direct SSH access | +| KingDezigns001 | NAS16 (192.168.150.40) | Direct SSH access | +| PLEX32 | NAS08 (192.168.150.35) | Backup script disk stats via SSH | + +Key type: ED25519. Keys generated per machine โ€” not shared. + +--- + +# ๐Ÿ–ฅ๏ธ KingDezigns001 โ€” Admin Workstation Tools + +KingDezigns001 is the primary Linux administrative workstation on VLAN 20 (Trusted). + +## Remote Launcher +A menu-driven launcher script provides one-click access to run scripts on PLEX32: + +- **Script:** `~/scripts/plex32_launcher.sh` +- **Desktop shortcut:** `~/Desktop/PLEX32-Launcher.desktop` +- **Terminal:** gnome-terminal + +### Menu Options +| Option | Runs on | Script | +|--------|---------|--------| +| 1) Update Check | PLEX32 | `/usr/scripts/plex32/plex32_update_check.sh` | +| 2) Health Report | PLEX32 | `/usr/scripts/plex32/plex32_health_report.sh` | +| 3) Plex32 Backup | PLEX32 | `/usr/scripts/plex32/plex32_backup.sh` | + +--- + +# ๐Ÿง  Notes for AI Systems + +- Each file is self-contained and structured consistently. +- Section headers follow a predictable pattern for easy parsing. +- No duplicated firewall rules โ€” all rules live in `firewall_policies.md`. +- CrowdSec configuration lives entirely on HAS โ€” see `server_homeassistant.md`. +- VLAN files contain: + - Purpose + - Access types + - Subnet + - Expected devices + - Security rules + - AI summary +- Plex Media Server migrated from NAS08 to PLEX32 (dedicated Dell Wyse 5070) on 2026-06-18 โ€” see `server_plex32.md`. +- PLEX32 now runs Plex + Tautulli with Intel Quick Sync hardware transcoding โ€” optimized 2026-06-20. +- KingDezigns001 has SSH keys to all VLAN 50 servers โ€” passwordless access for all admin tasks. +- PLEX32 has SSH key to NAS08 โ€” required for backup script to fetch disk stats. +- **NAS08 Nextcloud update check** runs daily at 7AM โ€” `/usr/scripts/omv/nextcloud_update_check.sh` โ€” emails HTML report with copy-paste `occ` commands. Never use the browser UI for Nextcloud app updates (causes Apache segfault โ†’ maintenance mode deadlock). +- **NAS08 Nextcloud update check โ€” core-update false negative fixed (2026-07-25):** the script previously checked app updates only (`occ app:update`) and had no code path for Nextcloud core/server releases, so it silently missed a core update (34.0.1 โ†’ 34.0.2) and emailed a false "up to date" report. Fixed by adding a core check via `occ update:check`, correcting the emailed Compose commands to the real path (`/kingdezignsnas/Docker/Compose/nextcloud/`, root-only, non-default filenames requiring `-f nextcloud.yml -f compose.override.yml`), and fixing a bash quoting bug that made emailed multi-line commands unreadable. Also documented: the official Nextcloud Docker image auto-upgrades on container restart via its own entrypoint โ€” checking version status immediately after `up -d` can race that process and show a stale version for ~30 seconds. Full detail in `server_nas08.md`. +- **NAS08 Nextcloud Office editing** uses ONLYOFFICE Document Server (free Community Edition) as of 2026-07-18. Only one Office integration app should ever be enabled at a time (exception: the Collabora trial below, run as a time-boxed disable/enable swap, never simultaneous). Requires a Pi-hole local DNS override for `cloud.kingdezigns.com`, `ALLOW_PRIVATE_IP_ADDRESS=true` on the container, and matching JWT secrets between Nextcloud and the container. Full details and troubleshooting history in `server_nas08.md`. +- **NAS08 Collabora Online trial (2026-07-21, ongoing):** A second Office editor, Collabora Online (CODE), was deployed as a standalone Docker container (`collabora`, its own OMV Compose project "Collabora Office") for a head-to-head reliability trial against ONLYOFFICE โ€” triggered by confirmed ONLYOFFICE bugs (conditional formatting crashing on formula cells; silent reverts to hours-old document versions). Only one Office app is ever enabled in Nextcloud at a time โ€” currently `richdocuments`/Collabora is enabled, `onlyoffice` is disabled but still installed for easy rollback. Public URL `https://collabora.kingdezigns.com/`, requires its own Pi-hole local DNS override and a WOPI allow-list (`192.168.150.35`). First real-document test succeeded with a notably fast load time. Full config, setup troubleshooting, and rollback steps in `server_nas08.md`. +- **NAS08 Collabora stock/fund price macro integration (2026-07-22, working):** The ONLYOFFICE `FUNDPRICE`/`FUNDPRICE_HIST`/`STOCKPRICE`/`STOCKTIME` custom functions were ported to Collabora as a LibreOffice Basic module (`StockFunctions.bas`), calling the same STOCKPROXY backend with no backend changes required. Two `coolwsd.xml` server-config changes were required โ€” macro execution enabling and a `net.lok_allow` hostname allowlist entry for `stocks.kingdezigns.com` โ€” plus a fix to the previously non-functional `stocks.kingdezigns.com` Pi-hole DNS override. Confirmed working with a live price pull as of 2026-07-22. Full troubleshooting detail, exact config edits, and remaining follow-up items (recreating the full ~50-fund table, conditional-formatting re-test, recalc performance) in `server_nas08.md`. +- **NAS08 systemd-resolved DNS bug** (fixed 2026-07-18): a stray global `DNS=` override in `/etc/systemd/resolved.conf` caused duplicate/incorrect DNS answers for internal hostnames network-wide on that host. Any future "works from browser but fails from a container/script on NAS08" symptom should check `resolvectl status` for stray global DNS servers first. +- **NAS08 STOCKPROXY service (2026-07-21):** Self-hosted Flask API, Docker container `stockproxy` (`~/docker/stockproxy/`), port 5005, public URL `https://stocks.kingdezigns.com/` proxied via NPM on HAS (see Proxy Hosts table in `server_homeassistant.md`). Supplies current + historical mutual fund prices to ONLYOFFICE custom functions (`FUNDPRICE`, `FUNDPRICE_HIST`) via Yahoo Finance, scraped server-side to dodge the browser CORS block that rules out calling Yahoo directly from a spreadsheet macro. Finnhub was evaluated first but dropped from this service โ€” confirmed no mutual fund NAV coverage on the free tier. Current-price cache: 15 min in-memory. Historical-price cache: permanent, SQLite. All endpoints require a shared-secret `key` param (Vaultwarden). Built to support the "HSA Investiments Choices" tab in `Rufus Retirement Account Tracking 2026.xlsx`. `stocks.kingdezigns.com` A record added to the daily Dreamhost DDNS refresh script. Full config and known limitations in `server_nas08.md`. +- **NAS08 RANK.EQ Err:522 circular reference fix (2026-07-23, working):** The + "HSA Investiments Choices" fund-ranking formula broke with a circular-reference error on + any sheet edit (worked fine on load) because it depended on ~50 cells fed by live + `FUNDPRICE`/`FUNDPRICE_HIST` network calls. Fixed with a button-triggered LibreOffice + Basic macro that snapshots the live price columns into static value-only columns via + `setDataArray()`, with percent-change and rank formulas rebuilt to read from the static + snapshot instead of the live columns. Full root-cause analysis and macro in + `server_nas08.md`. +- **NAS08 Collabora Finnhub removal (2026-07-23, working):** `STOCKPRICE`/`STOCKTIME` + (Finnhub-backed equity functions) errored due to a missing `net.lok_allow` entry for + Finnhub. Instead of allowlisting Finnhub, it was removed entirely โ€” `FUNDPRICE` (STOCKPROXY) + now covers equity tickers too, and a new `FUNDTIME` function replaces `STOCKTIME`, returning + STOCKPROXY's `resolved_date` (date only, not a time-of-day timestamp โ€” accepted tradeoff). + No `coolwsd.xml` changes required. Full detail in `server_nas08.md`. +- **Synaplan AI integration** (added 2026-07-18, not yet functional): Nextcloud connector app installed, but the required self-hosted Synaplan backend stack (PHP/MariaDB/Qdrant/optional Ollama) has not been deployed. Treat as a future project, not a bug โ€” see `server_nas08.md` for details before troubleshooting. + +--- + +# ๐Ÿ“Œ Future Expansion + +Additional files may be added for: + +- Device inventories +- Application-specific firewall rules +- Backup and recovery procedures +- Monitoring and alerting configuration +- CrowdSec Console dashboard enrollment +- Geo-blocking configuration on UCG Max +- Authelia / forward authentication layer +- Expand remote launcher to cover NAS08 and NAS16 scripts +- **Synaplan self-hosted AI stack deployment** (PHP backend, MariaDB, Qdrant, optional Ollama) โ€” scope resource requirements and deployment plan when ready +- **Collabora vs. ONLYOFFICE trial decision** (started 2026-07-21) โ€” once a winner is chosen, update `server_nas08.md` and this index to reflect the final state (remove trial framing, decommission or keep the losing container, update firewall/NPM docs if the losing editor's proxy host is removed) +- **STOCKPROXY hardening** โ€” not yet stress-tested at full ~50-ticker scale for Yahoo-side rate limiting; revisit if widespread simultaneous lookup failures are observed + +--- + +# โœ”๏ธ End of Index diff --git a/onlyoffice_stock_tracking.md b/onlyoffice_stock_tracking.md new file mode 100644 index 0000000..620bd18 --- /dev/null +++ b/onlyoffice_stock_tracking.md @@ -0,0 +1,153 @@ +# ONLYOFFICE Stock/Fund Tracking โ€” "Rufus Retirement Account Tracking 2026.xlsx" + +Last Updated: 2026-07-21 + +--- + +## ๐Ÿงฉ Overview +This workbook tracks live and historical prices for ~50 mutual funds (HSA investment +choices) plus a smaller set of regular equities (Dashboard tab), replacing a prior Google +Sheets `GOOGLEFINANCE()`-based workflow. Since ONLYOFFICE has no equivalent live-data +formula, price data is supplied via ONLYOFFICE **custom functions** (JavaScript, run in +the browser) โ€” some calling Finnhub directly, some calling the self-hosted STOCKPROXY +service (see `server_stockproxy.md`). + +--- + +## ๐Ÿ“‘ Tabs Involved +| Tab | Purpose | +|---|---| +| Dashboard | Regular equities โ€” live price via Finnhub (`STOCKPROXY` not needed) | +| HSA Investiments Choices | ~50 mutual funds โ€” current + historical price, ranked by return | + +--- + +## ๐Ÿ”‘ Key Cells โ€” "HSA Investiments Choices" tab +| Cell | Contents | +|---|---| +| `B1` | Prior-year marker date | +| `B2` | `=TODAY()` | +| `B3` | **Resolved calc/anchor date** โ€” rolling lookback (currently ~11 months back), computed from B1/B2/B4, with manual override via B12 ("Yes"/"No") + B13 | +| `B4` | Number of lookback months (currently 11) | +| `B12` / `B13` | Manual override toggle + override date (currently unused, set to "No") | + +Wide ticker table: rows for **Rate**, **$ Increase**, **Price** (current), **Price History** +(anchor-date price), **Fund**, **Ticker**, **Rating** โ€” cascaded across ~50 columns, one per +fund. + +Ranking table (top of sheet): **Rank / $ Increase / Ticker / Fund / Price / Rating**. + +--- + +## ๐Ÿงฎ Custom Functions in Use + +Full code archived separately โ€” see: +- `onlyoffice_macro_stockprice_finnhub.js` โ€” `STOCKPRICE(ticker)` / `STOCKTIME(ticker)` +- `onlyoffice_macro_fundprice_proxy.js` โ€” `FUNDPRICE(ticker)` / `FUNDPRICE_HIST(ticker, dateStr)` +- `onlyoffice_macro_autostart_recalc.js` โ€” delayed full-recalc Autostart macro (partial fix, see Known Issues) + +| Function | Data Source | Used On | Notes | +|---|---|---|---| +| `STOCKPRICE(ticker)` | Finnhub (direct) | Dashboard | Live quote โ€” equities only, **not** mutual funds | +| `STOCKTIME(ticker)` | Finnhub (direct) | Dashboard | Exchange-reported quote timestamp | +| `FUNDPRICE(ticker)` | STOCKPROXY `/current` | HSA Investiments Choices | Current mutual fund price | +| `FUNDPRICE_HIST(ticker, dateStr)` | STOCKPROXY `/historical` | HSA Investiments Choices | Historical price on/before a date; `dateStr` must be passed as text via `TEXT($B$3,"yyyy-mm-dd")` | + +Both custom function sets require their own placeholder credentials to be filled in +(Finnhub API key; STOCKPROXY shared secret) โ€” real values live in Vaultwarden, never +committed to these docs in plaintext. + +To recreate: **View โ†’ Macros โ†’ Custom Functions โ†’ +**, paste the relevant file's contents, +replace the placeholder credentials, Save. + +--- + +## โš ๏ธ Known Issues (status as of last session) + +### 1. `#NAME?` / `#BUSY` on file open โ€” **UNRESOLVED** +Custom function cells show errors on open until manually clicked into + Enter pressed. + +- **Tried:** Autostart macro (`onlyoffice_macro_autostart_recalc.js`) forcing + `Api.RecalculateAllFormulas()` 3 seconds after open. +- **Result:** Only partially effective โ€” a screenshot showed 2 of 34 `Price` cells resolved + correctly after the fix, the rest stayed `#NAME?`, while the `Price History` column + resolved for all rows. Asymmetry between the two columns not yet explained. +- **Next steps if resuming:** confirm exact formula text in a failing cell (rule out typos + from copy-paste across 50 columns); investigate what a user-added "Refresh Sheet" button + (seen in a screenshot, cause/origin not yet confirmed with user) does โ€” may indicate a + separate macro-poke mechanism exists alongside the live formulas; consider longer + Autostart delay or investigate whether `#NAME?` is a parse-time error that a value-only + recalculation cannot fix (may require literally re-entering formula text, not just + recalculating). + +### 2. Ctrl+D fill-down not supported โ€” **CONFIRMED, NOT A BUG** +Longstanding ONLYOFFICE feature gap (multiple open feature requests upstream). +**Workarounds:** drag the fill handle; copy/paste; or install the community +"onlyoffice-fill-down" plugin (github.com/une4s/onlyoffice-fill-down) โ€” not yet installed. + +### 3. Conditional formatting + formula cells crashes ONLYOFFICE โ€” **CONFIRMED KNOWN BUG** +Multiple open upstream GitHub issues (including one from May 2026) describe the editor +crashing/erroring when applying or editing conditional formatting on cells containing +formulas. This is the issue that triggered the Collabora comparison project below. + +### 4. Silent data loss / revert to hours-old version โ€” **CONFIRMED SERIOUS, ONGOING RISK** +Long-documented, recurring ONLYOFFICEโ†”Nextcloud bug class: document silently reverts to an +old saved state, sometimes losing hours of work, typically linked to stale browser +tabs/sessions or autosave/connectivity edge cases. **Not a one-off โ€” confirmed via multiple +upstream bug reports spanning years, including as recently as June 2026.** + +**Mitigations discussed (not all yet implemented):** +- Check Nextcloud's file **Versions** tab to recover lost work when this happens +- Enable **"Keep intermediate versions when editing" (forcesave)** in the Nextcloud + ONLYOFFICE admin settings โ€” creates more frequent real restore points +- Always close other tabs/sessions of the same file before editing +- Watch for the "all changes saved" indicator before navigating away + +--- + +## ๐Ÿ”€ Collabora Online Side-by-Side Comparison โ€” IN PROGRESS + +**Decision:** Rather than pick ONLYOFFICE vs. Collabora based on reputation, run both +against the same tracking logic and compare reliability directly. Explicitly **not** +a factor: the Nextcloud/ONLYOFFICE corporate/licensing dispute (Euro-Office fork, +partnership suspension, March 2026) โ€” user stated reliability is the only criterion. + +**Status: setup not yet started** โ€” next step when resuming is deploying the Collabora +Docker container. + +### Roadmap +1. Deploy **Collabora Online (CODE)** as a Docker container โ€” recommended on **NAS16** + (not NAS08) to avoid resource contention with Nextcloud/ONLYOFFICE/Pi-hole/Vaultwarden/ + STOCKPROXY already running there. +2. Expose via NPM with its own subdomain (e.g. `collabora.kingdezigns.com`), SSL via + Let's Encrypt โ€” same pattern as `stocks.kingdezigns.com`. +3. Install the Collabora Nextcloud connector app (`richdocuments`) alongside the existing + ONLYOFFICE app. **Confirmed both can coexist** โ€” default double-click opens whichever + app is set as default; right-click โ†’ "Open with" picks the other. +4. Make a **separate copy** of the tracking spreadsheet for Collabora โ€” do not share one + live file between both rendering engines simultaneously. +5. Rebuild `FUNDPRICE`/`FUNDPRICE_HIST` logic as **LibreOffice Basic or Python macros** + (Collabora's macro system is entirely different from ONLYOFFICE's JS-based one โ€” full + rewrite of the glue code required). **STOCKPROXY itself needs no changes** โ€” it's a + plain HTTPS/JSON API, callable identically from either editor. +6. Recreate the same conditional formatting that crashes ONLYOFFICE and specifically + stress-test that exact scenario on Collabora. +7. Also re-test on Collabora: file-open recalculation behavior, and general + session/reliability behavior (the data-loss concern above). + +--- + +## ๐Ÿง  Summary for AI Systems +- This workbook uses ONLYOFFICE custom functions (not native formulas) to pull live/ + historical prices, since ONLYOFFICE has no `STOCKHISTORY()`-equivalent. +- Two data paths: Finnhub direct (equities, Dashboard tab) and STOCKPROXY (mutual funds, + HSA tab) โ€” see `server_stockproxy.md` for the backend service. +- Several confirmed ONLYOFFICE reliability issues are actively being weighed against + Collabora Online as an alternative โ€” see the Collabora section above for current status. +- Real credentials (Finnhub API key, STOCKPROXY shared secret) are never stored in these + docs โ€” Vaultwarden is the source of truth, matching the rest of the KingDezigns + documentation convention. + +--- + +# โœ”๏ธ End of File diff --git a/server_homeassistant.md b/server_homeassistant.md new file mode 100644 index 0000000..887f845 --- /dev/null +++ b/server_homeassistant.md @@ -0,0 +1,305 @@ +# Home Assistant Server (HAS) +Device Type: **Raspberry Pi 5 โ€” 8GB** +Hostname: **homeassistant** +IP Address: **192.168.150.30** +VLAN: **50 โ€” Lab / Servers** + +Last Updated: 2026-07-21 + +--- + +## ๐Ÿงฉ Role & Purpose +The Home Assistant Server serves **three distinct roles** in the KingDezigns network: + +1. **Central smartโ€‘home automation controller** โ€” manages device integrations, automations, discovery protocols, and orchestrates communication between IoT devices and internal services. + +2. **Network-wide reverse proxy** โ€” runs Nginx Proxy Manager (NPM) as a Home Assistant add-on, acting as the single external entry point for all publicly accessible internal services. + +3. **Network-wide intrusion prevention** โ€” runs CrowdSec and CrowdSec Firewall Bouncer as Home Assistant add-ons, providing real-time threat detection, community-sourced IP blocking, and nftables-level enforcement at the network perimeter. + +This system is the **single point of coordination** for VLAN 30 (IoT), the **single ingress point** for all external web traffic, and the **primary intrusion prevention layer** for the KingDezigns network. + +--- + +## ๐Ÿ–ฅ๏ธ Hardware +- Raspberry Pi 5 (8GB RAM) +- Highโ€‘performance microSD or SSD (recommended) +- Gigabit Ethernet connection + +--- + +## ๐ŸŒ Network Placement +- VLAN: **50 โ€” Lab / Servers** +- IP: **192.168.150.30** +- Access Type: LAN / Cable + +This placement ensures: +- Isolation from trusted user devices +- Controlled access to IoT devices +- Direct access to Piโ€‘hole DNS +- Reduced attack surface +- Central proxy position for all VLAN 50 services + +--- + +## โš™๏ธ Primary Functions + +### **Home Automation** +- Home Assistant core platform +- Automation orchestration +- Device discovery +- Alarm integrations +- IoT control +- ESPHome, HomeKit, WiZ, Chromecast, AirPlay, and other integrations + +### **Reverse Proxy (Nginx Proxy Manager)** +- **Add-on:** Nginx Proxy Manager (NPM) +- **Admin UI:** `http://192.168.150.30:81` +- **Function:** Terminates all external HTTPS traffic and proxies to internal services +- **SSL:** Let's Encrypt certificates per proxy host +- **Real IP forwarding:** Passes `X-Forwarded-For` and `X-Real-IP` headers to all backends + +#### Current Proxy Hosts +| Destination | Notes | +|---|---| +| 192.168.150.40:80 | NAS16 Apache virtual hosts (multiple) | +| 192.168.150.40:8080 | NAS16 service | +| 192.168.150.40:10000 | Webmin | +| 192.168.150.40:3670 | NAS16 service | +| 192.168.150.35:8080 | NAS08 service | +| 192.168.150.35:8081 | NAS08 service | +| 192.168.150.35:8082 | NAS08 service | +| 192.168.150.35:8083 | NAS08 service | +| 192.168.150.35:3670 | NAS08 service | +| 192.168.150.35:32400 | Plex | +| 192.168.150.30:8123 | Home Assistant | +| 192.168.150.35:5005 | STOCKPROXY (self-hosted stock/fund price API โ€” `stocks.kingdezigns.com`, see `server_nas08.md`) | + +All proxy hosts are publicly accessible and SSL-terminated via Let's Encrypt. + +--- + +### **Intrusion Prevention (CrowdSec)** +- **Add-on:** CrowdSec (Agent + Local API) +- **Add-on:** CrowdSec Firewall Bouncer +- **Add-on Repository:** `https://github.com/crowdsecurity/home-assistant-addons` +- **LAPI Port:** `8080` โ€” exposed on host network (required for NAS16 notifier access) +- **CrowdSec version:** v1.7.8 + +#### Architecture +``` +Internet โ†’ UCG Max โ†’ NPM (192.168.150.30:80/443) + โ†“ + CrowdSec Firewall Bouncer (nftables) + โ†“ + CrowdSec Agent (log analysis) + โ†“ + Backend Services + โ†“ + NAS16 polls LAPI stream every 5 min + โ†’ immediate email (local attacks) + โ†’ midnight digest (CAPI cloud bans) +``` + +#### How It Works +- CrowdSec Agent reads logs from NPM, Home Assistant, and SSH via journald +- Detected attacks create ban decisions enforced by the Firewall Bouncer at the nftables level +- The community blocklist pulls known malicious IPs every 2 hours from the global CrowdSec network +- Banned IPs are blocked before traffic ever reaches NPM or backend services +- **Email notifications are handled externally by NAS16** โ€” not by CrowdSec's native notification system +- Native CrowdSec email notifications are **disabled** in profiles.yaml + +#### Active Collections +| Collection | Protects Against | +|---|---| +| `crowdsecurity/nginx-proxy-manager` | NPM log-based attack detection | +| `crowdsecurity/home-assistant` | HA brute force login attempts | +| `crowdsecurity/http-cve` | 40+ known CVE exploit attempts | + +#### Active Scenarios (60 total, key ones) +- HTTP brute force, probing, path traversal, SQLi, XSS +- Bad user agents, crawler detection +- WordPress scan, admin interface probing +- SSH brute force (slow, fast, time-based) +- CVE-2024-9474, CVE-2024-0012, CVE-2023-49103, and many more + +#### Key Configuration Files +| File | Path | Purpose | +|---|---|---| +| Acquisition config | CrowdSec add-on Configuration tab | journalctl sources and labels | +| profiles.yaml | `/config/.storage/crowdsec/config/profiles.yaml` | Ban decisions only โ€” notifications disabled | +| LAN whitelist | `/config/.storage/crowdsec/config/postoverflows/s01-whitelist/kingdezigns-lan-whitelist.yaml` | Prevents all LAN IPs from being banned | + +#### profiles.yaml (current โ€” notifications disabled) +```yaml +name: default_ip_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" +decisions: + - type: ban + duration: 4h +on_success: break +--- +name: default_range_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" +decisions: + - type: ban + duration: 4h +on_success: break +``` + +#### Acquisition Configuration +```yaml +acquisition: | + --- + source: journalctl + journalctl_filter: + - "--directory=/var/log/journal/" + labels: + type: syslog + --- + source: journalctl + journalctl_filter: + - "--directory=/var/log/journal/" + - "SYSLOG_IDENTIFIER=addon_a0d7b954_nginxproxymanager" + labels: + type: nginx-proxy-manager +disable_lapi: false +collections: + - crowdsecurity/home-assistant + - crowdsecurity/nginx-proxy-manager + - crowdsecurity/http-cve +parsers_to_disable: + - crowdsecurity/whitelists +``` + +#### LAN Whitelist โ€” Protected Subnets +All internal VLANs are whitelisted at the postoverflow stage โ€” LAN IPs can never be banned: +- `192.168.100.0/24` โ€” VLAN 1 Infrastructure +- `192.168.110.0/24` โ€” VLAN 10 Management +- `192.168.120.0/24` โ€” VLAN 20 Trusted +- `192.168.130.0/23` โ€” VLAN 30 IoT +- `192.168.140.0/24` โ€” VLAN 40 Guest +- `192.168.150.0/24` โ€” VLAN 50 Lab/Servers +- `127.0.0.1/8` โ€” localhost +- `::1` โ€” IPv6 localhost + +#### Registered Bouncers +| Name | Purpose | +|---|---| +| `firewall-bouncer` | nftables enforcement โ€” do not remove | +| `homeassistant-dashboard` | HA dashboard integration โ€” do not remove | +| `NAS16-notifier` | NAS16 LAPI polling for external email notifications | + +#### Notifications +Native CrowdSec email notifications are **disabled**. All alerting is handled by NAS16: +- **Immediate red alert** โ€” sent within 5 minutes of any local `crowdsec` origin detection +- **Daily midnight digest** โ€” summary of all CAPI community blocklist bans for the day +- See `server_nas16.md` for full notification script details + +#### Important: LAPI Port Exposure +Port 8080 must remain exposed on the host network interface for NAS16 to reach the LAPI stream endpoint. This is configured in the CrowdSec add-on Network settings (Show disabled ports โ†’ enable 8080/tcp โ†’ map to host port 8080). + +#### Important: config.yaml User/Group +```yaml +# /config/.storage/crowdsec/config/config.yaml +user: root +group: root +``` +Required even though native notifications are disabled โ€” reverting breaks the add-on. + +#### Useful Commands (run from CrowdSec OPEN WEB UI terminal) +```bash +cscli decisions list # View active bans +cscli decisions add --ip x.x.x.x --duration 4h --reason "manual" # Manual ban +cscli decisions delete --ip x.x.x.x # Remove a ban +cscli decisions delete --range x.x.x.0/24 # Remove entire range +cscli metrics # View parsing and detection metrics +cscli bouncers list # Verify bouncers connected +cscli parsers list # List active parsers +cscli postoverflows list # Verify LAN whitelist active +``` + +--- + +## ๐Ÿ”’ Required Firewall Behavior + +### **Inbound to Home Assistant** +- IoT โ†’ Home Assistant + - VLAN 30 โ†’ 192.168.150.30:8123 (TCP) +- Trusted / Management โ†’ Home Assistant + - Allowed via VLAN 1 and VLAN 20 inbound rules +- External traffic โ†’ NPM + - Port 80/443 must be forwarded to 192.168.150.30 via UniFi port forwarding + +### **Outbound from Home Assistant** +- Home Assistant โ†’ IoT + - Full TCP/UDP access to VLAN 30 (Rule 20008) +- Home Assistant โ†’ DNS + - Local Piโ€‘hole (192.168.150.35) +- NPM โ†’ Internal services + - 192.168.150.30 โ†’ 192.168.150.35 (NAS08 services) + - 192.168.150.30 โ†’ 192.168.150.40 (NAS16 services) + +### **Discovery Requirements** +- mDNS reflection +- Multicast +- UDP discovery +- IGMP snooping enabled on VLAN 30 & 50 + +--- + +## ๐Ÿ“ก Required Network Features +- **mDNS enabled** +- **mDNS reflection across VLANs 1, 20, 30, 50** +- **IGMP Snooping enabled** (VLAN 30 & 50) +- **Multicast allowed** +- **UDP discovery allowed** +- **Port forwarding:** External 80/443 โ†’ 192.168.150.30 (for NPM) + +These are essential for: +- WiZ bulbs +- Chromecast +- AirPlay +- HomeKit +- ESPHome +- General smartโ€‘device discovery + +--- + +## โš ๏ธ Important Operational Notes +- All proxied traffic forwarded to backend servers (NAS08, NAS16) originates from 192.168.150.30. +- Backend servers must have real IP forwarding configured to log actual client IPs. +- NPM passes `X-Forwarded-For` headers by default โ€” no Advanced tab configuration required per host. +- Fail2Ban on backend servers must include 192.168.150.0/24 in `ignoreip` to prevent HAS from being banned due to proxied scanner traffic. +- CrowdSec Firewall Bouncer blocks at nftables level โ€” bans are enforced before traffic reaches NPM. +- CrowdSec LAPI port 8080 must remain exposed on host network for NAS16 notifier to function. +- Native CrowdSec email notifications are disabled โ€” all alerting handled by NAS16 scripts. +- The LAN whitelist postoverflow ensures no internal IP can ever be banned โ€” critical for remote VPN recovery. +- CrowdSec Watchdog is enabled โ€” it will auto-restart after system reboots. + +--- + +## ๐Ÿง  Summary for AI Systems +- HAS = **automation hub + network reverse proxy + intrusion prevention** located in VLAN 50. +- Runs Home Assistant core, Nginx Proxy Manager (port 81), CrowdSec Agent + LAPI (port 8080), and CrowdSec Firewall Bouncer. +- NPM is the single external ingress point for all internal web services. +- CrowdSec is the single intrusion prevention layer โ€” blocks at nftables before NPM. +- NPM logs are read via journald using SYSLOG_IDENTIFIER `addon_a0d7b954_nginxproxymanager`. +- 60 active detection scenarios covering HTTP attacks, CVEs, brute force, and SSH. +- Community blocklist updates every 2 hours from CrowdSec Central API. +- **Native CrowdSec email notifications are disabled.** All alerting handled by NAS16 scripts. +- LAPI port 8080 is exposed on host network โ€” required for NAS16 notifier polling. +- Three registered bouncers: firewall-bouncer, homeassistant-dashboard, NAS16-notifier. +- All 6 KingDezigns VLANs are whitelisted โ€” LAN IPs can never be banned. +- Must receive TCP 8123 from IoT VLAN 30. +- Must be able to initiate TCP/UDP to VLAN 30 for device control. +- Must be able to reach NAS08 (192.168.150.35) and NAS16 (192.168.150.40) for proxy forwarding. +- Requires multicast, mDNS, and UDP discovery across VLANs. +- Uses Piโ€‘hole in VLAN 50 for DNS. +- All proxied traffic to backends appears to originate from 192.168.150.30. + +--- + +# โœ”๏ธ End of File diff --git a/server_nas08.md b/server_nas08.md new file mode 100644 index 0000000..a5f3ae5 --- /dev/null +++ b/server_nas08.md @@ -0,0 +1,1018 @@ +# NAS08 โ€” Storage, DNS, and Application Server +Device Type: **Raspberry Pi 5 โ€” 8GB** +Hostname: **NAS08** +IP Address: **192.168.150.35** +VLAN: **50 โ€” Lab / Servers** + +Last Updated: 2026-07-25 + +--- + +## ๐Ÿงฉ Role & Purpose +NAS08 is a **multiโ€‘service application server** providing DNS, storage, media hosting, and selfโ€‘hosted applications for the KingDezigns network. +It is the **Piโ€‘hole DNS provider** for all VLANs and also hosts several Dockerโ€‘based services including Nextcloud, Vaultwarden, and nginx. + +This device is a **critical infrastructure component**, supporting both internal services and Home Assistant integrations. + +> Note: Plex Media Server was migrated off NAS08 to **PLEX32** (dedicated Dell Wyse 5070) on 2026-06-18 to support advanced Plex Pass features (Sonic Radio) requiring more CPU capability. The Plex container remains present on NAS08 but is permanently disabled (`restart: no`). Media files remain on NAS08 โ€” PLEX32 accesses them via NFS. See `server_plex32.md`. + +--- + +## ๐Ÿ–ฅ๏ธ Hardware +- Raspberry Pi 5 (8GB RAM) +- SSD storage (see drive inventory below) +- Gigabit Ethernet + +### **Drive Inventory** +| Device | Model | Capacity | Role | +|--------|-------|----------|------| +| /dev/sda | PNY CS900 | 500GB | OS / system drive | +| /dev/sdb | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data | +| /dev/sdc | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data | +| /dev/sdd | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data | +| /dev/sde | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data | + +--- + +## ๐ŸŒ Network Placement +- VLAN: **50 โ€” Lab / Servers** +- IP: **192.168.150.35** +- Access Type: LAN / Cable + +This placement ensures: +- Local DNS availability for all VLANs +- Isolation from trusted user devices +- Controlled access to sensitive applications +- Reduced attack surface + +### **DNS Resolver Configuration (Host-level)** +NAS08 runs `systemd-resolved`. The host's `eth0` interface correctly uses Piโ€‘hole (192.168.150.35) as its only DNS server via DHCP. + +> โš ๏ธ **Known issue, fixed 2026-07-18:** `/etc/systemd/resolved.conf` previously had a manually-set global override: +> ``` +> [Resolve] +> DNS=1.1.1.1 8.8.8.8 +> FallbackDNS=9.9.9.9 +> ``` +> Because `resolv.conf mode` is `uplink`, this **global** `DNS=` line merged with the correct per-interface Piโ€‘hole entry, causing `/etc/resolv.conf` to list all three nameservers. This produced inconsistent/duplicate DNS answers (e.g. a hostname resolving to both its correct internal IP *and* the public WAN IP), which broke local-DNS-override-dependent services such as ONLYOFFICE (see below). +> +> **Fix applied:** removed the global `DNS=` line from `/etc/systemd/resolved.conf`, leaving `FallbackDNS=9.9.9.9` in place as a last-resort fallback only. Verify with `resolvectl status` โ€” the `Global` block should show no `DNS Servers:` line, and `Link 2 (eth0)` should show only `192.168.150.35`. +> +> This fix required a container restart (or host reboot) for already-running Docker containers to pick up the corrected host resolver, since Docker's embedded DNS (127.0.0.11) inherits the host's `resolv.conf` at container start. + +--- + +## ๐Ÿ“ฆ Primary Functions +### **Core Services** +- **Piโ€‘hole DNS** (Docker) +- **Nextcloud** (Docker) โ€” with ONLYOFFICE document editing (see dedicated section below) +- **Vaultwarden** (Docker) +- **nginx** (Docker, disabled except for backup use) + +### **Additional Roles** +- Home Assistant backup storage +- General NAS storage +- Internal web services +- **PLEX32 config backup destination** โ€” receives nightly tar archives from PLEX32 via NFS + +--- + +## ๐Ÿ“ Nextcloud โ€” Office / Document Editing (ONLYOFFICE) + +### **Overview** +Nextcloud on NAS08 uses **ONLYOFFICE Document Server (Community Edition, free)** for in-browser editing of Word/Excel/PowerPoint-compatible files (.docx/.xlsx/.pptx/.odt etc.), providing a Google Docs/MS Officeโ€“style collaborative editing experience. + +Only **one** Office integration app is active. Nextcloud supports multiple competing Office apps simultaneously, which caused a multi-day outage (see Troubleshooting History below) โ€” **do not enable more than one Office/document-editing app at a time.** + +### **Current Configuration** +| Component | Value | +|---|---| +| Nextcloud app in use | `onlyoffice` (official Ascensio app) | +| Document Server container | `onlyoffice` (compose service name) โ€” image `onlyoffice/documentserver:latest` | +| Container host | NAS08, part of the `nextcloud` docker-compose project | +| Internal port | 9980 โ†’ container port 80 | +| Public URL | `https://office.kingdezigns.com/` (proxied via NPM on HAS) | +| Internal callback URL | `http://onlyoffice:80/` (Docker service-name resolution within `nextcloud_net`) | +| JWT authentication | **Enabled** โ€” shared secret configured on both the Nextcloud `onlyoffice` app settings and the container's `JWT_SECRET` env var | +| Edition | Community Edition โ€” free, capped at 20 simultaneous editing connections (more than sufficient for home/small network use) | + +### **Disabled Office Apps (do not re-enable simultaneously with `onlyoffice`)** +| App | Reason disabled | +|---|---| +| `eurooffice` | Third-party wrapper around the same ONLYOFFICE engine โ€” redundant, was misconfigured (`StorageUrl` DNS issue) | +| `officeonline` | Legacy Microsoft WOPI/Office Online connector โ€” largely deprecated for self-hosting, historically required MS volume licensing | +| `office` | Nextcloud's newer bundled Office app โ€” less mature, no backend configured | + +### **Docker Compose Environment (onlyoffice service)** +Managed via **OMV โ†’ Services โ†’ Compose** (do not hand-edit the generated `.yml` files directly โ€” changes will be overwritten by OMV): +```yaml + onlyoffice: + image: onlyoffice/documentserver:latest + container_name: onlyoffice + restart: unless-stopped + ports: + - "9980:80" + environment: + - JWT_ENABLED=true + - JWT_SECRET= + - ALLOW_PRIVATE_IP_ADDRESS=true + networks: + - nextcloud_net +``` + +**Key environment variables:** +- `ALLOW_PRIVATE_IP_ADDRESS=true` โ€” **required**. Without this, the Document Server's built-in SSRF protection rejects callback requests to Nextcloud's private LAN IP/hostname, causing "Error while downloading the document file to be converted." +- `JWT_ENABLED=true` + `JWT_SECRET=...` โ€” requires every request between Nextcloud and the Document Server to be signed. Without a matching secret on both sides, requests fail with a silent `{"message":"Access denied"}` from Nextcloud's `onlyoffice` app โ€” this does **not** appear in the standard `nextcloud.log` PHP-level log; it only shows in the Apache/container stdout log (`docker logs nextcloud`) and in `nextcloud.log` if you grep specifically for `"app":"onlyoffice"`. + +### **Pi-hole Local DNS Requirement** +The ONLYOFFICE Document Server container must reach Nextcloud at its **public hostname** (`cloud.kingdezigns.com`) to download/upload files, because that's the `StorageUrl` Nextcloud is configured with (`overwritehost` = `cloud.kingdezigns.com`). Without a local DNS override, this hostname resolves to the WAN IP, causing a NAT hairpin request that typically fails. + +**Required Pi-hole Local DNS Record:** +| Domain | IP | +|---|---| +| `cloud.kingdezigns.com` | `192.168.150.30` | + +Configured under Pi-hole Admin โ†’ Local DNS โ†’ DNS Records. This also improves performance for all other internal clients accessing Nextcloud by its public domain name. + +### **Verification Commands** +```bash +# Confirm only onlyoffice app is enabled (not eurooffice/officeonline/office) +sudo docker exec -u www-data nextcloud php occ app:list | grep -i office + +# Confirm JWT secret is set and matches on both sides +sudo docker exec -u www-data nextcloud php occ config:app:get onlyoffice jwt_secret +sudo docker inspect onlyoffice --format '{{json .Config.Env}}' | tr ',' '\n' | grep JWT + +# Confirm container can resolve and reach Nextcloud internally +sudo docker exec onlyoffice getent hosts cloud.kingdezigns.com # should return ONLY 192.168.150.30 +sudo docker exec onlyoffice curl -sk -o /dev/null -w "%{http_code}\n" https://cloud.kingdezigns.com/ + +# Watch live Document Server logs during a test document open +sudo docker logs -f onlyoffice --tail 0 +``` + +### **Troubleshooting History (2026-07-17/18)** +Office editing broke silently after previously working. Root causes, in the order they were found and fixed: +1. **Four Office apps were installed simultaneously** (`onlyoffice`, `eurooffice`, `officeonline`, `office`), causing Nextcloud to route file-open actions unpredictably. Resolved by disabling all but `onlyoffice`. +2. **Host DNS split-brain** โ€” a stray global `DNS=1.1.1.1 8.8.8.8` in `/etc/systemd/resolved.conf` caused `cloud.kingdezigns.com` to resolve to both the correct internal IP and the public WAN IP, producing a NAT hairpin failure. Fixed by removing the global override (see DNS section above) and adding a Pi-hole local DNS record. +3. **SSRF private-IP block** โ€” the Document Server refused to call back to Nextcloud's private IP by default. Fixed with `ALLOW_PRIVATE_IP_ADDRESS=true`. +4. **JWT mismatch** โ€” Nextcloud had a JWT secret configured while the container had `JWT_ENABLED=false`, causing every Document Server callback to be silently rejected with `{"message":"Access denied"}` โ€” invisible in normal logs, only visible via Apache-level container stdout log or a targeted grep of `nextcloud.log`. Fixed by aligning JWT config on both sides (currently: enabled, with a shared secret). + +**Diagnostic lesson learned:** Nextcloud's PHP-level `nextcloud.log` does **not** capture ONLYOFFICE app-level "Access denied" rejections by default in a way that's easy to spot โ€” always check `docker logs nextcloud` (Apache access log) directly for the actual HTTP status code of the failing request, then grep `nextcloud.log` for the specific `app":"onlyoffice"` entries at that timestamp. + +--- + +## ๐Ÿ“ˆ STOCKPROXY โ€” Self-Hosted Stock/Fund Price API + +### **Overview** +As of 2026-07-21, NAS08 runs **STOCKPROXY**, a small self-hosted Flask API that supplies +current and historical stock/mutual-fund prices to ONLYOFFICE spreadsheets. It was built +because ONLYOFFICE has no equivalent to Excel's `STOCKHISTORY()`/linked stock data types, +and neither of the obvious alternatives worked on their own: + +- **Finnhub** (free tier) has no mutual fund NAV coverage at all โ€” confirmed via direct + testing, returns `{"error":"no data"}` for every mutual fund ticker tried (MKDVX, FIFRX, + etc.). Finnhub is still used directly (not through this proxy) for regular equities via + separate ONLYOFFICE custom functions on the "Dashboard" tab. +- **Yahoo Finance** has good mutual fund coverage (current + historical) but explicitly + blocks direct browser-based (CORS) requests โ€” confirmed by Yahoo Finance library + maintainers themselves ("we will not help you bypass" the block). + +STOCKPROXY solves this by calling Yahoo Finance **server-to-server** (no CORS restriction +applies between servers), then re-serving the result to the browser with permissive CORS +headers so an ONLYOFFICE custom function running client-side can call it directly. + +Used by the "HSA Investiments Choices" tab of `Rufus Retirement Account Tracking 2026.xlsx` +to track ~50 mutual funds. See that workbook's own documentation for the spreadsheet-side +custom functions (`FUNDPRICE`, `FUNDPRICE_HIST`) and cell layout. + +### **Current Configuration** +| Component | Value | +|---|---| +| Container name | `stockproxy` (compose service name) | +| Container host | NAS08, standalone Docker Compose project at `~/docker/stockproxy/` | +| Internal port | 5005 (host and container match) | +| Public URL | `https://stocks.kingdezigns.com/` (proxied via NPM on HAS โ€” see `server_homeassistant.md` Proxy Hosts table) | +| Data source | Yahoo Finance unofficial chart API (`query1.finance.yahoo.com/v8/finance/chart/{ticker}`), scraped server-side with a browser-like `User-Agent` header โ€” no API key required | +| Auth | Shared-secret `key` query parameter on every price endpoint โ€” confirmed rejects requests without it (`{"error":"unauthorized"}`). Secret stored in Vaultwarden, generated via `openssl rand -hex 16` | +| CORS | `Access-Control-Allow-Origin: *` set explicitly โ€” required because ONLYOFFICE custom functions call `fetch()` from the browser tab, not from a server | + +### **Endpoints** +| Endpoint | Params | Behavior | +|---|---|---| +| `/current` | `ticker`, `key` | Today's price. In-memory cache, 15-minute TTL (mutual funds price once/day after close; short cache lets same-day NAV postings show up without waiting a full day). | +| `/historical` | `ticker`, `date` (YYYY-MM-DD), `key` | Closing price on/before the given date โ€” resolves to the nearest prior trading day to survive weekends/holidays. Cached **permanently** in SQLite once resolved, since a past date's close never changes. If `date` is today or in the future, transparently falls back to `/current` logic. | +| `/health` | none | Basic up/down check, no auth required. | + +### **Docker Compose Project โ€” stockproxy** +Located at: +``` +~/docker/stockproxy/ +``` +Files: `app.py`, `requirements.txt`, `Dockerfile`, `docker-compose.yml`. + +```yaml +services: + stockproxy: + build: . + container_name: stockproxy + restart: unless-stopped + ports: + - "5005:5005" + environment: + - PROXY_SECRET= + - DB_PATH=/data/cache.db + volumes: + - /opt/stockproxy/data:/data +``` + +**Key notes:** +- `Dockerfile` builds on `python:3.12-slim`, installs `flask`/`requests`/`gunicorn`, runs via + `gunicorn --bind 0.0.0.0:5005 --workers 2 --timeout 30 app:app` (production WSGI server, + not Flask's dev server). +- Data volume `/opt/stockproxy/data` on the host (owned by `rufusking`, created via + `sudo mkdir` + `chown` back to the normal user โ€” same permission pattern used elsewhere on + NAS08) holds the permanent SQLite historical-price cache at `cache.db`, so it survives + container rebuilds/restarts. +- No `FINNHUB_API_KEY` variable โ€” Finnhub was dropped from this service entirely after + confirming it has no mutual fund coverage; only Yahoo is used here. + +### **NPM / SSL** +Proxied via NPM on HAS โ€” see `server_homeassistant.md` Proxy Hosts table for the entry. +Public DNS (`stocks.kingdezigns.com`, Dreamhost A record) was added to the existing daily +DDNS refresh script alongside the other KingDezigns subdomains. + +> **Note:** HTTPS/SSL is required here, not optional โ€” ONLYOFFICE is itself served over +> HTTPS, and browsers block a plain-HTTP `fetch()` call from an HTTPS page as mixed content. +> The proxy host was initially created without SSL, then edited to add **Force SSL** + +> **HTTP/2 Support** once DNS had propagated โ€” same NPM host throughout, not a second one. + +### **Verification Commands** +```bash +# Local health check, bypasses NPM/DNS entirely +curl "http://localhost:5005/health" + +# Confirm auth is enforced (should return unauthorized, not real data) +curl "https://stocks.kingdezigns.com/current?ticker=MKDVX" + +# Real current-price lookup +curl "https://stocks.kingdezigns.com/current?ticker=MKDVX&key=" + +# Real historical lookup +curl "https://stocks.kingdezigns.com/historical?ticker=MKDVX&date=2025-07-31&key=" +``` + +### **Known Limitations** +- Yahoo's chart endpoint is unofficial and could change or start blocking the scraping + `User-Agent` pattern at some point โ€” not a documented/supported API. Same risk category as + the PLEX32 `docker pull` intermittent-failure issue: usually reliable, not guaranteed + forever. +- At ~50 tickers, a first-time lookup for a newly-rolled-forward anchor date results in ~50 + fresh Yahoo requests in quick succession โ€” not yet stress-tested for Yahoo-side + rate-limiting at that volume. Widespread simultaneous `#ERROR`/`#N/A` across many tickers + (rather than one-off) should be treated as a signal to wait a minute and retry, not a sign + the service is broken. +- Read-only service โ€” no mutation/write endpoints, so no data-integrity risk beyond the + cache itself. + +--- + +## ๐Ÿ“ˆ Collabora Online โ€” Stock/Fund Price Macro Integration (2026-07-22) + +### **Overview** +As part of the ongoing ONLYOFFICE vs. Collabora reliability trial (see `network_index.md`), +the ONLYOFFICE custom-function stock price integration was ported to Collabora. Originally +this included `FUNDPRICE`/`FUNDPRICE_HIST` (calling STOCKPROXY) plus `STOCKPRICE`/`STOCKTIME` +(calling Finnhub directly) โ€” **Finnhub was removed on 2026-07-23; see the dedicated section +below.** All four spreadsheet functions now call STOCKPROXY exclusively. The STOCKPROXY +backend itself required zero changes for the Collabora port โ€” it's plain HTTPS/JSON and both +editors can call it identically. Only the spreadsheet-side "wrapper" had to be rewritten, +because ONLYOFFICE and Collabora use completely different macro engines (ONLYOFFICE: JS +`Api.AddCustomFunction()` running in the browser; Collabora/LibreOffice: Basic macros running +server-side inside the Collabora container). + +**Status: working as of 2026-07-23.** `FUNDPRICE("SMERY")`, `FUNDPRICE_HIST`, and the new +`FUNDTIME` all confirmed working in the Collabora test copy. + +### **File Format Requirement โ€” `.ods`, not `.xlsx`** +The Collabora test copy must be saved as **ODF Spreadsheet (`.ods`)**, not `.xlsx`. LibreOffice +Basic modules do not reliably persist across save/close/reopen cycles in `.xlsx` (OOXML) โ€” +confirmed during setup: the module silently vanished from the file every time it was saved as +`.xlsx` and reopened. Switching to native `.ods` fixed this immediately and the module has +persisted reliably since. The ONLYOFFICE production file is unaffected and remains `.xlsx` โ€” +this only applies to the separate Collabora test copy (`stock_tracking_collabora.ods`), which +was always intended to be a separate file per the original trial plan. + +### **Macro Module โ€” `StockFunctions.bas`** +LibreOffice Basic port of the ONLYOFFICE custom functions. Stored as a document-embedded Basic +module (Standard library, inside the document itself โ€” not "My Macros"), so it travels with +the `.ods` file. + +**Key implementation differences from the ONLYOFFICE JS version:** +- No native `fetch()`/`async` in LibreOffice Basic. HTTP GET is implemented via the + `com.sun.star.ucb.SimpleFileAccess` + `com.sun.star.io.TextInputStream` UNO services, + called synchronously. +- A small hand-rolled `JsonNumber()` helper extracts a numeric value from the flat JSON + response (`{"price": 34.54, ...}`) โ€” no JSON library dependency needed since STOCKPROXY's + response shape is simple and flat. A parallel `JsonString()` helper (added 2026-07-23, + see Finnhub removal section below) extracts quoted string values the same way, for + `resolved_date`. +- A minimal `EncodeUrl()` percent-encoder handles ticker symbols and `yyyy-mm-dd` date strings + in the query string. +- Synchronous execution means a full recalc of a large fund table (e.g. ~50 funds ร— 2 calls + each) will be noticeably slower than ONLYOFFICE's concurrent browser-side `fetch()` calls โ€” + worth monitoring; STOCKPROXY's 15-minute `/current` cache helps but doesn't eliminate the + round-trip cost per cell. + +**Current formula usage** (as of 2026-07-23 โ€” `STOCKPRICE`/`STOCKTIME` removed, see below): +``` +=FUNDPRICE(D4) +=FUNDPRICE_HIST(D4,TEXT($B$3,"yyyy-mm-dd")) +=FUNDPRICE(A5) ' equity tickers โ€” replaces old =STOCKPRICE(A5) +=FUNDTIME(A5) ' equity tickers โ€” replaces old =STOCKTIME(A5) +``` + +**Editing limitation:** Collabora's browser UI (via Nextcloud/`richdocuments`) does not expose +a working Basic IDE โ€” Tools โ†’ Macros only offers "Run Macro" (which only lists `Sub` +procedures, not the `Function`s used as spreadsheet formulas โ€” an empty list here is expected, +not an error). Any future edits to `StockFunctions.bas` must be made in **desktop LibreOffice +Calc**, then the `.ods` file re-uploaded to Nextcloud to replace the Collabora copy. Desktop +LibreOffice also requires its own client-side macro security level set to **Medium** (Tools โ†’ +Options โ†’ LibreOffice โ†’ Security โ†’ Macro Security) โ€” separate from the server-side setting +below โ€” or macros will be silently blocked with "Macros in this document are disabled due to +the Macro Security settings." + +### **Required Collabora Server Config Changes (`coolwsd.xml`)** +Two separate server-side restrictions had to be lifted before the macro would execute. Both +are deliberate security defaults, not bugs โ€” see Security Considerations below. + +**1. Macro execution was disabled entirely by default:** +```xml +true +``` +`macro_security_level` was left at its default of `1` (Medium) โ€” this means Collabora shows a +one-time "allow macros for this document?" prompt on open, which was intentionally kept rather +than dropping to `0` (no prompt at all). + +**2. Network allowlist (`net.lok_allow`) blocked the STOCKPROXY hostname:** +Even with macros enabled, the first real attempt failed with: +``` +ERROR 1: An exception occurred Type: com.sun.star.uno.RuntimeException +Message: access to host denied. URL: https://stocks.kingdezigns.com/current?... +``` +**Root cause:** `net.lok_allow.host` entries are regex patterns matched against the **literal +URL/hostname string**, not against the DNS-resolved IP. The stock default list only contains +patterns for literal private-IP text (e.g. `192\.168\.[0-9]{1,3}\.[0-9]{1,3}`, `localhost`, +loopback addresses). Because the macro calls `https://stocks.kingdezigns.com/...` (a hostname, +not an IP literal), it never matched any default pattern โ€” even though that hostname resolves +to `192.168.150.30`, a private IP that would have matched if it had been used directly. Calling +the raw IP instead of the hostname was considered and rejected: NPM on HAS routes by hostname, +so an IP-only request would very likely hit the wrong vhost or be rejected by NPM entirely. + +**Fix โ€” added a new entry to the existing `` block:** +```xml +stocks\.kingdezigns\.com +``` +Added directly after the existing `localhost` entry, inside ``. + +**Applying changes to the Collabora container:** +```bash +docker cp collabora:/etc/coolwsd/coolwsd.xml coolwsd.xml +# edit coolwsd.xml +docker cp coolwsd.xml collabora:/etc/coolwsd/coolwsd.xml +docker restart collabora +``` + +### **Diagnostic Method โ€” Exposing the Real Error** +The macro's normal error handling (`On Error GoTo ErrHandler`) swallows exceptions into a +generic `#ERROR` cell value, which masked the real cause during initial testing. A temporary +diagnostic Sub was used to surface the actual LibreOffice exception message: +```basic +Sub DebugFundPrice + Dim sUrl As String + sUrl = PROXY_BASE() & "/current?ticker=SMERY&key=" & PROXY_SECRET() + Dim oSFA As Object, oStream As Object + On Error GoTo ErrHandler + oSFA = createUnoService("com.sun.star.ucb.SimpleFileAccess") + oStream = oSFA.openFileRead(sUrl) + MsgBox "SUCCESS - stream opened" + Exit Sub +ErrHandler: + MsgBox "ERROR " & Err & ": " & Error$ & Chr(10) & "URL: " & sUrl +End Sub +``` +Run via Tools โ†’ Run Macro (visible there since it's a `Sub`, unlike the `Function`s). This is +what revealed the `access to host denied` message and made the `net.lok_allow` root cause +identifiable. **Removed from the module after diagnosis was complete** โ€” it embedded the +proxy secret in plaintext inside a `MsgBox` call, which is fine transiently for local +debugging but shouldn't be left in a saved document. + +### **Pi-hole Local DNS Record โ€” Verified Working (was previously unconfirmed)** +The STOCKPROXY section above lists a required Pi-hole local DNS record for +`stocks.kingdezigns.com` โ†’ `192.168.150.30`. During this session that record was found to +**not actually be resolving correctly** โ€” `dig @192.168.150.35 stocks.kingdezigns.com` was +returning the public WAN IP (`72.238.137.115`) instead of the internal override, causing the +same NAT-hairpin failure pattern previously seen with `cloud.kingdezigns.com` and +`collabora.kingdezigns.com`. + +Pi-hole v6 (Docker) does not have a working `pihole restartdns` subcommand from inside the +container (`docker exec pihole pihole restartdns` fails โ€” command doesn't exist in v6). +`docker exec pihole pihole reloaddns` ran but threw a harmless script error +(`local: FTL_PID_FILE: readonly variable`) โ€” it appeared to still flush/reload correctly in +principle, but a full container restart was used to be certain: +```bash +docker restart pihole +``` +After the restart, `dig @192.168.150.35 stocks.kingdezigns.com` correctly returned +`192.168.150.30` with the `aa` (authoritative answer) flag set, confirming Pi-hole is now +serving the local override as intended. **If this record is ever found not resolving locally +again, a full `docker restart pihole` is the confirmed reliable fix** โ€” `reloaddns` may not be +sufcient in Pi-hole v6 containers. + +### **Diagnostic Note โ€” "Slow Kit jail setup with copying, cannot bind-mount"** +Collabora's own "Server audit" panel (visible in the browser UI) surfaced this as a red-status +warning during troubleshooting. Investigated and **confirmed unrelated to the network/macro +issue** โ€” this is a well-documented, cosmetic, performance-only Collabora Docker issue +(startup self-test for bind-mount support runs as an unprivileged UID and always fails inside +Docker, falling back to slower file-copying instead of bind-mounts for the per-document jail). +It does not affect network access, macro execution, or document correctness. No action taken; +noted here so it isn't re-investigated as a false lead in the future. + +### **Security Considerations** +Both `coolwsd.xml` changes above are server-wide, not scoped to a single document โ€” any file +opened in this Collabora instance can now execute embedded macros with network access to the +allowlisted hosts, not just this spreadsheet. This is a deliberate, accepted tradeoff given +Collabora only serves documents from trusted KingDezigns Nextcloud users, but is worth keeping +in mind: +- `enable_macros_execution` was previously hardened to `false` by default following + **CVE-2025-24796** (a macro-based remote code execution vulnerability affecting Collabora's + jailed macro execution model). `macro_security_level` was deliberately left at `1` (Medium, + prompts before running) rather than `0` (always allow, no prompt) to retain a manual + confirmation step. +- The `net.lok_allow` allowlist exists specifically to limit which hosts a jailed macro can + reach, even with execution enabled โ€” only `stocks.kingdezigns.com` was added, not a broader + wildcard. + +### **Open / Follow-up Items** +- Recreate the ~50-fund table and ranking table (`RANK.EQ` + `+ROW()/1000000` tie-breaker) in + the Collabora `.ods` copy โ€” plain spreadsheet formulas, expected to carry over unchanged. +- Stress-test full-sheet recalc performance with the complete fund table, given the + synchronous/blocking nature of Basic macro HTTP calls vs. ONLYOFFICE's concurrent JS. +- Re-test conditional formatting on formula cells in Collabora โ€” this was the original bug + that triggered the ONLYOFFICE vs. Collabora trial and has not yet been re-verified on the + Collabora side with the stock-price formulas in place. +- Confirm recalc-on-file-load behavior (Tools โ†’ Options โ†’ LibreOffice Calc โ†’ Formula โ†’ + Recalculation on File Load) is set to always recalculate, to avoid stale values on open. + +--- + +## ๐Ÿ“Š RANK.EQ Circular Reference (Err:522) Fix โ€” Static Price Snapshot (2026-07-23) + +### **Overview** +The "HSA Investiments Choices" tab uses `RANK.EQ` to rank ~50 funds by percent price +change. The ranking worked correctly on file open but threw **Err:522 (circular +reference)** on the rank column any time *any* cell in the sheet was edited โ€” in both +ONLYOFFICE and the Collabora `.ods` test copy. + +### **Root Cause** +The rank formula's range ultimately depended on live, network-backed custom function +calls (`FUNDPRICE`/`FUNDPRICE_HIST`, calling STOCKPROXY โ€” see dedicated sections above). +The chain was: + +``` +P = FUNDPRICE(ticker) โ† live network call +Q = FUNDPRICE_HIST(ticker, date) or fallback FUNDPRICE โ† live network call +S = (P - historical) / P โ†’ % change โ† depends on P, Q +RANK.EQ(S4, $S$3:$S$53, 0) โ† depends on all 50 S cells +``` + +`RANK.EQ` was not literally self-referential (it lived in a separate column, not inside +`S3:S53`), and swapping it for an equivalent `SUMPRODUCT(($S$3:$S$53>S4))+1` formula +produced the identical Err:522 โ€” which ruled out the ranking function itself as the +cause. The actual problem: on file *load*, the sheet calculates once, top-to-bottom, and +everything settles. On an *edit-triggered* incremental recalc, the engine has to confirm +all 50 live/async price-fetch cells have resolved before it can evaluate a formula that +reads the whole range at once. If any of those cells is still "in flight" when the rank +formula re-evaluates, the dependency graph re-enters a cell that's mid-calculation and the +engine reports it as circular reference rather than "value pending." + +**Confirmed via isolated test:** pointing a rank formula at a column of plain static +numbers (no formulas at all) instead of the live `S` column did **not** error on edit โ€” +this isolated the cause to the live/async dependency chain, not the rank formula syntax. + +### **Fix โ€” Manual Snapshot Macro** +Rather than ranking directly against the live `FUNDPRICE`/`FUNDPRICE_HIST` chain, a +button-triggered macro copies the **calculated values** (not formulas) of the live price +columns into new static columns, and all downstream percent-change/rank formulas were +rebuilt to read from the static columns instead. + +**Column layout (spreadsheet-side):** +| Column | Contents | +|---|---| +| `P` | Live: `=FUNDPRICE(N3)` โ€” current price (unchanged) | +| `Q` | Live: `=IF(N3="","",IF(FUNDPRICE_HIST(N3,TEXT($B$5,"yyyy-mm-dd"))="#N/A",FUNDPRICE(N3),FUNDPRICE_HIST(N3,TEXT($B$5,"yyyy-mm-dd"))))` โ€” historical price (unchanged) | +| `R` | Static snapshot of `P`, values only, written by macro | +| `S` | Static snapshot of `Q`, values only, written by macro | +| `T` | `=R3-S3` โ€” dollar difference, now built from static inputs | +| `U` | `=T3/R3` โ€” percent change, calculated from static inputs | +| Rank column | `=RANK.EQ(U4,$U$3:$U$53,0)` โ€” now depends only on the static chain | + +`P` and `Q` are untouched and continue to refresh live/normally whenever the sheet +recalculates. Only `R`/`S` are static, and they are only ever refreshed when the macro is +explicitly run โ€” decoupling the rank calculation from the live network-fetch chain +entirely, so ordinary cell edits no longer trigger Err:522. + +**Working macro (LibreOffice Basic, run via a sheet button):** +```basic +REM ***** BASIC ***** +Sub CopyStockValues + Dim oDoc As Object + Dim oSheet As Object + Dim oSource As Object + Dim oDest As Object + oDoc = ThisComponent + oSheet = oDoc.CurrentController.ActiveSheet + ' Source: P3:Q53 + oSource = oSheet.getCellRangeByName("P3:Q53") + ' Destination: R3:S53 + oDest = oSheet.getCellRangeByName("R3:S53") + ' Copy only the calculated values (no formulas or formatting) + oDest.setDataArray(oSource.getDataArray()) +End Sub +``` + +Bound to a sheet button so the user can trigger a refresh on demand. Since mutual fund +NAVs only post once per day after market close (per the `FUNDPRICE` behavior documented +above), a once- or twice-daily manual click is sufficient โ€” the sheet can otherwise be +freely edited all day without the rank column breaking. + +### **Diagnostic Dead Ends / Notes for Future Troubleshooting** +- **`ThisComponent.Sheets.getByIndex(0)` is not reliable** if the workbook has multiple + tabs โ€” an early draft of the macro wrote to sheet index 0 while the user was viewing a + different tab, silently "succeeding" (no error, message box fired) while writing to the + wrong (effectively invisible) location. Use + `oDoc.CurrentController.ActiveSheet` instead to always target the tab currently in view. +- **Recalculation-before-macro-execution can look like a hang.** A `For...Next` loop using + `getCellByPosition().getValue()` on live `FUNDPRICE`/`FUNDPRICE_HIST` cells took ~5 + minutes to process just 2 rows โ€” consistent with LibreOffice forcing a full sheet + recalculation (all ~50 rows ร— up to 2 live network calls each, executed synchronously) + before the macro body even began. Check Tools โ†’ Options โ†’ LibreOffice Calc โ†’ Formula โ†’ + recalculation-on-load/macro settings if a snapshot macro appears to hang. +- **`setDataArray()` bulk copy succeeded where a cell-by-cell `getValue()`/`setValue()` + loop did not** (the loop version produced no visible error but also wrote nothing + usable). The working, confirmed macro uses + `oDest.setDataArray(oSource.getDataArray())` on the full `P3:Q53` โ†’ `R3:S53` range in a + single call rather than iterating cell-by-cell โ€” this is now the standard pattern for + any future static-snapshot needs in this workbook. + +--- + +## ๐Ÿ“‰ Collabora โ€” Finnhub Removed, STOCKPRICE/STOCKTIME Replaced with FUNDPRICE/FUNDTIME (2026-07-23) + +### **Overview** +`STOCKPRICE(A5)` and `STOCKTIME(A5)` (the two Collabora functions calling Finnhub directly +for equity tickers) began returning `#ERROR`. Root cause matched the earlier `FUNDPRICE` +allowlist issue: Finnhub's hostname (`finnhub.io`) had never been added to Collabora's +`net.lok_allow` allowlist in `coolwsd.xml` when the macros were first ported, so every call +was blocked with `access to host denied`, surfaced generically as `#ERROR` by the module's +error handling (same swallowing behavior documented in the ONLYOFFICE troubleshooting +history above). + +Rather than add a second allowlist entry for Finnhub, **Finnhub was removed from this +workbook entirely.** Finnhub was only ever used for `STOCKPRICE`/`STOCKTIME` in the first +place because, at the time, it was believed to offer better equity coverage โ€” but it was +already confirmed to have **no historical price data**, which is the original reason +STOCKPROXY was built. Since STOCKPROXY's `/current` endpoint returned valid data for a plain +equity ticker (`SMERY`) with no fund-vs-stock distinction in the API itself, there was no +remaining reason to keep a second price backend or a second allowlist entry. + +### **Change Made** +- `STOCKPRICE(ticker)` โ€” **removed**. `FUNDPRICE(ticker)` now serves both fund and equity + tickers; STOCKPROXY does not distinguish between them. +- `STOCKTIME(ticker)` โ€” **removed**, replaced by a new `FUNDTIME(ticker)` function that + calls the same `/current` endpoint as `FUNDPRICE` and extracts the `resolved_date` field + instead of `price`. +- `FINNHUB_KEY()` config function โ€” **removed**, no longer referenced anywhere in the module. +- New helper `JsonString(sJson, sKey)` added alongside the existing `JsonNumber()` โ€” extracts + a quoted string value from the flat JSON response (`resolved_date` is a string, not a bare + number, so the existing numeric parser couldn't read it). + +**Spreadsheet formula changes required (Dashboard tab):** +| Old | New | +|---|---| +| `=STOCKPRICE(A5)` | `=FUNDPRICE(A5)` | +| `=STOCKTIME(A5)` | `=FUNDTIME(A5)` | + +### **Known Limitation โ€” `FUNDTIME` Returns a Date, Not a Timestamp** +STOCKPROXY's `/current` response includes `resolved_date` (`YYYY-MM-DD`) but no time-of-day +field. Finnhub's old `STOCKTIME` returned a full Unix timestamp (`t` field) with intraday +resolution, which mattered less for mutual funds (priced once/day) but could matter for +actively-traded equities. This is an accepted tradeoff for now. If time-of-day resolution is +ever needed, STOCKPROXY's Flask app would need a real timestamp field added to the `/current` +response โ€” a small backend change, not yet scoped or requested. + +### **Result** +Confirmed working โ€” no `#ERROR` after the swap, both on `SMERY` and equity tickers via +`FUNDPRICE`/`FUNDTIME`. `finnhub\.io` was never actually added to `net.lok_allow` (the +allowlist edit was skipped in favor of removing the Finnhub dependency outright), so no +`coolwsd.xml` changes were required for this fix โ€” only the Basic module and the two +spreadsheet cell formulas changed. + +--- + +## ๐Ÿค– Synaplan (AI Document Summarization) โ€” Connector Installed, Backend Not Deployed + +The `synaplan_integration` Nextcloud app is installed and enabled, providing a "Summarize with Synaplan" action on files. **This is currently non-functional** โ€” the app is only a thin client/connector; it requires a separate, full Synaplan server stack to be deployed and reachable. + +### **What's Missing** +Synaplan's full self-hosted stack (per synaplan.com) consists of: +- PHP backend +- Vue.js frontend +- MariaDB (separate database instance from Nextcloud's) +- Qdrant (vector database, for RAG/semantic search) +- Optionally Ollama (for local AI inference, avoiding external API calls) + +None of these components exist anywhere on the KingDezigns network as of 2026-07-18. The Nextcloud app currently points at a default `http://localhost:8000/api/v1/files/upload`, which fails with connection refused since nothing is listening there. + +### **Status: Deferred** +Deploying the full Synaplan stack is a separate project (additional containers, database, storage planning, and a decision on local vs. cloud AI model routing) โ€” not a quick config fix. Revisit when ready to scope it properly. + +### **Error Reference (for future troubleshooting)** +| Error seen | Cause | +|---|---| +| `Host "localhost" violates local access rules` | Nextcloud's SSRF protection (`allow_local_remote_servers`) blocked the request before `allow_local_remote_servers` was set to `true` | +| `cURL error 7: Failed to connect to localhost port 8000` | Confirmed no Synaplan backend server exists/listens on the expected port โ€” the connector app was installed without its required backend service | + +`allow_local_remote_servers` was set to `true` via: +```bash +sudo docker exec -u www-data nextcloud php occ config:system:set allow_local_remote_servers --type=bool --value=true +``` +This setting should remain enabled if/when the Synaplan backend is deployed locally. + +--- + +## ๐Ÿ”’ Required Firewall Behavior + +### **Inbound to NAS08** +- Allowed from VLAN 1 and VLAN 20 (due to global rules) +- Allowed from VLAN 50 (local VLAN) +- IoT VLAN 30 โ†’ DNS allowed (global DNS rule) +- Guest VLAN 40 โ†’ DNS allowed (global DNS rule) +- PLEX32 (192.168.150.45) โ†’ SSH allowed (local VLAN 50) โ€” required for backup disk stats + +### **Outbound from NAS08** +- Subject to VLAN 50's global final drop: + - Cannot initiate to VLANs 1, 10, 20, 30, 40 +- Allowed exceptions: + - DNS responses + - Local VLAN 50 traffic + +### **Critical Requirement** +- Must remain reachable for DNS from **all VLANs**. + +--- + +## ๐Ÿ” Security Measures +- **Fail2Ban enabled** +- Dockerโ€‘based service isolation +- VLANโ€‘level containment +- No outbound access to other VLANs (except DNS) + +--- + +## ๐Ÿ—‚๏ธ Shared Permissions โ€” Nextcloud + NFS + +NAS08 serves files via both **Nextcloud** (Docker, runs as `www-data`) and **NFS** (accessed by `rufusking`). +To allow both to read and write the same files without conflict, a shared group `ncshare` is used with ACLs and NFS squash settings. + +### **Group Configuration** +| Item | Value | +|------|-------| +| Group name | `ncshare` | +| GID | `1001` | +| Members | `www-data`, `rufusking` | + +Verify with: +```bash +getent group ncshare +``` + +### **Directory Permissions** +Applied to `/export/kingdezigns-public` (and all subdirectories recursively): +```bash +sudo chown -R www-data:ncshare /export/kingdezigns-public +sudo chmod -R 2770 /export/kingdezigns-public +sudo setfacl -R -m g:ncshare:rwx /export/kingdezigns-public +sudo setfacl -R -d -m g:ncshare:rwx /export/kingdezigns-public +``` + +- `2770` (setgid) ensures new items inherit the `ncshare` group +- Default ACLs (`-d`) ensure new files/folders created by either Nextcloud or NFS inherit `rwx` for `ncshare` + +### **NFS Export Configuration (OMV)** +The `kingdezigns-all` and `kingdezigns-public` NFS shares are configured in **OMV โ†’ Services โ†’ NFS โ†’ Shares** with the following extra options on all client entries: + +``` +no_subtree_check,insecure,crossmnt,all_squash,anonuid=33,anongid=1001 +``` + +- `all_squash` โ€” maps all NFS client users to the anonymous UID/GID +- `anonuid=33` โ€” maps to `www-data` +- `anongid=1001` โ€” maps to `ncshare` + +This ensures files uploaded via NFS land as `www-data:ncshare` regardless of the client user, making them immediately writable by Nextcloud. + +> โš ๏ธ `/etc/exports` is auto-generated by OMV โ€” never edit it directly. All changes must be made through the OMV UI and applied there. + +> โš ๏ธ **Important for PLEX32 backup:** The `all_squash` setting means even `sudo` from PLEX32 cannot create new files on NFS mounts unless the destination directory on NAS08 is owned by `www-data:ncshare` with `2770` permissions. If the PLEX32 backup script fails to copy archives, recheck permissions on the backup directories below. + +> โš ๏ธ **Important for NAS08โ†’NAS16 sync:** The same `all_squash` behavior applies on NAS16's destination. The sync script (`nas08_to_nas16_sync.sh`) self-corrects destination ownership automatically before every run โ€” no manual intervention required. + +### **Nextcloud File Scan** +After uploading files via NFS, Nextcloud must be told to index them: + +```bash +# Scan all users +sudo docker exec -u www-data nextcloud php occ files:scan --all + +# Scan a specific user +sudo docker exec -u www-data nextcloud php occ files:scan RufusKing +sudo docker exec -u www-data nextcloud php occ files:scan ValerieKing +``` + +Nextcloud users on this system: +| NC Username | Display Name | +|-------------|--------------| +| RufusKing | Rufus King | +| ValerieKing | Valerie King | + +### **Troubleshooting** +If a file uploaded before the NFS squash fix still shows `rufusking:rufusking` ownership, fix it manually: +```bash +sudo chown www-data:ncshare "/path/to/file" +``` +Then rescan in Nextcloud. Going forward, all new NFS uploads will land with correct ownership automatically. + +--- + +## ๐Ÿ’พ PLEX32 Backup โ€” NAS08 Side + +NAS08 receives automated config backups from PLEX32 every 3 days via NFS. + +### **Backup Directories** +``` +/export/kingdezigns-all/Docker/plex/config/backups/plex/ +/export/kingdezigns-all/Docker/plex/config/backups/tautulli/ +``` + +### **Required Permissions** +All backup directories must be owned by `www-data:ncshare` with `2770` to allow PLEX32 to write via NFS all_squash: +```bash +sudo chown -R www-data:ncshare /export/kingdezigns-all/Docker/plex/config +sudo chmod -R 2770 /export/kingdezigns-all/Docker/plex/config +``` + +### **Verify backup directories exist and have correct ownership** +```bash +ls -la /export/kingdezigns-all/Docker/plex/config/ +ls -la /export/kingdezigns-all/Docker/plex/config/backups/ +``` + +Expected output: `www-data:ncshare` ownership, `drwxrws---` permissions on all directories. + +### **SSH Access from PLEX32** +PLEX32 has an SSH key installed on NAS08 (`rufusking` account) โ€” used by the backup script to fetch accurate disk stats via `df`. This is read-only and harmless. + +--- + +## ๐Ÿ› ๏ธ SSD Hardening & Maintenance + +### **TRIM** +- TRIM support confirmed on all drives (`DISC-GRAN: 4K`, `DISC-MAX: 4G`) +- systemd `fstrim.timer` **disabled** โ€” replaced by OMV Scheduled Job +- Weekly TRIM script: `/usr/scripts/omv/fstrim-report.sh` +- Scheduled: **Wednesday at 1:00 AM** via OMV Scheduled Jobs +- Script runs `fstrim -av`, gathers SMART health data, sends HTML email report, saves dated log to `/var/log/fstrim/` +- Log retention: 90 days + +### **noatime** +- All filesystems mounted with `noatime` โ€” confirmed via `findmnt` +- Managed automatically by the `openmediavault-flashmemory` plugin +- No manual configuration required + +### **openmediavault-flashmemory Plugin** +- Installed and active +- Automatically handles: `noatime`, tmpfs for `/var/log` and `/tmp`, swap disabled +- Significantly reduces unnecessary SSD write amplification + +### **SMART Monitoring** +- Monitored weekly as part of the TRIM report script +- Drive-specific attribute mapping: + +| Drive | Wear Attribute | Reallocated Attribute | Notes | +|-------|---------------|----------------------|-------| +| PNY CS900 (/dev/sda) | `SSD_Life_Left` (attr 231) | `Bad_Blk_Ct_Lat/Erl` (attr 170, first value) | No pending/uncorrectable attrs | +| Crucial BX500 (/dev/sdbโ€“sde) | `Percent_Lifetime_Remain` (attr 202) | `Reallocate_NAND_Blk_Cnt` (attr 5) | Uses `Reported_Uncorrect` for uncorrectable | + +--- + +## ๐Ÿ“… Scheduled Jobs (OMV) + +| Schedule | Script | Tag | +|----------|--------|-----| +| Every 3 days at 2:00 AM | `/usr/scripts/omv/nas08-backup.sh` | NAS08 Backup | +| Daily at 12:00 AM | `/usr/scripts/zfs/nas08_zfs_report.sh` | ZFS Status Report | +| Daily at 7:00 AM | `/usr/scripts/omv/nextcloud_update_check.sh` | Nextcloud Update Check | +| Sunday at 3:00 AM | `/usr/scripts/zfs/nas08_zfs_scrub.sh` | ZFS Scrub | +| Wednesday at 1:00 AM | `/usr/scripts/omv/fstrim-report.sh` | TRIM Report | +| Disabled (manual) | `/usr/scripts/nas/plex32_reboot.sh` | PLEX32 Remote Reboot | + +--- + +## ๐Ÿ”„ Nextcloud Update Procedure + +**Never use the Nextcloud browser UI to trigger app updates.** Apache workers handle browser-initiated updates and can segfault mid-update, leaving Nextcloud stuck in maintenance mode with cron also blocked โ€” a self-reinforcing deadlock that requires manual `occ` intervention to recover. + +All Nextcloud updates must be performed via `occ` on the command line. + +### **Daily Automated Check** +- **Script:** `/usr/scripts/omv/nextcloud_update_check.sh` +- **Schedule:** Daily at 7:00 AM via OMV Scheduled Jobs +- **SMTP password:** `/etc/nextcloud-smtp-pass` (chmod 600) +- **Log:** `/var/log/nextcloud-updates/` (90 day retention) +- **Email:** HTML report sent daily โ€” green (up to date) / amber (updates available) / red (maintenance mode or DB upgrade needed) +- The email includes a ready-to-paste command block for any action required + +### **Manual Update Commands** +Run these on NAS08 whenever the email reports updates available: +```bash +# Update all apps (instead of clicking Update in browser) +sudo docker exec -u www-data nextcloud php occ app:update --all + +# After docker compose pull + up (Nextcloud container version update) +sudo docker exec -u www-data nextcloud php occ upgrade --no-interaction +sudo docker exec -u www-data nextcloud php occ app:update --all + +# Verify clean state after any update +sudo docker exec -u www-data nextcloud php occ status +``` + +### **Recovery โ€” If Stuck in Maintenance Mode** +```bash +sudo docker exec -u www-data nextcloud php occ maintenance:mode --off +sudo docker exec -u www-data nextcloud php occ upgrade --no-interaction +sudo docker exec -u www-data nextcloud php occ app:update --all +sudo docker exec -u www-data nextcloud php occ maintenance:repair +``` + +--- + +## ๐Ÿ”„ Nextcloud Update Check Script โ€” Core Version Detection Bug Fix (2026-07-25) + +### **Overview** +`nextcloud_update_check.sh` (see Scheduled Jobs above) was reporting **"โœ… Nextcloud is +fully up to date"** by email while Nextcloud 34.0.2 was actually available โ€” a genuine +false negative, not a display glitch. Root-caused, fixed, and verified working the same +day. + +### **Root Cause** +The script only ever ran `occ app:update --all --showonly`, which checks **app** updates +only. It never checked for a Nextcloud **core/server** version update โ€” that requires the +separate `occ update:check` command, which queries the updater channel +(updater.nextcloud.com), not the app store. Since 34.0.2 is a core release, not an app +release, the script had no code path capable of seeing it at all โ€” it wasn't a parsing bug, +it was checking the wrong thing entirely. + +### **Fix Applied to the Script** +- Added a core-update check via `occ update:check`, parsed for `"Nextcloud X.Y.Z is + available"`. +- `NEEDS_ACTION` and the email's status color/badge now factor in a core update, not just + app updates. +- Added a dedicated "๐Ÿ†™ Core Update Available" block in the email showing current โ†’ new + version. +- Fixed a second, independent bug found during testing: the emailed copy-paste command + block used `"...\n\n"` inside plain double-quoted bash strings to build multi-line + output. Plain double quotes do **not** interpret `\n` as a newline in bash โ€” they insert + the two literal characters `\` and `n`. Every command in the email was arriving as one + unreadable line with literal `\n` text in it. Fixed by switching to ANSI-C quoting + (`$'...'`) for all multi-line command block construction. +- Removed one leftover unused variable (`CORE_ROW`) from the original script. + +### **Command Block Path Bug โ€” Found During Live Testing** +The first fixed version of the script emailed a generic guessed update command +(`cd ~/docker/nextcloud && docker compose pull && docker compose up -d`). Running it +live surfaced two additional real environment facts that had to be baked into the script: + +1. **Wrong directory.** NAS08's actual Nextcloud Docker Compose project lives at + `/kingdezignsnas/Docker/Compose/nextcloud/`, not `~/docker/nextcloud/`. Confirmed via: + ```bash + docker inspect nextcloud --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' + ``` +2. **Root-only directory + non-default filenames.** The directory is `drwx------ root + root` โ€” `rufusking` cannot `cd` into it without `sudo`. OMV's Compose plugin also does + not name the file `docker-compose.yml`; it uses `nextcloud.yml` + + `compose.override.yml`, which `docker compose` does not auto-discover without explicit + `-f` flags. + +**Corrected command now emitted by the script:** +```bash +sudo bash -c "cd /kingdezignsnas/Docker/Compose/nextcloud && docker compose -f nextcloud.yml -f compose.override.yml pull && docker compose -f nextcloud.yml -f compose.override.yml up -d" +``` + +### **False "No Upgrade Required" โ€” Race Condition, Not a Bug** +After running the corrected command, `occ upgrade --no-interaction` and `occ status` +immediately afterward both still reported the **old** version (34.0.1.2), even though +`docker compose pull` had genuinely fetched the new 34.0.2 image and the container had +restarted. This looked like the update silently failing and was re-attempted twice with +identical results before being root-caused. + +**Actual cause:** the official `nextcloud` image's entrypoint script performs the +version-upgrade itself automatically on container start โ€” copying in the new application +code and internally running `occ upgrade` โ€” it does **not** require (or expect) the admin +to run `occ upgrade` manually. Checking `occ status` immediately after `up -d` races the +container's own startup: the check fires before the entrypoint's internal upgrade has +finished, so it reports the pre-upgrade version. Confirmed via `docker logs nextcloud`, +which showed `Upgrading nextcloud from 34.0.1.2 ...` โ†’ `Update successful`, completing +about 30 seconds after container start โ€” after which `occ status` correctly showed +`34.0.2`. + +**Fix:** the script's emailed command block now includes a `sleep 30` between `up -d` and +the confirming `occ status` call, so the copy-pasted commands don't produce the same false +negative. + +### **Verification Method** +The corrected script was validated with a full mock test harness โ€” fake `docker`/`sudo` +binaries standing in for the real container, run under `bash -u` (aborts on any unset +variable) โ€” exercising all three code paths: core+app updates available, container not +running, and fully up to date. All three produced correct email content with real newlines +in the command block (verified via direct newline-count check, not just visual +inspection). Confirmed working against the live system same day โ€” `occ status` returned +`versionstring: 34.0.2` after following the corrected emailed commands, and a subsequent +run of the script correctly reported "up to date." + +### **Current Script Behavior (as of 2026-07-25)** +| Check | Command Used | Detects | +|---|---|---| +| Core/server version | `occ update:check` | Nextcloud core releases (e.g. 34.0.1 โ†’ 34.0.2) | +| App versions | `occ app:update --all --showonly` | Individual app updates (e.g. `files_lock`, `bookmarks`) | +| Maintenance mode / DB upgrade flag | `occ status` | Stuck maintenance mode, pending DB migration | + +--- + +## ๐Ÿง  Summary for AI Systems +- NAS08 = **DNS + application server** in VLAN 50. +- Provides Piโ€‘hole DNS to all VLANs. +- Hosts Nextcloud, Vaultwarden, and nginx. +- Inbound allowed from VLAN 1, VLAN 20, and local VLAN 50. +- Outbound blocked to all VLANs except local + DNS responses. +- Critical infrastructure component for networkโ€‘wide DNS and storage. +- SSD hardening active: TRIM (weekly Wednesday 1AM), noatime (flashmemory plugin), SMART monitoring via weekly HTML email report. +- Five drives: 1x PNY CS900 500GB (OS), 4x Crucial BX500 1TB (data). +- **Shared permissions:** `ncshare` group (GID 1001) shared by `www-data` and `rufusking` โ€” allows both Nextcloud and NFS to read/write the same files. +- NFS exports use `all_squash,anonuid=33,anongid=1001` โ€” all NFS uploads land as `www-data:ncshare` automatically. +- Default ACLs on `/export/kingdezigns-public` ensure all new files/folders inherit `ncshare` group permissions. +- After NFS uploads, run `occ files:scan` to index new files in Nextcloud. +- Nextcloud container name: `nextcloud` โ€” cron container: `nextcloud_cron` โ€” DB: `nextcloud_db`. +- **Never update Nextcloud apps via the browser UI** โ€” Apache segfaults mid-update leave Nextcloud stuck in maintenance mode. Always use `occ app:update --all` from the command line. +- **Nextcloud update check script:** `/usr/scripts/omv/nextcloud_update_check.sh` โ€” runs daily at 7AM, emails HTML report with ready-to-paste update commands. SMTP password at `/etc/nextcloud-smtp-pass`. +- **Nextcloud update check script โ€” core-update false negative fixed (2026-07-25):** the script previously only checked `occ app:update` (app updates), never `occ update:check` (core/server updates), so a core release like 34.0.2 was invisible and the email falsely reported "up to date." Fixed by adding a core-update check, fixing a `\n`-in-double-quotes bug that made emailed commands unreadable, and correcting the emailed update commands to use the real Compose path (`/kingdezignsnas/Docker/Compose/nextcloud/`, root-only, requires `sudo bash -c` + explicit `-f nextcloud.yml -f compose.override.yml` flags โ€” not the default `docker-compose.yml`/`~/docker/nextcloud` assumed originally). Also documented: the official Nextcloud image auto-upgrades on container restart via its own entrypoint โ€” running `occ upgrade` or checking `occ status` immediately after `docker compose up -d` can race that internal process and falsely show the old version for ~30 seconds. The emailed command block now includes a `sleep 30` before the confirming status check. See dedicated section above for full troubleshooting detail and the mock-harness verification method used to confirm the fix. +- **Maintenance mode recovery:** `occ maintenance:mode --off` โ†’ `occ upgrade --no-interaction` โ†’ `occ app:update --all` โ†’ `occ maintenance:repair`. +- **PLEX32 backup destination:** `/export/kingdezigns-all/Docker/plex/config/backups/` โ€” plex and tautulli subdirs receive tar archives every 3 days. Must be `www-data:ncshare 2770` or writes will fail. +- PLEX32 has SSH key installed on NAS08 rufusking account โ€” used for backup disk stats only. +- **NAS08 root SSH key installed on PLEX32** โ€” required for `plex32_reboot.sh` remote reboot script. +- **Remote reboot script:** `/usr/scripts/nas/plex32_reboot.sh` โ€” SSHs into PLEX32, stops containers, reboots, waits for recovery, starts containers, confirms Plex on port 32400 via curl. Registered as disabled OMV Scheduled Job โ€” enable and run manually when needed. Logs to `/var/log/plex32-reboot/`. +- **NAS08โ†’NAS16 sync destination permissions are self-healing** โ€” `nas08_to_nas16_sync.sh` on NAS16 automatically checks and corrects `www-data:ncshare` ownership on `/export/kingdezignsnas-16/Public` before every run. No manual chown/chmod/setfacl needed on NAS16. +- NFS `all_squash` on NAS08 exports is the root cause of rsync permission errors on NAS16 โ€” the sync script handles this transparently via `--no-perms`, `--omit-dir-times`, and the permission precheck. +- **ONLYOFFICE (2026-07-18):** Nextcloud document editing runs via ONLYOFFICE Document Server (Community Edition, free) โ€” Docker container `onlyoffice`, image `onlyoffice/documentserver:latest`, port 9980, public URL `https://office.kingdezigns.com/`. Only the `onlyoffice` Nextcloud app is enabled (eurooffice/officeonline/office disabled โ€” never re-enable more than one Office app simultaneously). Requires `ALLOW_PRIVATE_IP_ADDRESS=true` env var, JWT auth enabled with a shared secret between Nextcloud app config and container `JWT_SECRET`, and a Pi-hole local DNS record for `cloud.kingdezigns.com` โ†’ `192.168.150.30` to avoid a NAT hairpin failure. See dedicated section above for full troubleshooting history. +- **DNS fix (2026-07-18):** `/etc/systemd/resolved.conf` had a stray global `DNS=1.1.1.1 8.8.8.8` override that merged with the correct per-interface Pi-hole DNS, causing duplicate/incorrect resolution for internal hostnames. Removed โ€” only `FallbackDNS=9.9.9.9` remains as a last-resort. Docker containers require a restart to pick up host DNS changes. +- **Synaplan (2026-07-18):** `synaplan_integration` Nextcloud app is installed but non-functional โ€” it is only a connector; the required Synaplan backend server stack (PHP, MariaDB, Qdrant, optional Ollama) has not been deployed. Deploying it is a separate future project. `allow_local_remote_servers` was set to `true` system-wide to unblock local-network AI backend calls in general (needed for Synaplan and similar future integrations). +- **STOCKPROXY (2026-07-21):** Self-hosted Flask API, Docker container `stockproxy`, own OMV-style Compose project at `~/docker/stockproxy/`, port 5005, public URL `https://stocks.kingdezigns.com/` (proxied via NPM on HAS). Supplies current + historical mutual fund prices to ONLYOFFICE via Yahoo Finance (server-side scrape, bypasses browser CORS block) โ€” Finnhub was evaluated first but has no mutual fund NAV coverage and was dropped from this service. Current-price cache: 15 min in-memory. Historical-price cache: permanent, SQLite at `/opt/stockproxy/data/cache.db`. All endpoints require a shared-secret `key` param (Vaultwarden). Built to support the "HSA Investiments Choices" tab in `Rufus Retirement Account Tracking 2026.xlsx`. See dedicated section above for full config, endpoints, and known limitations. +- **RANK.EQ Err:522 circular reference fix (2026-07-23, working):** The "HSA + Investiments Choices" tab's fund ranking (`RANK.EQ`) broke with a circular-reference + error (Err:522) on any sheet edit โ€” but only after loading correctly โ€” because it + depended on ~50 cells fed by live `FUNDPRICE`/`FUNDPRICE_HIST` network calls, and an + incremental recalc could catch those async cells mid-flight. Fixed by adding a + button-triggered LibreOffice Basic macro (`CopyStockValues`) that snapshots the live + `P`/`Q` price columns into static value-only columns `R`/`S` via + `oDest.setDataArray(oSource.getDataArray())`; percent-change (`T`/`U`) and + `RANK.EQ` were rebuilt to read from the static columns instead of the live ones. `P`/`Q` + remain live and unaffected โ€” only the ranking chain was decoupled. See dedicated section + above for full root-cause analysis, macro code, and diagnostic dead ends (notably: + `ActiveSheet` must be used instead of `Sheets.getByIndex(0)` for multi-tab workbooks, and + a cell-by-cell `getValue()`/`setValue()` loop silently failed where a bulk + `setDataArray()` copy succeeded). +- **Collabora Finnhub removal โ€” STOCKPRICE/STOCKTIME replaced with FUNDPRICE/FUNDTIME + (2026-07-23, working):** `STOCKPRICE`/`STOCKTIME` errored because Finnhub's hostname was + never added to Collabora's `net.lok_allow` allowlist. Rather than add it, Finnhub was + dropped entirely โ€” `FUNDPRICE` now also serves equity tickers (STOCKPROXY doesn't + distinguish fund vs. stock), and a new `FUNDTIME` function reads STOCKPROXY's + `resolved_date` field in place of Finnhub's timestamp. Note: `FUNDTIME` returns a date + only, not time-of-day โ€” a regression from Finnhub's old intraday timestamp, accepted as a + tradeoff for now. `FINNHUB_KEY()` config and both old functions removed from + `StockFunctions.bas`; new `JsonString()` helper added for parsing quoted JSON string + fields. See dedicated section above. +- **Collabora stock/fund price macro integration (2026-07-22, working):** Ported the ONLYOFFICE `FUNDPRICE`/`FUNDPRICE_HIST`/`STOCKPRICE`/`STOCKTIME` custom functions to a LibreOffice Basic module (`StockFunctions.bas`) calling the same STOCKPROXY/Finnhub backends โ€” no backend changes needed. Required: (1) Collabora test copy saved as `.ods` not `.xlsx` โ€” Basic modules don't reliably persist in `.xlsx`; (2) `enable_macros_execution=true` in `coolwsd.xml` (disabled by default post-CVE-2025-24796); (3) added `stocks\.kingdezigns\.com` to the `net.lok_allow` host allowlist in `coolwsd.xml` โ€” this allowlist matches literal hostname/IP-text regex patterns, not DNS-resolved IPs, so the hostname needed its own explicit entry even though it resolves to an already-allowed private IP. Macro edits must be done in desktop LibreOffice (Collabora's browser UI has no working Basic IDE) then re-uploaded. Also fixed during this work: the `stocks.kingdezigns.com` Pi-hole local DNS override existed but was not actually resolving (returning WAN IP) โ€” fixed with `docker restart pihole` (v6's `reloaddns` errored, `restartdns` doesn't exist in v6). See dedicated section above for full troubleshooting detail, exact XML edits, and open follow-up items. + +--- + +# โœ”๏ธ End of File diff --git a/server_nas16.md b/server_nas16.md new file mode 100644 index 0000000..d073144 --- /dev/null +++ b/server_nas16.md @@ -0,0 +1,346 @@ +# NAS16 โ€” LAMP Stack & Database Server +Device Type: **Raspberry Pi** +Hostname: **NAS16** +IP Address: **192.168.150.40** +VLAN: **50 โ€” Lab / Servers** + +Last Updated: 2026-06-24 + +--- + +## ๐Ÿงฉ Role & Purpose +NAS16 is the **dedicated LAMP stack and database server** for the KingDezigns network. +It runs **OpenMediaVault (OMV)** as its base OS, which manages nginx as the web GUI frontend. +It provides backend services, web hosting, and database support for internal applications, development environments, and externally accessible virtual host websites. + +In addition to its LAMP role, NAS16 hosts the **CrowdSec external notification system** โ€” a pair of scripts that poll the CrowdSec LAPI on HAS and deliver split email alerts based on ban origin. + +This system is designed for: +- Web application hosting (internal and external) +- MariaDB database services +- Internal development and testing +- Lightweight administrative tools (Webmin via OMV) +- External virtual host websites proxied through HAS (NPM) +- CrowdSec email notification delivery (local attack alerts + CAPI daily digest) + +NAS16 operates entirely within VLAN 50 and benefits from the strict containment rules that prevent lateral movement into trusted networks. + +--- + +## ๐Ÿ–ฅ๏ธ Hardware +- Raspberry Pi (model unspecified) +- SSD storage (see drive inventory below) +- Gigabit Ethernet + +### **Drive Inventory** +| Device | Model | Capacity | Role | +|--------|-------|----------|------| +| /dev/sda | Unknown (standard SMART attrs) | ~256GB | OS / system drive | + +> Note: Drive model unconfirmed โ€” uses standard SMART attribute names (Wear_Leveling_Count, Reallocated_Sector_Ct, Current_Pending_Sector, Offline_Uncorrectable). Power_Cycle_Count of 7,079 at 891 power-on hours noted โ€” likely reflects frequent reboots during setup. Monitor over time. + +--- + +## ๐ŸŒ Network Placement +- VLAN: **50 โ€” Lab / Servers** +- IP: **192.168.150.40** +- Access Type: LAN / Cable + +This placement ensures: +- Isolation from trusted user devices +- Controlled access to sensitive database services +- Local access to Piโ€‘hole DNS +- Reduced attack surface + +--- + +## ๐Ÿ“ฆ Primary Functions + +### **Core Services** +- **OpenMediaVault (OMV)** โ€” base OS and NAS management UI +- **LAMP Stack** + - Linux + - Apache (serves internal and external virtual host websites) + - MariaDB + - PHP +- **Webmin** (administrative interface, managed via OMV) + +### **Additional Roles** +- Internal web hosting +- External virtual host websites (proxied via NPM on HAS) +- Database backend for internal applications +- Development and testing environment +- **CrowdSec external notification system** (see below) + +--- + +## ๐ŸŒ External Access Architecture + +NAS16 hosts multiple public-facing websites served by Apache. +External traffic reaches NAS16 via the following proxy chain: + +``` +Internet โ†’ NPM on HAS (192.168.150.30:80/443) โ†’ nginx on NAS16 (OMV frontend) โ†’ Apache (virtual hosts) +``` + +### **Real IP Forwarding** +Because all proxied traffic arrives at NAS16 from HAS's IP (192.168.150.30), both nginx and Apache are configured to extract and log the real client IP from the `X-Forwarded-For` header passed by NPM. + +**nginx** โ€” `/etc/nginx/conf.d/real_ip.conf`: +```nginx +set_real_ip_from 192.168.150.30; +real_ip_header X-Forwarded-For; +real_ip_recursive on; +``` + +**Apache** โ€” `/etc/apache2/conf-available/real_ip.conf`: +```apache +RemoteIPHeader X-Forwarded-For +RemoteIPTrustedProxy 192.168.150.30 +``` + +Apache module enabled: `remoteip` +Config enabled: `real_ip` + +This ensures Fail2Ban sees and bans real attacker IPs, not HAS's proxy IP. + +--- + +## ๐Ÿ”” CrowdSec External Notification System + +NAS16 is the sole email notification handler for CrowdSec. Native CrowdSec notifications are disabled on HAS. NAS16 polls the CrowdSec LAPI stream endpoint and routes decisions by origin. + +### **Why NAS16** +- Offloads notification work from HAS (which runs HA, NPM, CrowdSec Agent + Bouncer) +- NAS16 has the most headroom of any Pi in the fleet (16GB RAM, consistently underutilized) +- Fits cleanly into existing OMV Scheduled Jobs infrastructure + +### **Architecture** +``` +CrowdSec LAPI on HAS (192.168.150.30:8080) + โ†“ +NAS16 polls /v1/decisions/stream every 5 min + โ†“ + origin = crowdsec? origin = CAPI? + โ†“ โ†“ + Append to local_pending.json Append to capi_digest.json + โ†“ โ†“ + Every 4 hours: Midnight: send digest + send batched digest email + clear log + email + clear log +``` + +### **Scripts** +| Script | Path | Purpose | +|--------|------|---------| +| cs_notifier.sh | `/usr/scripts/crowdsec/cs_notifier.sh` | Polls LAPI stream every 5 min, routes decisions by origin into pending log files (no emails sent) | +| cs_local_digest.sh | `/usr/scripts/crowdsec/cs_local_digest.sh` | Reads local_pending.json, sends batched local-attack digest every 4 hours, clears log | +| cs_digest.sh | `/usr/scripts/crowdsec/cs_digest.sh` | Reads CAPI digest log, builds summary email, sends at midnight, clears log | + +### **Key Design Details** +- Uses `/v1/decisions/stream?startup=true` on first run to initialize cursor silently (no emails for existing decisions) +- Uses `/v1/decisions/stream?startup=false` on all subsequent runs โ€” LAPI maintains cursor server-side per API key +- First run flag: `/var/log/crowdsec-notifier/stream_initialized.flag` +- Local pending log: `/var/log/crowdsec-notifier/local_pending.json` (one JSON decision per line, cleared every 4 hours after digest send) +- CAPI digest log: `/var/log/crowdsec-notifier/capi_digest.json` (one JSON decision per line, cleared at midnight after digest send) +- Notifier log: `/var/log/crowdsec-notifier/notifier.log` +- All email sending uses inline Python with `timeout=15` on SMTP_SSL to prevent hangs +- Digest scripts use a single Python call to process all decisions โ€” handles large batches efficiently +- **IMPORTANT:** Python heredocs (`python3 << PYEOF`) do NOT read from a preceding pipe โ€” the heredoc itself becomes stdin. Data must be passed as an inline shell variable inside the heredoc (e.g. `snapshot = """$SNAPSHOT"""`), not piped in. + +### **Email Notifications** +| Type | Trigger | Style | Content | +|------|---------|-------|---------| +| Local attack digest | Every 4 hours (cron: `0 0,4,8,12,16,20 * * *`) โ€” skipped if zero bans | Red / CRITICAL | Stat cards (ban count, origin, CAPI today) + table of all banned IPs with scenario, type, duration, scope, detected time, and CTI/WHOIS/AbuseIPDB links | +| CAPI daily digest | Midnight cron | Amber / WARNING (or green if zero bans) | Total ban count, top 10 scenarios, first 20 sample IPs | + +> **Note:** Local attack alerting was changed from immediate per-IP emails (one email per ban, dozens-to-hundreds/week) to a 4-hour batched digest to reduce notification volume while preserving full visibility into attack activity. + +### **CrowdSec API Key** +- Bouncer name: `NAS16-notifier` +- Credentials stored in Vaultwarden under `CrowdSec NAS16-notifier` +- LAPI endpoint: `http://192.168.150.30:8080` + +### **LAPI Port Requirement** +Port 8080 must be exposed on HAS's host network interface. Configured in the CrowdSec add-on Network settings. If HAS is rebuilt or the add-on is reinstalled, re-enable port 8080 mapping. + +--- + +## ๐Ÿ—‚๏ธ Shared Permissions โ€” NFS + rsync Backup + +NAS16 receives rsync backups from NAS08 via `nas08_to_nas16_sync.sh` every 2 days. To ensure rsync can write files on the destination without errors, NAS16 uses the same `ncshare` group configuration as NAS08. + +### **Group Configuration** +| Item | Value | +|------|-------| +| Group name | `ncshare` | +| GID | `1001` | +| Members | `www-data`, `rufusking` | + +> GID **must match NAS08** (1001) โ€” rsync syncs permissions across both systems and they must be consistent. + +Verify with: +```bash +getent group ncshare +``` + +### **Directory Permissions** +Applied to `/export/kingdezignsnas-16/Public` (and all subdirectories recursively): +```bash +sudo chown -R www-data:ncshare /export/kingdezignsnas-16/Public +sudo chmod -R 2770 /export/kingdezignsnas-16/Public +sudo setfacl -R -m g:ncshare:rwx /export/kingdezignsnas-16/Public +sudo setfacl -R -d -m g:ncshare:rwx /export/kingdezignsnas-16/Public +``` + +- `2770` (setgid) ensures new items inherit the `ncshare` group +- Default ACLs (`-d`) ensure all new files/folders created by rsync inherit `rwx` for `ncshare` + +> **Note:** You should not need to run these commands manually. The sync script checks and self-corrects destination ownership before every run. These are documented here as reference only. + +### **Backup Script** +The sync runs on NAS16 via OMV Scheduled Jobs: +```bash +/usr/scripts/nas/nas08_to_nas16_sync.sh +``` +- Source: `/mnt/kingdezigns08/Public/` (NAS08 mounted via NFS) +- Destination: `/export/kingdezignsnas-16/Public/` +- Schedule: Every 2 days at 2:30 AM + +### **rsync Flags โ€” NFS all_squash Compatibility** +The sync script uses the following rsync flags to work correctly over NFS `all_squash`: + +``` +--archive Recursive, preserve symlinks, owner, group, devices +--no-perms Skip chmod โ€” NFS all_squash prevents non-owner chmod +--omit-dir-times Skip directory utimes โ€” NFS all_squash prevents utimes on dirs not owned by www-data +--update Skip files that are newer on destination +``` + +- **File timestamps are fully preserved** โ€” `--omit-dir-times` only skips directory mtimes, not file mtimes +- **Directory timestamps are intentionally skipped** โ€” directory mtimes are not meaningful for backup purposes +- **Permissions are not synced** โ€” destination ACLs (`ncshare` group, `2770`) control access instead + +### **Permission Precheck (Automatic)** +Before every sync run, the script checks that the destination root is owned by `www-data:ncshare`. If it is not, it automatically runs `chown`, `chmod`, and `setfacl` to correct it before rsync starts. This is logged and flagged in the email report with an amber warning notice if it fires. + +A clean run looks like: +``` +[TIMESTAMP] Destination ownership OK (www-data:ncshare). +[TIMESTAMP] ===== NAS08 โ†’ NAS16 Sync Started ===== +[TIMESTAMP] ===== Sync Completed Successfully ===== +Done โ€” Status: SUCCESS | Transferred: N file(s) | Duration: Xm Xs +``` + +--- + +## ๐Ÿ”’ Required Firewall Behavior + +### **Inbound to NAS16** +- Allowed from VLAN 1 (Infrastructure) +- Allowed from VLAN 20 (Trusted) +- Allowed from VLAN 50 (local VLAN) +- Proxied external traffic arrives from HAS (192.168.150.30) via NPM +- IoT and Guest access depend on default policy +- No explicit inbound blocks except those applied to VLAN 50 as a whole + +### **Outbound from NAS16** +- Subject to VLAN 50's global final drop: + - Cannot initiate to VLANs 1, 10, 20, 30, 40 +- Allowed exceptions: + - DNS responses + - Local VLAN 50 traffic (including LAPI calls to HAS at 192.168.150.30:8080) + +### **Critical Requirement** +- Must remain reachable from trusted administrative devices for management and database access. +- Must remain reachable from HAS (192.168.150.30) for proxied external traffic. +- Must be able to reach HAS LAPI at 192.168.150.30:8080 for CrowdSec notification polling. + +--- + +## ๐Ÿ” Security Measures +- **Fail2Ban enabled** (via OMV Fail2Ban plugin) +- **Fail2Ban ignoreip** includes `192.168.150.0/24` โ€” prevents HAS proxy IP from being banned +- **Real IP forwarding** configured in nginx and Apache โ€” ensures real attacker IPs are logged and banned +- Webmin access restricted to trusted VLANs +- Database ports not exposed outside VLAN 50 +- VLANโ€‘level containment prevents lateral movement + +### **Fail2Ban Configuration** +`/etc/fail2ban/jail.local`: +```ini +[DEFAULT] +ignoreip = 127.0.0.1/8 ::1 192.168.150.0/24 +``` + +This ensures internal VLAN 50 devices (including HAS) are never banned regardless of proxied traffic volume. + +--- + +## ๐Ÿ› ๏ธ SSD Hardening & Maintenance + +### **TRIM** +- TRIM support confirmed on all drives (`DISC-GRAN: 4K`, `DISC-MAX: 4G`) +- systemd `fstrim.timer` **disabled** โ€” replaced by OMV Scheduled Job +- Weekly TRIM script: `/usr/scripts/omv/fstrim-report.sh` +- Scheduled: **Wednesday at 1:30 AM** via OMV Scheduled Jobs +- Script runs `fstrim -av`, gathers SMART health data, sends HTML email report, saves dated log to `/var/log/fstrim/` +- Log retention: 90 days + +### **noatime** +- All filesystems mounted with `noatime` โ€” confirmed via `findmnt` +- Managed automatically by the `openmediavault-flashmemory` plugin +- No manual configuration required + +### **openmediavault-flashmemory Plugin** +- Installed and active +- Automatically handles: `noatime`, tmpfs for `/var/log` and `/tmp`, swap disabled +- Significantly reduces unnecessary SSD write amplification + +### **SMART Monitoring** +- Monitored weekly as part of the TRIM report script +- Drive uses standard SMART attribute names โ€” no vendor-specific mapping required + +--- + +## ๐Ÿ“… Scheduled Jobs (OMV) + +| Schedule | Script | Tag | +|----------|--------|-----| +| Every 3 days at 2:00 AM | `/usr/scripts/omv/nas16-backup.sh` | NAS16 Backup | +| Every 2 days at 2:30 AM | `bash /usr/scripts/nas/nas08_to_nas16_sync.sh` | NAS Drive Backup | +| Daily at 12:00 AM | `/usr/scripts/zfs/nas16_zfs_report.sh` | ZFS Status Report | +| Sunday at 3:00 AM | `/usr/scripts/zfs/nas16_zfs_scrub.sh` | ZFS Scrub | +| Every 15 minutes | `/usr/scripts/ddns/ddns_update.sh` | DDNS Update | +| Wednesday at 1:30 AM | `/usr/scripts/omv/fstrim-report.sh` | TRIM Report | +| Every 5 minutes | `/usr/scripts/crowdsec/cs_notifier.sh` | CrowdSec Local Notifier | +| Every 4 hours (0:00, 4:00, 8:00, 12:00, 16:00, 20:00) | `/usr/scripts/crowdsec/cs_local_digest.sh` | CrowdSec Local Attack Digest | +| Daily at 12:00 AM | `/usr/scripts/crowdsec/cs_digest.sh` | CrowdSec CAPI Digest | + +--- + +## ๐Ÿง  Summary for AI Systems +- NAS16 = **OMV-based LAMP + database server + CrowdSec notifier** in VLAN 50. +- Inbound allowed from VLAN 1, VLAN 20, local VLAN 50, and HAS proxy (192.168.150.30). +- Outbound blocked to all VLANs except local + DNS responses. +- Hosts Apache (virtual hosts), MariaDB, PHP, Webmin, and OMV management UI. +- External traffic is proxied through NPM on HAS โ€” real IPs forwarded via X-Forwarded-For. +- nginx and Apache both configured to log real client IPs, not proxy IP. +- Fail2Ban ignoreip covers VLAN 50 to prevent HAS from being banned. +- Protected by VLAN 50's strict containment rules. +- SSD hardening active: TRIM (weekly Wednesday 1:30AM), noatime (flashmemory plugin), SMART monitoring via weekly HTML email report. +- High Power_Cycle_Count (7,079) relative to power-on hours (891) โ€” monitor trend over time. +- **Hosts CrowdSec notification scripts** โ€” polls HAS LAPI stream at 192.168.150.30:8080 every 5 minutes. +- Local attack decisions are batched and sent as one digest email every 4 hours (skipped if zero bans) โ€” no longer one email per ban. +- CAPI cloud ban digest sent daily at midnight โ€” single Python call handles 10,000+ decisions efficiently. +- CrowdSec API key stored in Vaultwarden under `CrowdSec NAS16-notifier`. +- **Gotcha:** when piping data into `python3 -` with a heredoc, the heredoc replaces stdin โ€” pass data as an inline variable instead. +- **Shared permissions:** `ncshare` group (GID 1001) shared by `www-data` and `rufusking` โ€” must match NAS08 GID for rsync compatibility. +- Default ACLs on `/export/kingdezignsnas-16/Public` ensure all rsync-written files inherit `ncshare` group permissions. +- **NAS08โ†’NAS16 sync script self-heals destination permissions on every run** โ€” checks for `www-data:ncshare` ownership, runs `chown`/`chmod`/`setfacl` automatically if wrong. Flagged in email subject and body if it fires. +- Sync script uses `--no-perms` and `--omit-dir-times` for NFS `all_squash` compatibility โ€” all individual file timestamps are fully preserved; only directory timestamps are skipped (intentional, not meaningful for backup). +- **Do not manually run chown/chmod/setfacl on the sync destination** โ€” the script handles this automatically. + +--- + +# โœ”๏ธ End of File diff --git a/server_plex32.md b/server_plex32.md new file mode 100644 index 0000000..30b0938 --- /dev/null +++ b/server_plex32.md @@ -0,0 +1,468 @@ +# PLEX32 โ€” Plex Media Server +Device Type: **Dell Wyse 5070 Thin Client** +Hostname: **plex32** +IP Address: **192.168.150.45** +VLAN: **50 โ€” Lab / Servers** + +Last Updated: 2026-07-12 + +--- + +## ๐Ÿงฉ Role & Purpose +PLEX32 is the **dedicated Plex Media Server** for the KingDezigns network. +It replaced Plex on NAS08, migrating the service to a more capable x86 machine that supports advanced Plex Pass features including **Sonic Radio** โ€” an AI-driven music playlist and discovery feature that requires CPU capabilities not available on Raspberry Pi hardware. + +All media files remain stored on NAS08 and are accessed via NFS mounts. PLEX32 handles only transcoding and Plex server logic โ€” it does not store media locally. + +This device is the **single Plex Media Server** for the KingDezigns network and is publicly accessible via `plex.kingdezigns.com` through NPM on HAS. + +--- + +## ๐Ÿ–ฅ๏ธ Hardware +- Dell Wyse 5070 Thin Client +- Intel Celeron J4105 โ€” 1.50GHz (4 cores) โ€” Intel Quick Sync supported +- 8GB RAM +- 32GB eMMC (OS drive โ€” no SMART support, health monitored via filesystem) +- Gigabit Ethernet + +### **Storage** +| Device | Type | Capacity | Role | +|--------|------|----------|------| +| mmcblk0 | eMMC | 29.1GB | OS / system drive | +| /mnt/plex/config | NFS (NAS08) | โ€” | Plex config backup destination only โ€” not used by container | +| /mnt/plex/transcode | NFS (NAS08) | โ€” | Transcode scratch (local at /opt/plex/transcode) | +| /mnt/plex/media | NFS (NAS08) | 1.8TB | Media library | + +> Note: Plex config and transcode directories are stored **locally** at `/opt/plex/` โ€” not on NFS. The NFS config mount (`/mnt/plex/config`) is used as the backup destination by `plex32_backup.sh`. Only the media library is actively used by the container. + +--- + +## ๐ŸŒ Network Placement +- VLAN: **50 โ€” Lab / Servers** +- IP: **192.168.150.45** +- Access Type: LAN / Cable + +This placement ensures: +- Isolation from trusted user devices +- Access to NAS08 NFS shares over local VLAN 50 +- Direct access to Piโ€‘hole DNS +- Reduced attack surface +- Accessible via NPM reverse proxy on HAS + +--- + +## ๐Ÿ“ฆ Primary Functions + +### **Core Services** +- **Plex Media Server** (Docker โ€” linuxserver/plex:latest) +- **Tautulli** (Docker โ€” linuxserver/tautulli:latest) โ€” playback history, monitoring, and alerts + +### **Plex Libraries** +| Library | Path in Container | Source on NAS08 | +|---------|------------------|-----------------| +| Music | /media/03 - Music | /export/kingdezigns-public/03 - Music | +| Videos | /media/04 - Videos/Movies | /export/kingdezigns-public/04 - Videos/Movies | +| Photos | /media/02 - Photos | /export/kingdezigns-public/02 - Photos | + +### **Plex Pass** +- **Lifetime Plex Pass** โ€” confirmed active on this server +- Enables: Sonic Radio, hardware transcoding (Intel Quick Sync), mobile sync, offline downloads + +### **Library Codec Profile** +| Codec | Count | Notes | +|-------|-------|-------| +| HEVC Main 10 | ~90 | 10-bit HDR โ€” Direct Streams on Roku, hardware transcode via Quick Sync if needed | +| H.264 High | 29 | Direct Play on all clients | +| Audio | 119 AAC | 100% AAC โ€” never forces audio transcode | +| Subtitles | 1168 SRT + 279 PGS | PGS only burns in if manually selected โ€” Subtitle Mode set to Manually Selected | + +--- + +## ๐Ÿณ Docker Configuration + +### **Compose File Location** +``` +~/docker/plex/docker-compose.yml +``` + +### **Compose Contents** +```yaml +services: + plex: + image: lscr.io/linuxserver/plex:latest + container_name: plex + network_mode: host + environment: + - PUID=1000 + - PGID=1001 + - TZ=America/New_York + - VERSION=docker + devices: + - /dev/dri:/dev/dri + group_add: + - "44" + - "993" + volumes: + - /opt/plex/config:/config + - /opt/plex/transcode:/transcode + - /mnt/plex/media:/media + restart: unless-stopped + + tautulli: + image: lscr.io/linuxserver/tautulli:latest + container_name: tautulli + environment: + - PUID=1000 + - PGID=1001 + - TZ=America/New_York + volumes: + - /opt/tautulli/config:/config + ports: + - 8181:8181 + restart: unless-stopped +``` + +> **devices + group_add:** Required for Intel Quick Sync hardware transcoding. The container needs `/dev/dri` device access plus supplementary group membership for `video` (GID 44) and `render` (GID 993). Without `group_add`, Plex silently falls back to software transcoding even with hardware acceleration enabled in settings. + +### **Key Docker Commands** +```bash +# Start all services +cd ~/docker/plex && docker compose up -d + +# Stop all services +cd ~/docker/plex && docker compose down + +# Update Plex +cd ~/docker/plex && docker compose pull && docker compose up -d + +# View Plex logs +docker logs plex --tail 50 + +# View Tautulli logs +docker logs tautulli --tail 50 + +# Check container status +docker ps +``` + +--- + +## ๐ŸŽฌ Hardware Transcoding โ€” Intel Quick Sync + +The J4105 supports Intel Quick Sync via `/dev/dri`. Both decode and encode are offloaded to the GPU. + +### **Verification** +Check GPU access from inside the container: +```bash +docker exec -it plex id +# Should show groups 44 (video) and 993 (render/group01b4) in output +docker exec -it plex ls -la /dev/dri +# Should show card0 and renderD128 +``` + +Verify during active transcode โ€” Plex Dashboard should show: +``` +4K (HEVC Main 10) (hw) +SD (HEVC) โ€” Transcode (hw) +``` + +The `(hw)` suffix on both lines confirms Quick Sync is handling decode and encode. + +### **Plex Settings** +- Settings โ†’ Transcoder โ†’ **Use hardware acceleration when available** โœ… +- Settings โ†’ Transcoder โ†’ **Use hardware-accelerated video encoding** โœ… +- Settings โ†’ Transcoder โ†’ **Transcoder temporary directory:** `/transcode` + +--- + +## ๐Ÿ“Š Tautulli + +Tautulli provides playback history, live session monitoring, and email alerts. + +- **Web UI:** `http://192.168.150.45:8181` +- **Config:** `/opt/tautulli/config` +- **Connected to:** Plex at `192.168.150.45:32400` + +### **Active Notifications** +| Trigger | Destination | Purpose | +|---------|-------------|---------| +| Plex Server Down | rufus.king@kingdezigns.com | Alert when Plex stops responding | +| Plex Server Back Up | rufus.king@kingdezigns.com | Confirmation when Plex recovers | + +### **SMTP Configuration** +Uses same Zoho SMTP credentials as all other KingDezigns email alerts: +- Server: `smtppro.zoho.com` / Port: `465` / TLS enabled +- From: `rufus.king@kingdezigns.com` + +--- + +## ๐Ÿ“ NFS Mounts + +Configured in `/etc/fstab` โ€” mounts automatically on boot: + +``` +192.168.150.35:/export/kingdezigns-all/Docker/plex/config /mnt/plex/config nfs defaults,_netdev 0 0 +192.168.150.35:/export/kingdezigns-all/Docker/plex/transcode /mnt/plex/transcode nfs defaults,_netdev 0 0 +192.168.150.35:/export/kingdezigns-public /mnt/plex/media nfs defaults,_netdev 0 0 +``` + +> Note: `/mnt/plex/config` is used by `plex32_backup.sh` as the backup destination โ€” not by the Plex container. `/mnt/plex/transcode` exists in fstab but Plex uses the local `/opt/plex/transcode` directory instead. + +--- + +## ๐ŸŒ External Access + +- **Domain:** `plex.kingdezigns.com` +- **Proxy:** NPM on HAS (192.168.150.30) โ†’ 192.168.150.45:32400 +- **SSL:** Let's Encrypt via NPM +- **Port:** 32400 (Plex default) +- **Port forwarding:** UCG Max forwards external 32400 โ†’ 192.168.150.45 + +--- + +## ๐Ÿ”’ Required Firewall Behavior + +### **Inbound to PLEX32** +- Allowed from VLAN 1 (Infrastructure) and VLAN 20 (Trusted) โ€” global rules +- Allowed from VLAN 50 (local VLAN) +- External Plex traffic arrives via NPM on HAS (192.168.150.30) + +### **Outbound from PLEX32** +- Subject to VLAN 50's global final drop +- Cannot initiate to VLANs 1, 10, 20, 30, 40 +- Allowed: local VLAN 50 traffic (NFS to NAS08, DNS to Pi-hole, SSH to NAS08 for backup) + +### **Critical Requirements** +- Must reach NAS08 (192.168.150.35) on NFS ports for media library and backup destination +- Must be reachable from HAS (192.168.150.30) for proxied external traffic +- Port 32400 must be forwarded on UCG Max for direct Plex remote access + +--- + +## ๐Ÿ” Security Measures +- **Fail2Ban enabled** โ€” `ignoreip` includes `127.0.0.1/8`, `::1`, `192.168.150.0/24` +- Docker-based service isolation +- VLAN-level containment +- No outbound access to other VLANs + +### **Fail2Ban Configuration** +`/etc/fail2ban/jail.local`: +```ini +[DEFAULT] +ignoreip = 127.0.0.1/8 ::1 192.168.150.0/24 +``` + +### **System Timezone** +- Timezone corrected to `America/New_York` on 2026-06-21 โ€” was previously `Etc/UTC` +- The UTC default caused cron jobs to fire 4 hours early (e.g. 2 AM UTC = 10 PM EDT) +- Always verify timezone after fresh installs: `timedatectl | grep "Time zone"` +- Fix: `sudo timedatectl set-timezone America/New_York` + +### **sudo Without Password** +`/etc/sudoers.d/rufusking-nopasswd`: +``` +rufusking ALL=(ALL) NOPASSWD: /usr/bin/apt-get update, /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status sshd, /sbin/reboot +``` +Required by update check script (`apt-get update`), health report script (`fail2ban-client`), and remote reboot script (`reboot`). + +### **SSH Key Authentication** +PLEX32 has an SSH key configured to NAS08 โ€” required for the backup script to fetch accurate disk stats without a password prompt: +```bash +# Key location on PLEX32 +~/.ssh/id_ed25519 + +# Test +ssh rufusking@192.168.150.35 "echo NAS08 OK" +``` + +### **AC Power Recovery (Auto Power-On After Outage)** +Configured **2026-06-30** after a power outage left PLEX32 powered off (manual power button press required to bring it back up). + +Dell exposes AC Power Recovery as a writable SMBIOS token โ€” this means it can be set **remotely from within Linux**, without ever entering BIOS setup. + +**Tooling:** +```bash +sudo apt-get install -y libsmbios-bin smbios-utils +``` + +**Relevant tokens (mutually exclusive โ€” only one should be `true`):** +| Token | Setting | Desired State | +|-------|---------|----------------| +| `0x00a1` | AC Power Recovery Mode (Off) โ€” stays off | `false` | +| `0x00a2` | AC Power Recovery Mode (Last) โ€” restores prior state | `false` | +| `0x00a3` | AC Power Recovery Mode (On) โ€” powers on automatically | `true` โœ… | + +**Command used to set it:** +```bash +sudo smbios-token-ctl --activate --token-id=0x00a3 +``` + +**Verify current state:** +```bash +sudo smbios-token-ctl --dump-tokens | grep -i -A2 "AC Power Recovery" +``` + +> Note: `smbios-token-ctl --dump-tokens` throws a harmless `StopIteration` traceback after printing all tokens (hits a malformed token entry near the end of the table) โ€” this does not affect the accuracy of the data already printed, including the AC Power Recovery tokens. + +**Result:** PLEX32 will now power back on automatically as soon as AC power is restored after an outage โ€” no physical power button press required. (HAS already had equivalent behavior configured.) + +--- + +## ๐Ÿ—‚๏ธ Shared Permissions + +PLEX32 uses the same `ncshare` group as NAS08 and NAS16 for consistency across the network. + +| Item | Value | +|------|-------| +| Group name | `ncshare` | +| GID | `1001` | +| Members | `rufusking` | + +Verify with: +```bash +getent group ncshare +``` + +--- + +## ๐Ÿ’พ Config Backup + +Plex and Tautulli configs are backed up to NAS08 every 3 days via `plex32_backup.sh`. + +### **Backup Script** +- **Location:** `/usr/scripts/plex32/plex32_backup.sh` +- **Method:** tar locally to `/tmp/`, then single `cp` to NFS (rsync avoided โ€” too slow over NFS for Plex's thousands of small metadata files) +- **Plex destination (NAS08):** `/export/kingdezigns-all/Docker/plex/config/backups/plex/plex-config-YYYY-MM-DD.tar.gz` +- **Tautulli destination (NAS08):** `/export/kingdezigns-all/Docker/plex/config/backups/tautulli/tautulli-config-YYYY-MM-DD.tar.gz` +- **Retention:** 3 copies of each, older ones auto-deleted +- **Excludes:** Cache, Logs, Crash Reports +- **Email:** HTML report sent on completion โ€” SUCCESS / PARTIAL / FAILED +- **SMTP password:** `/etc/plex32-smtp-pass` (chmod 600, owned by rufusking) +- **Log:** `/var/log/plex32-backup/backup-YYYYMMDD.log` (90 day retention) + +### **Restore Procedure (run on plex32)** +```bash +# 1. Stop containers +cd ~/docker/plex && docker compose down + +# 2. Copy archive from NAS08 +cp /mnt/plex/config/backups/plex/plex-config-YYYY-MM-DD.tar.gz /tmp/ + +# 3. Clear current config +sudo rm -rf /opt/plex/config/Library + +# 4. Extract backup +sudo tar -xzf /tmp/plex-config-YYYY-MM-DD.tar.gz -C /opt/plex +sudo chown -R rufusking:ncshare /opt/plex/config + +# 5. Restart containers +cd ~/docker/plex && docker compose up -d + +# 6. Verify +docker ps +# Both plex and tautulli should show Up +# Open http://192.168.150.45:32400/web and confirm library and watch history intact + +# 7. Cleanup +rm /tmp/plex-config-YYYY-MM-DD.tar.gz +``` + +--- + +## ๐Ÿ–ฅ๏ธ Remote Launcher (KingDezigns001) + +A launcher script on KingDezigns001 provides a menu-driven interface to run PLEX32 scripts remotely via SSH. + +- **Launcher script:** `~/scripts/plex32_launcher.sh` +- **Desktop shortcut:** `~/Desktop/PLEX32-Launcher.desktop` +- **SSH:** Passwordless โ€” ED25519 key from KingDezigns001 installed on PLEX32 + +### **Menu Options** +| Option | Script | +|--------|--------| +| 1) Update Check | `/usr/scripts/plex32/plex32_update_check.sh` | +| 2) Health Report | `/usr/scripts/plex32/plex32_health_report.sh` | +| 3) Plex32 Backup | `/usr/scripts/plex32/plex32_backup.sh` | + +--- + +## ๐Ÿ“… Scheduled Jobs (cron) + +| Schedule | Script | Purpose | +|----------|--------|---------| +| Daily at 7:00 AM | `/usr/scripts/plex32/plex32_update_check.sh` | Check for OS, Plex, Docker, Fail2Ban updates โ€” email report | +| Daily at 7:05 AM | `/usr/scripts/plex32/plex32_health_report.sh` | Server health report โ€” disk, NFS, memory, CPU, container status | +| Every 3 days at 2:00 AM | `/usr/scripts/plex32/plex32_backup.sh` | Archive Plex and Tautulli configs to NAS08 โ€” email report | + +View cron jobs: +```bash +sudo crontab -l +``` + +> Note: All three PLEX32 scheduled jobs run via **root's crontab**, not `rufusking`'s. + +### **plex32_update_check.sh โ€” Docker Pull Fix (2026-07-12)** +The daily update check was intermittently reporting `Plex Media Server = Check failed โ€” could not reach registry` even though `lscr.io` was reachable and disk space was healthy (8.6GB free of 26GB). + +**Root cause:** The script's `docker pull` step runs under root's cron environment, which uses a minimal default `PATH` that didn't reliably match the interactive `rufusking` shell environment. The script also silently discarded the actual `docker pull` error output, so the true failure reason was never visible in the log or email โ€” every failure was mislabeled with the same generic "could not reach registry" text regardless of cause. + +**Fix applied:** +- Added an explicit `export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` at the top of the script so behavior is consistent under cron regardless of invoking user/environment. +- The script now logs the full `docker pull` output/exit code to `/var/log/plex32-updates/` on failure, and includes it as a red error block in the email report, instead of showing a generic message. + +Resolved โ€” manual `sudo` run completed cleanly after the fix, and the failure message no longer appears. + +### **plex32_health_report.sh โ€” Issues List Word-Wrapping Fix (2026-07-12)** +The "โš  Issues Detected" block in the daily health report email was rendering each word of an issue message on its own line (e.g. "WARNING: NAS08 โ€” Plex Config is getting full" appeared as one word per line) instead of as a single readable sentence. + +**Root cause:** The script joined multiple issue messages into one string and then split them back apart using `IFS='. ' read -ra ISSUE_LIST`. This does not split on the two-character sequence `". "` as intended โ€” `IFS` treats every character it contains as its own independent delimiter, so both `.` and every space in the text were treated as separate breaks, shattering each sentence into individual words. + +**Fix applied:** +- Issue messages are now joined with actual newline characters instead of `". "` when assembled into `ALL_ISSUES`. +- The splitting logic was replaced with a `while IFS= read -r issue; do ... done <<< "$ALL_ISSUES"` loop, which reads one full issue line at a time instead of relying on ambiguous multi-character `IFS` splitting. + +Resolved โ€” verified with a manual run; each issue now renders as one complete sentence in the "Issues Detected" section of the email. + +### **Email Reports** +| Report | Trigger Colors | Content | +|--------|---------------|---------| +| Update Check | ๐ŸŸข Green (current) / ๐Ÿ”ต Blue (updates available) / ๐ŸŸ  Orange (check failed) | Component versions, available updates, ready-to-run commands | +| Health Report | ๐ŸŸข Green (healthy) / ๐ŸŸ  Orange (warning) / ๐Ÿ”ด Red (critical) | Disk usage, NFS mount status, memory, CPU load, container status, Fail2Ban, reboot flag | +| Config Backup | ๐ŸŸข Green (success) / ๐ŸŸก Amber (partial) / ๐Ÿ”ด Red (failed) | Archive sizes, durations, copies retained, container status, restore instructions | + +--- + +## ๐Ÿง  Summary for AI Systems +- PLEX32 = **dedicated Plex Media Server + Tautulli** on VLAN 50 at 192.168.150.45. +- Runs on Dell Wyse 5070 (Celeron J4105, 8GB RAM, 32GB eMMC). +- Plex and Tautulli run as Docker containers via `~/docker/plex/docker-compose.yml`. +- **Intel Quick Sync hardware transcoding active** โ€” requires `/dev/dri` device passthrough and `group_add: ["44","993"]` in compose. Without group_add, Plex silently falls back to software transcoding. +- **Lifetime Plex Pass** active โ€” Sonic Radio, hardware transcoding, all Plex Pass features enabled. +- Config and transcode stored **locally** at `/opt/plex/` โ€” not on NFS. +- Media library mounted from NAS08 via NFS at `/mnt/plex/media`. +- `/mnt/plex/config` NFS mount used as backup destination only โ€” not by Plex container. +- Library is ~75% HEVC Main 10, ~25% H.264, 100% AAC audio โ€” audio never transcodes. +- Roku TV (Hisense 55R8BX) Direct Streams video, converts AACโ†’AC3 for audio (lightweight, expected). +- Subtitle Mode set to Manually Selected โ€” prevents accidental PGS burn-in transcode. +- Publicly accessible via `plex.kingdezigns.com` โ€” proxied through NPM on HAS. +- Port 32400 forwarded on UCG Max for direct Plex remote access. +- Tautulli on port 8181 โ€” Plex down/up email alerts active. +- Fail2Ban active โ€” VLAN 50 subnet whitelisted in ignoreip. +- sudo NOPASSWD configured for apt-get update, fail2ban-client, and reboot โ€” required by automated and remote reboot scripts. +- **System timezone:** `America/New_York (EDT, -0400)` โ€” corrected from `Etc/UTC` on 2026-06-21. UTC default causes cron jobs to fire 4 hours early. Always verify after fresh installs. +- SSH key from PLEX32 โ†’ NAS08 configured โ€” required for backup script disk stats. +- **NAS08 root โ†’ PLEX32 SSH key configured** โ€” required for remote reboot script (`plex32_reboot.sh` on NAS08). +- SSH key from KingDezigns001 โ†’ PLEX32 configured โ€” enables passwordless remote launcher. +- **AC Power Recovery set to "On"** (2026-06-30) via SMBIOS token `0x00a3` using `smbios-token-ctl` โ€” PLEX32 now auto powers-on after AC power is restored following an outage. Configured remotely via SSH, no BIOS access required. Tool: `libsmbios-bin` / `smbios-utils`. +- Config backup runs every 3 days at 2AM โ€” tar + NFS copy, 3 copies retained, HTML email report. +- `ncshare` group (GID 1001) configured for network-wide permission consistency. +- eMMC has no SMART support โ€” health monitored via daily health report email. +- No swap configured โ€” 7.6GB RAM, consistently low utilization. +- Plex on NAS08 is **permanently disabled** (restart: no in compose). +- Migrated from NAS08 on 2026-06-18 โ€” full library, watch history, and playlists preserved. +- **`plex32_update_check.sh` fixed 2026-07-12** โ€” added explicit `PATH` export to fix intermittent `docker pull` failures under root's cron environment; script now logs real `docker pull` error output on failure instead of a generic "could not reach registry" message. All three PLEX32 scheduled jobs run under root's crontab. +- **`plex32_health_report.sh` fixed 2026-07-12** โ€” fixed the "Issues Detected" email block rendering each word on its own line, caused by a multi-character `IFS='. '` split; issues are now newline-delimited and read back with a `while read` loop. + +--- + +# โœ”๏ธ End of File diff --git a/server_ucg.md b/server_ucg.md new file mode 100644 index 0000000..b7dc487 --- /dev/null +++ b/server_ucg.md @@ -0,0 +1,120 @@ +# UCG Gateway Max โ€” Network Gateway & Firewall +Device Type: **UniFi Cloud Gateway Max (UCG Max)** +Hostname: **KD-UCG-Max** +IP Address: **192.168.100.1** +VLAN: **1 โ€” Infrastructure** + +Last Updated: 2026-05-25 + +--- + +## ๐Ÿงฉ Role & Purpose +The UCG Max is the **primary network gateway, firewall, and UniFi controller** for the KingDezigns network. +It manages all inter-VLAN routing, firewall policies, DHCP, DNS forwarding, and UniFi device adoption. + +It is the **single point of ingress and egress** for all internet traffic and the central controller for all UniFi network hardware. + +--- + +## ๐Ÿ–ฅ๏ธ Hardware +- UniFi Cloud Gateway Max +- Built-in UniFi Network application (self-hosted on device) +- Gigabit WAN + LAN ports +- 2.5G uplink support + +--- + +## ๐ŸŒ Network Placement +- VLAN: **1 โ€” Infrastructure** +- IP: **192.168.100.1** +- Access Type: LAN / Cable + +--- + +## โš™๏ธ Primary Functions +- Internet gateway and NAT +- Inter-VLAN firewall enforcement +- DHCP server for all VLANs +- DNS forwarding to Pi-hole (192.168.150.35) +- UniFi Network controller (device adoption, configuration) +- Matter/Thread controller (port 5540) +- Port forwarding โ€” external 80/443 โ†’ NPM on HAS (192.168.150.30) + +--- + +## ๐Ÿ” SSH Access + +- **SSH command:** `ssh root@192.168.100.1` +- **Credentials:** Stored in **Vaultwarden** under entry `UCG Gateway SSH` + +> Note: The UCG web UI (`https://192.168.100.1`) uses your Ubiquiti SSO account with MFA. SSH uses the local root account with a separate password. These are two different credential sets. + +--- + +## ๐Ÿ’พ Backup + +The UCG Max uses **Ubiquiti Cloud Backup** as its backup solution. This is the intended and only fully supported method for complete system restore on the UCG Max. + +### **What is backed up** +- Full UniFi OS configuration including all applications, VLANs, firewall rules, SSIDs, and device adoption records + +### **Backup schedule** +- Automatic weekly backup to Ubiquiti Cloud +- Additional backup generated automatically prior to each major firmware update + +### **Restore process** +- Log in to **account.ui.com** +- Navigate to **Backups** +- Select the desired backup and restore to device + +### **Manual backup** +A manual backup can be triggered and downloaded locally at any time: +1. Open `https://192.168.100.1` +2. Go to **Settings โ†’ Control Plane โ†’ Backups** +3. Click **Generate** then **Download** +4. Save the `.unifi` file to a safe location + +> Note: The manual `.unifi` file is generated on demand only and is never stored persistently on the UCG disk. Local automated backup of this file is not supported by the UCG Max without third-party tooling. + +--- + +## ๐Ÿ”’ Firewall Overview +All firewall rules are documented in [firewall_policies.md](firewall_policies.md). +Key rules managed by the UCG: + +- Global DNS โ†’ Pi-hole (all VLANs) +- Global Established/Related allow +- VLAN 50 outbound block (lab containment) +- IoT โ†’ Home Assistant only (port 8123) +- Matter/Thread โ†’ UCG controller (port 5540) +- Port forwarding: external 80/443 โ†’ 192.168.150.30 (NPM on HAS) + +--- + +## ๐Ÿ“ก Port Forwarding +| External | Internal | Destination | Purpose | +|----------|----------|-------------|---------| +| 80/TCP | 80/TCP | 192.168.150.30 | NPM HTTP | +| 443/TCP | 443/TCP | 192.168.150.30 | NPM HTTPS | + +--- + +## โš ๏ธ Important Operational Notes +- MFA is required for web UI login (Ubiquiti SSO) โ€” TOTP authenticator app required +- SSH uses the local root account โ€” not affected by MFA +- SSH authorized_keys and files written to the base filesystem may not survive firmware updates โ€” verify SSH access after major updates +- Cloud backup is the only full one-file restore solution for the UCG Max + +--- + +## ๐Ÿง  Summary for AI Systems +- UCG Max = **primary gateway, firewall, and UniFi controller** on VLAN 1 at 192.168.100.1. +- SSH access via local root account โ€” credentials in Vaultwarden under `UCG Gateway SSH`. +- Full system backup handled by Ubiquiti Cloud โ€” weekly automatic, restorable from account.ui.com. +- Manual `.unifi` backup can be downloaded via GUI but is never stored on disk automatically. +- Firewall rules, port forwarding, DHCP, and DNS forwarding all managed here. +- Matter/Thread controller runs on port 5540 (192.168.100.1). + +--- + +# โœ”๏ธ End of File diff --git a/vlan10_management.md b/vlan10_management.md new file mode 100644 index 0000000..ca49738 --- /dev/null +++ b/vlan10_management.md @@ -0,0 +1,67 @@ +# VLAN 10 โ€” Management Network +Subnet: **192.168.110.0/24** + +Last Updated: 2026-05-21 + +--- + +## ๐Ÿงฉ What This Network Is +VLAN 10 is a **dedicated management network** intended for administrative access, secure configuration tasks, and future wired management devices. +Although currently unused, it is fully prepared to serve as an expansion network for administrative workstations or as a **fallback network** if other VLANs become unavailable. + +This VLAN is considered **fully trusted**, but intentionally kept minimal until needed. + +--- + +## ๐Ÿ“ก Access Types +- LAN / Cable +- WiFi + +--- + +## ๐ŸŒ Subnet +- **192.168.110.0/24** + +--- + +## ๐Ÿ–ฅ๏ธ Expected Devices +- *(None at this time โ€” reserved for future administrative use)* + +--- + +## ๐Ÿ”’ Security Rules for VLAN 10 + +### **Outbound (VLAN 10 โ†’ Others)** +- No explicit outbound rules beyond: + - Global **Established/Related** allow + - Global **DNS โ†’ Piโ€‘hole** allow +- Other outbound behavior depends on the default LAN policy. + +### **Inbound (Others โ†’ VLAN 10)** +- **Allowed:** + - VLAN 1 (Infrastructure) โ†’ VLAN 10 + - VLAN 20 (Trusted) โ†’ VLAN 10 +- **Not explicitly blocked:** + - VLAN 30 (IoT) + - VLAN 40 (Guest) + - VLAN 50 (Lab) + *(These depend on default policy.)* + +### **Net Effect** +- Management (VLAN 1) and Trusted (VLAN 20) can reach VLAN 10. +- No explicit isolation from IoT, Guest, or Lab โ€” governed by default policy. +- Outbound traffic is mostly unrestricted unless default policy blocks it. + +--- + +## ๐Ÿง  Summary for AI Systems +- VLAN 10 = **trusted internal management network**, currently unused. +- No explicit outbound permissions except DNS + established/related. +- Reachable from VLAN 1 and VLAN 20. +- No explicit blocks from IoT, Guest, or Lab โ€” default policy applies. +- Functions as a **neutral, lowโ€‘activity VLAN** ready for future administrative expansion. + +--- + +# โœ”๏ธ End of File + diff --git a/vlan1_infrastructure.md b/vlan1_infrastructure.md new file mode 100644 index 0000000..893deb4 --- /dev/null +++ b/vlan1_infrastructure.md @@ -0,0 +1,79 @@ +# VLAN 1 โ€” Infrastructure / Default Network +Subnet: **192.168.100.0/24** + +Last Updated: 2026-05-21 + +--- + +## ๐Ÿงฉ What This Network Is +VLAN 1 is the **primary infrastructure network** for the KingDezigns environment. +It hosts core systems, gateway services, UniFi management, and essential network hardware. +This VLAN is treated as a **semiโ€‘trusted zone**, allowing broad internal communication while still blocking access from lowerโ€‘trust networks such as IoT and Lab. + +This network remains active during ongoing migration and should not be aggressively restricted until all infrastructure is fully relocated. + +--- + +## ๐Ÿ“ก Access Types +- LAN / Cable + +--- + +## ๐ŸŒ Subnet +- **192.168.100.0/24** + +--- + +## ๐Ÿ–ฅ๏ธ Expected Devices +- Infrastructure devices +- Core services +- UniFi gateway management +- Switches & access points +- Emergency recovery access +- At least one wired management workstation + +--- + +## ๐Ÿ”’ Security Rules for VLAN 1 + +### **Outbound (VLAN 1 โ†’ Others)** +- VLAN 1 can initiate connections to **all internal VLANs** + (VLAN 1, 10, 20, 30, 40, 50). + +### **Inbound (Others โ†’ VLAN 1)** +- **Allowed:** + - VLAN 20 (Trusted) โ†’ VLAN 1 +- **Blocked:** + - VLAN 30 (IoT) โ†’ VLAN 1 + - VLAN 50 (Lab/Servers) โ†’ VLAN 1 +- **Undefined (depends on default policy):** + - VLAN 40 (Guest) โ†’ VLAN 1 + +### **Net Effect** +- VLAN 1 can reach every internal VLAN. +- Trusted devices may reach VLAN 1. +- IoT and Lab cannot initiate into VLAN 1. +- Guest access depends on global default rules. + +--- + +## โš ๏ธ Important Notes +- Infrastructure remains on VLAN 1 during migration. +- Avoid strict firewall enforcement until migration is complete. +- Maintain at least one wired management workstation for safety. + +--- + +## ๐Ÿง  Summary for AI Systems +- VLAN 1 = **infrastructure + management zone**, semiโ€‘trusted. +- Can initiate connections to all VLANs. +- Trusted VLAN 20 may initiate into VLAN 1. +- IoT VLAN 30 and Lab VLAN 50 are blocked from initiating into VLAN 1. +- Guest VLAN 40 behavior depends on default policy. +- All VLANs may send DNS to Piโ€‘hole in VLAN 50. +- VLAN 1 is a reachable core network but protected from lowerโ€‘trust zones. + +--- + +# โœ”๏ธ End of File + diff --git a/vlan20_trusted.md b/vlan20_trusted.md new file mode 100644 index 0000000..da33a91 --- /dev/null +++ b/vlan20_trusted.md @@ -0,0 +1,74 @@ +# VLAN 20 โ€” Trusted Devices +Subnet: **192.168.120.0/24** + +Last Updated: 2026-05-21 + +--- + +## ๐Ÿงฉ What This Network Is +VLAN 20 is the **primary trusted user network** for dayโ€‘toโ€‘day administrative and productivity devices. +It is designed for laptops, desktops, phones, and tablets used by authorized users who require reliable access to internal systems. + +This VLAN provides a **secure but flexible environment**, allowing trusted devices to access internal services while remaining isolated from lowerโ€‘trust networks such as IoT, Guest, and Lab. + +It is the main operational network for users who need consistent access to internal tools, servers, and management interfaces. + +--- + +## ๐Ÿ“ก Access Types +- LAN / Cable +- WiFi + +--- + +## ๐ŸŒ Subnet +- **192.168.120.0/24** + +--- + +## ๐Ÿ–ฅ๏ธ Expected Devices +- Linux desktops +- Trusted laptops +- Mobile devices +- Administrative systems + +--- + +## ๐Ÿ”’ Security Rules for VLAN 20 + +### **Outbound (VLAN 20 โ†’ Others)** +- VLAN 20 can initiate connections to **all internal VLANs except itself**: + - VLAN 1 โ€” Infrastructure + - VLAN 10 โ€” Management + - VLAN 30 โ€” IoT + - VLAN 40 โ€” Guest + - VLAN 50 โ€” Lab + +### **Inbound (Others โ†’ VLAN 20)** +- **Blocked:** + - VLAN 30 (IoT) โ†’ VLAN 20 + - VLAN 50 (Lab/Servers) โ†’ VLAN 20 +- **Allowed:** + - VLAN 1 (Infrastructure) โ†’ VLAN 20 +- **Undefined (depends on default policy):** + - VLAN 40 (Guest) โ†’ VLAN 20 + +### **Net Effect** +- Trusted devices can reach every other VLAN. +- IoT and Lab cannot initiate into VLAN 20. +- Management (VLAN 1) can reach VLAN 20. +- Guest access depends on global default rules. + +--- + +## ๐Ÿง  Summary for AI Systems +- VLAN 20 = **highโ€‘trust user network** with broad outbound access. +- Can initiate connections to all other VLANs. +- Blocked from IoT and Lab inbound. +- Reachable from VLAN 1; Guest behavior depends on default policy. +- Acts as a **trusted control network** with strong protection from lowerโ€‘trust zones. + +--- + +# โœ”๏ธ End of File + diff --git a/vlan30_iot.md b/vlan30_iot.md new file mode 100644 index 0000000..4b45ca7 --- /dev/null +++ b/vlan30_iot.md @@ -0,0 +1,113 @@ +# VLAN 30 โ€” IoT Network +Subnet: **192.168.130.0/23** + +Last Updated: 2026-05-24 + +--- + +## ๐Ÿงฉ What This Network Is +VLAN 30 is the **dedicated smartโ€‘home and automation network** for all IoT and consumer smart devices. +It is intentionally isolated from trusted networks to reduce security risk while still supporting full functionality for Home Assistant and other automation systems. + +This VLAN is designed for devices such as WiZ bulbs, TVs, smart plugs, cameras, sensors, and other automation hardware that require internet access or controller communication but should not interact with trusted user devices. + +The network provides a **controlled, lowโ€‘trust environment** with carefully scoped access to Home Assistant and DNS services. + +--- + +## ๐Ÿ“ก Access Types +- LAN / Cable +- WiFi + +--- + +## ๐ŸŒ Subnet +- **192.168.130.0/23** + +--- + +## ๐Ÿ–ฅ๏ธ Expected Devices +- Lights +- Switches +- Outlets +- Blinds +- TV & streaming devices +- Alarm systems +- HVAC controllers +- Fans +- Voice assistants +- VOIP phones +- Vacuums +- Door locks +- Garage door openers +- Smoke detectors + +## ๐Ÿ“Œ Notable Devices +| Device | IP | Role | +|--------|-----|------| +| Aqara M100 | 192.168.130.30 (static reservation) | Thread Border Router / Matter hub | + +--- + +## ๐Ÿ”’ Security Rules for VLAN 30 + +### **Outbound (VLAN 30 โ†’ Others)** +- **Allowed:** + - IoT โ†’ Home Assistant (192.168.150.30:8123, TCP/UDP) + - IoT โ†’ Piโ€‘hole DNS (192.168.150.35, TCP/UDP 53) + - IoT โ†’ UCG Max Matter controller (192.168.100.1:5540, TCP) +- **Blocked:** + - IoT โ†’ VLAN 20 (Trusted) + - IoT โ†’ VLAN 1 (Infrastructure) โ€” except Matter controller port above +- **Defaultโ€‘Policy Dependent:** + - IoT โ†’ VLAN 10 (Management) + - IoT โ†’ VLAN 40 (Guest) + +### **Inbound (Others โ†’ VLAN 30)** +- **Allowed:** + - VLAN 1 (Infrastructure) โ†’ VLAN 30 + - VLAN 20 (Trusted) โ†’ VLAN 30 + - Home Assistant (192.168.150.30) โ†’ VLAN 30 (TCP/UDP, any port) +- **Blocked:** + - VLAN 50 (Lab/Servers) โ†’ VLAN 30 + +### **Net Effect** +- IoT devices can communicate with Home Assistant and DNS. +- IoT cannot reach Trusted or Infrastructure networks. +- IoT may reach Management or Guest depending on default policy. +- Management, Trusted, and Home Assistant can initiate into IoT. +- Lab cannot reach IoT. + +--- + +## โš™๏ธ Important Configuration +- **mDNS reflection enabled** (VLANs 1, 10, 20, 30, 50) +- **IGMP Snooping enabled** +- **Multicast/discovery traffic required** for many integrations +- **Client Isolation must be disabled** โ€” required for Matter/Thread device pairing and intra-VLAN device communication + +--- + +## ๐Ÿ” Discovery Protocols Expected +- mDNS +- Multicast +- UDP discovery +- Home Assistant autoโ€‘discovery + +--- + +## ๐Ÿง  Summary for AI Systems +- VLAN 30 = **untrusted IoT zone**, tightly restricted. +- Outbound allowed only to: + - Home Assistant (8123/TCP/UDP) + - Pi-hole DNS + - UCG Max Matter controller (192.168.100.1:5540/TCP) +- Blocked from initiating to VLAN 20 and VLAN 1 (except Matter controller port 5540). +- May reach VLAN 10 or VLAN 40 depending on default policy. +- Inbound allowed from VLAN 1, VLAN 20, and Home Assistant. +- Designed to contain IoT devices while preserving automation functionality. + +--- + +# โœ”๏ธ End of File + diff --git a/vlan40_guest.md b/vlan40_guest.md new file mode 100644 index 0000000..a241032 --- /dev/null +++ b/vlan40_guest.md @@ -0,0 +1,75 @@ +# VLAN 40 โ€” Guest Network +Subnet: **192.168.140.0/24** + +Last Updated: 2026-05-21 + +--- + +## ๐Ÿงฉ What This Network Is +VLAN 40 is the **dedicated guest WiFi network** for temporary visitors and untrusted personal devices. +It provides safe, isolated internet access without exposing internal systems, trusted devices, or infrastructure resources. + +This VLAN is intentionally designed as a **highโ€‘isolation, lowโ€‘trust environment**, ensuring that guest traffic cannot interfere with administrative, IoT, or server networks while still offering convenient connectivity. + +--- + +## ๐Ÿ“ก Access Types +- WiFi only + +--- + +## ๐ŸŒ Subnet +- **192.168.140.0/24** + +--- + +## ๐Ÿ–ฅ๏ธ Expected Devices +- Guest phones +- Guest laptops +- Visitor tablets +- Temporary personal devices + +--- + +## ๐Ÿ”’ Security Rules for VLAN 40 + +### **Outbound (VLAN 40 โ†’ Others)** +- **Allowed:** + - DNS โ†’ Piโ€‘hole (192.168.150.35) +- **Defaultโ€‘Policy Dependent:** + - Guest โ†’ VLAN 1 + - Guest โ†’ VLAN 10 + - Guest โ†’ VLAN 20 + - Guest โ†’ VLAN 30 + - Guest โ†’ VLAN 50 + *(No explicit allow/deny rules beyond DNS.)* + +### **Inbound (Others โ†’ VLAN 40)** +- **Allowed:** + - VLAN 1 (Infrastructure) โ†’ VLAN 40 + - VLAN 20 (Trusted) โ†’ VLAN 40 +- **Blocked:** + - VLAN 50 (Lab/Servers) โ†’ VLAN 40 +- **Undefined:** + - VLAN 30 (IoT) โ†’ VLAN 40 (depends on default policy) + +### **Net Effect** +- Guest devices can resolve DNS via Piโ€‘hole. +- Guest access to other VLANs is governed by the default LAN policy. +- Management and Trusted VLANs can reach Guest. +- Lab VLAN is explicitly blocked from reaching Guest. + +--- + +## ๐Ÿง  Summary for AI Systems +- VLAN 40 = **guest WiFi**, minimal privileges. +- Only guaranteed outbound access is DNS โ†’ Piโ€‘hole. +- Reachable from VLAN 1 and VLAN 20. +- Blocked from Lab VLAN 50 inbound. +- No explicit outbound blocks to other VLANs โ€” default policy applies. +- Designed as a **highโ€‘isolation, lowโ€‘trust network** for visitors. + +--- + +# โœ”๏ธ End of File + diff --git a/vlan50_lab.md b/vlan50_lab.md new file mode 100644 index 0000000..2f029c4 --- /dev/null +++ b/vlan50_lab.md @@ -0,0 +1,86 @@ +# VLAN 50 โ€” Lab / Servers Network +Subnet: **192.168.150.0/24** + +Last Updated: 2026-06-18 + +--- + +## ๐Ÿงฉ What This Network Is +VLAN 50 is the **dedicated server and lab environment** for the KingDezigns network. +It hosts Home Assistant, NAS systems, Piโ€‘hole, the Plex Media Server, and various selfโ€‘hosted services that require controlled access, stability, and isolation. + +This VLAN is intentionally **tightly contained**, preventing lab systems or highโ€‘privilege services from initiating connections into trusted or infrastructure networks. +Only specific exceptionsโ€”such as Home Assistant controlling IoT devicesโ€”are permitted. + +This network functions as a **sandboxed, semiโ€‘trusted zone** where critical services can operate safely without exposing the rest of the network to unnecessary risk. + +--- + +## ๐Ÿ“ก Access Types +- LAN / Cable +- WiFi + +--- + +## ๐ŸŒ Subnet +- **192.168.150.0/24** + +--- + +## ๐Ÿ–ฅ๏ธ Expected Devices +- Home Assistant +- NAS08 (Piโ€‘hole, Nextcloud, Vaultwarden, nginx) +- NAS16 (LAMP stack, MariaDB, Webmin) +- PLEX32 (Plex Media Server โ€” Dell Wyse 5070) +- Additional lab servers +- Test systems +- Development containers + +--- + +## ๐Ÿ”’ Security Rules for VLAN 50 + +### **Outbound (VLAN 50 โ†’ Others)** +- **Blocked (Global Final Drop):** + VLAN 50 cannot initiate connections to: + - VLAN 1 โ€” Infrastructure + - VLAN 10 โ€” Management + - VLAN 20 โ€” Trusted + - VLAN 30 โ€” IoT + - VLAN 40 โ€” Guest + +- **Allowed Exceptions:** + - Home Assistant (192.168.150.30) โ†’ VLAN 30 (IoT), any port, TCP/UDP + - DNS โ†’ Piโ€‘hole (local to VLAN 50) + +### **Inbound (Others โ†’ VLAN 50)** +- **Allowed:** + - VLAN 1 (Infrastructure) โ†’ VLAN 50 + - VLAN 20 (Trusted) โ†’ VLAN 50 + - VLAN 30 (IoT) โ†’ Home Assistant (192.168.150.30:8123) +- **Defaultโ€‘Policy Dependent:** + - VLAN 10 โ†’ VLAN 50 + - VLAN 40 โ†’ VLAN 50 + +### **Net Effect** +- Lab/Server systems are **heavily isolated** from initiating connections to other VLANs. +- Only Home Assistant is allowed to control IoT devices. +- Management and Trusted VLANs can reach Lab systems. +- All VLANs use Piโ€‘hole in VLAN 50 for DNS. +- IoT can reach Home Assistant only on port 8123. + +--- + +## ๐Ÿง  Summary for AI Systems +- VLAN 50 = **sandboxed server/lab zone**, semiโ€‘trusted but highly restricted. +- Outbound traffic is blocked to all VLANs except: + - Home Assistant โ†’ IoT + - DNS โ†’ Piโ€‘hole +- Inbound allowed from VLAN 1 and VLAN 20. +- IoT can reach Home Assistant only on port 8123. +- Designed to contain highโ€‘privilege services and prevent lateral movement. +- PLEX32 (192.168.150.45) added 2026-06-18 โ€” dedicated Plex Media Server, replaced Plex on NAS08. See `server_plex32.md`. + +--- + +# โœ”๏ธ End of File