I Turned an Old PC Into a Blue Team Homelab
Building a small SOC with Proxmox, Wazuh, Suricata, Zeek, and a managed-switch mirror—and debugging the traffic path when normal unicast never reached the sensor.
- Host
- Intel Core i3-6100, 2 cores / 4 threads, 8 GB DDR4
- Platform
- Proxmox VE with Debian 12 LXCs
- SIEM
- Wazuh 4.14.6 all-in-one
- Network sensor
- Suricata and Zeek on a dedicated SPAN interface
- Switch
- Omada by TP-Link ES208G, 8-port Gigabit Easy Managed
- Status
- End-to-end traffic capture and SIEM ingestion verified
I had an old desktop with a sixth-generation Core i3, 8 GB of RAM, one normal Gigabit Ethernet port, and no realistic future as a modern daily-use machine. It was already running Proxmox and one small DNS container, but most of its CPU time and storage were doing nothing.
I wanted to see how far that hardware could go as a defensive security lab. Not a list of tools installed for screenshots, and not a huge enterprise stack that spent all day swapping. I wanted a small SOC that could receive host events, inspect copied network traffic, produce useful metadata, and put the result somewhere searchable.
The final design used two new Linux containers. One ran Wazuh as the central SIEM. The other ran Suricata and Zeek against traffic copied by a managed switch. A Wazuh agent on the sensor forwarded both tools’ JSON logs back to the SOC.
The interesting part was not getting the containers to start. It was finding out why a mirror port that looked alive was showing almost no useful traffic. My first 25-second test captured 25 packets, mostly ARP and IGMP. After fixing the dedicated Proxmox bridge, the same length test captured 2,201 packets, in both directions, with zero drops reported by tcpdump.
This is the story of the build, the decisions behind it, what broke, and the evidence that showed the complete path was finally working.
Why I built this
Running a security tool is easy. Building a useful detection path is a different problem.
I wanted the lab to answer a few practical questions:
- Can I collect endpoint events in one place?
- Can I inspect home-network traffic without placing an IPS inline?
- Can I get both signature alerts and richer connection metadata?
- Can I prove that a packet seen by the switch becomes a searchable event in the SIEM?
- Can all of that stay usable on an old two-core PC with 8 GB of RAM?
That last constraint shaped the project more than any product checklist. It ruled out Security Onion, a second Elastic stack, MISP, OpenCTI, TheHive with Cortex, and an always-on Windows VM. Those are useful platforms, but installing all of them would have turned the host into a memory-pressure demonstration.
Instead, I chose one central system and one network sensor:
| Component | Job |
|---|---|
| Proxmox | Runs and isolates the lab guests |
| Wazuh | Receives, analyzes, indexes, and presents security events |
| Suricata | Produces signature-based IDS alerts and protocol events |
| Zeek | Produces structured connection and application metadata |
| Omada by TP-Link ES208G | Copies real traffic to the passive sensor with port mirroring |
Suricata and Zeek overlap, but they do not tell the same story. Suricata is good at saying, “this traffic matched a rule” or “this protocol behavior looks wrong.” Zeek is good at describing what happened: connections, DNS questions, TLS sessions, HTTP requests, SSH metadata, files, and other context. I wanted both views of the same traffic.
What I had to work with
The machine was modest but usable:
| Resource | Starting point |
|---|---|
| CPU | Intel Core i3-6100 at 3.70 GHz, 2 cores / 4 threads |
| Memory | 8 GB DDR4 |
| Fast storage | Approximately 256 GB SSD for Proxmox and latency-sensitive workloads |
| Bulk storage | Approximately 500 GB HDD for sensor data |
| Main network | One 1 Gb/s Realtek Ethernet interface |
| Monitor network | One ASIX USB 2.0 Ethernet interface at 100 Mb/s |
| Managed switch | Omada by TP-Link ES208G, 8-port Gigabit Easy Managed |
The 100 Mb/s USB adapter looks like the obvious weak point. In this layout it was acceptable because the monitored Internet connection was 40 Mb/s. That does not make it universally sufficient: a mirror can carry traffic in both directions, and sustained local traffic or a faster uplink could overwhelm it. For this specific WAN-side capture path, it fit the expected load.
Before creating anything, I checked the host instead of estimating from its sticker:
pveversion -v
lscpu
free -h
pvesm status
ip -br link
ip -br addr
pct list
The host was mostly idle and had enough free memory to try a consolidated lab, as long as Wazuh received most of the RAM and I resisted adding every security product I could name.
The architecture
I separated normal management traffic from passive packet capture. The
sanitized topology below uses 10.0.10.0/24 for infrastructure and
10.0.20.0/24 behind the downstream router.
Internet
|
Main router / gateway (10.0.10.1)
|
Omada by TP-Link ES208G
|-- Port 1: uplink to main router
|
|-- Port 2: downstream router WAN (10.0.10.6)
| |
| `-- downstream LAN/Wi-Fi (10.0.20.0/24)
|
`-- Port 3: mirror destination
|
`-- USB Ethernet adapter
|
`-- Proxmox vmbr1
|
`-- bt-sensor span0
|-- Suricata
|-- Zeek
`-- Wazuh agent
|
`-- soc-core (10.0.10.30)
|-- Wazuh Manager
|-- Wazuh Indexer
|-- Filebeat
`-- Wazuh Dashboard
Proxmox vmbr0, normal management network
|-- Proxmox host (10.0.10.3)
|-- soc-core (10.0.10.30)
`-- bt-sensor eth0 (10.0.10.31)
The sensor had two interfaces for a reason:
eth0onvmbr0had a normal IP address. It handled updates, Wazuh agent traffic, and administration.span0onvmbr1had no IPv4 address. It existed only to receive copied Ethernet frames from the switch.
Keeping the capture interface unnumbered reduced accidental traffic on the monitor path and made the direction of data easier to reason about. The sensor observed traffic; it was not the gateway and it was not inline.
Creating soc-core and bt-sensor
I used LXCs because they share the Proxmox kernel and cost much less memory than two full virtual machines. That mattered on an 8 GB host.
The final allocations were:
| VMID | Name | CPU | RAM | Swap | Disk | Role |
|---|---|---|---|---|---|---|
| 200 | soc-core |
2 vCPU | 4608 MiB | 2048 MiB | 80 GiB SSD | Wazuh all-in-one |
| 201 | bt-sensor |
2 vCPU | 1536 MiB | 512 MiB | 40 GiB HDD | Suricata, Zeek, Wazuh agent |
The sensor was a privileged LXC. I made that trade-off because low-level packet capture plus Docker networking and capabilities are harder to support inside an unprivileged LXC. This was a narrow lab decision, not a reason to make every container privileged.
The creation commands looked like this after replacing the real network with the documentation range:
pct create 200 local:vztmpl/debian-12-standard_<version>_amd64.tar.zst \
--hostname soc-core \
--cores 2 --memory 4096 --swap 1024 \
--rootfs <storage>:80 \
--net0 name=eth0,bridge=vmbr0,firewall=1,ip=10.0.10.30/24,gw=10.0.10.1,type=veth \
--nameserver 10.0.10.1 \
--features nesting=1,keyctl=1 \
--unprivileged 1 --onboot 1
pct create 201 local:vztmpl/debian-12-standard_<version>_amd64.tar.zst \
--hostname bt-sensor \
--cores 2 --memory 1536 --swap 512 \
--rootfs <storage>:40 \
--net0 name=eth0,bridge=vmbr0,firewall=1,ip=10.0.10.31/24,gw=10.0.10.1,type=veth \
--net1 name=span0,bridge=vmbr1,firewall=0,ip=manual,type=veth \
--nameserver 10.0.10.1 \
--features nesting=1 \
--unprivileged 0 --onboot 1
pct start 200
pct start 201
Storage names and template versions vary between Proxmox installations, so those are intentionally placeholders rather than fake universal values.
I tested the ordinary network before installing the stack:
pct exec 200 -- ping -c 2 10.0.10.1
pct exec 201 -- ping -c 2 10.0.10.1
pct exec 200 -- getent hosts packages.wazuh.com
pct exec 201 -- getent hosts deb.debian.org
That sounds basic, but it established a boundary: if an installer failed next, I already knew the LXC bridge, gateway, and DNS path were working.
Setting up Wazuh
I installed Wazuh 4.14.6 as an all-in-one deployment inside soc-core. That put
the manager, indexer, Filebeat, and dashboard together because splitting them
across more guests would have consumed memory the host did not have.
pct exec 200 -- bash -lc '
cd /root
curl -fsSLO https://packages.wazuh.com/4.14/wazuh-install.sh
bash ./wazuh-install.sh -a -i
'
This was where the first resource decision came back to bite me. I had placed
the 80 GiB SOC root disk on the large HDD. While the dashboard package was
unpacking a very large number of small files, dpkg entered uninterruptible
disk wait. The process was alive, the disk queue was busy, and there were no
ATA or filesystem errors. It was not a corrupt drive; it was the wrong workload
on the slower storage path.
I stopped only CT200, moved its root volume to SSD-backed local-lvm, increased
its memory to 4608 MiB and swap to 2048 MiB, then repaired the interrupted
package state:
# On the Proxmox host; use the storage name from your own system.
pct shutdown 200
pct move-volume 200 rootfs local-lvm --delete 1
pct set 200 --memory 4608 --swap 2048
pct start 200
pct exec 200 -- dpkg --configure -a
pct exec 200 -- apt-get -f install -y
The interrupted run also left the dashboard without the TLS files it expected. I restored the generated dashboard key, certificate, and CA from Wazuh’s secure installer archive with restrictive ownership and permissions. I am deliberately not including the archive contents or any credential commands here.
One final problem remained: the dashboard tried to reach the indexer through
https://localhost:9200, which resolved to IPv6 ::1, while the indexer was
listening on IPv4 localhost. I changed the dashboard configuration to use the
explicit loopback address:
server.host: 0.0.0.0
server.port: 443
opensearch.hosts: https://127.0.0.1:9200
After restarting the dashboard, the local check returned HTTP 302—the expected redirect to its login flow:
systemctl restart wazuh-dashboard
curl -k -s -o /dev/null -w '%{http_code}\n' https://127.0.0.1/
302
The important lesson was not “always use an SSD.” It was that service status alone does not explain an installation failure. Process state, I/O wait, kernel errors, and the workload’s file pattern told a much better story.
Adding the Wazuh agent to the sensor
The sensor needed a Wazuh agent so its local JSON logs could enter the central pipeline. I installed the agent from the Wazuh repository with the manager and agent name supplied at install time. The Wazuh repository was already configured at this point, so I am only showing the agent-specific part below:
export WAZUH_MANAGER=10.0.10.30
export WAZUH_AGENT_NAME=bt-sensor
apt-get install wazuh-agent
systemctl enable --now wazuh-agent
On the manager, the sensor appeared as agent ID 001. I confirmed that instead of assuming a running local service meant successful enrollment:
/var/ossec/bin/agent_control -l
At this point Wazuh could monitor the sensor as a Linux endpoint, but it still needed to read the network tools’ output.
Adding Suricata and Zeek
My first package transaction asked Debian 12 for suricata,
suricata-update, and docker-compose-plugin. Those candidates were not
available from the enabled repositories, so the transaction installed nothing.
Rather than quietly rewriting that attempt out of the story, I switched the
network tools to containers and kept the supporting investigation utilities on
the LXC itself.
I created persistent log directories for both engines. The commands below show
the final configuration on span0; during early validation I temporarily ran
the sensors against eth0 until the mirror path was proven.
install -d -m 0755 /var/log/suricata /var/log/zeek /var/lib/suricata/rules
docker run -d --name suricata --restart unless-stopped \
--network host \
--cap-add NET_ADMIN --cap-add NET_RAW --cap-add SYS_NICE \
-v /var/log/suricata:/var/log/suricata \
-v /var/lib/suricata:/var/lib/suricata \
jasonish/suricata:latest -i span0
docker run -d --name zeek --restart unless-stopped \
--network host \
--cap-add NET_RAW --cap-add NET_ADMIN \
-v /var/log/zeek:/logs -w /logs \
zeek/zeek:lts zeek -i span0 LogAscii::use_json=T local
I used mutable latest and lts tags during the original build. That was
convenient, but it is weaker for reproducibility because those tags can point
to different images later. If I rebuilt this from scratch, I would pin tested
image digests.
Suricata’s rule update produced 68,043 rules. Of those, 52,106 were enabled, with another 136 enabled to satisfy flowbit dependencies:
docker run --rm --entrypoint suricata-update \
-v /var/lib/suricata:/var/lib/suricata \
jasonish/suricata:latest
wc -l /var/lib/suricata/rules/suricata.rules
The two useful log roots were now:
/var/log/suricata/eve.json
/var/log/suricata/fast.log
/var/log/zeek/conn.log
/var/log/zeek/dns.log
/var/log/zeek/ssl.log
Zeek was launched with LogAscii::use_json=T, so Wazuh could treat its output
as structured JSON rather than trying to decode presentation-oriented text.
Getting the logs into Wazuh
I added two localfile entries to the sensor agent’s
/var/ossec/etc/ossec.conf:
<localfile>
<log_format>json</log_format>
<location>/var/log/suricata/eve.json</location>
</localfile>
<localfile>
<log_format>json</log_format>
<location>/var/log/zeek/*.log</location>
</localfile>
The agent’s manager block pointed to the central LXC over TCP 1514:
<client>
<server>
<address>10.0.10.30</address>
<port>1514</port>
<protocol>tcp</protocol>
</server>
</client>
Then I restarted the agent and checked both ends:
# Inside bt-sensor
systemctl restart wazuh-agent
/var/ossec/bin/wazuh-control status
# Inside soc-core
/var/ossec/bin/agent_control -l
The data path I was trying to build was now explicit:
Mirrored frame
-> span0
-> Suricata eve.json / Zeek JSON logs
-> Wazuh agent logcollector
-> TCP 1514
-> Wazuh Manager
-> Filebeat
-> Wazuh Indexer
-> Wazuh Dashboard
I had the software path. I still did not have trustworthy mirrored traffic.
Getting traffic into the sensor with SPAN
Why I chose the TP-Link Omada ES208G
The switch was a TP-Link Omada ES208G, an eight-port Gigabit Easy Managed switch. It was not just a convenient way to add more Ethernet ports; it was the component that made passive monitoring possible.
An unmanaged switch would have been cheaper, but it could not deliberately copy another port’s unicast traffic to the sensor. At the other end of the market, a larger enterprise switch would have added cost, noise, power use, and features this small network did not need. I was building the lab around reused hardware, so buying a large switch just to unlock one monitoring feature would have missed the point.
The ES208G sat in the useful middle ground. TP-Link specifies:
- eight 10/100/1000 Mb/s RJ45 ports;
- one-to-one and many-to-one port mirroring;
- ingress, egress, or bidirectional mirror selection;
- a local web interface, alongside optional Omada centralized management;
- port statistics, VLANs, port isolation, loop prevention, and IGMP snooping;
- a fanless metal enclosure with desktop or wall mounting;
- 16 Gb/s switching capacity and an 11.9 Mpps forwarding rate.
Those are the broader capabilities; this build used only a small subset. I
configured the switch through its local web UI, selected the downstream
router’s port as the source, selected the sensor cable as the destination, and
mirrored both ingress and egress. The switch duplicated the frames—it did not
analyze them—and Suricata and Zeek handled inspection after those copies reached
span0.
Eight Gigabit ports were enough for the router uplink, downstream router, sensor destination, and spare connections without paying for PoE, 10 Gigabit, SFP, or Layer-3 routing that this design did not require. It was compact, silent, simple to configure, and inexpensive enough to make sense beside an old PC rather than becoming the most expensive part of the project.
The switch itself was not the 100 Mb/s bottleneck. All eight switch ports were Gigabit; the limit came from the separate USB Ethernet adapter used as the SPAN destination. That remained acceptable for the 40 Mb/s Internet link monitored in this build.
The relevant specifications are available on TP-Link’s official ES208G product page and in the official ES208G datasheet.
A normal Ethernet switch does not send every frame to every port. Once it learns where a destination MAC address lives, it sends known unicast only to that port. Plugging a sensor into another ordinary switch port therefore shows broadcasts, multicast, and traffic addressed to the sensor—not everyone else’s conversations.
The managed switch solved that by copying traffic from one source port to one destination port:
| Switch setting | Value used |
|---|---|
| Session | 1, enabled |
| Source / mirrored port | Port 2 |
| Source direction | Ingress and egress |
| Destination / mirroring port | Port 3 |
Port 2 carried the WAN side of a downstream router. Port 3 went to the USB NIC on Proxmox. Selecting both ingress and egress mattered because one direction would show only half of each conversation.
The Proxmox side used a separate bridge:
iface enx<redacted> inet manual
auto vmbr1
iface vmbr1 inet manual
bridge-ports enx<redacted>
bridge-stp off
bridge-fd 0
I have redacted the predictable interface suffix because it is derived from
the adapter’s MAC address. On another machine, use the real interface name
shown by ip -br link.
Physically, the link came up at 100 Mb/s full duplex. span0 also showed
packets. That looked promising—until I asked what those packets actually were.
The part that broke
The first sample contained broadcasts, ARP, IGMP, and IEEE 1905.1 traffic. In other words, the capture interface was not dead. A focused 35-second test around the downstream router, however, saw only two inbound ARP requests and nothing sourced from the router.
I generated more traffic behind it and ran a bounded capture:
timeout 25 tcpdump -ni span0 -s 128 -w /tmp/span-before.pcap
tcpdump -nr /tmp/span-before.pcap
The result was only 25 packets in 25 seconds, still mainly ARP and IGMP. The mirror configuration looked correct, the physical link was up, and broadcast traffic reached the LXC—but normal mirrored unicast did not.
That distinction was the key. “I can see packets” was not the same as “the mirror is delivering both directions.” Broadcasts are exactly the traffic most likely to survive an incomplete forwarding path.
I checked the path one layer at a time:
# Proxmox host
ethtool enx<redacted>
ip -s link show enx<redacted>
ip -d link show vmbr1
bridge link show
bridge fdb show br vmbr1
# Sensor
pct exec 201 -- ethtool span0
pct exec 201 -- ip -s link show span0
The evidence pointed at the dedicated Linux bridge between the physical USB port and the LXC veth.
Why bridge-ageing 0 fixed it
A Linux bridge normally learns source MAC addresses and remembers which port they came from. That is useful on a normal virtual switch because it avoids flooding every frame to every attached guest.
A mirror destination is not normal traffic. Both sides of conversations are copied into the bridge through the same physical USB-facing port. The best-supported explanation for what I observed is that normal MAC learning caused mirrored unicast to be suppressed instead of being flooded to the sensor’s veth. Broadcast and multicast still appeared, which is why the path looked partly alive.
I placed the physical interface, bridge, and sensor interface into promiscuous mode, then disabled ageing on the dedicated monitor bridge:
# Proxmox host
ip link set dev enx<redacted> promisc on
ip link set dev vmbr1 promisc on
ip link set dev vmbr1 type bridge ageing_time 0
# Sensor interface inside CT201
pct exec 201 -- ip link set span0 promisc on
Promiscuous mode allowed the interfaces to accept frames addressed to other
devices. With ageing time zero, vmbr1 did not retain learned MAC destinations;
on this dedicated bridge, mirrored frames were flooded to span0 instead of
being hidden by ordinary learned-port behavior.
I repeated the same 25-second capture. This time it recorded:
| Measurement | Before fix | After fix |
|---|---|---|
| Capture duration | 25 seconds | 25 seconds |
| Total packets | 25 | 2,201 |
| Outbound non-multicast packets from monitored router | Not meaningfully present | 589 |
| Inbound non-multicast packets to monitored router | Not meaningfully present | 1,598 |
| Drops reported by tcpdump | No useful full-flow test | 0 |
The exact inbound and outbound counts do not sum to the total because the direction filters were separate classifications and the capture also contained other traffic. The important result was that both unicast directions appeared and tcpdump reported no drops for that bounded test.
I persisted the behavior in /etc/network/interfaces:
auto vmbr1
iface vmbr1 inet manual
bridge-ports enx<redacted>
bridge-stp off
bridge-fd 0
bridge-ageing 0
Then I checked the saved configuration and running bridge:
ifquery --check vmbr1
ip -d link show vmbr1
That was the moment the network sensor became real. Until then, Suricata and Zeek were installed, but the intended data source was not proven.
Moving both engines onto the real feed
During early testing I temporarily pointed Suricata and Zeek at eth0 so I
could generate logs while SPAN was unresolved. That proved the containers and
log pipeline, but it did not prove passive network visibility.
Once the bridge test passed, I recreated both containers on span0 and checked
their actual command lines:
docker inspect suricata --format '{{json .Config.Cmd}}'
docker inspect zeek --format '{{json .Config.Cmd}}'
docker stats --no-stream
At a historical resource snapshot, Suricata used roughly 436–483 MiB of RAM and Zeek about 95–114 MiB. Consolidating them in one 1536 MiB sensor LXC was tight but reasonable for this traffic level.
Testing the whole pipeline
I wanted more than a green process list, so I tested each boundary separately.
1. Is the mirror delivering packets?
The 2,201-packet capture proved that span0 received bidirectional copied
traffic. A later capture-health sample also reported zero kernel drops. Those
are bounded observations, not a promise that the sensor can never drop traffic.
2. Is Suricata detecting anything?
I used the harmless testmynids.org response, which is designed to trigger a
known IDS test signature:
curl -fsS --max-time 20 http://testmynids.org/uid/index.html >/dev/null || true
grep -F 'GPL ATTACK_RESPONSE' /var/log/suricata/fast.log | tail
jq -c 'select(.event_type=="alert") | {timestamp,src_ip,dest_ip,alert}' \
/var/log/suricata/eve.json | tail
Suricata produced:
SID: 2100498
Signature: GPL ATTACK_RESPONSE id check returned root
Classification: Potentially Bad Traffic
Severity: 2
That did not mean the machine had been compromised. It meant a controlled, known response traversed the capture path and matched the expected rule.
3. Is Zeek producing useful context?
Zeek generated more than 1,800 records during the initial validation and wrote JSON connection, DNS, and TLS logs:
tail -n 1 /var/log/zeek/conn.log | jq .
tail -n 1 /var/log/zeek/dns.log | jq .
tail -n 1 /var/log/zeek/ssl.log | jq .
Later logs covered HTTP, SSH, DHCP, QUIC, files, known hosts, known services, notices, analyzer output, and other protocol metadata. Suricata was finding signatures; Zeek was making the surrounding conversations searchable.
4. Do the records reach Wazuh?
I checked the manager’s agent list, searched its alerts for Suricata records,
and then used Wazuh Threat Hunting with the sensor selected. One post-SPAN
check found 263 Suricata records. A later reviewed dashboard range showed
1,419 hits attributed to bt-sensor.
Useful searches included:
agent.name:"bt-sensor" AND rule.groups:suricata
agent.name:"bt-sensor" AND rule.level:>=7
agent.name:"bt-sensor" AND data.alert.signature_id:*
That closed the loop. The evidence now covered every stage:
real traffic
-> switch mirror
-> USB NIC
-> Proxmox vmbr1
-> LXC span0
-> Suricata and Zeek JSON
-> Wazuh agent
-> manager and indexer
-> searchable dashboard events
What the finished lab can actually see
The lab can now provide:
- Wazuh endpoint monitoring for the sensor and central SOC guest;
- centralized security-event search and alerting;
- Suricata alerts, flows, DNS, TLS, HTTP, QUIC, DHCP, SSH, anomaly, and related EVE records observed during operation;
- Zeek connection, DNS, TLS, HTTP, SSH, file, software, notice, and other metadata;
- bidirectional visibility into traffic crossing the mirrored router WAN port;
- a passive architecture that does not interrupt connectivity if the sensor is stopped.
There is an important boundary: the mirror sits on the downstream router’s WAN
side. By the time client traffic reaches that link, NAT has normally replaced a
10.0.20.x client address with the router’s 10.0.10.6 WAN address. The sensor
can still see destinations, ports, protocols, DNS questions, TLS metadata, and
signatures, but it often cannot identify which individual downstream phone or
laptop created a connection.
For per-device attribution, I would need at least one of these:
- a mirror or TAP on the LAN side before NAT;
- DHCP lease and router connection logs that preserve client identity;
- endpoint agents on important devices;
- DNS logs that retain the original client address.
Encrypted traffic is another normal limitation. TLS prevents the sensor from reading most application payloads, but DNS, addresses, ports, timing, certificates, SNI where available, flow behavior, and protocol metadata can still be useful.
What I would change
The finished lab works, but it is not a miniature enterprise SOC with infinite retention.
First, I would pin the Suricata and Zeek container images by digest. Mutable tags made the first build quicker but made later reproduction less exact.
Second, I would define log retention earlier. Network telemetry grows quickly; letting EVE and Zeek logs expand without a deliberate rotation policy is not a backup strategy.
Third, I would use a Gigabit capture adapter if the monitored link or local traffic grew beyond this 40 Mb/s WAN use case. A 100 Mb/s destination is a physical ceiling no software setting can remove.
Fourth, I would keep tuning separate from suppression. Mirrored traffic can generate noisy unknown-EtherType and invalid-checksum alerts. I would inspect their fields and frequency first, filter dashboard views next, and only then consider suppressing a rule. A quiet dashboard is not automatically an accurate dashboard.
Finally, I would avoid adding heavyweight platforms just because memory appears free in one snapshot. Wazuh already owns most of this host’s RAM, and the sensor needs headroom during bursts. Reliability is more useful than a longer tool list.
Final result
The old PC ended up doing exactly what I wanted:
- Proxmox isolates the workloads;
soc-coreruns the central Wazuh stack;bt-sensorreceives an unnumbered SPAN feed;- Suricata produces signature-driven detections;
- Zeek produces investigation-friendly network metadata;
- the Wazuh agent forwards both JSON streams;
- Wazuh makes the resulting events searchable in one place.
More importantly, the result is backed by tests rather than a diagram. The mirror went from 25 mostly broadcast/multicast packets to 2,201 packets in the same 25-second window. Both unicast directions appeared. tcpdump reported zero drops in that test. Suricata fired the expected SID 2100498 test alert. Zeek produced structured logs. Wazuh displayed the sensor’s events.
The most useful lesson was that partial visibility is dangerous because it looks like success. The link was up. Packets were moving. The tools were running. Only a focused, bidirectional capture showed that the traffic path was still broken.
That is also what made this project feel like a real blue-team lab instead of a software installation exercise: observe, form a hypothesis, change one layer, and prove the result at the next boundary.
Next: one view for the whole homelab
Once the SOC pipeline was stable, a different problem appeared: checking Proxmox, container health, Wazuh, Suricata, Zeek, DNS filtering, storage, and network devices meant opening several interfaces and terminals.
So I built a separate dashboard for the entire homelab.
That later project is not part of the detection or log-transport architecture described here. Part 2 will cover how I collected those systems into one small API and responsive interface, what the dashboard reports, how network discovery was added, and how I kept status visibility separate from the security tools it observes.