From b92b00c31273d9f2983fae26c5a69e37fed58688 Mon Sep 17 00:00:00 2001 From: Garrett Allen <98648590+Gerrrt@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:16:09 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(runbook):=20four=20rules=20not=20three?= =?UTF-8?q?,=20and=20the=20switch=20step=20=C2=A70=20never=20had=20(#413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-the-nas.md §0 is complete and §1–§7 is blocked on two drives that have not been delivered. So this corrects the documents the build will be run from rather than advancing the build. Four things were wrong, and three of them were files contradicting themselves. THE RULE COUNT. Four passes were created on 2026-09-16; §0.5's heading, its table and two other documents still said three. §0.5 now lists four rows, one per port, and carries each rule's description — which §0.6 has started matching on. network.md said "the three rules created that day" seven lines above a bullet correctly saying four. security.md was worse: "Neither is created yet; both wait on the host that would use them", of two decisions, only one of which is still pending. Checked on the live ruleset: ADR-0016's passes exist, ADR-0035's pass to the Hue bridge genuinely does not, so the sentence is split rather than replaced. stacks/media/README.md called the Hicks access "the one rule" and it is two. THE RECORDED PROOF. §0.6's evidence was "167-168 before 169 on igc0.99, 194-195 before 196 on igc0.50". Those numbers are right for `pfctl -sr | grep -n` and wrong for `pfctl -sr -vv`, which numbers the same four rules @148, @149, @175 and @176 — both read off the live ruleset in one invocation, the two disagreeing by however many scrub rules precede the filter set. The command §0.6 needs is the one the numbers do not match, because the tripwire check reads a counter and counters only come from -vv. §0.6 now asserts the invariant by rule description and says to read the order rather than the numbers. The tripwire wording separates packets, which must be zero, from evaluations, which only climb: 118,621 then 128,629 with nothing wrong on either day. THE SWITCH STEP. §0 never said the port had to move from VLAN 30 to VLAN 40, nor which port it was. Added as §0.2b — before §0.3, because a static on a port still carrying VLAN 30 is a host with no gateway and makes §0.4's reservation and §0.5's four rules silently pointless. Lettered rather than renumbering §0.3 onward, because ADR-0016 and ADR-0040 both cite §0.5 by name and ADRs are immutable. THREE GAPS THAT WOULD BITE MID-BUILD. §2 offered SSH as an alternative to the console; TrueNAS ships it disabled and §0.5 says the port-22 pass is inert. §6 said "deploy the stack" with no command. §6's QSV check was `ls -l /dev/dri` on the host, which re-confirms the half that was never in doubt — ADR-0040's reopen condition is whether a container reaches the iGPU, so the check now runs inside it and looks at the supplementary groups group_add exists to set. §7's 9100 bullet read as a step that would pass, when nothing in §1–§6 stands up a listener and #256 has yet to settle its shape. Also adds the /stacks/media Dependabot entry. ADR-0040 keeps the media stack in this repository so that "Dependabot, the digest pins and make validate keep reaching it", and build-the-nas.md §6 and the stack README repeat the claim. The pins and make validate did reach it; Dependabot did not. Verified against the live firewall from morpheus: all four passes above Block access to CasaBonita on their interfaces, 443 still refused from Winterfell, igc0.40 tripwire zero packets. Refs #413 Closes #481 Co-Authored-By: Claude Opus 5 --- .github/dependabot.yml | 17 ++++ docs/network.md | 17 +++- docs/runbooks/build-the-nas.md | 179 ++++++++++++++++++++++++++------- docs/security.md | 10 +- stacks/media/README.md | 2 +- 5 files changed, 180 insertions(+), 45 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 635bc031..4203d02a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -76,6 +76,23 @@ updates: wazuh: patterns: ["wazuh/*"] + # The media stack on smaug. One image, so no groups. Present before the pool + # is, for the reason the entries above give — but this one was absent while + # three documents said it was here: ADR-0040 keeps stacks/media in this + # repository rather than making it a TrueNAS catalogue app precisely so + # "Dependabot, the digest pins and make validate keep reaching it", and + # build-the-nas.md §6 and the stack's own README repeat the claim. The pins + # and make validate did reach it. This is the third. + - package-ecosystem: docker-compose + directory: /stacks/media + schedule: + interval: weekly + day: sunday + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(deps)" + labels: ["dependencies", "media"] + - package-ecosystem: github-actions directory: / schedule: diff --git a/docs/network.md b/docs/network.md index 79c0a1dc..2b6ffffa 100644 --- a/docs/network.md +++ b/docs/network.md @@ -290,11 +290,18 @@ Televisions and consoles. Internet only. and running TrueNAS by [ADR-0040](adr/0040-run-truenas-on-smaug-and-keep-the-media-stack-in-this-repository.md) ([#413](https://github.com/Gerrrt/HomeLab/issues/413)). Its ZFS mirror does - not exist yet and neither does `stacks/media`; what exists is a host on its - address. **It does not change the *Reaches* column**, and that is the point - ADR-0016 made in advance: nothing on this segment initiates anywhere, and the - three rules created that day all let a more trusted segment reach **in**. - That is the direction this row records, and it is the one that is unchanged. + not exist yet, and `stacks/media` is authored and undeployed against that + absence; what exists is a host on its address. **It does not change the + *Reaches* column**, and that is the point ADR-0016 made in advance: nothing + on this segment initiates anywhere, and the four rules created that day all + let a more trusted segment reach **in**. That is the direction this row + records, and it is the one that is unchanged. +- **`smaug` is on port 15 of the switch**, untagged with PVID 40. That port was + ImaginationLAN's when the map was last read on 2026-09-04 and was moved for + the install ([#481](https://github.com/Gerrrt/HomeLab/issues/481)). It is + recorded because a single-NIC host on an access port has its segment decided + at the switch and nowhere else: put it back on 30 and the address, the + reservation and all four rules are silently pointless. - **Inbound is no longer nothing, and that is deliberate.** Since 2026-09-16 Hicks reaches `10.0.40.30` on `443` and `8096`, and `10.0.99.20` reaches it on `9100` and `22` — four host-scoped, port-scoped passes above *Block access diff --git a/docs/runbooks/build-the-nas.md b/docs/runbooks/build-the-nas.md index 6f550193..5edc7091 100644 --- a/docs/runbooks/build-the-nas.md +++ b/docs/runbooks/build-the-nas.md @@ -5,9 +5,10 @@ `10.0.40.30` **Time:** §0 is about half an hour and needs no drives. §1–§7 is about twenty minutes once the drives are in hand. -**You will need:** a console on `smaug` (or its web UI), the pfSense UI on -`morpheus`, a shell on the monitoring host for §0.6, and the two Exos X20 -drives for §1 onward. +**You will need:** a console on `smaug` — its web UI cannot run §2 or §6 and +SSH is off — the switch UI on `neo` for §0.2b, the pfSense UI on `morpheus`, a +shell on the monitoring host for §0.6, and the two Exos X20 drives for §1 +onward. > **Status — 2026-09-16: §0 is the work that can be done before the drives > land, and it is the whole of what is blocking.** @@ -18,16 +19,27 @@ drives for §1 onward. > listing and recorded in [`hardware.md`](../hardware.md). > > **§0 is complete as of 2026-09-16.** BIOS flashed (§0.1), AMT found on its -> factory-default credential and disabled (§0.2), the static set (§0.3), the -> reservation added (§0.4), and the rules created as four host- and port-scoped -> passes rather than three (§0.5) — `443,8096` from Hicks split into two rules, -> `9100` and `22` from `10.0.99.20`. +> factory-default credential and disabled (§0.2), the switch port moved from +> VLAN 30 to VLAN 40 (§0.2b), the static set (§0.3), the reservation added +> (§0.4), and the rules created as four host- and port-scoped passes rather +> than three (§0.5) — `443,8096` from Hicks split into two rules, `9100` and +> `22` from `10.0.99.20`. > > §0.6 verified from `morpheus` rather than from the UI: every pass sits above -> *Block access to CasaBonita* on its interface (167–168 before 169 on -> `igc0.99`, 194–195 before 196 on `igc0.50`), Winterfell is still correctly -> refused on `443`, and the `igc0.40` tripwire reads **118,621 evaluations and -> zero packets**. +> *Block access to CasaBonita* on its interface, Winterfell is still correctly +> refused on `443`, and the `igc0.40` tripwire matched **zero packets**. +> +> **No rule numbers are recorded here, and that is deliberate.** This block +> first carried them — 167–168 before 169 on `igc0.99`, 194–195 before 196 on +> `igc0.50`. Those are right for `pfctl -sr | grep -n` and wrong for +> `pfctl -sr -vv`, which on 2026-09-17 numbered the same four rules `@148`, +> `@149`, `@175` and `@176`. Both readings came off the live ruleset in one +> invocation: `-vv` numbers each ruleset from zero, `grep -n` counts output +> lines, and the two therefore disagree by however many `scrub` rules precede +> the filter set. **The command §0.6 needs is the one the numbers do not +> match**, because a tripwire check reads a counter and counters only come +> from `-vv`. So match on the rule descriptions, which do not depend on how +> the ruleset is being printed. > > What is left is the drives. The pool does not exist and nothing is deployed. @@ -85,6 +97,41 @@ its own issue — the precedent is There is also an `ME_DIS` header on the board if the answer turns out to be "remove it entirely". +### §0.2b — Put its switch port on CasaBonita + +**An address on the right segment is worth nothing if the cable is on the +wrong one.** `smaug` is on **port 15** of `neo`. That port was +ImaginationLAN's when the map was last read on 2026-09-04 and is CasaBonita's +now — the move happened during the install, and this runbook did not say to +make it ([#481](https://github.com/Gerrrt/HomeLab/issues/481)). + +**It is numbered `0.2b` rather than appended after §0.4 because it has to +happen first.** `smaug` is single-NIC on an access port, so the VLAN is +decided at the switch and nowhere else: a static of `10.0.40.30` set in §0.3 +on a port still carrying VLAN 30 is a host with no gateway, §0.4's reservation +is on a pool that is never asked, and §0.5's four rules point at an address +nothing answers on. The numbering is lettered rather than shifting §0.3 +onward, because **ADR-0016 and ADR-0040 both cite §0.5 by name** and ADRs are +immutable (ADR-0001) — renumbering would break the only pointer two settled +decisions have at the corrected rule table. + +Add the port to VLAN 40 **untagged**, and set its **PVID to 40**. Both halves, +because they answer different questions: membership decides whether a frame +for that VLAN is allowed out of the port, PVID decides which VLAN an untagged +frame arriving *from* the NAS is placed in. A port with one and not the other +half works, which is worse than neither. + +**Write the port number down** — here, and in the switching notes. A successor +re-cabling this machine with no record of which port it used has to rediscover +it by moving cables on a live switch. + +> **Name this failure mode, because it does not look like itself.** A NAS left +> on a port tagged for the lab segment takes a lab address from the wrong DHCP +> scope, and then nothing reaches it on `10.0.40.30` — which is exactly the +> symptom of a pass rule sitting in the wrong position. §0.5 spends its length +> on position for good reason, and this would be diagnosed there, at the +> firewall, for as long as it took someone to think of the switch. + ### §0.3 — Give it the static address In the TrueNAS UI, **Network → Interfaces**, edit the onboard NIC: remove DHCP @@ -109,18 +156,25 @@ the box and in the firewall, not here. The static is set on the host and the reservation is set on the server, and both are done because either alone is a single point of drift. -### §0.5 — Create the three rules, in order and in position +### §0.5 — Create the four rules, in order and in position -**Position is the whole difficulty.** Two of these sit above a deny that has -been in place since 2025; appended where new rules naturally land they would -match nothing, and *"can I reach the NAS"* would still pass for the wrong -reason. +**Position is the whole difficulty.** All four sit above a deny that has been +in place since 2025; appended where new rules naturally land they would match +nothing, and *"can I reach the NAS"* would still pass for the wrong reason. -| On interface | Protocol / source → destination | Position | -| --- | --- | --- | -| Hicks (50) | `tcp` `vlan50 net` → `10.0.40.30` ports `443,8096` | **above** *Block access to CasaBonita* | -| Winterfell (99) | `tcp` `10.0.99.20` → `10.0.40.30` port `9100` | **above** *Block access to CasaBonita* | -| Winterfell (99) | `tcp` `10.0.99.20` → `10.0.40.30` port `22` | **above** *Block access to CasaBonita* | +| On interface | Protocol / source → destination | Description | Position | +| --- | --- | --- | --- | +| Hicks (50) | `tcp` `vlan50 net` → `10.0.40.30` port `443` | `Allow HTTPS to smaug` | **above** *Block access to CasaBonita* | +| Hicks (50) | `tcp` `vlan50 net` → `10.0.40.30` port `8096` | `Allow 8096 to smaug` | **above** *Block access to CasaBonita* | +| Winterfell (99) | `tcp` `10.0.99.20` → `10.0.40.30` port `9100` | `Allow 9100 to smaug` | **above** *Block access to CasaBonita* | +| Winterfell (99) | `tcp` `10.0.99.20` → `10.0.40.30` port `22` | `Allow SSH to smaug` | **above** *Block access to CasaBonita* | + +**Four, where [ADR-0016](../adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md) +wrote three.** The Hicks pass is one rule per port rather than one rule +carrying a port list — functionally identical, and worth the extra row because +the description is what §0.6 matches on and a description naming one port is +unambiguous about which rule answered. **Set these descriptions exactly**; they +are load-bearing in the next section, not decoration. **The Hicks rule's ports differ from ADR-0016's table, and deliberately.** That table says `22,8096`, which assumed a box administered over SSH — ADR-0016 @@ -141,7 +195,25 @@ bought by putting the server with its clients. ### §0.6 — Prove the rules did what you meant -Two checks, and the second is the one that matters. +**Do this from `morpheus` and from the monitoring host, not from the pfSense +UI.** An appended rule matches nothing while looking perfectly present in the +UI, and that is the failure this whole section exists to catch. + +**First, position — the check the other three assume.** On `morpheus`: + +```bash +pfctl -sr -vv \ + | grep -E 'on igc0\.(99|50) ' \ + | grep -E 'descr=(Allow .* to smaug|Block access to CasaBonita)' +``` + +Each pass must appear **above** the *Block access to CasaBonita* rule on its +own interface: `Allow 9100`/`Allow SSH` before the block on `igc0.99`, and +`Allow HTTPS`/`Allow 8096` before it on `igc0.50`. **Read the order, not the +numbers.** `-vv` numbers each ruleset from zero rather than counting output +lines, so its `@` indices match neither `pfctl -sr | grep -n` nor anything +written down here — they are a printing artefact, and only the sequence is a +fact about the firewall. From a Hicks workstation, the NAS's UI should answer: @@ -150,8 +222,9 @@ curl -kIs https://10.0.40.30 | head -1 ``` From the monitoring host, which is on Winterfell, `443` should **still be -refused** — that rule is scoped to `10.0.99.20` and to port `9100`, so a -success here would mean the rule is wider than it reads: +refused** — the two Winterfell rules are scoped to `10.0.99.20` and to ports +`9100` and `22`, so a success here would mean one of them is wider than it +reads: ```bash nc -z -w3 10.0.40.30 443 && echo "WRONG: 99 can reach 443" || echo "correct: blocked" @@ -160,9 +233,11 @@ nc -z -w3 10.0.40.30 443 && echo "WRONG: 99 can reach 443" || echo "correct: blo Then read **the tripwire counter on `igc0.40`** ([#223](https://github.com/Gerrrt/HomeLab/issues/223)). It matches packets *originating* on CasaBonita, and the return traffic for a session Hicks opened -is carried by state and never reaches the ruleset. **It must still be zero.** -If it has moved, something on 40 is initiating outward and that is a bigger -finding than anything in this runbook. +is carried by state and never reaches the ruleset. **Its packet count must +still be zero.** The evaluation counter beside it climbs constantly and means +nothing — it is every packet the rule was tested against. If *packets* have +moved, something on 40 is initiating outward and that is a bigger finding than +anything in this runbook. --- @@ -180,7 +255,11 @@ drives, and two 7200 rpm Exos under a scrub will want it. ## §2 — Read the drives before trusting them -From **option 8, Open Linux Shell**, or over SSH once enabled: +From **option 8, Open Linux Shell**, at the console — **not over SSH**. +TrueNAS ships SSH disabled, and §0.5's port-22 pass is inert until someone +turns it on. Enabling it here to save a walk to the machine widens this host's +attack surface for the sake of five commands; §8's backup path is the reason +to turn it on, and this is not it. ```bash lsblk @@ -266,19 +345,38 @@ a compose file this repository owns, run under TrueNAS's app runtime, **not** a catalogue app. That is what keeps Dependabot, the digest pins and `make validate` reaching it. +Copy `stacks/media/.env.example` to `.env`, confirm `RENDER_GID` still matches +what this host reports, and bring it up: + +```bash +make up STACK=media +``` + Jellyfin binds `8096`, reads `erebor/media`, and writes its state to `erebor/apps`. -> **The check ADR-0040 named as its reopen condition belongs here.** Confirm -> the iGPU reaches the container: +> **The check ADR-0040 named as its reopen condition belongs here, and it has +> to run *inside* the container.** The host half is already settled — +> `Active Video: IGD` on an E3-1225 v6, and render node `107 render` read off +> this machine on 2026-09-16. Running `ls -l /dev/dri` on the host re-confirms +> the half that was never in doubt and says nothing about the condition, which +> is whether the device reaches a container: +> +> ```bash +> docker exec media-jellyfin ls -l /dev/dri +> ``` +> +> The node has to be present **and** the container's supplementary groups have +> to include the render GID — which is what `group_add` in the compose file is +> there to do, and the thing most likely to be silently wrong: > > ```bash -> ls -l /dev/dri +> docker exec media-jellyfin id > ``` > -> and that Jellyfin's playback settings offer **QSV** hardware transcoding. The -> CPU half is already confirmed — `Active Video: IGD` on an E3-1225 v6 — but a -> live P630 and a P630 a container can use are different claims. +> Then check that Jellyfin's playback settings offer **QSV** hardware +> transcoding, and transcode something with it. A device node a container can +> list and a device node it can *use* are still different claims. > > If it does not pass, decision 2 of ADR-0040 reopens: catalogue apps that > manage the passthrough, or the media stack moves off this host. **Check it @@ -289,8 +387,17 @@ Jellyfin binds `8096`, reads `erebor/media`, and writes its state to - A television on CasaBonita finds Jellyfin and plays something **without** any firewall rule being involved - A Hicks workstation reaches `https://10.0.40.30` and `http://10.0.40.30:8096` -- The monitoring host reaches `9100` and **nothing else** -- The `igc0.40` tripwire counter is **still zero** +- The monitoring host reaches `9100` and **nothing else** — but note that + *nothing in §1–§6 stands anything up on `9100`*. Until + [#256](https://github.com/Gerrrt/HomeLab/issues/256) settles whether the + target is `node_exporter` or TrueNAS's own metrics endpoint, only the **and + nothing else** half of this line is checkable: `443` and `8096` must both be + refused from the monitoring host. A listener answering on `9100` is that + issue's to deliver, not this runbook's +- **Port 15 on `neo`** still has PVID 40 and `smaug`'s MAC is learned on it in + VLAN 40 (§0.2b) +- The `igc0.40` tripwire's **packet** count is **still zero** — its evaluation + count will have climbed, and that is not a finding - `zpool status erebor` is `ONLINE` with no errors - Both Exos self-tests from §2 completed without error diff --git a/docs/security.md b/docs/security.md index 9709cb7e..8c346170 100644 --- a/docs/security.md +++ b/docs/security.md @@ -219,9 +219,13 @@ writes three passes into CasaBonita for the NAS, and [ADR-0035](adr/0035-scope-the-99-to-20-rule-to-the-hue-bridge.md) one into Skids — `10.0.99.40 → 10.0.20.104:80,443/tcp`, Home Assistant to the Hue bridge, the one device on that segment with a local API — above the block -that has stood between 99 and 20 since the segments existed. Neither is -created yet; both wait on the host that would use them. The row ADR-0008 -wrote as `99 → 20` is narrower than it read: one host to one device on two +that has stood between 99 and 20 since the segments existed. **The first set +exists and the second does not.** ADR-0016's passes were created on +2026-09-16, as **four** rather than three — Hicks reaching `smaug` on `443` +and `8096`, the monitoring host on `9100` and `22` — once there was a host on +`10.0.40.30` to test them against. ADR-0035's is still uncreated and still +waits on the host that would use it. The row ADR-0008 wrote as `99 → 20` is +narrower than it read: one host to one device on two ports, with the twenty other devices on Skids still unreachable from anywhere, and the segment still initiating nothing. The tripwire below is the check that the second half holds when the first lands. diff --git a/stacks/media/README.md b/stacks/media/README.md index e261851e..b2bee378 100644 --- a/stacks/media/README.md +++ b/stacks/media/README.md @@ -46,7 +46,7 @@ every television would have to trust, and a second thing to be down. | Who | Reaches it how | | --- | --- | | Televisions on CasaBonita | Natively, same broadcast domain — the firewall never sees the packet | -| A Hicks workstation | The one rule in [`build-the-nas.md`] §0.5, `50 → 10.0.40.30:443,8096` | +| A Hicks workstation | Two of the four rules in [`build-the-nas.md`] §0.5 — `50 → 10.0.40.30:443` and `50 → 10.0.40.30:8096`, one per port | | Everything else on the estate | Not at all — default deny | [ADR-0012] asks for a named off-host consumer before a port is published, and From 3b1ff96a75ebba91179494716795d1c112040a6f Mon Sep 17 00:00:00 2001 From: Garrett Allen <98648590+Gerrrt@users.noreply.github.com> Date: Fri, 18 Sep 2026 01:02:32 +0000 Subject: [PATCH 2/2] docs(network): count the CasaBonita passes in one bullet, not two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The count was stated in two adjacent bullets and they drifted apart — one said three long after four existed, which is how #501 found it. Correcting both would have left the drift vector in place, so the first bullet no longer counts them at all and the second is the authority. Taken from #502, which reached the same three files independently and had the better answer on this one point. Refs #413 Co-Authored-By: Claude Opus 5 --- docs/network.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/network.md b/docs/network.md index 4d9c0d2b..ca3da120 100644 --- a/docs/network.md +++ b/docs/network.md @@ -303,15 +303,17 @@ Televisions and consoles. Internet only. not exist yet, and `stacks/media` is authored and undeployed against that absence; what exists is a host on its address. **It does not change the *Reaches* column**, and that is the point ADR-0016 made in advance: nothing - on this segment initiates anywhere, and the four rules created that day all - let a more trusted segment reach **in**. That is the direction this row - records, and it is the one that is unchanged. + on this segment initiates anywhere, and the rules created that day all let a + more trusted segment reach **in**. That is the direction this row records, + and it is the one that is unchanged. **They are counted in the next bullet + and nowhere else** — this bullet used to count them too, the two drifted + apart, and this one still said three long after four existed. - **`smaug` is on port 15 of the switch**, untagged with PVID 40. That port was ImaginationLAN's when the map was last read on 2026-09-04 and was moved for the install ([#481](https://github.com/Gerrrt/HomeLab/issues/481)). It is recorded because a single-NIC host on an access port has its segment decided at the switch and nowhere else: put it back on 30 and the address, the - reservation and all four rules are silently pointless. + reservation and every inbound rule are silently pointless. - **Inbound is no longer nothing, and that is deliberate.** Since 2026-09-16 Hicks reaches `10.0.40.30` on `443` and `8096`, and `10.0.99.20` reaches it on `9100` and `22` — four host-scoped, port-scoped passes above *Block access