A segmented home network and its observability stack, managed as code.
Architecture · Network · Observability · Security · Runbooks · Decisions · Roadmap
Seven VLANs behind a pfSense firewall, default-deny between every segment, with a Prometheus/Loki/Grafana stack watching all of it. Every config in this repository is the config that runs, validated on every push.
It started as a place to practise security work and turned into the network the house actually depends on, which changed the requirements considerably — a broken experiment is a learning opportunity, a broken DHCP server is a domestic incident.
- Network segmented by trust, not by function. Seven VLANs; IoT, media and guest segments are terminal — egress only, no path to anything else. Three inter-VLAN rules exist, each directional and documented. Why
- Full observability pipeline for a mixed estate. Grafana Alloy agents push
metrics and logs from Linux hosts;
snmp_exporterpolls the four devices that can't run an agent (firewall, switch, UPS, iLO). One agent config, deployed identically everywhere. How - Dashboards and alerting as code. 5 provisioned dashboards, 84 panels, and 48 alert rules — 35 metric-based in Prometheus, 13 log-based in Loki — sharing one Alertmanager routing tree. No dashboard exists only in a database.
- Secrets encrypted in-repo with SOPS + age. Per-device credentials,
decrypted at deploy time into gitignored paths, with
git logshowing which credential rotated and when — but never to what. Why - CI that actually validates the infrastructure.
docker compose config,promtool,amtool,alloy fmt, a real Loki boot to parse the LogQL rules, dashboard-JSON and datasource checks, every dashboard's PromQL parsed, plusgitleaksover the full history. - 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-digestsre-resolves them from the registry. - Documented decisions and runbooks. Eight ADRs covering what was chosen and what was rejected — including the costs accepted knowingly; nine runbooks for the operations that are easy to get wrong at 1am.
graph TB
INET([Internet]) --- FW{{"morpheus · pfSense<br/>HP ProDesk 600 G4"}}
FW --- SW[neo · 26-port managed switch]
subgraph V99["VLAN 99 · Winterfell · Management"]
MON["<b>prometheus</b><br/>observability stack"]
UPS["mjolnir · UPS"]
end
subgraph V50["VLAN 50 · Hicks · Trusted"]
WS["workstations"]
end
subgraph V30["VLAN 30 · ImaginationLAN · Lab"]
HV["Saruman · Proxmox<br/>BMC: shiva"]
end
subgraph Terminal["VLANs 40 / 20 / 10 · egress only"]
TV["40 · CasaBonita<br/>media"]
IOT["20 · Skids<br/>IoT"]
GUEST["10 · Degens<br/>guest"]
end
SW --- V99
SW --- V50
SW --- V30
SW --- Terminal
WS -.->|management| V99
WS -.->|lab| V30
%% Fill is the patch-cable colour in the rack. A dashed border means the
%% segment is terminal — egress only. Grey carries every VLAN, so it gets
%% no colour of its own. See docs/adr/0009.
classDef vlan99 fill:#6e2c2c,stroke:#f85149,color:#fff
classDef vlan50 fill:#7a3f12,stroke:#db6d28,color:#fff
classDef vlan30 fill:#1f6f4a,stroke:#2ea043,color:#fff
classDef infra fill:#30363d,stroke:#8b949e,color:#e6edf3
classDef vlan40 fill:#a87f00,stroke:#e3b341,color:#0d1117,stroke-dasharray: 6 4
classDef vlan20 fill:#1f4e79,stroke:#388bfd,color:#fff,stroke-dasharray: 6 4
classDef vlan10 fill:#4a3f7a,stroke:#a371f7,color:#fff,stroke-dasharray: 6 4
class MON,UPS vlan99
class WS vlan50
class HV vlan30
class TV vlan40
class IOT vlan20
class GUEST vlan10
class FW,SW infra
style V99 fill:#161b22,stroke:#f85149,stroke-width:2px,color:#f85149
style V50 fill:#161b22,stroke:#db6d28,stroke-width:2px,color:#db6d28
style V30 fill:#161b22,stroke:#2ea043,stroke-width:2px,color:#2ea043
style Terminal fill:#161b22,stroke:#8b949e,stroke-width:2px,color:#8b949e,stroke-dasharray: 6 4
Dotted lines are the only two paths between segments. Everything else reaches
the internet and nothing more. Segment colour matches the patch cable in the
rack; a dashed border means egress only. Full topology and data flow in
docs/architecture.md.
| Layer | Tool | Role |
|---|---|---|
| Firewall / routing | pfSense on FreeBSD 15 | VLANs, DHCP, default-deny |
| Virtualisation | Proxmox VE | Lab hypervisor |
| Metrics | Prometheus | 30-day retention, remote-write receiver |
| Logs | Loki | Single-binary, filesystem storage |
| Collection | Grafana Alloy | node + cAdvisor metrics, Docker/journal/syslog/auth logs |
| Network polling | snmp_exporter | pfSense, switch, UPS, iLO |
| Alerting | Alertmanager | Severity routing, inhibition |
| Visualisation | Grafana | 5 provisioned dashboards |
| Secrets | SOPS + age | Encrypted in-repo |
| CI | GitHub Actions | Lint, config validation, secret scanning, digest pinning |
.
├── stacks/observability/ # the deployed stack — one compose file, six services
│ ├── compose.yaml
│ ├── prometheus/ # config, file_sd targets, 35 alert rules
│ ├── alertmanager/ # routing and inhibition
│ ├── loki/ # single-binary config + 13 LogQL rules
│ ├── alloy/ # one agent config, used on every host
│ ├── snmp-exporter/ # generator.yaml is the source of truth
│ └── grafana/ # provisioning + 5 dashboards
├── secrets/ # SOPS-encrypted; see secrets/README.md
├── scripts/ # bootstrap, render, validate, pin-digests, purge
├── SECURITY.md # disclosure policy and known exposure
├── docs/
│ ├── architecture.md network.md hardware.md
│ ├── observability.md security.md roadmap.md
│ ├── adr/ # 10 architecture decision records
│ └── runbooks/ # deploy, add device, rotate creds, certs, key backup,
│ # purge, restore the firewall, ship firewall logs,
│ # enable suricata
└── Makefile # make help
Requires Docker with the compose plugin, plus sops
and age.
git clone https://github.com/Gerrrt/HomeLab.git && cd HomeLab
make secrets-init # generate an age keypair, create the encrypted secrets file
make secrets-edit # fill in real values
make validate # everything CI runs
make up # render config and start the stackGrafana on :3000, Prometheus on :9090. Full procedure, verification steps and
troubleshooting in docs/runbooks/deploy-stack.md.
$ make help
up Render config and start the stack
down Stop the stack (volumes are preserved)
reload Hot-reload Prometheus, Alertmanager and snmp-exporter (no restart)
secrets-init Generate an age keypair and create the encrypted secrets file
secrets-edit Edit the encrypted secrets in $EDITOR
secrets-verify-backup Check a backup age key decrypts the secrets
validate Run every check CI runs
backup Back up the stack's volumes to ./backups/
...Rendered from the running stack by make screenshots, over a 24-hour window.
Four of the five provisioned dashboards are here; docs/images/README.md
explains why the Logs dashboard is deliberately not among them.
The entire observability stack runs on a 2012 MacBook Pro with Ubuntu Server on
it. Four SNMP devices at a 60-second interval, Alloy agents, and 30 days of
metrics, on hardware that was otherwise going to landfill. Hardware details in
docs/hardware.md.
Segmentation rationale, threat model, secrets handling, and an explicit account
of what this repository deliberately does not publish (full MAC addresses,
owner-linked device names, camera placement) are in
docs/security.md.
Historical credential exposure in this repository's git history is documented
there too, along with the runbooks to remediate it — including the parts not yet
done. SECURITY.md carries the disclosure policy and a summary of
what is known.
Container images are pinned by tag and digest. A tag is a mutable pointer; a
digest is the content hash, so a moved tag cannot change what gets deployed. CI
enforces it, and make pin-digests re-resolves them.
Open work is tracked in
Issues;
docs/roadmap.md is the narrative — what is outstanding and
why it is in that order.
The current top items: replace the UPS battery and rack the shelf switch, which are one purchase — the pack alone leaves the monitoring path half-protected (#93, #110); get the firewall backup off the machine it protects, and buy the spare that turns its restore runbook from a hypothesis into something rehearsed (#92); and take 64-bit interface counters off the switch, which is unblocked now that it is polling again (#87).



