← All writeups triagewall.io
Written May 2026 for the v0.1 launch and published as a time capsule. Since then I swapped the production model from Mistral 7B to Foundation-Sec-8B after a more rigorous benchmark — that’s the subject of the v0.2 writeup. The “more rigorous experiment” I mention near the end is what drove that decision.

How I Cut 13,000 Suricata Alerts an Hour Down to 200 Worth Reading

It's 11pm on a Tuesday. My phone buzzes with another Suricata alert from my homelab. I open Wazuh, scroll past hundreds of "ET POLICY" notices, find what I'm almost sure is just my wife's iPhone doing STUN traffic for FaceTime, and close the laptop. The alerts keep coming. They've been coming for hours.

This was a recurring problem. I'd built a self-hosted SIEM stack — OPNsense with Suricata for the IDS, Wazuh as the manager, Zeek for protocol-level analysis — and it worked. It worked too well, actually. At peak my homelab was generating between 6,000 and 13,000 alerts per hour. There was no chance I was reading them. I started ignoring the dashboard entirely, which is exactly the failure mode IDS deployments are supposed to prevent.

I looked at commercial XDR products. The pricing pages were not designed for someone running this at home. So I closed the tab and started thinking about what a homelab-budget version would actually look like.

That project turned into Triagewall. This post is the writeup of how I built it, why I made the choices I did, what surprised me along the way, and what I learned recently when I tried to run it on hardware much smaller than what I designed it for.

The problem with self-hosted IDS at this scale

Suricata is genuinely good at what it does. The Emerging Threats ruleset is comprehensive enough that on a residential network with a normal mix of streaming, gaming, IoT devices, and Microsoft Teams traffic, you'll get thousands of alerts a day that are technically true positives — meaning Suricata is correctly firing on traffic that matches its signatures — but functionally noise, because the signatures are written for enterprise environments where that traffic would actually be suspicious.

A few examples from my own deployment:

You can solve some of this with manual rule tuning — disabling specific SIDs, writing pass rules, modifying the suppression config — but the maintenance burden is real, and any time you update the ruleset you potentially undo your tuning. The alternative I kept seeing recommended is "just send your alerts to a SIEM and write detection logic there," but that's the same problem one layer up. Now I'm writing Wazuh rules instead of Suricata rules and the alerts still pile up.

What I actually wanted was something between the raw alerts and my eyeballs. A triage layer.

What I considered and rejected

Before I started writing code, I tried to be honest with myself about whether this was a problem someone had already solved.

Commercial XDR (Sentinel, CrowdStrike, Arctic Wolf): The right answer for an enterprise. Wrong answer for a homelab. The pricing alone is a non-starter, and most of these products don't even sell to individuals.

Open-source SOAR (Shuffle, TheHive, Cortex): Closer to what I wanted, but the assumption is that you're building playbooks to act on alerts, not to decide which ones deserve attention in the first place. The triage problem is upstream of the response problem.

Just tuning the rules harder: I tried this. Spent a weekend writing suppressions and pass rules. The alert volume dropped by maybe 60%. Then a ruleset update landed and I was mostly back to where I started. Tuning is a treadmill, not a fix.

Doing nothing: Honestly tempting, but it defeats the purpose of running an IDS in the first place.

What I kept coming back to was the observation that I, sitting at a keyboard, can usually look at a Suricata alert and decide in 5-10 seconds whether it's worth investigating. The problem isn't that the decision is hard; it's that there are too many decisions. That's a problem an LLM can plausibly help with, as long as I'm willing to accept some false negatives.

The architecture

Triagewall is a two-tier classifier in front of the alert stream:

OPNsense (Suricata IDS)
    │
    │ rsync over SSH, every 60s
    ▼
omv1 server: /var/log/suricata-opnsense/eve.json
    │
    │ Python daemon tails the file
    ▼
ingest.py
    │
    ├─► Prefilter check (in-memory dict lookup)
    │   │
    │   └─► 99% of alerts: classified as false_positive immediately
    │
    └─► Otherwise: HTTP POST to local Ollama
        │
        └─► Mistral 7B classifies as real_threat / false_positive / uncertain
            │
            ▼
SQLite (/var/lib/triagewall/triage.db)
    │
    │ FastAPI dashboard polls every 5s
    ▼
Web UI on port 8084

Tier 1: the prefilter. A JSON config file maps known-noise Suricata signature IDs to "false_positive" with a brief reason. STUN traffic, Discord voice, ET POLICY rules I've decided I don't care about, Pi-hole NXDOMAINs. The prefilter is an in-memory dictionary lookup, which means it runs in microseconds and lets me handle the bulk of alert volume without ever touching the LLM. After a few weeks of tuning, the prefilter catches 99.9% of alerts.

Tier 2: the LLM. Anything the prefilter doesn't catch gets sent to a local Ollama instance running Mistral 7B Instruct on a separate machine with an RTX 4060. The LLM gets the alert details and returns a classification plus a one-sentence reason. I run it locally because (a) it's free and (b) I'm not sending my home network alerts to a third party.

