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
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
```
Expand Down
78 changes: 78 additions & 0 deletions docs/adr/0006-detect-at-the-chokepoint.md
Original file line number Diff line number Diff line change
@@ -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.
84 changes: 84 additions & 0 deletions docs/adr/0007-defensive-estate-and-offensive-range.md
Original file line number Diff line number Diff line change
@@ -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.
108 changes: 108 additions & 0 deletions docs/adr/0008-place-services-by-data-trust.md
Original file line number Diff line number Diff line change
@@ -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.
Loading