Skip to main content

System Architecture

kguardian is designed as a distributed system: four core components (Controller, Broker, CLI, UI) plus an audit-mode evaluator (deployed by default) and an optional AI assistant (the LLM Bridge, running tools and policy/seccomp generation in-process) work together to observe, analyze, and secure your Kubernetes workloads.

Components

Controller

Language: Rust + eBPF (C)Deployment: DaemonSet (one per node)Purpose: Observes kernel-level events

Broker

Language: Rust + Actix-webDeployment: Deployment with PostgreSQLPurpose: Stores and serves telemetry data

CLI

Language: GoDeployment: kubectl pluginPurpose: Generates security policies

UI

Language: React + TypeScriptDeployment: Web applicationPurpose: Visualizes network topology

Evaluator

Deployment: Deployment (on by default; disable with evaluator.enabled=false)Purpose: Replays observed flows against policies and records would-deny verdicts

AI Assistant (Optional)

Language: TypeScriptDeployment: LLM Bridge (opt-in via ai.enabled)Purpose: Natural-language queries and conversational policy generation; optionally serves the same tools to external MCP clients at /mcp

Data Flow

1. eBPF Monitoring

The Controller attaches eBPF programs to kernel hooks on each node — fentry probes on tcp_v4_connect, tcp_set_state, udp_sendmsg, and tcp_retransmit_skb, a kprobe/kretprobe pair on inet_csk_accept, the raw_syscalls/sys_enter tracepoint, and tp_btf/cgroup_mkdir (so a container is captured from cgroup creation, before its pod is registered). When pods make network connections or execute syscalls, eBPF programs capture:
  • Source/destination IPs and ports
  • Protocol (TCP/UDP)
  • Syscall names and arguments
  • Container network namespace inode
  • Timestamp
eBPF runs in the kernel with low overhead, avoiding the need for sidecar proxies or agent injection.

2. Event Enrichment

The Controller’s userspace component:
  1. Receives events from eBPF via BPF ring buffers
  2. Queries containerd to map network namespace inodes to container IDs
  3. Matches containers to Kubernetes pods via the API server
  4. Enriches events with pod name, namespace, labels, and owner references

3. Data Transmission

Enriched events are sent to the Broker via HTTP POST:
  • Network traffic → /pod/traffic (batched via /pod/traffic/batch)
  • Pod metadata → /pod/spec
  • Syscall data → /pod/syscalls
  • Service mappings → /svc/spec
  • Pod deletions → /pod/mark_dead
The Controller batches events to reduce network overhead.

4. Storage & Indexing

The Broker stores all telemetry in PostgreSQL with indexes on:
  • Pod IP address
  • Pod name + namespace
  • Timestamp (for time-range queries)
  • Traffic type (INGRESS/EGRESS)
This enables fast lookups when generating policies.

5. Policy Generation

The CLI queries the Broker API:
  1. Fetches all traffic for a specific pod
  2. Identifies unique source/destination IPs
  3. Reads the peer identity the Broker stamped on each row at ingest; falls back to /pod/ip/{ip}?at= for unresolved rows (how peers are attributed)
  4. Groups traffic by protocol and port
  5. Deduplicates rules
  6. Generates K8s/Cilium NetworkPolicy YAML
For seccomp, it aggregates all observed syscalls and creates an allowlist.

6. Visualization

The UI fetches data from the Broker and renders:
  • An interactive graph of pod-to-pod communication (React Flow)
  • Tables of traffic details with filtering
  • Live view (polled, ~5s) — the UI re-fetches from the Broker on a short interval

Technology Stack

Controller (Rust + eBPF)

  • Memory safety without garbage collection (critical for low-level system programming)
  • Performance on par with C/C++ for userspace eBPF handling
  • libbpf-rs bindings provide excellent eBPF ergonomics
  • Tokio async runtime for efficient event processing
  • Written in C, compiled with clang -target bpf
  • Use libbpf-cargo to generate Rust skeleton bindings (.skel.rs)
  • Attach fentry probes to tcp_v4_connect, tcp_set_state, udp_sendmsg, and tcp_retransmit_skb; a kprobe/kretprobe pair to inet_csk_accept; the raw_syscalls/sys_enter tracepoint; and tp_btf/cgroup_mkdir for startup syscall capture
  • Use BPF maps for kernel ↔ userspace communication
  • kube-rs: Rust Kubernetes client for pod watching
  • Watches pods on the current node via field selector
  • Filters by excluded namespaces (kube-system, kguardian, etc.)
  • Handles pod lifecycle events (create, update, delete)

Broker (Rust + Actix-web + PostgreSQL)

  • Actix-web: High-performance HTTP server (thousands of req/sec)
  • RESTful API with JSON payloads
  • Async request handling with Tokio
  • Diesel ORM for type-safe SQL queries
Tables:
  • pod_details: Pod metadata (name, namespace, IP, labels, spec)
  • svc_details: Service metadata (name, namespace, cluster IP, selectors)
  • pod_traffic: Network connections (src/dst IP, port, protocol, type) plus the peer identity resolved at ingest (peer_kind, peer_namespace, peer_name, peer_uid, peer_workload_kind, peer_workload_name, peer_resolved_at)
  • pod_syscalls: Syscall observations (pod, syscall names, count, architecture)
  • audit_verdicts: Evaluator would-deny verdicts for observed flows
  • install_info: Installation metadata
