MiBeeSteward v0.3.0: Full L2 Topology + TLS Certificate Inventory + Container Images

🔊

Take over an unfamiliar LAN and the first thing you need is to figure out what’s on it — office, server room, and home subnets tangled together, no documentation left behind, a switch full of MACs nobody can name. MiBeeSteward exists for exactly that: scan the network and tell you what devices are there, what they are, and how they connect.

v0.3.0 goes after three specific pains left over from the last two releases:

  • Topology was half-drawn. v0.2.0 used Bridge-MIB to learn “which MAC sits behind which switch port,” but that alone — cross-vendor neighbor identity, VLAN-aware forwarding, and STP port roles were all blind, so the topology graph had a giant hole.
  • TLS certs were unmanaged. Since v0.1.0 the TLS service probe only grabbed the cert CN as a fingerprint — “how many days until the HTTPS cert on this device expires” was unanswerable. Internal networks are full of self-signed certs; the day one expires, the service silently breaks.
  • Deployment friction was high. Until now it was “download a binary, write a systemd unit” — even a quick try was inconvenient.

v0.3.0 fixes all three: full L2 topology, a TLS certificate inventory, and official container images. Full release notes at GitHub Release v0.3.0. To see it work fastest, pull the image and run one line:

bash
1
2
3
4
5
6
7
docker pull ghcr.io/mi-bee-studio/mibeesteward:0.3.0
docker run -d --name mibee --network host \
  --cap-add NET_RAW --cap-add NET_ADMIN \
  -v mibee-data:/data \
  -p 8080:8080 \
  ghcr.io/mi-bee-studio/mibeesteward:0.3.0
# Open http://<this-host>:8080 in a browser

--network host is the key — in an isolated netns the container won’t find any MACs (explained below). Open the Web UI, wait a few minutes for the scan to finish, and the device list / topology graph / TLS panel all fill in automatically.

Topology: now it’s actually complete

What you see: a full L2 topology graph centered on a switch. Each device is colored by type (router / switch / camera / printer / IoT…), each edge colored by the protocol that discovered it (CDP / LLDP / Bridge-MIB / Q-BRIDGE / STP), and unidentified neighbors use dashed edges. Click any node to jump to the device detail page and see all of its neighbors with resolved name/IP/type.

mermaid
flowchart TD
    SW["Switch<br/>SNMP reachable"] --> PROBE["Three MIBs<br/>in parallel"]
    PROBE --> CDP["CDP-MIB<br/>vendor neighbor"]
    PROBE --> QB["Q-BRIDGE-MIB<br/>VLAN forwarding"]
    PROBE --> STP["STP-MIB<br/>port role"]
    CDP --> EDGE["device_neighbors<br/>tagged by protocol"]
    QB --> EDGE
    STP --> EDGE

    classDef dev fill:#E3F2FD,stroke:#1565C0,color:#1565C0
    classDef proc fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef store fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    class SW dev
    class PROBE,CDP,QB,STP proc
    class EDGE store

Why three MIBs are needed: v0.2.0 used only Bridge-MIB, which drops edges in cross-vendor environments. Neighbor identity on Cisco-family switches comes from CDP-MIB (device id, platform string, IP); VLAN-aware forwarding entries live in Q-BRIDGE-MIB (dot1qTpFdbPort); STP port roles (which edge is forwarding / blocked / who is root bridge) come from STP-MIB (dot1dStp). Three MIBs, three jobs; neighbor edges are tagged by protocol and land uniformly in device_neighbors.

IF-MIB ifName resolution turns numeric ifIndex into a human-readable name — the topology shows GigabitEthernet0/1, not 1011-style nonsense.

Neighbor identity inference is also closed: CDP/LLDP platform strings (e.g. Cisco ISR4321) feed into v0.2.0’s RuleClassifier to infer vendor/model/type, then MAC is used as the merge key to line up the “neighbor” in topology with the “known device” in the device table. In v0.2.0, neighbor_device_id was always NULL — the device was clearly online, clearly probed, but anonymous in the topology. v0.3.0 resolves it at query time, so the Neighbors panel on the detail page can show a neighbor table with actual names.

TLS Certificate Inventory: know before it expires

What you see: the full certificate chain for every TLS service on every device. Not just the leaf — the issuer chain all the way up. The frontend shows one clickable row per port with a status-colored border you can read at a glance: green = valid / amber = expiring within 15 days / red = expired. A day-count badge tells you exactly how long is left. Click to open CertificateModal for the full chain + PEM with a copy button.

What this solves: a common trap on internal networks is HTTPS / LDAPS / IMAPS services using self-signed certs. Operations forgets to renew them, the cert expires, and the service silently breaks that day — you spend hours troubleshooting before realizing it’s a cert problem. v0.3.0 shows you “which certs expire in the next 15 days” at a glance, so you fix them ahead of time instead of firefighting after the fact.

