- Add feature bullet in the Features list - Add LDAP env vars to the configuration table - Add full LDAP section: how it works, configuration table, linking existing users, security notes - Update runtime config note to mention LDAP settings
▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄ ▓ ▄ █ ▓ ▄ ▓ ▓ ▄ ▄▄▄ ▄▀▀▄ ▓ ▄ ▄▄ ▄▀▀▄▄▀ ▄ ▀▄ ▄▀▀▄ ▄▄ ▄▀▀▄ █ ▄▄▄ ▄ ▄▄▄ ▄▀▀▄ ▒ ▓ ▒ ░ █ ▒ ▒ ▒ ▀▄▀▄ ▒ ▒ ▒ ▀▄▀▄ ▄▄ ▀▄ ▒ ▒ ▄▀▄▀ ▀▄▀▄ ▒ ▀▀▀ ▒ ▓ ▀▄▀ ▒ ░ ▀▄▀▄ ░ ▓ ░ ░ ░ ░ ░ ▓ ░ ░ ░ ░ ░ ▓ ░ ░ ▓ ░ ░ ▓ ░ ░ ▓ ░ ░ ░ ▒ ▀▀▀▀▀ ▀▄ ▀ ▀▀▀ ▒ ▒ ▀▀▀▀▀ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ░ ▀▀▀▀▀░ ░ ░ ░ ▓ ░▀▀▀░ ░ ░ ░ ▓ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ▓ ░ ▒▀▀▀▒ ▒ ▒ ▒ █ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ ▒ ▒ ▒ ▀▄ ▄▀ ▒ ▒ █ ▒ ▓ ▀ ▀▀▀▀ ▄▀ ▓ ▀ ▓ ▓ ▀ ▓ ▓ ▀ ▓ ▓ ▀ ▓ ▓ ▀ ▓ ▀▄▄ ▀▀ ▄▄▀ ▓ ▀ ▓ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀
YAMOT — Yet Another Monitoring Tool
Monitor remote Linux servers, Proxmox VE hosts, and SNMP devices. No agent to install on the target machines.
Screenshots
![]() Dashboard — server cards with live metrics and sparklines |
![]() Server detail — charts, processes, services, disks |
![]() Fleet dashboard — aggregated CPU, memory, and uptime across all servers |
|
License: PolyForm Noncommercial 1.0.0 Copyright (c) 2026 Uberwald. All rights reserved. Commercial use requires a separate license from Uberwald. Send email to contact@uberwald.me
Features
- Agentless monitoring — connects to remote servers via SSH; SNMP devices are polled via UDP
- SSH key-based authentication only — password-based SSH auth is not supported; all monitored servers must use SSH private keys for authentication
- SNMP v2c support — poll network devices (switches, routers, UPS, printers) via SNMPv2c with configurable community string and OIDs; no SSH required for SNMP devices
- SSH host key verification (TOFU) — host keys are recorded on first connection and verified on all subsequent connections to prevent MITM attacks
- 19 built-in monitors — CPU, memory, disk, network, load average, uptime, login activity, processes, services, SMART disk health, CPU temperature, custom checks, SNMP, plus 5 Proxmox VE monitors
- Custom check scripts — run user-defined shell commands on remote servers via SSH and parse the output as a number or JSON; metrics are exposed as
custom.<name>for alerting - HTTP/URL health checks — standalone HTTP/HTTPS endpoint checks that run from the YAMOT host (no SSH required); monitors status code, response time, and body match
- SSL certificate and domain expiration checks — standalone monitors that check SSL/TLS certificate expiry via TLS and domain name expiry via WHOIS (no SSH required); runs once per day by default
- Fleet dashboard — aggregated view of CPU, memory, disk, and uptime across all servers with group filtering and per-server alert counts
- Proxmox VE support — 5 additional monitors for Proxmox hosts: cluster status, VM inventory, container inventory, storage pools, node status
- Alerting — threshold-based rules with severity levels, cooldown, and server scoping. Multiple notification channels (email/SMTP, Slack, Mattermost) can be configured and enabled independently
- Alert acknowledgment — acknowledge active alerts with a note to suppress repeated notifications; unacknowledge to resume
- Maintenance windows — suppress alert evaluation during planned downtime for a specific server, a group, or all servers
- Default alert templates — alert rules are automatically created when a server is added, with sensible thresholds for Linux (10 rules), Proxmox VE (13 rules), and SNMP devices (1 rule). Templates can be re-applied to existing servers from the alerts page
- Server unreachable detection — automatically fires a critical alert when all monitors for a server fail, and resolves it when connectivity is restored
- Web dashboard — real-time view of all servers with server detail pages, charts, sparklines, and Socket.io live updates
- Server groups — organize servers into groups for scoped alert rules
- Reports — historical data analysis with JSON export
- Uptime percentage — rolling uptime tracking per server, exposed via API and displayed on the dashboard
- Service health checks — monitor systemd services on remote servers with configurable watch lists
- Process monitoring — top processes by CPU and memory usage on server detail pages
- RBAC — three roles (admin, operator, viewer) with granular permissions
- LDAP/ActiveDirectory — authenticate users against an LDAP or ActiveDirectory server; LDAP users are auto-provisioned on first login with the viewer role
- CLI — manage servers, alerts, users, and generate reports from the terminal
- Credential encryption — SSH private keys and passphrases encrypted at rest with AES-256-GCM
- CSRF protection — double-submit cookie pattern for all state-changing API requests
- Account lockout — accounts are temporarily locked after 5 consecutive failed login attempts
- JWT revocation — tokens are invalidated when passwords or roles change via a token version mechanism
- Rate limiting — global API rate limiting and per-IP login rate limiting
- SQLite storage — zero external database dependency, single-file deployment
- Offline operation — all frontend libraries (Font Awesome, Chart.js) vendored locally; no CDN dependencies required
Requirements
- Node.js >= 18
- SSH key-based access to the target servers (password authentication is not supported)
- SNMP v2c enabled on network devices (for SNMP monitoring)
Quick Start
# Clone and install
git clone <repo-url> yamot
cd yamot
npm install
# Copy and edit configuration
cp .env.example .env
# Edit .env: set JWT_SECRET and ENCRYPTION_KEY to random values
# Compile LESS to CSS
npm run less
# Frontend libraries (Font Awesome, Chart.js) are vendored in public/vendor/
# — no internet connection or CDN required at runtime
# Start the server
npm start
On first boot, YAMOT creates a default admin user and writes a randomly generated password to a file at data/.initial-admin-password. Delete this file after noting the password.
Open http://localhost:3000 in your browser and log in with admin and the generated password.
Production deployment with PM2
PM2 is a process manager that keeps YAMOT running, restarts it on crashes, and starts it on boot.
# Install PM2 globally
npm install -g pm2
# Start YAMOT with PM2
pm2 start src/index.js --name yamot
# Save the process list so YAMOT restarts on reboot
pm2 save
# Enable PM2 startup script (follow the printed instructions)
pm2 startup
# View logs
pm2 logs yamot
# Restart after updates
pm2 restart yamot
# Stop
pm2 stop yamot
Make sure .env is configured with production values (JWT_SECRET, ENCRYPTION_KEY, NODE_ENV=production) before starting under PM2.
Configuration
All configuration is via environment variables, loaded from a .env file at the project root.
| Variable | Default | Description |
|---|---|---|
YAMOT_PORT |
3000 |
Web server port |
YAMOT_HOST |
0.0.0.0 |
Web server bind address |
NODE_ENV |
development |
production enforces secure defaults |
DB_PATH |
./data/yamot.db |
SQLite database file path |
JWT_SECRET |
change-me-... |
JWT signing secret (must be changed in production) |
JWT_EXPIRES_IN |
24h |
JWT token lifetime |
ENCRYPTION_KEY |
default-... |
AES-256-GCM key for SSH credential encryption (must be changed in production) |
SSH_DEFAULT_PORT |
22 |
Default SSH port |
SSH_CONNECTION_TIMEOUT |
10000 |
SSH connection timeout in ms |
SSH_KEEPALIVE_INTERVAL |
30000 |
SSH keepalive interval in ms |
MONITOR_INTERVAL_MS |
60000 |
Default monitoring interval in ms |
SMART_INTERVAL_MS |
21600000 |
SMART disk health check interval in ms (6 hours) |
LOG_LEVEL |
info |
Winston log level |
LOG_DIR |
./logs |
Log directory |
YAMOT_LDAP_ENABLED |
false |
Enable LDAP authentication |
YAMOT_LDAP_URL |
(empty) | LDAP server URL (ldap:// or ldaps://) |
YAMOT_LDAP_BIND_DN |
(empty) | Service account DN for LDAP bind |
YAMOT_LDAP_BIND_PASSWORD |
(empty) | Service account password |
YAMOT_LDAP_SEARCH_BASE |
(empty) | Base DN for user search (e.g. DC=corp,DC=local) |
YAMOT_LDAP_SEARCH_FILTER |
(sAMAccountName={{username}}) |
LDAP search filter with {{username}} placeholder |
YAMOT_LDAP_TLS_REJECT_UNAUTHORIZED |
true |
Reject unauthorized TLS certificates for LDAPS |
Notification channels (email/SMTP, Slack, Mattermost) and LDAP settings are configured at runtime via the Settings page in the web UI or the API — not via environment variables. Each stores its own configuration in the database, overriding the env-var defaults.
Production requirements
When NODE_ENV=production, YAMOT refuses to start if JWT_SECRET or ENCRYPTION_KEY are still set to their default values. Generate random values:
# JWT secret (64 hex chars)
openssl rand -hex 32
# Encryption key (64 hex chars)
openssl rand -hex 32
CLI Usage
node src/cli/cli.js <command>
Or after npm install (creates a yamot symlink):
yamot <command>
Commands
# List configured servers
yamot servers
# Add a server (key auth)
yamot add-server -n my-server -h 192.168.1.10 -u root --key ~/.ssh/id_rsa --interval 60000
# Add a server (key auth with passphrase)
yamot add-server -n my-server -h 192.168.1.10 -u root --key ~/.ssh/id_rsa --passphrase mypassphrase --interval 60000
# Add a Proxmox VE host
yamot add-server -n pve-node1 -h 192.168.1.20 -u root --key ~/.ssh/id_rsa --type proxmox
# Add an SNMP device (community string via --community, default: public)
yamot add-server -n switch-1 -h 192.168.1.30 -u root --type snmp --community public
# Remove a server
yamot remove-server <id>
# List available monitors
yamot monitors
# List alert rules
yamot alerts
# Add an alert rule
yamot add-alert -n "High CPU" -m cpu.usagePercent -o ">" -t 90 -c email --cooldown 300000
# List users
yamot users
# Add a user
yamot add-user -u operator1 -p mypassword -r operator
# Set a user's password (or omit -p to be prompted interactively)
yamot set-password -u admin -p newpass123
yamot set-password -u admin
# Generate a report
yamot report -s <server-id> --from <timestamp-ms> --to <timestamp-ms>
# Start the web server
yamot start
Alert rule parameters
| Flag | Description |
|---|---|
-n, --name |
Rule name |
-m, --metric |
Metric path (e.g. cpu.usagePercent, memory.usagePercent, disk.usagePercent) |
-o, --operator |
Comparison operator: >, <, >=, <=, ==, != |
-t, --threshold |
Threshold value (number) |
-c, --channel |
Notification channel: email, slack, mattermost |
--severity |
Alert severity: info, warning, critical (default: warning) |
--cooldown |
Cooldown in ms before re-alerting (default: 300000 = 5 min) |
Notification Channels
Alert notifications are sent through configured channels. Each channel is an independent instance (email, Slack, or Mattermost) with its own configuration stored in the database. Multiple channels of the same type can be created (e.g. two separate Slack webhooks for different teams).
Channel types
| Type | Configuration | Notes |
|---|---|---|
| SMTP host, port, TLS, username, password, from address, recipients | Password is encrypted at rest. The recipients field accepts a comma-separated list of email addresses. Falls back to the SMTP username if no recipients are set. |
|
| Slack | Webhook URL | Uses Slack incoming webhooks. URL must start with https://. |
| Mattermost | Webhook URL | Uses Mattermost incoming webhooks. URL must start with https://. |
Channels can be created, edited, enabled/disabled, tested, and deleted from the Settings page in the web UI or via the API. A "Test" button sends a test notification to verify the configuration.
Server unreachable alerts
When all monitors for a server fail on a given tick (typically because SSH is unreachable), YAMOT automatically fires a critical alert using the built-in Server Unreachable rule. This rule appears alongside user-defined rules in the alert list and can be enabled or disabled like any other rule. When connectivity is restored, the alert is automatically resolved.
The unreachable alert has a 5-minute cooldown to avoid repeated notifications during transient network issues.
Default alert templates
When a new server is created, YAMOT automatically populates alert rules from predefined templates based on the server type. This gives every server a baseline of monitoring out of the box — no manual rule configuration needed.
Linux servers get 10 rules:
| Rule | Metric | Operator | Threshold | Severity |
|---|---|---|---|---|
| High CPU Usage | cpu.usagePercent |
> | 90 | critical |
| High Memory Usage | memory.usagePercent |
> | 90 | critical |
| Disk Nearly Full | disk.usagePercent |
> | 90 | critical |
| Disk Warning | disk.usagePercent |
> | 80 | warning |
| High Load Average | load.load1 |
> | 5 | warning |
| CPU Temperature High | temperature.maxTemperature |
> | 80 | warning |
| CPU Temperature Critical | temperature.maxTemperature |
> | 90 | critical |
| SMART Disk Failure | smart.failedDisks |
> | 0 | critical |
| Failed SSH Logins Spike | login.failedCount |
> | 10 | warning |
| SMART Reallocated Sectors | smart.reallocatedSectors |
> | 0 | warning |
Proxmox VE servers get all 10 Linux rules plus 3 PVE-specific rules (13 total):
| Rule | Metric | Operator | Threshold | Severity |
|---|---|---|---|---|
| PVE CPU High | proxmox-node.cpuUsage |
> | 0.9 | critical |
| PVE Cluster Not Quorate | proxmox-cluster.quorate |
== | 0 | critical |
| PVE Node Count Mismatch | proxmox-cluster.nodesOnline |
< | 1 | critical |
All template rules are scoped to the specific server (scopeType: 'server') and enabled by default. Users can modify or delete individual rules as needed.
SNMP devices get 1 rule:
| Rule | Metric | Operator | Threshold | Severity |
|---|---|---|---|---|
| SNMP Device Rebooted | snmp.sysUpTime |
< | 60000 | warning |
This rule fires when sysUpTime drops below 10 minutes (in hundredths of seconds), indicating a recent device reboot.
Templates can also be re-applied to existing servers from the alerts page via the Apply Templates button, which supports two modes:
- Apply to a specific server — select a server from a dropdown
- Apply to all servers of a type — select a server type (Linux, Proxmox VE, or SNMP) to apply templates to all servers of that type at once
Re-applying templates is idempotent: rules that already exist (same name + same server scope) are skipped, so no duplicates are created.
Web Dashboard
| Route | Description |
|---|---|
/login |
Login page (public) |
/ |
Dashboard — overview of all servers |
/fleet |
Fleet dashboard — aggregated CPU, memory, disk, uptime across all servers with group filtering |
/servers |
Server list |
/servers/:id |
Server detail — per-monitor metrics, charts, sparklines, process table, service status, disk/network tables, custom checks |
/http-checks |
HTTP health checks — CRUD for standalone URL monitors |
/cert-checks |
Certificate checks — CRUD for SSL/domain expiry monitors |
/groups |
Server group management |
/alerts |
Active alerts + alert rule management + alert history + alert ack + maintenance windows |
/reports |
Report generation and export |
/users |
User management (admin only) |
/settings |
Notification channel settings (admin only) |
API endpoints
All API routes are under /api and require a JWT token (via Authorization: Bearer <token> header or token cookie).
| Method | Path | Permission | Description |
|---|---|---|---|
POST |
/api/auth/login |
public | Authenticate, returns JWT + sets cookie |
POST |
/api/auth/logout |
public | Clear auth cookie |
GET |
/api/auth/verify |
authenticated | Verify current token |
GET |
/api/servers |
servers:read |
List all servers (credentials masked) |
GET |
/api/servers/:id |
servers:read |
Get server details |
POST |
/api/servers |
servers:write |
Add a server |
PUT |
/api/servers/:id |
servers:write |
Update a server |
DELETE |
/api/servers/:id |
servers:delete |
Remove a server |
POST |
/api/servers/:id/toggle |
servers:write |
Enable/disable monitoring |
GET |
/api/metrics/servers/:id/latest |
authenticated | Latest metrics for all monitors |
GET |
/api/metrics/servers/:id/:monitor/latest |
authenticated | Latest metric for a specific monitor |
GET |
/api/metrics/servers/:id/:monitor/history |
authenticated | Metric history (query: from, to) |
GET |
/api/metrics/servers/:id/uptime |
authenticated | Uptime percentage for a server (query: from, to) |
GET |
/api/metrics/fleet |
metrics:read |
Fleet-wide aggregated metrics (query: groupId) |
GET |
/api/alerts/rules |
alerts:read |
List alert rules |
POST |
/api/alerts/rules |
alerts:write |
Create alert rule |
PUT |
/api/alerts/rules/:id |
alerts:write |
Update alert rule |
DELETE |
/api/alerts/rules/:id |
alerts:delete |
Delete alert rule |
GET |
/api/alerts/active |
alerts:read |
List active (unresolved) alerts |
GET |
/api/alerts/history |
alerts:read |
Alert history (query: status, serverId, severity, limit, offset) |
POST |
/api/alerts/:alertId/ack |
alerts:write |
Acknowledge an alert |
POST |
/api/alerts/:alertId/unack |
alerts:write |
Unacknowledge an alert |
GET |
/api/alerts/maintenance-windows |
alerts:read |
List maintenance windows |
POST |
/api/alerts/maintenance-windows |
alerts:write |
Create a maintenance window |
DELETE |
/api/alerts/maintenance-windows/:id |
alerts:write |
Delete a maintenance window |
POST |
/api/alerts/templates/:serverId |
alerts:write |
Apply default alert templates to a specific server |
POST |
/api/alerts/templates/type/:type |
alerts:write |
Apply default alert templates to all servers of a given type |
GET |
/api/servers/:serverId/custom-checks |
servers:read |
List custom checks for a server |
POST |
/api/servers/:serverId/custom-checks |
servers:write |
Create a custom check |
PUT |
/api/servers/:serverId/custom-checks/:id |
servers:write |
Update a custom check |
DELETE |
/api/servers/:serverId/custom-checks/:id |
servers:delete |
Delete a custom check |
GET |
/api/http-checks |
servers:read |
List all HTTP checks |
POST |
/api/http-checks |
servers:write |
Create an HTTP check |
PUT |
/api/http-checks/:id |
servers:write |
Update an HTTP check |
DELETE |
/api/http-checks/:id |
servers:delete |
Delete an HTTP check |
POST |
/api/http-checks/:id/test |
servers:write |
Run an HTTP check immediately |
GET |
/api/cert-checks |
servers:read |
List all certificate checks |
POST |
/api/cert-checks |
servers:write |
Create a certificate check |
PUT |
/api/cert-checks/:id |
servers:write |
Update a certificate check |
DELETE |
/api/cert-checks/:id |
servers:delete |
Delete a certificate check |
POST |
/api/cert-checks/:id/test |
servers:write |
Run a certificate check immediately |
GET |
/api/users |
users:read |
List users |
POST |
/api/users |
users:write |
Create user |
PUT |
/api/users/:id |
users:write |
Update user |
DELETE |
/api/users/:id |
users:delete |
Delete user |
GET |
/api/reports/:serverId |
reports:read |
Generate report |
GET |
/api/settings/notification-channels |
settings:read |
List notification channels |
POST |
/api/settings/notification-channels |
settings:write |
Create notification channel |
PUT |
/api/settings/notification-channels/:id |
settings:write |
Update notification channel |
DELETE |
/api/settings/notification-channels/:id |
settings:write |
Delete notification channel |
POST |
/api/settings/notification-channels/:id/test |
settings:write |
Send a test notification |
Roles and permissions
| Permission | Admin | Operator | Viewer |
|---|---|---|---|
servers:read |
yes | yes | yes |
servers:write |
yes | yes | no |
servers:delete |
yes | yes | no |
alerts:read |
yes | yes | yes |
alerts:write |
yes | yes | no |
alerts:delete |
yes | yes | no |
users:read |
yes | no | no |
users:write |
yes | no | no |
users:delete |
yes | no | no |
reports:read |
yes | yes | yes |
settings:read |
yes | yes | yes |
settings:write |
yes | no | no |
LDAP / ActiveDirectory Authentication
YAMOT supports LDAP/ActiveDirectory as an external authentication provider. When enabled, login attempts are tried against LDAP first, then fall back to the local database. The local admin account always works as a fallback.
How it works
- User submits username and password at the login page
- YAMOT connects to the LDAP server and binds with a service account (if configured)
- It searches for the user by the configured search filter (default:
(sAMAccountName={{username}})) - It rebinds as the found user's DN with the provided password to verify credentials
- On success: a local user record is created (if it doesn't exist yet) with the
viewerrole, and a JWT is issued - On failure (user not found in LDAP or wrong password): falls through to local DB authentication
LDAP-provisioned users have a sentinel password (__ldap__) stored locally. This means they cannot authenticate via the local password path — they must always go through LDAP. An admin can promote an LDAP user to operator or admin via the Users page; the role is preserved across logins.
Configuration
LDAP can be configured via environment variables (see the table above) or at runtime via the Settings page in the web UI. Settings saved in the database override env-var values and take effect immediately without restarting.
| Setting | Description |
|---|---|
| Enable | Toggle LDAP authentication on/off |
| Server URL | ldap://host:389 or ldaps://host:636 for TLS |
| Bind DN | Distinguished name of a service account used for the initial bind and user search (e.g. CN=yamot-svc,OU=Service Accounts,DC=corp,DC=local) |
| Bind Password | Password for the service account (encrypted at rest) |
| Search Base | Base DN for the user search (e.g. DC=corp,DC=local) |
| Search Filter | LDAP filter with {{username}} placeholder. Default: (sAMAccountName={{username}}). For OpenLDAP, use (uid={{username}}) |
| TLS reject unauthorized | When unchecked, allows self-signed certificates for ldaps:// connections (not recommended for production) |
The "Test Connection" button on the Settings page verifies connectivity by binding with the service account and performing a base search on the search base. It uses the values currently entered in the form, so you can test before saving.
Linking existing local users to LDAP
When an LDAP user logs in for the first time, a local record is auto-created. If a local user with the same username already exists, LDAP authentication will use that existing record (keeping its current role). However, the old local password still works — the user can authenticate via either path.
To make an existing local user LDAP-only (block local password login), an admin can set their password to the LDAP sentinel:
UPDATE users SET password = '__ldap__' WHERE username = 'jdoe';
This forces the user to authenticate through LDAP. Their role and permissions are unchanged.
Security notes
- The bind password is encrypted at rest using AES-256-GCM (same as SSH credentials)
- Usernames are escaped per RFC 4515 before being inserted into the search filter, preventing LDAP injection
- Account lockout applies to LDAP users: if an LDAP user's local record is locked (e.g. by an admin), they cannot log in even with valid LDAP credentials
- LDAP settings changes take effect on the next login — no restart needed
Monitors
| Monitor | Metrics collected |
|---|---|
| CPU | usagePercent, userPercent, systemPercent |
| Memory | total, available, used, usagePercent, buffers, cached, swapTotal, swapUsed, swapUsagePercent |
| Disk | Per-filesystem: filesystem, fstype, size, used, available, usagePercent, mount |
| Network | Per-interface: interface, rxBytes, rxPackets, rxErrors, rxDropped, txBytes, txPackets, txErrors, txDropped |
| Load | load1, load5, load15, runningProcesses, totalProcesses |
| Uptime | uptimeSeconds, idleSeconds, bootTime |
| Login | recentLogins[] (user, terminal, ip, loginTime), failedAttempts[], failedCount |
| Processes | processes[] (pid, user, cpu, mem, command) — top processes by CPU and memory |
| Services | services[] (name, loadState, activeState, subState) — systemd service health status |
| Proxmox Cluster | quorate, nodes, nodesOnline, expectedVotes, totalVotes — cluster quorum status (Proxmox hosts only) |
| Proxmox VMs | vms[] (vmid, name, status, cpuCount, memoryMB), totalVms, runningVms, stoppedVms — VM inventory (Proxmox hosts only) |
| Proxmox Containers | containers[] (ctid, name, status), totalContainers, runningContainers, stoppedContainers — LXC container inventory (Proxmox hosts only) |
| Proxmox Storage | storagePools[] (name, type, total, used, available, usagePercent, active), totalStorageGB, usedStorageGB — storage pool usage (Proxmox hosts only) |
| Proxmox Node | pveVersion, cpuUsage, memoryTotal, memoryUsed, memoryFree, swapTotal, swapUsed, uptime, subscriptionStatus — PVE node status (Proxmox hosts only) |
| SMART | Per-disk: device, type, smartPassed, temperature, powerOnHours, reallocatedSectors, pendingSectors, uncorrectableSectors, model, serial, attributes[]. Aggregates: totalDisks, failedDisks, maxTemperature, reallocatedSectors, pendingSectors, uncorrectableSectors |
| Temperature | Per-sensor: name, label, temperature. Aggregate: maxTemperature |
| Custom Checks | Per-check: custom.<name> — output of user-defined shell commands parsed as number or JSON |
| SNMP | Per-OID: configurable labels (default: sysUpTime, sysDescr). Polled via SNMPv2c UDP, no SSH required |
| HTTP Checks | Per-check: http-check.<name>.statusCode, http-check.<name>.responseTimeMs, http-check.<name>.success. Runs from YAMOT host, no SSH required |
| Certificate Checks | Per-check: cert-check.sslDaysLeft, cert-check.domainDaysLeft, cert-check.success. SSL expiry via TLS, domain expiry via WHOIS. Runs from YAMOT host, no SSH required |
Metrics are collected every intervalMs (per-server, default 60s) and stored in SQLite. A daily cleanup job removes metrics older than 30 days.
SMART disk health
The SMART monitor retrieves disk health data from physical disks using smartctl (from the smartmontools package). It runs at a lower frequency than other monitors — by default every 6 hours (SMART_INTERVAL_MS) — because SMART data changes slowly and smartctl can be slow on some drives. The monitor self-throttles on every scheduler tick, only executing SSH commands when the interval has elapsed.
Requirement: smartmontools must be installed on each monitored server. On Debian/Ubuntu: apt install smartmontools. On RHEL/CentOS: dnf install smartmontools. The SSH user must have read access to disk devices — typically this means connecting as root, or configuring sudo rules that allow the SSH user to run smartctl without a password.
The monitor discovers disks via smartctl --scan, then queries each disk with smartctl -j -a (JSON output) in a single SSH round-trip. It supports both ATA/SATA and NVMe drives. If smartctl is not installed on a server, the monitor silently fails and the SMART section does not appear on that server's detail page.
Alert rules can target SMART metrics: smart.failedDisks (alert when > 0), smart.maxTemperature, smart.reallocatedSectors, smart.pendingSectors, smart.uncorrectableSectors.
CPU temperature
The Temperature monitor reads CPU and system temperatures from two Linux kernel interfaces via SSH:
/sys/class/hwmon/— the hardware monitoring interface (coretemp, k10temp, acpitz, etc.). Present on most x86 systems./sys/class/thermal/thermal_zone*/— the kernel thermal framework interface. Available since kernel 2.6.x on all Linux distributions; often the only source on ARM boards and some VMs.
Both are kernel-provided sysfs interfaces — no additional packages need to be installed on target servers. Works on Ubuntu, Fedora, CentOS, openSUSE, Debian, and other Linux distributions. The monitor runs on every tick (default 60s) since reading sysfs is instant.
Each sensor is reported with its source name (e.g. coretemp, k10temp, thermal_zone), optional label (e.g. Package id 0, Core 0, x86_pkg_temp), and temperature in degrees Celsius. The maxTemperature aggregate is the highest reading across all sensors.
Alert rules can target temperature.maxTemperature to trigger when CPU temperature exceeds a threshold.
Service health checks
The Services monitor checks the status of systemd services on the remote server. By default, it reports on common services (ssh, cron, systemd-journald). To watch specific services, add a watchServices array to the server configuration:
yamot add-server -n my-server -h 192.168.1.10 -u root --key ~/.ssh/id_rsa --watchServices nginx,postgresql,redis
Service names are validated against a whitelist of allowed characters (alphanumeric, hyphen, underscore, dot).
Proxmox VE monitoring
YAMOT supports Proxmox VE hosts alongside standard Linux servers. When a server is declared with type: 'proxmox', five additional monitors run automatically alongside the standard Linux monitors (Proxmox is Debian-based, so CPU, memory, disk, and network monitors work normally).
All Proxmox data is collected via SSH using Proxmox CLI tools — no API tokens or REST API calls are required.
| Monitor | Command | Metrics |
|---|---|---|
| proxmox-cluster | pvecm status |
Cluster quorum, node counts, vote counts |
| proxmox-vms | qm list |
VM inventory (VMID, name, status, CPU, memory) |
| proxmox-containers | pct list |
LXC container inventory (CTID, name, status) |
| proxmox-storage | pvesm status |
Storage pool usage (total, used, available, usage %) |
| proxmox-node | pvesh get /nodes/$(hostname)/status --output-format json |
Node CPU, memory, swap, uptime, PVE version, subscription |
To add a Proxmox host via CLI:
yamot add-server -n pve-node1 -h 192.168.1.20 -u root --key ~/.ssh/id_rsa --type proxmox
Or via the web UI, select "Proxmox VE" from the Server Type dropdown in the Add Server modal.
Proxmox-specific data is displayed on the server detail page: cluster status, VM inventory table, container inventory table, storage pool usage with usage bars, and PVE node information (version, subscription status). On the dashboard, Proxmox servers show a "PVE" badge on their server card.
SNMP device monitoring
YAMOT supports SNMP v2c devices (switches, routers, UPS units, printers) alongside SSH-based servers. When a server is declared with type: 'snmp', only the SNMP monitor runs — SSH-based monitors are excluded.
SNMP polling uses a built-in BER (Basic Encoding Rules) encoder/decoder over raw UDP datagrams — no external SNMP library dependency. The device's community string, port, and OIDs are stored in the server's snmpConfig.
Default OIDs polled if none are configured:
| Label | OID | Description |
|---|---|---|
sysUpTime |
1.3.6.1.2.1.1.3.0 |
System uptime in hundredths of seconds |
sysDescr |
1.3.6.1.2.1.1.1.0 |
System description string |
To add an SNMP device via the web UI, select "SNMP Device" from the Server Type dropdown in the Add Server modal and enter the community string. SSH key fields are hidden for SNMP devices.
Collected SNMP values are displayed on the server detail page in a metrics table. Alert rules can target snmp.<label> (e.g. snmp.sysUpTime).
Custom check scripts
Custom checks let you run arbitrary shell commands on remote servers via SSH and use the output as metrics. Each check has a name, a command, and a format (number or json). The output is trimmed and parsed accordingly:
- number — parsed with
parseFloat(); non-numeric output becomesnull - json — parsed with
JSON.parse(); invalid JSON becomesnull
Metrics are exposed as custom.<checkName> and can be targeted by alert rules. Custom checks are managed per-server from the server detail page in the web UI or via the API.
HTTP health checks
HTTP checks are standalone monitors that run from the YAMOT host — no SSH or SNMP required. Each check polls a URL at its own interval and records:
- Status code — compared against an expected status (default 200)
- Response time — measured in milliseconds
- Body match — optional regex or substring match against the response body
- Success —
1if status code matches and body match passes (if configured),0otherwise
HTTP checks run on a separate scheduler from the main SSH-based monitors. Results are published on the event bus with a synthetic serverId of http-check:<id>, so alert rules can target http-check.<name>.statusCode, http-check.<name>.responseTimeMs, or http-check.<name>.success.
HTTP checks are managed from the dedicated HTTP Checks page in the web UI or via the API. A "Test" button runs a check immediately to verify the configuration.
SSL certificate and domain expiration checks
Certificate checks are standalone monitors that run from the YAMOT host — no SSH or SNMP required. Each check monitors two things independently:
- SSL/TLS certificate expiry — connects to the domain via TLS (
tls.connect) and reads the certificate'svalid_todate, computing days until expiry. Set port to 0 to skip SSL entirely (domain-only monitoring). - Domain name expiry — queries the authoritative WHOIS server via raw TCP (port 43) and parses the expiry date from the response. Supports 40+ TLD-specific WHOIS servers with fallback to
whois.iana.org. Uncheck the WHOIS option to skip domain expiry (SSL-only monitoring).
Both checks have a 10-second timeout. The default interval is once per day (86400000 ms) but can be configured per check.
Metrics are published on the event bus with a synthetic serverId of cert-check:<id>, so alert rules can target:
cert-check.sslDaysLeft— days until SSL certificate expires (-1 if check failed or skipped)cert-check.domainDaysLeft— days until domain registration expires (-1 if check failed or skipped)cert-check.success—1if at least one check (SSL or WHOIS) produced a result
Recommended alert rules:
| Rule | Metric | Operator | Threshold | Severity |
|---|---|---|---|---|
| SSL expiring soon | cert-check.sslDaysLeft |
< | 30 | warning |
| SSL expired | cert-check.sslDaysLeft |
< | 0 | critical |
| Domain expiring soon | cert-check.domainDaysLeft |
< | 30 | warning |
| Domain expired | cert-check.domainDaysLeft |
< | 0 | critical |
Scope: "All Servers" (matches all cert checks via the synthetic serverId).
Certificate checks are managed from the dedicated Certificates page in the web UI or via the API. A "Test" button runs a check immediately and displays the days remaining for both SSL and domain.
Architecture
yamot/
├── src/
│ ├── index.js # Application entry point
│ ├── config/index.js # Environment-based configuration
│ ├── cli/cli.js # CLI (commander-based)
│ ├── core/
│ │ ├── event-bus.js # Pub/sub event bus (class, injected via constructors)
│ │ ├── scheduler.js # Per-server monitoring scheduler
│ │ ├── http-check-scheduler.js # Standalone HTTP check scheduler
│ │ ├── cert-check-scheduler.js # Standalone SSL/domain expiry check scheduler
│ │ ├── snmp/snmp-client.js # SNMP v2c BER encode/decode + UDP client
│ │ └── ssh/
│ │ ├── connection-manager.js # SSH connection pool with host key verification (TOFU)
│ │ └── executor.js # Remote command execution
│ ├── monitors/ # 20 monitors + registry (11 standard + 5 Proxmox + custom checks + SNMP + HTTP checks + cert checks)
│ ├── alerts/
│ │ ├── alert-manager.js # Evaluates rules on metric events + unreachable detection + maintenance windows
│ │ ├── alert-rule.js # Threshold comparison logic
│ │ ├── alert-templates.js # Default alert rule templates per server type
│ │ └── channels/ # email, slack, mattermost (WebhookChannel base)
│ ├── storage/
│ │ ├── database.js # SQLite init + migrations
│ │ ├── migrations/ # Versioned schema migrations
│ │ └── repositories/ # server, metric, alert, user, uptime, notification-channel, known-host-key, maintenance-window, custom-check, http-check, cert-check CRUD
│ ├── auth/
│ │ ├── auth-service.js # JWT login/verify
│ │ ├── rbac.js # Permission middleware
│ │ └── roles.js # Role definitions
│ ├── reports/ # Report generation
│ ├── web/
│ │ ├── server.js # Express + Handlebars + Socket.io
│ │ ├── routes/ # API and page routes
│ │ ├── middleware/ # Auth, error handling
│ │ ├── socket/ # Socket.io manager (JWT-authenticated)
│ │ └── views/ # Handlebars templates
│ └── utils/
│ ├── helpers.js # Formatting + password hashing (scrypt)
│ ├── crypto.js # AES-256-GCM encrypt/decrypt
│ ├── validator.js # Input validation
│ └── logger.js # Winston logger
├── public/
│ ├── less/ # LESS source (variables, mixins, components)
│ ├── js/ # Client-side JS (charts, dashboard, socket)
│ └── vendor/ # Vendored libraries (Font Awesome, Chart.js)
├── scripts/compile-less.js # LESS → CSS compiler
├── test/ # Node.js test runner (291 tests)
└── .env.example
Key design decisions
- SQLite (via
better-sqlite3) — zero-dependency deployment, single file - Event bus — decouples monitors from storage and alerts. A single instance is created at startup and injected via constructors (not a module-level singleton)
- Dependency injection — all repositories and services receive their dependencies via constructors. No service locator fallback
- SSH connection pool — persistent connections reused across monitor runs with host key verification (TOFU)
- Scrypt with per-user salt for password hashing
- AES-256-GCM encryption for SSH private keys, passphrases, and SMTP passwords at rest
- CSRF protection — double-submit cookie pattern for state-changing API requests
- JWT revocation — token version mechanism invalidates tokens on password/role changes
- LESS — variables and mixins at root, self-contained component files, compiled to single CSS
Network traffic estimates
Each SSH-based server runs 13-14 monitors per tick (default 60s). Each monitor opens a separate SSH channel on a persistent pooled connection (no re-authentication per tick). The table below shows the estimated traffic per monitor per tick:
| Monitor | Command size | Output size | SSH overhead | Total |
|---|---|---|---|---|
| cpu | 50 B | 100 B | 130 B | ~280 B |
| memory | 20 B | 2 000 B | 130 B | ~2 150 B |
| disk | 100 B | 500 B | 130 B | ~730 B |
| network | 20 B | 300 B | 130 B | ~450 B |
| login | 300 B | 2 500 B | 130 B | ~2 930 B |
| load | 20 B | 20 B | 130 B | ~170 B |
| uptime | 40 B | 50 B | 130 B | ~220 B |
| process | 80 B | 3 500 B | 130 B | ~3 710 B |
| service | 100 B | 50 B | 130 B | ~280 B |
| temperature | 500 B | 200 B | 130 B | ~830 B |
| file-integrity | 400 B | 500 B | 130 B | ~1 030 B |
| suid-files | 300 B | 1 500 B | 130 B | ~1 930 B |
| listening-ports | 400 B | 500 B | 130 B | ~1 030 B |
| Per tick, per server | ~15 700 B |
SSH overhead (~130 B/exec) covers channel open/close, cipher padding, and TCP ACKs. SMART runs every 6 hours (~5-10 KB per run) and is negligible when amortized.
10 SSH servers, 60s interval
| Period | Estimated traffic |
|---|---|
| Per minute | ~157 KB |
| Per hour | ~9.4 MB |
| Per day | ~226 MB |
| Per month | ~6.7 GB |
SSH keepalive packets (~100 B every 30s per server) add ~5.7 MB/day for 10 servers. The initial SSH key exchange at startup is ~5-10 KB per server (one-time cost).
Reducing traffic
- Disable unneeded monitors —
suid-files,file-integrity, andlistening-portsare the most verbose and rarely critical - Increase the interval — doubling to 120s halves the traffic
- Enable SSH compression — can reduce text output sizes by 50-70%
FAQ
Does each monitor open a new SSH connection?
No. YAMOT maintains one persistent SSH connection per server, reused across all monitors and all ticks. On first use, the connection is established (TCP handshake, key exchange, authentication). On every subsequent monitor run, the existing connection is reused -- each monitor opens a new SSH channel on the same TCP socket (channels are multiplexed, no re-authentication).
Server tick (every 60s)
└─ Monitor cpu → executor.exec(server, cmd) → channel on existing connection
└─ Monitor memory → executor.exec(server, cmd) → channel on existing connection
└─ Monitor disk → executor.exec(server, cmd) → channel on existing connection
└─ ... (13 monitors, 13 channels, 1 TCP connection)
The connection stays open between ticks. Keepalive packets are sent every 30s to prevent idle disconnection. If the connection drops, the next monitor run triggers an automatic reconnect.
Why do I see "Command timeout" errors in the logs?
The default command timeout is 60s per SSH command. Timeouts typically occur when:
- The remote server is under heavy load (e.g.
suid-filesruns a fullfind / -xdevscan) - The network between YAMOT and the target server is slow or saturated
- The SSH server has a low
MaxSessionslimit and channels are exhausted
If timeouts are frequent, consider disabling expensive monitors (suid-files, file-integrity) or increasing the monitoring interval.
Why do I see "previous tick still running" warnings?
This means the total time for all 13+ monitors on a server exceeded the tick interval (default 60s). Monitors run sequentially within a tick. The cpu monitor sleeps 1 second (to sample /proc/stat twice), and suid-files can take 10-30s on large filesystems. If the combined time exceeds 60s, the next tick is skipped.
Solutions: disable expensive monitors, increase the per-server interval, or reduce the number of monitored servers on a single YAMOT instance.
Development
# Install dependencies
npm install
# Compile LESS (one-shot)
npm run less
# Compile LESS in watch mode during development
npm run less:watch
# Start in development mode (auto-restart on file changes)
npm run dev
# Run tests
npm test
# Start with a custom DB path
YAMOT_PORT=3001 DB_PATH=/tmp/yamot-dev.db npm start
Testing
npm test
Tests use the Node.js built-in test runner (node --test) and cover:
- Monitor parsing (CPU, memory, disk, network, load, uptime, processes, services, SMART, temperature)
- Proxmox monitor parsing (cluster status, VM inventory, container inventory, storage pools, node status)
- SNMP BER encode/decode (integer, OID, octet string, PDU construction, PDU parsing, round-trip, edge cases)
- SNMP monitor (default OIDs, configured OIDs, error handling, multiple OIDs, port configuration)
- Custom check monitor (number/JSON parsing, repository injection, per-server checks)
- HTTP check monitor (status code, response time, body match, timeout)
- Certificate check monitor (SSL expiry via TLS, domain expiry via WHOIS, buildMetrics, WHOIS parsing, TLD routing)
- Alert rule evaluation (all operators, missing metrics, scoping, severity levels)
- Default alert templates (Linux, Proxmox, and SNMP definitions, auto-population, duplicate prevention, server scoping)
- Alert acknowledgment (ack/unack, maintenance window suppression)
- Server unreachable alert (trigger, dedup, resolve, no-op on recovery)
- Fleet API endpoint (aggregation, group filtering, alert counts)
- RBAC role permissions
- Password hashing and verification (scrypt)
- Server repository CRUD + credential encryption at rest
- Server group repository CRUD + membership management
- Notification channel repository CRUD + SMTP password encryption
- Uptime repository (uptime percentage tracking)
- Crypto utility (round-trip, tamper detection, null handling)
License
PolyForm Noncommercial License 1.0.0 — see LICENSE for the full text.
Free for non-commercial use. Commercial use requires a separate license from the copyright holder.
- License text: https://polyformproject.org/licenses/noncommercial/1.0.0
- SPDX identifier: PolyForm-Noncommercial-1.0.0


