From 913a526f438d1b237af29a2fb01e5bfbc8937b5c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 21:06:59 +0000 Subject: [PATCH] docs(adr): record the three decisions the lab buildout rests on Three questions came up while planning what to actually build, and each turned on a judgment worth recording rather than a fact worth looking up. ADR-0006 puts network detection on the firewall. A sensor on the hypervisor sees VLAN 30, which is to say it sees the hypervisor; the segment worth inspecting is the one with seven cameras and a Tuya device on it. Mirroring the trunk into the lab would fix the visibility and break the trust boundary, so morpheus gets Suricata instead. The unconfigured Snort package goes. Honest cost: more load on the single point of failure, and a FreeBSD package rather than a digest-pinned container. ADR-0007 splits the lab in two. Saruman becomes the estate you practise defending; ifrit becomes the range that attacks it. Sharing one box would confound "my detection fired" with "my own tooling made that noise". Lab telemetry stays on VLAN 30 behind its own stack rather than remote-writing into management, because security.md names a lab VM reaching the house as a threat this design defends against. Costs a second observability stack and a machine not yet bought. ifrit's isolation mechanism is deliberately left open rather than assumed. ADR-0008 places services by what their data is worth. A password vault and a film library are not the same kind of thing, so they do not get the same segment: the sensitive tier lands on management under the rule that already exists, and the media tier lands with the televisions it serves, which answers the terminal-VLAN problem by placement instead of by exception. No new segment; the seven VLANs stand and ADR-0002 needs no amendment. That one carries a real cost and says so at length. Putting Immich on Winterfell dilutes the boundary ADR-0002 calls total, and that is the best argument for the dedicated segment this rejects. Single sign-on is deferred on the strength of two users and no remote access, which leaves the MFA gap in security.md open knowingly. When either premise changes, so does the decision. Also corrects two counts the previous pass missed in the README: three inter-VLAN rules rather than two, and eight ADRs rather than five. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013fvaAiKsdyFkX9HgqmuBoj --- README.md | 13 ++- docs/adr/0006-detect-at-the-chokepoint.md | 78 +++++++++++++ ...07-defensive-estate-and-offensive-range.md | 84 ++++++++++++++ docs/adr/0008-place-services-by-data-trust.md | 108 ++++++++++++++++++ 4 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 docs/adr/0006-detect-at-the-chokepoint.md create mode 100644 docs/adr/0007-defensive-estate-and-offensive-range.md create mode 100644 docs/adr/0008-place-services-by-data-trust.md diff --git a/README.md b/README.md index e19fb61..0133e4a 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ incident. ## Highlights - **Network segmented by trust, not by function.** Seven VLANs; IoT, media and - guest segments are terminal — egress only, no path to anything else. Exactly - two inter-VLAN rules exist. [Why](docs/adr/0002-vlan-segmentation-strategy.md) + guest segments are terminal — egress only, no path to anything else. Three + inter-VLAN rules exist, each directional and documented. + [Why](docs/adr/0002-vlan-segmentation-strategy.md) - **Full observability pipeline for a mixed estate.** Grafana Alloy agents push metrics and logs from Linux hosts; `snmp_exporter` polls the four devices that can't run an agent (firewall, switch, UPS, iLO). One agent config, deployed @@ -57,9 +58,9 @@ incident. - **Supply chain pinned by digest.** Every image carries both a tag and a `sha256:` digest, so a moved tag cannot change what deploys. CI enforces it; `make pin-digests` re-resolves them from the registry. -- **Documented decisions and runbooks.** Five ADRs covering what was chosen and - what was rejected; six runbooks for the operations that are easy to get wrong - at 1am. +- **Documented decisions and runbooks.** Eight ADRs covering what was chosen and + what was rejected — including the costs accepted knowingly; six runbooks for + the operations that are easy to get wrong at 1am. ## Architecture @@ -136,7 +137,7 @@ the internet and nothing more. Full topology and data flow in ├── docs/ │ ├── architecture.md network.md hardware.md │ ├── observability.md security.md roadmap.md -│ ├── adr/ # 5 architecture decision records +│ ├── adr/ # 8 architecture decision records │ └── runbooks/ # deploy, add device, rotate creds, certs, key backup, purge └── Makefile # make help ``` diff --git a/docs/adr/0006-detect-at-the-chokepoint.md b/docs/adr/0006-detect-at-the-chokepoint.md new file mode 100644 index 0000000..80975f8 --- /dev/null +++ b/docs/adr/0006-detect-at-the-chokepoint.md @@ -0,0 +1,78 @@ +# ADR-0006: Put network detection on the firewall, not on the hypervisor + +**Status:** Accepted · 2026-08 + +## Context + +[`security.md`](../security.md) states plainly that this network has no IDS/IPS. +That is the largest remaining hole in a design otherwise built around +segmentation: traffic is contained, but nothing inspects it. + +A `pfSense-pkg-snort` package was installed at some point and never configured, +so the gap was real rather than merely undocumented. + +The question is where a sensor should live. Three options: + +1. **A Suricata/Zeek VM on `Saruman`, fed by a SPAN port from `neo`.** The + MokerLink does support port mirroring — this was verified, and it is + currently disabled. Mirroring the trunk would give the sensor every VLAN. +2. **A Suricata VM on `Saruman`, fed by a trunk port.** Same visibility, + achieved by making the hypervisor VLAN-aware. +3. **Suricata on `morpheus`, inline on the interfaces already terminating + there.** + +The deciding fact is where the interesting traffic actually is. `Saruman` is +single-homed on a VLAN 30 access port, so a sensor there sees VLAN 30 — which is +to say, it sees `Saruman`. The segment that warrants inspection is **Skids +(20)**: seven cameras, a doorbell, an alarm hub, five voice assistants, two baby +monitors and a $20 Tuya device, every one of them running firmware nobody +outside its vendor has audited. + +Options 1 and 2 both fix that by giving the *contained* segment a view of every +other segment. A SPAN destination is receive-only and unrouted, so it does not +technically violate the segmentation rules — but an all-VLAN tap terminating on +the box explicitly designated for breaking things is a worse trade than it +appears. Option 2 is worse still: it makes the lab hypervisor routable +everywhere. + +Meanwhile every VLAN already terminates on `morpheus`. It is the one device that +sees inter-VLAN and egress traffic for the entire house, by construction. + +## Decision + +Suricata runs on **`morpheus`**, the firewall. + +- **IDS mode only** to begin with — alert, never block — on the IoT and guest + interfaces first, where a detection is most likely to be real. +- Alerts ship to Loki over the existing remote-syslog path, which + `alloy/config.alloy` already ingests. No new collection mechanism. +- The unconfigured Snort package is removed rather than left installed. Two IDS + packages, one of them dormant, is how you end up debugging the wrong one. + +Port mirroring on `neo` stays disabled. + +This ADR covers detection **for the house**. A sensor scoped to the lab segment +is a separate concern and is addressed in +[ADR-0007](0007-defensive-estate-and-offensive-range.md). + +## Consequences + +- The only point in the network that sees untrusted traffic is now instrumented, + and it required no new hardware, no tap, and no firewall rule. +- The contained segment is not granted visibility of every other segment, so the + trust boundary ADR-0002 establishes stays intact. +- **Load moves onto the single point of failure.** `morpheus` is an i5-8500T with + 32 GB, which is ample for Suricata at residential line rates — but it is also + the box whose failure takes the house offline, and this makes it do more. + Accepted knowingly; the mitigation is the restore story, not a smaller + workload. +- Blocking stays off until the false-positive rate is known. A firewall that + drops the baby monitor is a domestic incident, which is the standard this + project set for itself. +- Cross-VLAN full packet capture for forensics is given up. If that is ever + genuinely needed, `neo` can mirror on demand — a deliberate, temporary action + rather than a standing configuration. +- Detection now depends on a FreeBSD package on an appliance rather than a + container pinned by digest. That is a real inconsistency with how everything + else here is deployed, and it is the honest cost of putting the sensor where + the traffic is. diff --git a/docs/adr/0007-defensive-estate-and-offensive-range.md b/docs/adr/0007-defensive-estate-and-offensive-range.md new file mode 100644 index 0000000..f272381 --- /dev/null +++ b/docs/adr/0007-defensive-estate-and-offensive-range.md @@ -0,0 +1,84 @@ +# ADR-0007: Split the security lab into a defensive estate and an offensive range + +**Status:** Accepted · 2026-08 + +## Context + +This repository began as a place to practise security work. **ImaginationLAN +(30)** exists for that: it is the segment where things get broken on purpose. +[`roadmap.md`](../roadmap.md) has long carried an entry to procure a second +server, `ifrit`, for a deliberately-vulnerable playground. + +`Saruman` — the ProLiant on VLAN 30 — has 48 threads and 128 GB and, at the time +of writing, runs no guests at all. So the question is not whether there is +capacity. It is what the lab is *for*, and how the pieces divide. + +Three options: + +1. **One box does everything.** Attack tooling and target estate share a + hypervisor. Cheapest, and it needs no second machine. But the attacker and + the victim then share a fault domain: a mis-scoped scan or a guest escape + reaches the thing you were trying to observe, and you can never fully + separate "my detection fired" from "my own tooling made that noise." +2. **`Saruman` as a general lab, `ifrit` as the vulnerable playground.** The + roadmap's implicit plan. Better, but it leaves `Saruman` without a defined + purpose — a hypervisor is not a role. +3. **`Saruman` as the defended estate, `ifrit` as the offensive range.** Each + machine has one job, and the boundary between them is the thing being + exercised. + +There is a second, harder question underneath: whether lab telemetry should +reach the production log stack on VLAN 99. It would be convenient — Loki, the +dashboards and 40 alert rules already exist there. But +[`security.md`](../security.md) names "a lab VM escaping into the house" as a +threat this design defends against, and the control is that VLAN 30 is reachable +only *from* trusted, never *to* it. A remote-write path from the lab into +management inverts exactly that. + +## Decision + +**`Saruman` defends. `ifrit` attacks.** + +- `Saruman` hosts the estate you practise defending: a small Windows domain, + realistic endpoints, Wazuh, Velociraptor, and Proxmox Backup Server. It stays + single-homed on VLAN 30 — no trunk, no VLAN-aware bridge. +- `ifrit` holds C2 and attack tooling, and the vulnerable targets. It is sized + for on-demand use and powered off between sessions. +- **Lab telemetry stays in the lab.** `Saruman` runs its own Loki, Grafana and + Alloy under `stacks/lab/`, reusing `alloy/config.alloy` unchanged — the two + `*_URL` variables are the only difference. It does not remote-write to + `10.0.99.20`. The lab's Grafana is reached from Hicks, which the existing + 50→30 rule already permits. +- Network detection scoped to the lab segment belongs on `Saruman`. Detection + for the *house* does not, and lives on the firewall per + [ADR-0006](0006-detect-at-the-chokepoint.md). + +The vulnerable playground is deliberately the least important part. Popping a +box built to be popped teaches very little; detecting a real technique against a +realistic estate teaches a great deal. + +## Consequences + +- Attacker and target are separately owned, so "did the detection fire?" has a + clean answer instead of a confounded one. +- The production log store never ingests potentially-hostile lab telemetry, and + no inter-VLAN rule is needed to make the lab observable. +- **The lab runs a second observability stack.** That is duplicated + infrastructure and duplicated upkeep, and it is the price of the isolation + above. Mitigated by the stack being identical in form to the one that already + exists — same agent config, same patterns, per ADR-0004. +- **It requires a second machine that does not yet exist.** Until `ifrit` is + procured, the estate has nothing attacking it, so the loop this design exists + to close stays open. +- `ifrit`'s isolation mechanism is **not settled here**. Whether it gets a + dedicated VLAN or a physically separate network is deferred deliberately — + adding a segment is a decision this project has declined once already, and it + should be made on its own merits rather than assumed. +- The constraint on `Saruman` is storage, not compute: 128 GB and 48 threads + against a single mirrored pair of 7.2K disks. The fleet is sized against + spindles, not RAM. +- A DL360 Gen9 is loud and lives in an occupied office. Running the estate + continuously has a comfort cost that a quieter box would not impose — which is + a genuine argument for keeping house services off this machine entirely, and + is part of why [ADR-0008](0008-place-services-by-data-trust.md) puts them + elsewhere. diff --git a/docs/adr/0008-place-services-by-data-trust.md b/docs/adr/0008-place-services-by-data-trust.md new file mode 100644 index 0000000..4ea3a24 --- /dev/null +++ b/docs/adr/0008-place-services-by-data-trust.md @@ -0,0 +1,108 @@ +# ADR-0008: Place self-hosted services by the trust of their data + +**Status:** Accepted · 2026-08 + +## Context + +The network is well segmented and thinly served. Beyond the observability stack +it hosts nothing the household actually uses — no password manager, no photo +library, no document archive, no media server. Adding them raises a question the +existing design does not answer: **which segment does a self-hosted service +belong to?** + +ADR-0002 sorts *devices* by trust. Applying the same test to services is the +obvious move, but it only works once you stop treating "self-hosted services" as +a single category. They are not one thing. A password vault and a film library +have almost nothing in common in terms of what their compromise would cost. + +Three options were considered: + +1. **A dedicated services VLAN.** Clean in principle: household services are a + distinct trust tier, so give them their own segment. Costs a new VLAN, a DHCP + scope, switch configuration, and three or four new firewall rules — and it + would have required amending ADR-0002, a document load-bearing in three + places. +2. **Everything on Winterfell (99).** Simplest, and needs no new rules at all, + since 50→99 already exists. But ADR-0002 describes 99 as the segment where + "compromise here is total," and this would put a large, fast-moving codebase + with a media upload endpoint on the same segment as the firewall's admin UI. +3. **Split by the trust of the data.** Sensitive services on management, + streaming services with the media clients they serve. + +Two discovered facts shaped the outcome. First, `oracle` — long earmarked as the +spare that would host this — turned out to be a dual-core A6-9200 with 4 GB and +a 5400 rpm disk, not the 32 GB machine the inventory claimed. It cannot host any +of this, so new hardware was required regardless of segment. Second, **CasaBonita +(40) is terminal by design and its televisions need a media server.** Any design +placing the media server elsewhere has to punch a rule through that terminal +property. + +## Decision + +Services are placed by what their data is worth, across two hosts. **No new +segment is created; the seven VLANs stand.** + +**The sensitive tier — Winterfell (99).** A dedicated low-power mini PC hosts +Vaultwarden, Immich, Paperless-ngx and Home Assistant, behind Caddy and step-ca +for certificates, with AdGuard Home, ntfy and Homepage alongside. Everything +here holds data whose loss or exposure genuinely hurts. Clients on Hicks reach +all of it under the existing 50→99 rule, so this adds no rule at all. + +**The streaming tier — CasaBonita (40).** A quiet N100-class NAS holds the bulk +media library and runs Jellyfin, with Plex beside it for household convenience. +This data is replaceable; its loss is annoying rather than catastrophic. Placing +it *with* the televisions means they reach it natively — **which answers the +terminal-VLAN problem by placement rather than by exception.** + +Two new rules, both management-or-trusted initiating into something less +trusted: + +| Flow | Purpose | +| --- | --- | +| 50 → 40 | Reach the NAS and media server from a workstation | +| 99 → 20 | Home Assistant reaching IoT devices | + +Home Assistant sits on 99 rather than among the devices it controls. It holds +credentials to the locks and cameras, and does not belong on the segment whose +stated assumption is that everything on it is already compromised. + +**Single sign-on is deferred.** Authelia was planned and is not being deployed +yet: with two users and no external exposure, per-application authentication with +TOTP is proportionate, and an SSO layer is operational weight that has not yet +earned its place. This leaves the "no MFA on the internal services" gap in +`security.md` open, knowingly. + +## Consequences + +- **Winterfell becomes busier and less special, and this is the real cost.** + ADR-0002 says compromise there is total. Immich is a large, rapidly-moving + codebase with an upload endpoint, and it will now sit on the same segment as + the firewall's administrative interface, the UPS and the observability stack. + That is a genuine dilution of the most valuable trust boundary in the design, + and it is the strongest argument for the dedicated segment that was rejected. + + What makes it acceptable rather than reckless: nothing here is exposed to the + internet and no ports are forwarded; the realistic threat is a supply-chain + compromise in a container image, which `security.md` already names as accepted + and undefended and which digest pinning mitigates; containers sit on private + bridge networks with only the reverse proxy publishing a port, exactly as + `snmp-exporter` already does; and pfSense's admin UI is already restricted to + specific hosts on Hicks. + + Recording the cost is the point. A tradeoff written down is a decision; one + that is not is an accident. +- The rule count rises from three to five. Every one remains directional — + nothing untrusted ever initiates upward — which is the property that actually + matters, and matters more than the count. +- ADR-0002 needs no amendment. The segment model is unchanged; only its + population grows. +- **New hardware is required.** `oracle` cannot host this, and the loud lab + hypervisor is the wrong home for services the household depends on (see + ADR-0007). Two purchases where the plan originally assumed zero. +- Separating the tiers means two hosts to maintain and back up rather than one. + The compensation is that a failure of the media box costs a film night, and a + failure of the sensitive box costs a restore — never both at once. +- If the household grows, or if external access is ever wanted, this should be + revisited. Both changes push toward a dedicated services segment and toward + Authelia, and both were declined here on the strength of *two users, no remote + access*. When that premise changes, so does the decision.