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 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 (
noevictionpolicy) — 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 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, 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.