Architecture
XCloak is a modular monolith, not a microservices system — one Go binary hosts the API, the detection schedulers, the Kafka producers/consumers, and the WebSocket hub. Statefulness lives in Postgres, Redis, and Kafka, so the API and dashboard tiers scale horizontally.
The four parts
| Component | Tech | Notes |
|---|---|---|
| Frontend | Next.js, TypeScript, Tailwind | Port 3000. Talks only to the backend, never directly to the database. |
| Backend | Go, Gin, PostgreSQL | Port 8080 (8443 with TLS). Hosts the REST API, WebSocket hub, and all scheduled detectors. |
| Agent | Go, single static binary | Linux and Windows. Outbound-only connections to the backend — no inbound ports required. |
| Observability | Prometheus, Grafana, Kafka | Metrics scraping is bearer-token-gated. |
Backend layering
The backend follows a conventional layered architecture, applied consistently across the codebase:
routes → api (HTTP handlers) → services (business logic, detectors) → repositories (SQL) → database
There is no ORM — every database query is explicit, parameterized SQL. This was a deliberate tradeoff: more boilerplate per query, but query plans and tenant-scoping stay visible and auditable rather than hidden behind an abstraction layer.
Request flow: telemetry to alert
- The agent collects telemetry (or streams real-time connection events via eBPF on Linux) and sends it to
/api/logs/ingest. - The backend normalizes the log across supported formats (CEF, LEEF, JSON, NDJSON, plain syslog) into a common field set.
- The normalized log is evaluated against the tenant's cached Sigma ruleset synchronously, on the ingest path.
- A match creates an alert, triggers cross-source correlation into incidents, and publishes to Kafka for fan-out.
- Connected dashboard sessions receive the alert over WebSocket in real time, via Redis pub/sub — so it doesn't matter which backend replica originally handled the ingest request.
- Independently, 23 scheduled behavioral detectors evaluate recent telemetry windows per tenant on their own timers, producing alerts through the same path.
Multi-tenancy
tenant_id is a column on essentially every business table, enforced at the application layer via an explicit WHERE tenant_id = $N on every query. PostgreSQL row-level security policies are also defined on the six highest-value tables as an additional hardening layer — this is being progressively wired into request-scoped transactions, so treat application-layer scoping as the primary enforcement mechanism today.
Real-time delivery
Rather than a single in-process WebSocket hub (which would only reach clients connected to the same replica), alert creation publishes to a Redis pub/sub channel; every backend replica subscribes and forwards to its own local hub. WebSocket authentication uses a single-use, 30-second ticket minted by a separate authenticated REST call — not a session token in the URL, which avoids leaking credentials into proxy and access logs.
Async work
IOC matching is deliberately kept off the synchronous ingest path — it publishes a job to a Kafka topic (xcloak.ioc_match_jobs) and a dedicated consumer performs the match asynchronously, so a large IOC list doesn't add latency to every log write.