Building a university SOC: Wazuh, MISP and CrowdSec
Why I built a detection-and-response stack from open-source parts instead of buying one, how the enrichment loop fits together, and two lessons that only show up once it's running in production.
A public university is a hard environment to defend. The network is large, deliberately open, full of unmanaged devices, and the budget is fixed by a pay scale rather than a threat model. “Buy the managed SOC” is not a sentence that survives contact with that reality.
So I built one. This is how it’s put together and what running it has taught me.
Why custom
Two reasons, one practical and one that turned out to matter more.
The practical one is cost. A commercial SOC platform priced per endpoint or per GB of ingest does not fit a university budget, and the managed-service option means handing detection logic to someone who doesn’t know the environment.
The one that mattered more: building it myself meant I understood every detection decision in the stack. When you buy detection, you inherit someone else’s assumptions about what’s normal. When you build it, the blind spots are yours — which means you know exactly where they are. That knowledge has turned out to be the most valuable thing the project produced.
The stack
The core is Wazuh — open-source SIEM and XDR, doing log collection, agent management, and rule-based alerting across the estate.
Around it sit two sources of external context:
- MISP for threat intelligence — known-bad indicators with confidence levels and an “intended for detection” flag I lean on heavily.
- CrowdSec for community IP reputation — a continuously updated view of addresses currently misbehaving across a large sensor network.
Wazuh on its own tells you what happened on your hosts. MISP and CrowdSec tell you what the wider world already knows about the addresses involved. The interesting work is connecting the two.
The enrichment loop
That connection is a custom integration — a piece of code that sits between a raw Wazuh alert and a decision.
When an alert fires, the integration pulls every observable out of it: IP addresses, domains, URLs, email addresses, file hashes. Each observable is checked against MISP, and each IP is additionally checked against a local CrowdSec decision store. The integration then scores the result, assigns a severity, and writes a structured record of what it found.
That structured record is the clever part. Wazuh reads it back in as a log source, which means a handful of purpose-built rules can match on it like any other event. An enriched finding becomes a first-class alert — and a first-class alert can trigger active response, dropping the offending address at the agent firewall automatically.
The result is a closed loop: alert in, enrichment, scoring, rule match, enforced block — without a human in the path for the high-confidence cases.
Feeding the reputation store
CrowdSec’s official community feed is good, but I wanted more breadth, so a second node contributes additional decisions. The trick is avoiding duplication: before those supplementary decisions are imported, anything already covered by the official feed is removed, and each remaining decision keeps its original expiry time rather than being flattened to a default.
That import runs on a schedule via a systemd timer. It has to: imported reputation data has a TTL, and if the sync stops, the extra coverage quietly expires and the enrichment step goes half-blind without anything obviously breaking. Scheduled automation isn’t a nice-to-have here — it’s load-bearing.
Two lessons from production
A architecture diagram makes a system look finished. Running it teaches you where it isn’t.
Intelligence TTL is not enforcement TTL
This one cost me an afternoon of confusion. The dashboard shows how long a piece of intelligence is considered valid — a CrowdSec decision might carry a multi-day expiry. But the actual firewall block on the agent follows the SIEM’s active-response timeout, which is a separate, static value.
So the dashboard can confidently show a three-day reputation TTL while the host-level block already lifted hours ago. Both are correct. They are answering different questions. If you build a stack like this, decide deliberately whether enforcement should track intelligence — and until you implement that explicitly, assume it doesn’t.
Intel-only blocking is a stronger claim than it looks
Blocking on a corroborated CrowdSec match is comfortable: it means an address is misbehaving right now, across many sensors. Blocking purely on a MISP indicator is a different kind of decision — you’re acting on someone’s prior assertion that an address is bad, which may be stale, scoped to a context that isn’t yours, or simply wrong.
Intel-driven auto-blocking is powerful and I use it — but only for high-confidence indicators explicitly flagged for detection, and I treat it as a deliberate, narrow policy rather than a blanket rule. The failure mode of “block everything MISP has ever seen” is a self-inflicted outage.
Where this connects
I’m moving toward red team work, and people sometimes ask why a defensive engineering project belongs on a site about that.
This is the answer. Building this stack means I know precisely what an enriched SOC sees: which observables get pulled from an alert, how reputation and intelligence corroborate or fail to, where the timing gaps are, and which detections lean on assumptions thin enough to slip past. You cannot get that from the outside. The blue work is the red preparation.
More on that in a future post.