Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ is a very different thing from an overlooked one. Full detail in
| The monitoring host's disk and swap are unencrypted | **Accepted residual, not a fix in progress** — see below. |
| Loki's log volume has no size ceiling | **Accepted residual, not a fix in progress** — see below. Prometheus has a byte ceiling since [#184](https://github.com/Gerrrt/HomeLab/issues/184); Loki has no size-based retention to set, only time. `HostDiskWillFillIn24h` is the control. |
| The hypervisor's BMC shares a broadcast domain with the attack VM | **Accepted residual, not a fix in progress.** `shiva`, the iLO 4 at `10.0.30.10` on firmware 2.82, sits on ImaginationLAN, the segment [ADR-0014](docs/adr/0014-put-ifrit-on-imaginationlan-and-give-the-targets-no-route.md) gives to `ifrit`'s Kali VM, and a BMC does not get patched the way a guest does. [ADR-0033](docs/adr/0033-keep-the-ilo-on-the-lab-segment.md) keeps it there on purpose: the iLO is part of the estate under attack, and a BMC compromise in the lab costs the lab. Moving it to Winterfell would put an end-of-line BMC beside the firewall's admin UI and open three more ports on the Hicks list to reach its console. Controls: the BMC's own hardening (IPMI-over-LAN and unused services off, a credential shared with nothing in the house), and the lab tripwire — anything the BMC initiates toward another segment is a logged pass and a `LabSegmentReachedInternalNetwork` alert. Both done 2026-09-09: IPMI-over-LAN, SSH and iLO Federation off on the BMC, its account's credential shared with nothing in the house, its security log read for a baseline; and the `10.0.30.10 → 10.0.99.20/udp` "return path" rule deleted from `morpheus` — it had been evaluated 7.4 million times and matched zero packets, because pf state carries the scrape's replies, and it was the BMC's only path to the monitoring host's syslog listener. The scrape failed once at the reload and has been clean since; the lab interface now passes nothing across a segment except `Saruman`'s two agent ports. |
| `smaug` ships no logs, and no absence rule can say so | **Accepted residual, not a fix in progress.** [ADR-0016](docs/adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md) reverses the monitoring direction for CasaBonita (40) so that nothing on that segment initiates upward: `prometheus` scrapes `10.0.40.30` over `99 → 40:9100`, and `smaug` runs no Alloy agent. Loki has no pull — ingest is a push or it is nothing — so the NAS ships no logs, and with the agent go its SMART attributes and its patch state for the same reason ([#483](https://github.com/Gerrrt/HomeLab/issues/483) owns that half). It is the one host in the estate whose compromise is invisible: Prometheus shows it alive and busy, and nothing shows what it did — on the segment whose stated assumption is that everything on it is already compromised. That is already true with nothing deployed on it: the blind spot is a property of the direction rather than of what runs there, and Jellyfin's network-facing indexer will land on a host that was invisible before it arrived. **Collecting the logs costs more than the gap.** A `40 → 99:3100` rule would be the first upward path in the estate, and it would hand a low-trust host write access to the unauthenticated ingest ports recorded above, so the two residuals would compound rather than one closing the other. What still watches the segment is the `igc0.40` tripwire and nothing else — Suricata runs on Skids and Degens, not on 40. **No new alert is warranted, and that is the second half of this row.** `TerminalSegmentReachedInternalNetwork` fires on a `pass` from a terminal VLAN toward `30`, `50`, `99` or the switch LAN, which catches a compromise *leaving*; anything that stays on CasaBonita or egresses to the internet leaves no record at all, and a rule cannot be written against a record that does not exist. Decided under [#255](https://github.com/Gerrrt/HomeLab/issues/255); the detail is in [`docs/security.md`](docs/security.md). |

The switch is the honest gap, and it is a deliberate one. `neo` (10.7.7.2) is
rotated and polling. Its firmware does not persist a deletion from the SNMP
Expand Down
2 changes: 1 addition & 1 deletion docs/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ Televisions and consoles. Internet only.
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**.
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.
- **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
Expand Down
60 changes: 52 additions & 8 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,27 @@ Everything else — IoT, media, guest — gets internet and nothing more.
That sentence is about what those segments *initiate*, and two decisions now
reach into them without touching it.
[ADR-0016](adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md)
writes three passes into CasaBonita for the NAS, and
writes 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
Skids, and they are at different stages — this paragraph said neither was
created, and half of that stopped being true on 2026-09-16. **CasaBonita's
exist.** Hicks reaches `10.0.40.30` on `443` and `8096`, and `10.0.99.20`
reaches it on `9100` and `22`: four host- and port-scoped passes above *Block
access to CasaBonita*, verified in position with `pfctl` rather than read off
the web UI, where an appended rule looks present while matching nothing.
ADR-0016 wrote three and four exist, because the Hicks pass is split rather
than carrying a port list, and the `22` is inert — TrueNAS ships SSH disabled
([ADR-0040](adr/0040-run-truenas-on-smaug-and-keep-the-media-stack-in-this-repository.md)).
[`network.md`](network.md) holds the current list. **Skids' does not exist.**
`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 — still waits above the block that
has stood between 99 and 20 since the segments existed, on the host that would
use it ([#134](https://github.com/Gerrrt/HomeLab/issues/134)). 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.
ports, with the twenty other devices on Skids still unreachable from anywhere,
and both segments still initiating nothing. The tripwire below is the check
that the second half holds — waiting on Skids, and since 2026-09-16 actually
checking on CasaBonita.

That sentence is now checked rather than asserted. Four **tripwire** rules
sit below the block rules that stop each cross-segment path and above the
Expand All @@ -246,6 +257,39 @@ against a firewall that logged only blocks, so it could not fire for any input
firewall restore from a backup older than 2026-09-01 drops them silently; the
restore runbook checks for them.

**On CasaBonita that tripwire is now the only control watching the segment**,
and since 2026-09-16 there is something on it worth watching. `smaug` at
`10.0.40.30` is scraped rather than pushing, by the same ADR-0016 decision that
keeps the segment terminal outward: it runs no Alloy agent, and with the agent
go its logs, its SMART attributes and its patch state, because every one of
those rides a push this host does not make. Loki has no pull — ingest is a push
or it is nothing — so the only rule that would centralise the logs is the
`40 → 99:3100` the ADR refused, and refusing it is the residual recorded in
[`SECURITY.md`](../SECURITY.md)
([#255](https://github.com/Gerrrt/HomeLab/issues/255) for the logs,
[#483](https://github.com/Gerrrt/HomeLab/issues/483) for the rest). Suricata
does not watch `igc0.40` either; it runs on Skids and Degens only. So the
tripwire is what is left, and it is worth being exact about what that buys:
`TerminalSegmentReachedInternalNetwork` fires on a `pass` from 10, 20 or 40
toward 30, 50, 99 or the switch LAN, which is a compromise *leaving* the
segment. Anything `smaug` does that stays on CasaBonita, or goes straight out
to the internet, is not merely unalerted — it is unrecorded.

**There is no alert for that and there cannot be one**, which is a decision and
not an omission. A rule reading "no logs from `smaug`" is an absence rule, and
an absence rule needs a declared expectation to measure the silence against —
`homelab_suricata_expected_interface` is that pattern, and `SuricataLogsStopped`
only became writable once somebody measured what a normal silence looks like:
22 days of stream, a worst gap of 75 minutes, nine hours chosen as seven times
it. Here the expected volume is zero and always was, so silence is the design
working and no threshold separates it from a compromise. The two
collection-completeness rules are no help for the same reason.
`JournalSourceStopped` and `LogEntriesDropped` are both keyed on an agent that
exists, and `check_loki_coverage.py` takes its denominator from the hosts Loki
is already hearing from — so a host that has never shipped a line is absent
from the question rather than failing it. The firewall and the tripwire are the
control. This paragraph is the record that there is nothing else.

Segmentation is doing more work here than it should have to. Prometheus and Loki
publish unauthenticated ingest ports for `oracle`'s agent to use, so anything
that can route to `10.0.99.20:9090` or `10.0.99.20:3100` can write to the metric
Expand Down
Loading