mermaid
flowchart TD
    SCAN["Scan TLS ports"] --> CC["CollectCertChain<br/>shared by 8 protocols"]
    CC --> CHAIN["Full cert chain<br/>leaf + issuers"]
    CHAIN --> DB[("host_tls_certs<br/>indexed by not_after")]
    DB --> UI["Frontend panel<br/>status color + day badge"]

    classDef proc fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef store fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    classDef out fill:#E3F2FD,stroke:#1565C0,color:#1565C0
    class SCAN,CC,CHAIN proc
    class DB store
    class UI out

How it works: eight TLS-wrapped handlers share one CollectCertChain core — https / ldaps / smtps / imaps / pop3s / ftps / ircs / telnets, bringing total handler count from 21 to 29. Each cert carries Subject/Issuer/SAN, serial, validity, signature & key algorithms (RSA/ECDSA/Ed25519 + bit size), CA/self-signed flags, SHA-256 fingerprint, and PEM.

HandlerDefault portHandlerDefault port
https443imaps993
ldaps636pop3s995
smtps465ftps990
ircs6697telnets992

Data lands in the host_tls_certs table, one row per cert in each port’s chain (cert_index 0 = leaf, 1..N = issuers). Indexed on (ip, port) and not_after — the first serves “certs on this device’s ports,” the second serves “certs expiring within N days.” Read via GET /api/v1/devices/{id}/certificates. Retention is retention.host_tls_certs_days, default 30 days.

Container Image: one command to run

What you can do: v0.3.0 ships the first official multi-arch container image (linux/amd64 + linux/arm64) on GHCR. No binary download, no systemd unit, no Go toolchain — just docker pull and run. arm64 means Raspberry Pi, Banana Pi, and other ARM boards use the same image.

Tags follow releases: :0.3.0 / :0.3 / :latest / :sha-xxx. Auto-published on every v* tag.

Three deployment shapes, pick by intent:

ProfileProbe fidelityUse case
bridge (default)TCP/SNMP/HTTP ok; MAC/ARP degradedUI demo, dev
host (recommended)≈ bare-metalproduction scanning
macvlanown LAN IP, ARP workscontainer as a LAN device

bridge looks like the most convenient because it’s the docker default, but for the scanner it’s a trap — measured on the 31-device test LAN, the default bridge found 0 device MACs, host networking found 30. The root cause is /proc/net/arp: the scanner’s passive discovery reads the kernel ARP cache, but a NAT’d container’s ARP table only contains the bridge gateway, blind to everything else on the LAN. That’s why the docker run at the top specifies --network host. Full root-cause analysis and the trade-offs across the three profiles are in the technical deep-dive post.

This is the unprivileged variant — LLDP/CDP raw-frame send + the eBPF passive observer ship as no-op stubs (raw sockets need CAP_NET_RAW). Build the privileged variant locally with make docker-build-priv if you need those capabilities. Plain docker run under host networking already discovers the vast majority of devices — Bridge-MIB / Q-BRIDGE / CDP / STP / TLS / port scanning none of them depend on raw sockets.

Operational

v0.3.0 also pays down operational debt left by v0.2.0:

  • Retention hardening: device_neighbors and host_services had no sweeper in v0.2.0 and grew unbounded. v0.3.0 adds them — defaults 90d neighbors, 30d host_services; per-table configurable + a days<=0 safety guard (no accidental full-table wipe).
  • Test coverage: taskservice (the scan-task state machine, previously zero-tested) and a fingerprint golden test (regression guard on real-world evidence samples).
  • Fingerprint library: snmp-data.yaml extended with consumer/SMB sysObjectID prefixes (ASUS, D-Link, Zyxel, Tenda, DrayTek); new lldp-cdp.yaml ruleset with broader CDP/LLDP platform-string recognition.
  • Fixes: removed deprecated tls.VersionSSL30 (staticcheck SA1019); gofmt + golangci-lint cleanup (QF1008, unused params, embedded selectors); a latent sqlc v1.27.0 codegen bug (a non-ASCII char in a query comment corrupted sibling-query codegen).
  • CI: a docker-build smoke-test job now boots the image and waits for /health on every PR, so Dockerfile/compose regressions surface before a tag.

Upgrade

Binary path: replace in place. Container path: docker pull the new tag and restart.

The new host_tls_certs table is created via CREATE TABLE IF NOT EXISTSno manual migration. The new retention.host_tls_certs_days / retention.device_neighbors_days / retention.host_services_days keys all have defaults and work without configuration. Existing device data is untouched.

If you’ve ever taken over an undocumented LAN and wanted to know what’s actually running on it, which devices plug into which switch port, and when the HTTPS certs expire — v0.3.0 is built for that. One docker run --network host, wait a few minutes for the scan, and read the results yourself.