Indexes optimize queries by pod IP and name/namespace.
  • audit_verdicts are pruned automatically: broker.audit.retention.days (default 30), with intervalSeconds and batchSize controlling the pruning loop.
  • pod_traffic rows of departed pods (dead, no longer in the pod table, or an earlier pod with the same name) are pruned after broker.traffic.retention.days (default 14), with the same intervalSeconds and batchSize knobs. Rows of running pods are kept however old: each flow is recorded once, when first seen, and a running pod never reports it again, so pruning them would drop rules from its next generated policy. The exception is a running pod’s row that a newer row supersedes (same direction, protocol, port, decision and in-cluster peer workload), which also goes after days. Rows whose peer has no identity (external or unresolved) can be bounded with the opt-in broker.traffic.retention.maxRowsPerPod.
  • pod_syscalls is retained indefinitely.
  • The image inventory (images, workload_containers) is sized by what runs: running pods refresh their rows continuously, and a digest no running pod has reported for broker.imageInventory.retention.days (default 30) is pruned, followed by images nothing references, with the same intervalSeconds and batchSize knobs. A digest counts as running while a pod refreshed it within broker.imageInventory.runningWindowSeconds (default 900) or while the pod that last reported it is live; every live pod is re-posted every 60 s whether or not it is Ready, so crash-looping workloads stay visible. The container must also be running (or crash-looping) in its latest report: a completed init container is listed as ranAsInit and a digest that never started as waiting, neither counted as running.
  • Vulnerability and SBOM data from the supplychain component (vuln_sources, image_vulnerabilities, image_sbom_components, supplychain_image_links, and the per-CVE summary vuln_cve_summary rebuilt each retention pass) is keyed by image digest, never by pod. A payload is deleted once no inventory digest it applies to has had a workload container seen for broker.supplychain.retention.days (default 30) and none is running; a payload nothing in the inventory matches is kept for unlinkedGraceHours (default 24). Incomplete paged SBOMs expire after sbomPageTtlSeconds. See Vulnerability endpoints.
  • The cluster-wide GET /pod/traffic endpoint is capped to protect the broker: 5000 rows by default, up to 20000 via ?limit=.

CLI (Go)

  • kubectl plugin ecosystem: Natural fit for K8s tooling
  • client-go: Official Kubernetes Go client
  • cobra: Industry-standard CLI framework
  • Cross-platform binaries without dependencies
Network policies:
  1. Query /pod/traffic/{pod} from Broker
  2. Use the row’s stored peer_* identity; for rows without one, query /pod/ip/{ip}?at=<time_stamp> or /svc/ip/{ip}
  3. Build NetworkPolicyPeer with podSelector + namespaceSelector
  4. Group by direction (ingress/egress) and port/protocol
  5. Deduplicate rules via JSON marshaling comparison
  6. Generate YAML with sigs.k8s.io/yaml
  1. Query /pod/syscalls/{pod} from Broker
  2. Extract unique syscall names from aggregated data
  3. Group by architecture (x86_64, arm64, etc.)
  4. Generate JSON with defaultAction: SCMP_ACT_ERRNO
  5. Add syscalls array with action: SCMP_ACT_ALLOW

UI (React + TypeScript)

  • React 19: Modern UI framework
  • TypeScript: Type safety for complex state
  • Vite: Fast build tooling
  • TailwindCSS 4: Utility-first styling
  • React Flow: Interactive network graph rendering
  • Pods rendered as collapsible nodes (expand to show containers)
  • Edges show traffic direction with arrows
  • Namespace-based grouping with color coding
  • Interactive filtering by namespace, pod, traffic type
  • Traffic table with sorting and pagination
  • Pod details panel with labels and metadata
  • Live view (polled, ~5s) via the Broker API
  • Dark mode support (matches Cilium Hubble aesthetic)

Deployment Architecture

Standard Deployment (Helm)

The LLM Bridge is the only optional workload, and it adds no listener of its own beyond its Service port: the assistant’s SSE chat route and the optional MCP endpoint (/mcp, see Connect an MCP Client) are both routes on port 8080.

High Availability

For production, configure:
  • Broker replicas: 2-3 for load balancing
  • PostgreSQL: Use managed service (RDS, Cloud SQL) or PostgreSQL operator with replication
  • Controller: Automatically HA via DaemonSet (one per node)
  • UI: 2+ replicas behind ingress/load balancer
The Controller must run on every node you want to monitor. Use node selectors or tolerations if you have specialized node pools.

Security Considerations

The Controller requires:
  • Privileged container with CAP_BPF added (to load eBPF programs)
  • hostNetwork: true (to access the node network namespace)
These are necessary for eBPF functionality but should be carefully reviewed in your security policy.
  • The Broker API is unauthenticated by default, with opt-in scoped bearer tokens via broker.auth: read (frontend proxy, llm-bridge, CLI), ingest (controller), supplychain (supply-chain writes) and optional admin. Every route declares its scope. /health and /metrics stay open
  • The chart can also manage a NetworkPolicy restricting Broker access
  • For production, additionally consider:
    • Ingress with authentication (OAuth2 proxy, etc.)
    • mTLS between components
The PostgreSQL database contains:
  • Pod names, namespaces, labels
  • IP addresses and ports of communication
  • Syscall activity
This is metadata, not application data, but should still be protected. Use encryption at rest and in transit.

Performance Characteristics

The chart ships with these default resource requests and limits:
For measured numbers from a real deployment, see the reference deployment in the project README.

Scalability

  • Controllers: Scale linearly with node count (DaemonSet)
  • Broker: Stateless, can add replicas behind a load balancer
  • PostgreSQL: Vertical scaling or read replicas for high query loads
  • CLI: No scaling concerns (client-side)
  • UI: Horizontal scaling behind ingress
For larger clusters, consider:
  • Sharding data by namespace
  • Time-windowed queries
  • Data aggregation/rollup

Deep Dive: eBPF Monitoring

Learn how eBPF programs capture kernel events

Controller Implementation

Explore the Controller source code

Broker API Reference

View all API endpoints and schemas

Troubleshooting

Diagnose common architecture-related issues