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) orX-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 withfillfactor=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.