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 logic, no DB write
  • Goal: lowest possible latency, highest throughput

Redis stream

  • Backpressure-capable ingress queue (XADD / XREADGROUP)
  • Multiple workers consume in parallel
  • Full stream → receiver rejects (noeviction policy) — no silent drops

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, anomaly baselines, auto-purge, tester phone-home
  • Distributed locking via Redis — with multiple API replicas, each watcher runs only once

Frontend

  • React 18 + TypeScript + Vite
  • Themed via CSS variables (20 themes × dark/light)
  • 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 (nmap)
  • 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 ORM-level apply_tenant_filter() (api/app/auth.py) enforces filtering — a query without tenant scope raises a runtime error.

Super admins have tenant_scope = null and see everything. Regular users are bound to one tenant, with optional cross-tenant read in custom roles.

flowchart TB
    subgraph Super-Admin
        SA[user.tenant_scope = null] --> ALLES[(all tenants)]
    end
    subgraph Tenant A
        UA[user.tenant_id = A] --> A[(Hosts/Alerts A)]
    end
    subgraph Tenant B
        UB[user.tenant_id = B] --> B[(Hosts/Alerts B)]
    end

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
User JWT RS256 Browser/mobile local End-user login
Agent token vesana_agent_ + 32 url-safe base64 SHA256 hash in agent_tokens.token_hash Agent → receiver
API key Custom prefix + 32 bytes 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 and 2FA-verify endpoints capped at 10 req/min per IP, 2FA resend at 3 req/min (slowapi). Goal: slow brute force against weak passwords and 2FA codes.

5 — RBAC — deny by default

Every API endpoint requires an explicit permission (host.create, alert_rule.edit, …) — without a matching permission you get 403, not implicit allow. Four base roles (Super Admin, Admin, Operator, Viewer) plus custom roles with granular permission selection. tenant_access controls whether a user sees only their own tenant or several/all.

6 — Script execution — apply scope + sandbox

Script-capable check types (agent_script, custom, ssh_script, ssh_custom) 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. 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 profile (e.g. „APC Smart UPS"). 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.

Policies

Declarative rule system: match conditions (JsonLogic subset) + actions that automatically set tags or create checks — bulk configuration without SSH grunt work. A form builder and an AI generator help formulate the conditions.

Implemented action types: add_check (auto-create a check), tag_assignment (auto-set a tag), config_patch (patch a config override). Further action types exist in the schema but aren't implemented yet — the UI only lists implemented types, and the backend rejects policies that consist solely of unimplemented actions.

Safety net: a dry run is mandatory before saving, a circuit breaker stops the sync on unusually many deletes or operations, new policies don't retroactively apply to existing hosts by default (adoption must be chosen explicitly). Policies that would roll out scripts are super-admin-only regardless of otherwise-granted policy permissions (see pillar 6 above).

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 nmap. SNMP sysOID matches profiles. On match, profile is suggested.

NSCA receiver

Optional receiver on port 5667. Accepts packets from send_nsca clients. Migration path for Nagios estates.

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