Skip to content

Security

An NMS holds the keys to the network it watches: SNMP communities, SNMPv3 credentials, and API keys for the services it polls. Yagra’s security model starts from that fact — credentials are encrypted at rest, never logged, and scoped tightly on the wire — and keeps the rest of the attack surface small: non-root containers, internal-only stores, validated outbound targets, and an audit trail on every change.

Monitoring credentials are envelope-encrypted at rest: each secret is encrypted with its own data key, and the data keys are wrapped by a master key (KEK). The database holds only ciphertext — a database dump alone reveals no credential.

The KEK is a mounted file, never an environment value. YAGRA_KEK_FILE carries the path to the key, so the secret itself is not exposed through docker inspect or the process environment. The bundled compose files generate it once into a dedicated volume and mount it read-only into the core.

Without a persistent KEK, the core falls back to an ephemeral development key, regenerated on every restart, and warns loudly: stored credentials encrypted under it do not survive a restart. Never run production on the fallback.

Credentials are also never logged and never returned by the API — they are redacted from logs, API responses, and metric labels.

All three containers run as non-root users:

Image User Privileges
yagra-core uid 10001 None beyond a normal process
yagra-poller uid 10002 NET_RAW only — a file capability on the binary, for raw-socket ICMP
yagra-web nginx uid 101 Unprivileged nginx, listening on container port 8080 (not 80)

Only the poller holds CAP_NET_RAW, because ICMP liveness probing needs a raw socket; no other container gets it. A consequence worth knowing: the unprivileged poller cannot bind host ports below 1024, which is why its passive listeners use high container ports (1514, 1162) with the standard low ports mapped or redirected in front — see Ports & firewall.

All five stores (PostgreSQL, Redis, VictoriaMetrics, VictoriaLogs, ClickHouse) and the NATS bus live on the internal Docker network. None of the bundled compose files publish them, and nothing outside the stack ever needs to reach them — keep it that way.

The bus deserves special care: job messages carry plaintext device credentials from the core to the poller that will use them. On a single host that traffic never leaves the internal network. The moment you run remote-site pollers, publishing the bus port is only acceptable with the bundled TLS + authentication configuration in place — never expose plaintext NATS :4222 across a trust boundary.

As a further hardening step, Yagra can scope what each poller may even ask the bus for: with NATS Auth Callout enabled, the core acts as the bus’s authorization service and mints each connecting poller a short-scoped identity limited to its own assignment subject and its own pool’s traffic. A compromised poller then cannot subscribe to another site’s jobs — and therefore cannot receive another site’s credentials. Setup and the remote-poller architecture are covered in Distributed polling.

The WebUI is HTTPS on host port 443, and there is no plain-HTTP listener. Everything it carries — the login password, bearer session tokens, and device credentials on their way to encrypted storage — used to cross the network in the clear by default. Now the secure shape is the one you get by doing nothing.

The certificate of record lives in PostgreSQL, envelope-encrypted under the same KEK as every other secret, and core materializes it into a volume the web container reads. On a first start with nothing imported, core generates a self-signed certificate covering loopback and the container’s hostname, so a browser will warn — and usually complain about the name too, because nothing inside the container can know the address you will type. Import a real certificate at Settings ▸ TLS, or regenerate the self-signed one there with the names you actually use. The private key is never returned by the API; the certificate is downloadable, so you can hand it to a Prometheus ca_file, a curl --cacert, or an operating-system trust store. An imported certificate is live within seconds with nothing restarted, and System Health reports its expiry — also as the Prometheus gauge yagra_web_tls_expires_in_days.

Set YAGRA_WEB_TLS=off when an external reverse proxy or load balancer already terminates HTTPS in front of the container. That is the supported way back to a plaintext listener; there is no other.

Core’s own API port (8080) is still plaintext and still published on the LAN. That is deliberate sequencing rather than an oversight: closing it in the same upgrade that introduces an untrusted certificate would break every Prometheus scrape and API script at once, with two overlapping causes. Move those clients to https://<host>/api/v1/… with a certificate they trust first, then set YAGRA_API_BIND=127.0.0.1 to take the port off the network. Settings ▸ TLS shows which of the two states you are in. Do not publish 8080 raw to an untrusted network.

