v0.1.22 HTTPS by default, importable TLS certificates, a network map that draws itself, derived alert suppression, LDAP/AD sign-in, unmonitored-host discovery, MCP system health, half the memory
📋 Full release notes: RELEASE_NOTES.md
Images (published by CI to GHCR; :latest now points at this release):
ghcr.io/horryworks/yagra-core:v0.1.22
ghcr.io/horryworks/yagra-poller:v0.1.22
ghcr.io/horryworks/yagra-web:v0.1.22
http://<host>:3000 stops answering, everyone is signed out, and SSO needs its redirect URI updated in two places.
Breaking changes
- The WebUI is now HTTPS, on port 443, and there is no plain-HTTP listener. Everything the UI
carries — the login password, bearer tokens, and device credentials on their way to being
encrypted — used to cross the network in the clear by default. v0.1.9 shipped instructions for
fixing that and a commented-out configuration block; a year later nobody had uncommented it. So
the secure shape is now the one you get by doing nothing.http://<host>:3000no longer answers. If your.envstill setsYAGRA_WEB_PORT=3000you
keep port 3000 and it becomeshttps://<host>:3000— the port stayed, the scheme changed.
Delete the line to land on 443.- A redirect was considered and deliberately rejected. Most webhook senders do not follow
redirects, and those that do turn a301onPOSTinto aGET, so
POST /api/v1/ingest/webhook/:source_idwould have gone on returning success while events
stopped arriving. Connection-refused is the failure you can see. Move webhook senders and any
other machine client tohttps://<host>/api/v1/…, or to core's unchanged plaintext
http://<host>:8080/api/v1/…. - Everyone is signed out and loses their saved UI state.
http://host:3000and
https://hostare different origins, so the session token, dashboard layouts, theme and table
preferences do not carry over. There is no migration for this that would be worth its
complexity; sign in again and the layouts are rebuilt as you go. - SSO stops working until two things are updated. The stored OIDC redirect URI is an absolute
URL and must now use the new scheme and port, and the same value has to be updated at the
identity provider. Settings ▸ Auth shows a warning when the stored value no longer matches the
address you are browsing from. Yagra will not rewrite it for you — changing where an IdP is
permitted to send an authorization code is not something an upgrade should do on your behalf. - Your browser will warn on first visit. The certificate is self-signed until you import one,
and because Yagra cannot know the hostname you will use, the name usually will not match either.
Import a real certificate at Settings ▸ TLS, or regenerate the self-signed one with the
correct names from the same page. - Set
YAGRA_WEB_TLS=offto keep serving plain HTTP from the container — the supported shape
when an external reverse proxy or load balancer already terminates HTTPS in front of it. - Core's own API port is unchanged: still plaintext, still published on the LAN. That is
deliberate sequencing, not 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. Once those clients are on the TLS edge with a certificate they trust, set
YAGRA_API_BIND=127.0.0.1to take it off the network. Settings ▸ TLS shows the current state. - If you maintain your own compose file, core's container now has a fixed group as well as a
fixed user — both10001. The certificate bundle is written0640owned by that group, and
the web container joins it withgroup_add: ["10001"]in order to read it. Leave the group to
the base image and nginx traverses the directory, is refused the file itself, and the WebUI
never comes up while every other signal stays green. The shipped compose files already do this.
New Features
- Import your own TLS certificate from the WebUI. Settings ▸ TLS shows what is being served —
subject, issuer, the names it covers, expiry and fingerprint — and takes a PEM certificate chain
and private key, either pasted or from a file. The new certificate is live within seconds, with
nothing restarted. Yagra will not accept a pair that does not go together, has already expired,
or carries no subject alternative name, and says which of those it is rather than failing at the
next handshake. Encrypted private keys and.pfxbundles are refused with theopensslcommand
that converts them. The self-signed certificate can be regenerated with the hostnames and IP
addresses you actually use, and renews itself before it expires.- The private key is envelope-encrypted at rest like every other secret and is never returned by
the API. The certificate is downloadable, so you can hand it to a Prometheusca_file, a
curl --cacert, or an operating-system trust store. - System Health says when it is about to expire. An expired certificate takes the whole UI
down — including the page you would use to fix it — so it is one more row there, and reaches the
MCPget_system_healthtool with it. Degraded is reserved for something somebody has to act
on: expired, or an imported certificate inside its last 30 days. A self-signed one nearing
expiry renews itself and is not reported as a problem. The same fact is on the Prometheus
endpoint asyagra_web_tls_expires_in_days, for your own alerting. - This does not manage the NATS bus certificate, which the bus reads for itself at startup.
- The private key is envelope-encrypted at rest like every other secret and is never returned by
- An AI client can now ask whether Yagra itself is healthy. The MCP surface had no way to see
the monitoring system's own state, which matters more than it sounds: with no way to tell that a
poller is offline or a store unreachable, a model reads missing data as a healthy quiet and
reports that the fleet is fine. Six new tools close that (/mcp, off by default):get_system_health(section=…)— the poller fleet and per-pool summary, poll-loop counters,
which nodes one poller holds, which poller owns one node, recent core↔poller outages,
per-store reachability, core/poller host resources and their trends, forwarding delivery
status, whether stored credentials still decrypt, the running version, and which optional
tiers are enabled.get_report_runs,get_audit(who changed or acknowledged what),
fleet_state_history,get_dns_chain, andrun_rca— an LLM explanation of one
incident, the same one the WebUI's "Explain this incident" produces.get_fleet_summary(kind="coverage")answers which nodes have actually reported recently,
with a watchlist of the ones that have not.- Sections require different permissions, matching the WebUI exactly: most need view,
forwarding status needs manage-config, credential health needs manage-credentials, the audit
log needs view-audit, andrun_rcaneeds ack-alerts. Read-only does not mean
readable-by-anyone. Reports and the state timeline refuse a group-scoped token rather than
showing it the whole fleet, as the REST endpoints do.
- The Network map now draws the network, not a list of parent links you typed in. Yagra derives
the connectivity graph from what devices report: CDP/LLDP adjacency, and nodes that have an
interface address in the same IP subnet. Two nodes sharing a subnet are adjacent as a matter of
fact, so a map appears without anyone entering a single link. Each edge carries the evidence
behind it — LLDP, CDP, or shared subnet — and the map labels and legends them. Redundant paths are
kept rather than collapsed: a server reached through two routers shows both links. Drawing the
map is all this does on its own; alert suppression keeps following the dependency graph you
maintain by hand until you deliberately hand it over, which is the next feature down. - A new read endpoint,
GET /api/v1/topology/links, returns that graph in keyset pages, with a
summaryof everything the derivation observed but declined to turn into a link (unmatched
neighbours, ambiguous management addresses, segments with no identifiable router). A group-scoped
caller sees only links whose both endpoints are visible to them. - The network map now finds the links that share no subnet. Until now a link was derived from
two devices holding an address in the same prefix, which structurally cannot see a point-to-point
/32(a PPPoEDialer, a tunnel endpoint), an unnumbered OSPF link, or a peering across a
segment whose addressing has not been collected. Yagra now also reads each device's OSPF
neighbours and BGP peers, and asks its routing table about specific destinations — so those
links appear on the map withOSPF neighbor,BGP peerorConnected routeas their evidence.
A link seen both ways is still one link carrying both.- On by default, at the same hourly cadence as the other two automatic walks, and switchable
at Settings ▸ System settings ▸ Discovery walks. Unlike the ARP walk, the tables it reads are
sized by the device's own peering mesh, not by the network. - The routing table is never walked. A router carrying a full table has hundreds of thousands
of routes, so Yagra asks about one destination at a time — and only a device that holds a host
address of its own is asked at all, capped at 64 destinations. On a fleet of ordinary devices
this issues no route queries whatsoever. - A down session still draws its link. A BGP session in
activeis a link with a fault, and
that is usually the thing being investigated — making the topology disappear in step with the
outage it exists to explain would be exactly backwards. - An iBGP session between loopbacks does not become a link. A BGP peer is only treated as
adjacent when it sits on a network the reporting device terminates, so a route reflector does
not acquire a false star to every client it peers with. The count of peers declined this way is
reported alongside the map's other diagnostics. - Known limits, stated rather than half-answered: BGP4-MIB is IPv4-only, so IPv6 BGP peers are
out of scope; OSPF collection is OSPFv2; and virtual links (ospfVirtNbrTable) are not read.
One older limit also stands: a segment with more than two members where no member can be
identified as routing for the others produces no links rather than a guessed one, and is
counted in the map's summary instead.
- On by default, at the same hourly cadence as the other two automatic walks, and switchable
- Yagra can now tell you what is on your network that it is not watching. Turn on the new
ARP / IPv6 neighbor cache walk (Settings ▸ System settings ▸ Discovery walks) and every
monitored router reports the hosts it has actually spoken to. Anything not already in the
inventory appears under Nodes ▸ Discovery ▸ Seen on the network, with the address, its MAC,
and which device saw it on which port — and a Monitor button that turns it into a node through
the same import path a subnet scan uses. No scan required; it is a by-product of the polling you
already do.⚠️ Off by default, deliberately. Unlike the other two discovery walks, this one reads a
table sized by the network rather than by the device — thousands of rows on a campus switch —
so an upgrade will not start issuing it against your fleet. The default cadence is six hours.- The list says which kind of empty it is: "no device has reported a cache yet" (nobody looked)
reads differently from "0 unmonitored addresses" (nothing to find), and if any router's cache
hit its row budget the list declares itself a sample rather than a complete answer. - Endpoints are deliberately not drawn on the network map. An unmonitored host has no state
to show, and a few thousand stateless boxes would bury the nodes that do. Importing one makes it
a node, and the ordinary derivation picks it up from there. - New:
GET /api/v1/discovered-endpoints(keyset-paged, group-scoped through the observing node)
andPOST /api/v1/discovered-endpoints/{id}/import, plus the MCP tool
list_discovered_endpoints. - Rows age out seven days after they were last seen, and the table is capped fleet-wide.
- The interface-address walk finally has a UI. It has been running since it shipped, but the
settings card only ever knew about CDP/LLDP. Settings ▸ System settings now shows all three
discovery walks — neighbours, interface addresses and ARP — each with its own switch and cadence. - One box can be excluded from derived suppression entirely. Tick Never suppress against a
node on Topology ▸ Dependencies (orPUT /api/v1/nodes/{id}/suppression-opt-out) and its
alert always stands on its own, whatever the discovered graph says. The node keeps its place in
the graph, so everything behind it still resolves through it. This only ever removes
suppression, so it cannot cause an outage to go unreported — which is why it is a per-node switch
where per-edge approval was rejected. - The comparison now shows how much of the fleet the derived graph actually covers: how many
nodes would get an upstream, how many are excluded by hand, and how long the deployment has been
comparing. Advisory, not a gate — the one blocking condition is still an unplaced poller. - See what the derived graph would do to your alerts, before it does anything.
Topology ▸ Dependencies gained a mode switch with three positions. The hand-authored graph
is the default and is what every existing deployment stays on. Comparing changes nothing about
alerting and shows, node by node, where the graph Yagra derived and the one you maintain by hand
disagree — plus the two numbers that matter: how many active alerts the derived graph would
newly suppress (the risky direction — each of those is an alert that would stop being raised)
and how many it would stop suppressing. The derived graph hands suppression over. Nothing moves
between these on its own; an upgrade lands on the mode you were already on. - Dependency suppression can now have more than one upstream per node. The derived graph gives a
node every neighbour that sits one hop closer to a poller, so a server reached through a redundant
pair of routers gets both as parents — and its alert keeps standing while either one is alive.
This is whatis_suppressed's "suppressed only when every parent is down" rule was written for;
a single hand-typedparent_idcould never express it. - Correct a wrong link instead of working around it. New endpoints record operator decisions
about a link —pinit into existence,hideit, or declare which end is upstream — and those
always beat what was derived, on every recomputation.GET/POST /api/v1/topology/link-overrides
andDELETE /api/v1/topology/link-overrides/{id}. A pinned link never expires the way an
unobserved derived link does. GET /api/v1/topology/shadowreturns the whole comparison: edge counts, the differing edges
in each direction, the affected active alerts, the nodes acting as graph roots, and any pools
whose poller could not be placed.- The MCP
get_topologytool takes akindparameter:dependency(the default, and what
every existing call keeps doing),linksfor the connectivity graph, andoverridesandshadow
for the operator decisions and the comparison. Asked whether derived suppression is safe to
enable, an AI client can now answer from the same data an operator sees. - Pollers report their own interface addresses, and can be given an anchor node. Direction in
the derived graph comes from distance to a poller, so Yagra has to know where each poller sits. It
works that out from the addresses the poller reports — but⚠️ a poller running in a container
reports a container-network address that matches no monitored node, which is the normal case
rather than an unusual one. Settings ▸ Pollers gained an Anchor node column for naming where
such a poller really attaches (PUT /api/v1/pollers/{id}/anchor), andGET /api/v1/pollersnow
returnsmgmt_addrsandanchor_node_id. Switching to the derived graph is refused while a
pool that has nodes has an unplaced poller — such a pool would contribute no roots, so nothing in
it would ever be suppressed while the screen showed the feature as on. PUT /api/v1/settings/topologysets the mode (manual/shadow/derived). There is
deliberately no matchingGET: the current mode is part of the/topology/shadowresponse.- Sign in with an LDAP or Active Directory account. Configure your directory at
Settings ▸ Auth ▸ Directory (LDAP/AD) and people log in with their corporate credentials at the
ordinary login form — there is no second button and no separate URL. Yagra searches for the person
with a service account and then re-binds as the entry it found, so no DN pattern has to be guessed;
group membership maps to a Yagra role through the same mapping the SSO provider uses, matching a
group by its full DN or just its name. An account is created on first successful sign-in.
Local accounts are always tried first, so a directory that is unreachable can never lock an
administrator out — keep one local admin and a rollback stays survivable. LDAPS and StartTLS only,
with a field for your private CA; there is deliberately no way to skip certificate verification.
A Test button reports each stage separately and, given a username, shows the DN, the groups and
the role that person would receive — including when the answer is "denied", which the login
form otherwise reports as an ordinary wrong password. Nothing changes for a deployment that does
not configure a directory. - SAML is answered with a documented bridge rather than an implementation.
DEPLOYMENT.mdnow
describes putting Keycloak or Dex in front as a SAML→OIDC bridge, and says why Yagra does not
verify XML signatures itself.
Improvements
- Yagra-core and Yagra-poller use far less memory, and stop growing. Both binaries now use the
mimalloc allocator instead of the system one. Measured on a 50,000-node deployment over a
20-minute window, at identical polling throughput: core's resident set averaged 183 MB instead
of 397 MB, and grew by 6 MB instead of 162 MB across the window; the poller's stopped
creeping upward at all. The old behaviour was not a leak but the system allocator holding on to
per-thread arenas it never returned — which meant core's footprint kept climbing for as long as it
was watched, and could not be sized with confidence. If you provisioned a host from the previous
profile, it will now sit comfortably under it. Building with--no-default-featuresrestores the
system allocator. - Upgrading a poller now downloads half as much. The poller image stored its binary twice: once
where it was copied in, and again because granting itCAP_NET_RAWrewrote the file into a second
layer — and both layers change with every release. Placing the binary and granting the capability
are now one layer, so the per-release download foryagra-pollerdrops from 10.2 MB to 4.7 MB.
Most of that is the duplicate going away; the rest is a smaller binary. The image behaves
identically and raw-socket ICMP is unaffected.⚠️ yagra-coremoved the other way, and it is only fair to say so: the release build now
uses fat link-time optimization, which grew core's binary from 11.9 to 13 MiB compressed. On
top ofcodegen-units = 1there is no duplicate code left for LTO to collapse, so what it adds
is inlining across crate boundaries — and inlining duplicates code. The poller's binary went
the other way, which is where the "smaller binary" above comes from. Taken together these two
changes still subtract about 4.4 MB from an upgrade that pulls both images, but core alone
costs a little more than it did.
- The Dependency / root-cause dashboard widget now lists each root cause with the alerts rolled
up under it, biggest first, instead of an indented parent→child tree. The dependency graph is no
longer a tree — a node can have two upstreams — and a tree could only have shown one of them. - Interface addresses are collected and their changes recorded, the same way CDP/LLDP adjacency
already was: one current set per node plus an append-on-change history, visible under
Settings ▸ Data retention as Interface address changes. Collection is on by default at the
same hourly cadence, with its own toggle atPUT /api/v1/settings/neighbors
(l3_enabled/l3_interval_secs; omitting them leaves those settings unchanged). - LLDP neighbours now carry the peer's management address. That is what lets an adjacency be
matched to a monitored node, and it is why the map can be built from L2 at all.⚠️ The first poll
after upgrading records one extra neighbour-change row per LLDP-speaking node, because the
recorded set genuinely gained a field. Devices that do not implement LLDP-MIB record nothing new. - An API token owned by an LDAP account now expires with its owner's silence, the same way an
SSO-owned one already did. A directory disabling somebody is not something Yagra is told about, so
the owner going quiet is the only signal there is — previously that rule was written for OIDC
alone, and a token owned by a disabled directory account would have kept working indefinitely.
YAGRA_PAT_OIDC_IDLE_DAYSkeeps its name and now governs both.
Bug Fixes
- A Troubleshoot analysis started over MCP now appears in the audit log. The identical run
started from the WebUI or the REST API was recorded; the one launched through/mcpleft no
trace at all, because auditing is REST middleware that the MCP surface does not pass through. Any
deployment with/mcpenabled has been under-recording who started analyses. - Signing in as a disabled SSO account now answers 401 instead of 500. The refusal was correct;
the status code said Yagra had broken. - Resetting the password of an SSO or directory account is now refused with a message saying so.
It used to answer 200 and write a hash that can never be used, telling an administrator they had
set a password when they had not.