The feedback loop. Every alert that reaches the LLM tier shows up in the dashboard with "Agree" and "Mark Different" buttons. When I disagree with the classifier, that gets logged. The point is to build a labeled dataset that can eventually inform either better prefilter rules or fine-tuning down the road. Even without using it for training, the feedback loop forces me to actually look at the LLM's reasoning, which is how I caught the Conficker / Teams misclassification in the first place.

The rest of the stack: Python, FastAPI for the dashboard, SQLite for storage, Docker Compose to bring it all up. I picked each of these because they're boring and they work. SQLite-is-overkill criticism is the most common one I expect to hear — but for a single-node homelab deployment with a few hundred LLM-classified alerts a day, Postgres would be operational overhead with no real benefit. I'll switch when there's a real reason to.

The numbers, after a few weeks of real-world tuning

I've been running Triagewall against my own homelab alerts for several weeks now. The current state:

The 200-per-day number is what actually matters. That's a volume I can review. It's a small enough number that I'll occasionally read through everything before bed and confirm or correct the classifications. It turns my IDS from a thing I ignore into a thing I actually use.

The 99.9% prefilter number is also worth dwelling on for a second. That sounds impressive and slightly suspicious, and the honest framing is: most of the noise in residential IDS deployments is dominated by a small number of very high-volume rules. Once you identify them and write prefilter entries, you bypass enormous fractions of the alert stream without losing much signal. The hard part isn't the math, it's the discipline of actually doing the tuning instead of installing the system and walking away.

What I learned testing this on a 2018 laptop

Recently the maintainer of awesome-suricata, who was generous enough to merge Triagewall into the official tools list, asked a really good question: any suggestions for smaller models to try on an integrated GPU?

The honest answer at the time was "I haven't tested it, my deployment runs Mistral 7B on a dedicated RTX 4060." But the question stuck with me. So I pulled out an old laptop — an i5-8265U from 2018, Intel UHD 620 integrated graphics, 15.5GB of RAM, running Pop!_OS — and ran a quick experiment with three smaller models in Ollama. (Note: Ollama doesn't meaningfully accelerate on Intel iGPUs, so everything below ran CPU-only.)

I used two test prompts: one alert that's commonly a false positive (an ET POLICY HTTP Basic Auth alert with 8.8.8.8 as the destination, which is a known distractor pattern for models), and one alert that's clearly a real threat (an ET MALWARE BlackHole user-agent alert). Here's what I saw:

Model Warm latency Quality notes
Phi-3 Mini (3.8B) ~12s Got distracted by surface features. Saw 8.8.8.8 and reasoned its way to "this is DNS traffic, false positive" while completely ignoring the actual signature.
Llama 3.2 (3B) ~7-10s Concerning. Twice misclassified a clear malware alert as a false positive, both times inventing plausible-sounding but completely fabricated justifications.
Gemma 2 (2B) ~7-9s The best of the three. Correctly flagged the malware alert as a real threat. Got a bit vague on a harder alert but did not fabricate.

The interesting insight isn't really about speed. It's about failure mode. Mistral 7B on the RTX 4060 sometimes hedges when it's uncertain, which is fine — uncertain is a valid classification in this system, and the dashboard handles it. The smaller models I tested mostly didn't hedge. They produced confident, well-formatted JSON output that was factually wrong about why the alert fired. Llama 3.2 in particular invented different fake justifications on different runs for the same alert, which is exactly the failure pattern you don't want in a triage tool.

Practical takeaway: Gemma 2 2B is the only model from this small set I'd cautiously suggest for CPU-only or integrated-GPU setups, and even then the Agree/Mark Different feedback loop matters more on smaller models, not less. If you're running Triagewall on something modest, plan on reviewing the LLM tier carefully until you're confident the model is calibrated.

There's a much more rigorous version of this experiment I want to do for v0.2 — broader model lineup, larger sample of real alerts, side-by-side comparison against the Mistral baseline. But the quick test gave me a much more honest answer than I had before, which is the whole point of running experiments.

What's next

A few things are on the v0.2 list, in rough priority order:

None of those are committed timelines. The next month is mostly about publishing this writeup, getting feedback from people who actually try the tool, and letting that feedback shape what gets built next.

Try it / find me

Triagewall is open source under AGPL-3.0. The repo is at github.com/aaronphifer/triagewall. There's a demo mode that runs against synthetic fixtures so you can see the dashboard without needing a working Suricata deployment — docker compose up with DEMO_MODE=true in the env, then visit port 8084. If you run a self-hosted IDS at home and you've been drowning in alerts, I'd genuinely love feedback.

A note about me: I'm a Navy nuclear mechanic transitioning to a civilian career mid 2027, looking for data center operations roles. The homelab work, the Triagewall project, and writing things like this post are how I'm building toward that next chapter. If your team is hiring for hands-on infrastructure or critical facilities work and you'd be interested in a former nuke who builds his own tools in his spare time, I'd love to talk. Reach out via GitHub or LinkedIn.