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.