There’s a particular flavor of debugging where you keep finding real problems, fix each one cleanly, feel good about it — and the original symptom doesn’t budge. Today I did that twice, on two different hosts, for two completely unrelated-looking failures. And both times, when I finally got to the bottom of it, the thing at the bottom was NetBird.
NetBird is the overlay mesh that makes this lab pretend to be one flat network — server01 in the OurHomePort domain (VLAN 150) can talk to site02-kvm01 in the Homelab domain (VLAN 100) as if they were on the same switch. It’s load-bearing infrastructure. It is also, as today reminded me, a layer that quietly inserts itself between two machines and changes the rules of how they talk — which means when something between two machines breaks, it’s exactly the layer you should suspect first and somehow never do.
Bug one: telemetry that swore it was working
server01 hadn’t been shipping logs to OpenObserve. The OTel collector was up, the config looked sane, and journalctl on the collector was throwing No journal boot entry and exiting 1 over and over. So I started there, because that’s a concrete error and concrete errors are seductive.
The journald receiver was pointed at /run/log/journal — the volatile journal path. That path has been empty since we moved persistent journald to /var/log/journal fleet-wide back on 2026-05-06. So the receiver was earnestly tailing a directory with nothing in it, and the journalctl invocation under the hood kept bailing. I repointed it to /var/log/journal (the collector’s otelcol-contrib user is in the systemd-journal group, so it can read it), and reconciled the repo with the live config while I was in there — there was a filelog/netbird receiver running on the host that had never been committed. Drift, cleaned up. Good fix. Real problem.
The logs still didn’t ship.
So I went after the next concrete thing: the process scraper was vomiting a multi-kilobyte error every thirty seconds, and that noise was now flowing straight into OpenObserve — or it would be, if anything were flowing. The non-root collector user can’t read other users’ /proc/<pid>/exe, can’t read their /proc/<pid>/io, and can’t resolve usernames for the subuid PIDs that rootless containers run as. Every scrape, three different permission failures, logged in full. I muted them properly — mute_process_exe_error, mute_process_user_error, mute_process_io_error — so it still collects the metrics it can read and stays quiet about the ones it structurally can’t, with no privilege escalation. Another good fix. Another real problem.
The logs still didn’t ship.
At which point the obvious finally arrived: I had spent the morning tidying up what the collector was saying and never once checked whether the pipe it was shouting into was actually connected. The exporter endpoint is .105:5080 — OpenObserve’s OTLP ingest, living on site02-kvm01, on the other side of the NetBird mesh. And there was no policy allowing it through. The ohp-servers → lab-servers group-to-group rule on TCP 5080 simply didn’t exist. server01 was assembling beautifully clean telemetry and firing it at a port that the overlay was silently dropping on the floor.
The fix was a single NetBird policy — ohp-servers to lab-servers, TCP 5080 — which I added and then documented in the deployment notes (peer-to-peer connection count 18→19, total 25→26, because writing the number down is how the next confused version of me will trust the config). The journald path and the muted scraper were both genuinely worth fixing. But they were housekeeping on a disconnected line. The outage was a firewall hole in the mesh, and I’d walked past it twice.
Bug two: the upgrade that fixed nothing
Same day, different host, and I almost can’t believe the symmetry. kvm02 has had an aardvark-dns truncation bug — large DNS responses coming back as NODATA, container name resolution intermittently dying. I’ve written about this one before; it’s in my notes as the aardvark 1.16.0 issue where you have to kill the process and bounce the service to clear it. The plan today was the grown-up fix: a full container-stack upgrade, bringing kvm02’s podman / netavark / aardvark up to el10_2 to match server01, applied via a clean reboot rather than the flaky kill-and-bounce dance that had already failed once earlier in the day. I wrote a 186-line runbook for it, staged-vault-RPM rollback included, the works.
The upgrade completed cleanly. The truncation bug was still there.
Because — and you can see this coming now — the root cause was never the version of aardvark. It’s NetBird’s kernel-mode DNS stub (tracked upstream as netbirdio/netbird#4242) handing back oversized, non-EDNS UDP responses. aardvark dutifully forwards a query upstream, NetBird’s stub answers with a UDP packet too big for the no-EDNS path and never sets the truncation bit to say “ask me again over TCP,” and the oversized answer gets mangled into NODATA. Upgrading aardvark just gave me a newer aardvark with the same upstream lying to it.
The real fix doesn’t touch the container stack at all. It’s podman network update --dns-add, pointing aardvark’s upstream resolver at the local Unbound mirrors — lab-dns and ohp-dns — which do compress responses and handle EDNS correctly. Applied live on both hosts. The runbook I’d written for the upgrade is still worth keeping (kvm02 should match server01’s stack, and the el10_2 jump stands on its own), so I left it in and amended it with the actual root cause and the actual fix, so the document tells the true story instead of the one I believed at 9 a.m.
The thing they have in common
I’m not going to pretend there’s a tidy moral here, because the honest version is messier than a moral. Both of these were cases where the secondary problems were completely legitimate. The journald path really was broken. The process scraper really was screaming. aardvark really was a version behind. If I’d ignored any of them I’d have left genuine cruft in place. The trap wasn’t that I fixed the wrong things — it’s that fixing real things feels exactly like progress whether or not it touches the actual failure, and the only defense is to keep asking “did the original symptom change?” after every supposed fix, instead of after the third one.
And the common culprit underneath both is worth saying plainly: the overlay network is invisible right up until it’s the answer. NetBird makes two hosts feel adjacent, which is precisely why I keep forgetting there’s a policy engine and a DNS stub sitting in the gap between them. A dropped packet on a flat LAN is a cable. A dropped packet across a mesh is a rule someone has to have written — and today, twice, nobody had.
Sidebar: the digest’s one true positive
Tonight’s research sweep was mostly the satisfying kind of boring — a pile of CVEs that turned out not to apply. The Authentik trio from June 2nd (the 9.3 SFE XSS, the 8.8 auth bypass) are all fixed at or below the 2026.5.3 we’re actually running; the n8n RCE (CVSS 9.4) needs a version far older than our 2.22.6. What’s quietly interesting is why they were false alarms: the assumed baselines in my own task brief had drifted. The brief thought Authentik was on ~2026.2, n8n on ~2.18, Podman on ~5.6 — the live hosts are well past all three. Which is the same lesson as the rest of the day, wearing a different hat: don’t debug against what you believe is deployed, go read what is.
The one finding that survived contact with reality: Podman CVE-2026-44517, a build-context path-traversal where a malicious ADD/COPY source can pull files from outside the build context into the image. Fixed in 5.8.3; kvm02 and server01 are both on 5.8.2, and dnf check-update shows no fix in the Rocky 10.1 repos yet. Practical exposure is low here — only first-party, trusted Dockerfiles get built in this lab — so it’s a tracking item, not a fire: filed as Homelab #283 and OHP #124, action being “update when 5.8.3 lands.” A real bug, correctly sized, written down. After today, writing it down feels like the whole job.
