XCloak / docs

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

ComponentTechNotes
FrontendNext.js, TypeScript, TailwindPort 3000. Talks only to the backend, never directly to the database.
BackendGo, Gin, PostgreSQLPort 8080 (8443 with TLS). Hosts the REST API, WebSocket hub, and all scheduled detectors.
AgentGo, single static binaryLinux and Windows. Outbound-only connections to the backend — no inbound ports required.
ObservabilityPrometheus, Grafana, KafkaMetrics 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

  1. The agent collects telemetry (or streams real-time connection events via eBPF on Linux) and sends it to /api/logs/ingest.
  2. The backend normalizes the log across supported formats (CEF, LEEF, JSON, NDJSON, plain syslog) into a common field set.
  3. The normalized log is evaluated against the tenant's cached Sigma ruleset synchronously, on the ingest path.
  4. A match creates an alert, triggers cross-source correlation into incidents, and publishes to Kafka for fan-out.
  5. 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.
  6. 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.