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. Exactly two inter-VLAN rules exist. 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, 79 panels, and 40 alert rules — 32 metric-based in Prometheus, 8 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. Five ADRs covering what was chosen and what was rejected; four 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 · Management"]
MON["<b>prometheus</b><br/>observability stack"]
UPS["mjolnir · UPS"]
end
subgraph V50["VLAN 50 · Trusted"]
WS["workstations"]
end
subgraph V30["VLAN 30 · Lab"]
HV["shiva · Proxmox"]
end
subgraph Terminal["VLANs 40 / 20 / 10 · egress only"]
UNTRUSTED["media · IoT · guest"]
end
SW --- V99
SW --- V50
SW --- V30
SW --- Terminal
WS -.->|management| V99
WS -.->|lab| V30
classDef mgmt fill:#1f6f4a,stroke:#2ea043,color:#fff
classDef trusted fill:#1f4e79,stroke:#388bfd,color:#fff
classDef untrusted fill:#6e2c2c,stroke:#f85149,color:#fff
classDef infra fill:#4a3f7a,stroke:#a371f7,color:#fff
class MON,UPS mgmt
class WS trusted
class UNTRUSTED untrusted
class HV,FW,SW infra
Dotted lines are the only two paths between segments. Everything else reaches
the internet and nothing more. 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, 32 alert rules
│ ├── alertmanager/ # routing and inhibition
│ ├── loki/ # single-binary config + 8 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/ # 5 architecture decision records
│ └── runbooks/ # deploy, add device, rotate creds, purge history
└── 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 and Alertmanager without a restart
secrets-init Generate an age keypair and create the encrypted secrets file
secrets-edit Edit the encrypted secrets in $EDITOR
validate Run every check CI runs
backup Back up the stack's volumes to ./backups/
...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 docs/roadmap.md. The current top
items: rotate the SNMP communities, purge the old ones from git history, replace
the UPS battery, and get 64-bit interface counters off the switch.