Skip to content

Architecture

Overview

Vesana is push-based: agents and collectors connect outbound to the server and send check results in. The server has no outbound connection to monitored machines.

Three monitoring modes

Every check runs in exactly one of three modes (check_mode). All three run the same check code — an SNMP or HTTP check returns the same result no matter who runs it. Only the executor differs:

Mode Executor Typical use
passive Collector (Linux VM in the customer network) SNMP, ping, SSH, HTTP against devices without their own agent
agent Agent (Go binary directly on the target machine) CPU/RAM/disk/services on Windows and Linux servers
active Active Collector (the server checks itself) No collector present or wanted in the customer network

Mixing modes on one host is normal, not the exception. A host can have agent checks (local CPU load) and passive SNMP checks via a collector at the same time, or SNMP checks that run as active for lack of a collector. check_mode is a property of the individual check (host_services.check_mode), not of the host — so each check can freely pick its executor, as long as the capabilities match (see Profiles & checks → Capabilities filter check types).

If a check created as "passive" has no collector available, it automatically falls back to active instead of staying unrunnable — no check is ever lost this way.

flowchart LR
    subgraph "Customer network"
        A[Agent on servers] -->|HTTPS POST| EXIT[outbound 443]
        C[Collector VM] -->|HTTPS POST| EXIT
    end

    subgraph "Vesana server"
        EXIT --> R[Receiver]
        R --> RS[(Redis stream)]
        RS --> W1[Worker 0]
        RS --> W2[Worker 1]
        RS --> W3[Worker N]
        W1 --> DB[(Postgres + TimescaleDB)]
        W2 --> DB
        W3 --> DB
        DB --> API[REST API]
        API --> FE[React frontend]
        API --> M[Installed web app / browser]
    end

Components

Receiver

  • FastAPI service receiving agent and collector packets
  • Authenticates via X-API-Key (collector) or X-Agent-Token (agent)
  • Validates schema, writes immediately to Redis stream — no check evaluation
  • Goal: lowest possible latency, highest throughput

Redis stream

  • Backpressure-capable ingress queue (XADD / XREADGROUP)
  • Multiple workers consume in parallel
  • In normal operation the worker keeps the stream short. If the workers stand still for longer (crash, database gone), the stream is capped at about 100,000 entries — beyond that the oldest entries are dropped so Redis does not fill up. Redis itself runs with noeviction: when memory is full, writes fail instead of silently evicting data

Worker

  • Reads messages, fetches host/service context from DB
  • Applies profile-check effective config, normalizes values
  • Writes check results to check_results (hypertable)
  • Updates current_status (hot table with fillfactor=80)
  • Triggers alert evaluation, notification dispatch, AI analysis cache invalidation

API

  • FastAPI with JWT auth, automatic tenant scope via ORM filter
  • Endpoints: hosts, services, profiles, discoveries, alerts, reports, wiki, AI, admin
  • Background tasks: downtime watcher, dead-collector watcher, anomalies & forecasts (normal ranges, evaluation, forecasts), auto-purge, instance pulse, license fetch
  • Distributed locking via Redis — with multiple API replicas, each watcher runs only once

Frontend

  • React 18 + TypeScript + Vite
  • Themed via CSS variables (13 themes plus one custom theme, each light/dark)
  • Lazy-loaded ECharts, lazy-loaded ReactMarkdown for wiki

Agent (Go)

  • Single binary, statically linked (CGO_ENABLED=0), ~6.5 MB
  • Fetches config every 5 minutes, runs checks locally
  • Auto-update on config refresh when server reports a newer version

Collector (Go)

  • Single binary, runs on a Linux VM in customer network
  • Runs remote checks: SNMP, ping, SSH, HTTP, discovery (built-in network scanner)
  • Fetches config every 60 seconds, sends results + discovery output to server

Active Collector (Go)

  • The same collector binary, but running as a systemd service (vesana-active-collector) directly on the Vesana machine
  • Runs check_mode='active' checks across all tenants — server-global, max. one instance per Vesana install
  • Useful when no collector should run in the customer network, or when a passive check would otherwise have no executor for lack of a collector
  • If the Active Collector is offline, a Python hybrid fallback in the worker steps in — checks stay runnable, just slower

Smartphone (installable web app)

  • No native app any more: Vesana is added to the home screen as a web app (Android, iPhone, iPad)
  • Push via the browser standard Web Push (VAPID) — every instance generates its own key pair, no Google/Apple account needed
  • Tap on a push → host detail page with the check opened