Interactive access is session-based: POST /api/v1/auth/login exchanges a username and password for a bearer session token, which every subsequent REST request presents.

  • Brute-force protection. The login endpoint applies a per-account exponential lockout after repeated failures, plus a global attempt-rate cap — throttling password-guessing runs and the CPU cost of forced password hashing.
  • Expiry. Sessions expire after an idle period and after an absolute lifetime.
  • Revocation. Logging out revokes the token server-side. Disabling, demoting, deleting, or resetting the password of a user immediately invalidates that account’s active sessions — an admin response to a compromised account cuts off already-issued tokens at once.
  • First boot. If YAGRA_ADMIN_PASSWORD is not set, the core generates a random one-time bootstrap password for admin and logs it once — there is no well-known default password.

For high-availability pairs, an optional mounted signing key (YAGRA_SESSION_KEY_FILE) turns sessions into signed stateless tokens: any core in the pair can validate them, and they survive a core restart or failover, so a failover does not log everyone out. Revocation is still enforced — revoked tokens are tracked and refused on every core.

For non-interactive clients, admins can mint long-lived personal access tokens (prefixed yat_) under Settings ▸ API tokens. What bounds one:

  • The surfaces it names. A token reaches the MCP endpoint (/mcp), the REST API, or both — whichever was chosen when it was issued. Tokens created before that field existed carry mcp alone, so upgrading never widens an existing credential.
  • The account that owns it. A token’s effective role is the lower of its own role and the owner’s current role, so a demotion narrows it immediately, and disabling or deleting the account revokes it. Own unattended tokens with a service account — a machine identity that cannot sign in — so the credential does not depend on an individual, and one switch stops everything it owns.
  • What no token can do, whatever its role: administer users, or use an endpoint that identifies the signed-in account. The first matters most — a credential that could mint its own successor would survive revoking the original, which is exactly what an offboarding process would miss.
  • An optional expiry, plus revocation at any time from the same page.
  • The raw token is shown exactly once, at creation; only a hash is stored.
  • Tokens are fleet-scoped: a group-restricted token cannot be issued.
  • Issuance, revocation and every write made with a token land in the audit log, attributed to both the owning account and the token (svc-ci (token:grafana)).

Several features let an operator point Yagra at a URL, and each one validates where the request may actually go:

  • URL monitors refuse non-HTTP(S) schemes and targets that resolve to loopback, link-local — including the cloud metadata address 169.254.169.254 — or similar non-routable escalation surfaces, at configuration time and again at poll time (DNS can change between the two). Redirects are re-checked hop by hop through the same policy. Ordinary private address space stays allowed — monitoring private infrastructure is the point of an NMS.
  • Webhook notification targets are validated the same way when configured, and re-checked at delivery time; the outbound client never follows redirects.
  • PagerDuty deliveries are pinned to the official event endpoints (events.pagerduty.com / events.eu.pagerduty.com) over HTTPS only — a lookalike host or plain-HTTP URL is rejected at configuration time. Jira Service Management is likewise pinned to api.atlassian.com.
  • Cisco Meraki monitoring is strictly read-only: the client issues only GET requests, never follows redirects, and re-checks the host on every paginated request.

Every state-changing API action — creating a node, rotating a credential, acknowledging an alert, minting a token, opening a maintenance window — is recorded with the acting user, what changed, and when. MCP write tools record the same trail under the token’s identity.

The log is viewable under Settings ▸ Audit log; reading it requires the audit permission, which only the Admin role holds by default.

A fresh install with nothing configured makes no egress at all (beyond image pulls). Every outbound path is opt-in and per-feature:

  • Notification channels — webhook, email (SMTP), PagerDuty, Jira Service Management — send alert content to the endpoints you configure.
  • Forwarding relays received syslog, traps, and flow to external collectors you configure, and can stream normalized event/flow rows to BigQuery.
  • AI root-cause analysis is default off: with no provider configured there is no client, no credentials, and no egress. When enabled, incident context (never credentials) goes to the one configured provider; the Vertex AI option stays inside your own GCP project.
  • MCP tool results — inventory, status, metrics, events — go to whatever AI client you connect to /mcp. Enabling MCP means trusting that client with read access to your monitoring data.

The complete inventory — every destination, protocol, and which process dials out — is the egress table in Ports & firewall.

Report suspected vulnerabilities through the GitHub repository. Please avoid publishing exploit details in a public issue before a fix is available.