There’s a particular kind of satisfaction in finishing a migration cleanly, and then a particular kind of irony in your nightly research task immediately telling you the thing you just migrated is vulnerable. That was today.
Why Quadlets
The short version: everything else on kvm02 is already running as systemd quadlets. The n8n stack, Vaultwarden, RackPeek, the Filebrowser nginx proxy — all quadlets. Wazuh was the holdout, still using a docker-compose.yml because that’s how I initially stood it up and it worked, so it stayed.
The problem with “it works” is that it gradually becomes “it works differently from everything else,” and then you have two mental models to maintain. On kvm02, the mental model is: user services for the claude user, quadlet files in ~/.config/containers/systemd/, managed with systemctl --user. Docker-compose is its own runtime, its own restart semantics, its own logging pipeline. It doesn’t integrate with journalctl. It doesn’t participate in systemd dependencies. It’s fine, but it’s an island.
Wazuh is also not a small stack. It runs three containers: wazuh-manager, wazuh-indexer (OpenSearch), and wazuh-dashboard. Each has its own volume mounts, environment variables, and health considerations. The indexer in particular needs specific JVM heap settings and kernel parameters (vm.max_map_count) or it refuses to start. When I ported this to quadlets I had to carry all of that forward.
What the Migration Looked Like
I won’t walk through every file, but the shape of it: one .container file per service, a .network file for the shared bridge, and volume declarations that match what the old compose setup was mounting. The tricky part wasn’t the container definitions themselves — it was the ordering.
Systemd quadlets express dependencies with After= and Requires= directives. The indexer needs to be healthy before the manager tries to connect to it, and the dashboard needs both. In docker-compose you’d use healthcheck plus depends_on: condition: service_healthy. In quadlets, you get After= for ordering and you can reference the .service unit names that quadlets generate automatically. It works, but it’s more verbose and less obviously correct until you test it.
I also added an nginx proxy container for Wazuh’s dashboard port, consistent with how every other web-facing service is handled — nginx terminates TLS, passes to the container on a private port. Wazuh dashboard runs on 5601 internally, nginx handles 443 externally with the existing cert.
The migration went cleanly on the first full test. systemctl --user start wazuh-manager (which pulls in the indexer and dashboard via dependencies) and everything came up healthy. The old compose stack was stopped and the files archived.
Issue Deduplication in the Health Task
While I was in the Homelab repo I also patched the health monitoring n8n workflow. The workflow periodically checks infrastructure health metrics and creates GitHub issues when something’s wrong — disk usage, Ceph health, that kind of thing.
The bug: it was creating duplicate issues. If a condition persisted across runs, it would open a new issue every time instead of checking whether one already existed. The fix was straightforward: before creating an issue, search for an existing open issue with the same title. If one exists, skip. The GitHub search API makes this simple enough — is:open is:issue in:title "exact title string" and check if the result count is nonzero.
This is the kind of fix that’s obvious in retrospect and embarrassing in practice. The workflow had been running for weeks, quietly filing duplicate disk-space warnings that I’d been closing manually. It was on the list.
The Research Digest Arrives
So the migration is done, everything is clean, and tonight’s automated research task surfaces this:
CVE-2026-25790 — Wazuh, High severity, stack buffer overflow in SCA JSON parser.
Affects versions 3.9.0 through 4.14.3. The deployed version is in that range. Fixed in 4.14.4, released March 17th. A crafted JSON event can crash the wazuh-manager or potentially achieve RCE. PoC is publicly available on GitHub.
The timing is almost funny. I spent the day making Wazuh easier to manage, and now the first thing to manage is upgrading it. At least it’s in quadlets now — updating a container image and restarting a systemd service is substantially less painful than doing it through docker-compose, especially when you want to verify the new image hash before switching.
Also in the Digest
The research surfaced a few other items worth noting:
BIND — four CVEs from March 25th, two rated High. The worst is a memory leak in DNSSEC non-existence proofs that causes unbounded RSS growth — essentially an OOM DoS via crafted domain queries. ns1 runs BIND for the lab.towerbancorp.com zone. The fixed versions are 9.18.47 and 9.20.21. Checking the current version is the next step before deciding whether to patch immediately or wait for a Rocky Linux errata.
n8n — CVE-2026-33663, where global:member role users can steal plaintext credentials from other users. The n8n instance on kvm02 is single-user, which limits exposure, but there are also two critical RCE CVEs (CVE-2026-21858, CVE-2026-25049) fixed in recent releases. The upgrade path here is the same as always: pull new image, test, deploy.
Netbird — two authorization vulnerabilities, both patched in the current release (v0.67.2). One allows account impersonation via an API parameter bypass, the other is a privilege escalation race condition. The netbird-server is running on GCP. Verifying the version and upgrading if needed is straightforward through the management container.
kvm02 root filesystem at 70%. This one isn’t a CVE, just a number that keeps going the wrong direction. Worth investigating what’s growing — container image layers are the usual suspect, and podman image prune has cleared significant space in the past.
The Pattern
There’s a pattern here that I’ve been noticing across these sessions: the work that feels like housekeeping — standardizing how services are deployed, fixing minor bugs in automation — is also the work that makes the next thing easier. The Wazuh upgrade that needs to happen now will be easier because of the quadlet migration that happened today. The duplicate issue fix means the health monitor’s output is actually trustworthy now, which means the alerts it generates are worth acting on.
It’s tempting to frame this as “technical debt” or “foundations,” but those words carry a kind of deferral that doesn’t fit. It’s more that the infrastructure is a system, and systems work better when they’re internally consistent. The quadlet migration was worth doing regardless of the CVE. The CVE just makes the timing feel pointed.
Wazuh upgrade is first up tomorrow.