Multi-tenant isolation

Tenants are the central separator. Every DB table with customer data has a tenant_id column; the API filters every query to the tenants the caller may see.

Super admins see everything. All other accounts get their rights via role assignments: role × scope, where the scope is one tenant, a tenant group or "all tenants". Assignments go to users or to teams. An account can thus have different roles in different tenants — e.g. admin at customer A, read-only at customer B. Details: Roles.

flowchart LR
    U[User] --> Z1[Assignment: Admin @ tenant A]
    U --> T[Team]
    T --> Z2[Assignment: Viewer @ tenant group customers]
    Z1 --> A[(Tenant A)]
    Z2 --> B[(Tenant B)]
    Z2 --> C[(Tenant C)]

Security architecture

Eight pillars:

1 — Encryption of sensitive fields

shared/encryption.py provides encrypt_field() / decrypt_field() (AES-256-GCM). Encrypted: SNMP communities, SSH passwords, etc. Key: FIELD_ENCRYPTION_KEY (Base64url, 32 bytes). The server holds plaintext only briefly in RAM.

Details: Security → Encryption.

2 — Token-based authentication

Token Format Storage Used by
Session token (JWT) HS256 HttpOnly cookie in the browser End-user login
Agent token vesana_agent_ + 32 random bytes (URL-safe) SHA256 hash in agent_tokens.token_hash Agent → receiver
API key vesana_ + 32 random bytes (URL-safe) SHA256 hash in api_keys.key_hash Collector → receiver

Plaintext is never in the DB — only hashes. Keys are shown exactly once (at creation).

3 — Two-factor authentication

Selectable per user: TOTP (authenticator app) or WebAuthn (hardware key/passkey). Email codes no longer exist. Details: Security → Hardening checklist.

4 — Rate limiting

Login, 2FA verification and identity confirmation are capped at 10 requests/min per IP, password change at 5/min. Goal: slow brute force against weak passwords and 2FA codes.

5 — RBAC — deny by default

Every API endpoint requires an explicit permission (hosts.create, alert_rules.edit, …) — without a matching permission you get 403, not implicit allow. Four built-in roles (Super Admin, Admin, Operator, Viewer) plus custom roles with granular permission selection. Which role applies in which tenant is set by the role assignments (see Multi-tenant isolation).

6 — Script execution — apply scope + sandbox

Script-capable check types (agent_script, agent_custom, plugin_local, ssh_script, ssh_custom, ssh_command, api_script) can run arbitrary code — so a dedicated authorization layer applies: the apply scope per user (by tenant, tag, or individual host), deny by default, super admins exempted. Whoever creates scripts or changes their content additionally confirms their identity — super admins too. The check executor itself runs in a sandbox with restricted capabilities.

Details: Security → Hardening checklist.

7 — Container isolation

API, worker, receiver, and AI-service containers run as non-root. Only exception: the updater container stays root because it needs to write to the Docker socket to run updates.

8 — Distributed locking

Multiple API replicas? downtime_expiry_watcher, dead_collector_watcher, etc. run only once — Redis locks with 55 s timeout ensure that.

Subsystems

Profile + checks

Two-tier model. Hosts have a primary profile (e.g. „APC Smart UPS"), optionally further ones. Profiles have profile-checks (e.g. „Battery voltage"). host_services are instances per host with optional overrides.

Details: Profiles & checks.

Aggregates (K-of-N)

An aggregate is a standalone object (not a regular check) that evaluates the status of a homogeneous group of redundant members (e.g. 2 uplinks, 3 power supplies) read-time against a threshold K: healthy == N → OK, K ≤ healthy < N → WARNING ("redundancy dented, still functional"), healthy < K → CRITICAL ("function lost"). The WARNING level deliberately does not suppress downstream dependencies — only a real CRITICAL triggers normal inhibition.

Wiki + AI

Built-in knowledge base (Markdown, FTS, pgvector). AI hits the wiki first via RAG, falls back to web search, marks sources.

Auto-discovery

Collector scans the network with its built-in scanner (ARP, ICMP, TCP, service detection). SNMP sysOID matches profiles. On match, profile is suggested.

Performance model

On self-hosting defaults (1 worker, 256 MB shared_buffers): ~960 checks/s sustained, p95 ≤ 150 ms, 0 errors over 30 min soak. Scaling: Administration → Scaling.

Next