From Dropbox to Daily Driver Part 6: Standing Up a Wazuh Manager, and Why the VM Wouldn't Boot

Part 5 left the Pi reachable from anywhere, gated by an identity check, a key, and a TOTP code, with no listening port anywhere on my home network. Every one of those layers is preventative. None of them tells me anything after the fact. If a login succeeds that shouldn’t have, or someone starts probing the tunnel, or a config file quietly drifts out of the state Part 3 put it in, nothing so far would tell me. This part starts fixing that: a Wazuh manager, standing on its own VM on pve, that this Pi will eventually ship logs to.

Scoping the build

This isn’t a single-purpose VM just for the Pi. Wazuh’s own single-node all-in-one deployment, manager, indexer, and dashboard all on one host, is documented as comfortable up to 100 agents and 90 days of indexed alert data, which is a lot more than one Raspberry Pi needs. Building one properly-sized shared manager now, rather than a Pi-specific toy instance, means anything else in this homelab that eventually wants centralised logging or file-integrity monitoring has somewhere to plug into later instead of me standing up a second one.

Wazuh’s current documentation puts the minimum spec for that all-in-one profile at 4 vCPU, 8 GiB RAM, and 50 GB of disk. Storage on pve isn’t remotely tight, so I rounded the disk up to 100 GB rather than sit close to the documented floor.

A deliberate OS choice, not a default

Everything else on this pve host is Debian. I considered just matching that here and questioned, reasonably, why bother switching. The answer isn’t that Debian would fail: Wazuh ships as ordinary .deb packages through its own apt repository, and Ubuntu is Debian-derived, so there’s no real technical reason to expect a problem. The answer is narrower than that. Wazuh’s own certified support matrix lists Ubuntu, RHEL, CentOS Stream, and Amazon Linux. Debian isn’t on it. For a VM that’s meant to be permanent, shared infrastructure rather than a one-off lab box, I’d rather build on the path Wazuh actually tests than the path that would probably also work. So this one’s Ubuntu 24.04 LTS, a deliberate break from the rest of the homelab, not an oversight.

Getting the image onto pve

pve already had a Debian cloud image cached from an earlier series, but nothing for Ubuntu, so the first step was pulling one down directly onto the host:

sudo pvesh create /nodes/pve/storage/local/download-url \
  --url https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img \
  --content import \
  --filename ubuntu-24.04-server-cloudimg-amd64.img
400 Parameter verification failed.
filename: invalid filename or wrong extension

Ubuntu ships its cloud image with a plain .img extension. pve’s download-url API validates the filename against a set of recognised disk-image extensions for anything tagged content=import, and .img isn’t one of them, even though the file underneath is genuinely qcow2. The fix is just to rename it on the way in:

sudo pvesh create /nodes/pve/storage/local/download-url \
  --url https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img \
  --content import \
  --filename ubuntu-24.04-server-cloudimg-amd64.qcow2

That downloaded cleanly. Worth actually confirming the format matches the new name rather than assuming a rename made it true:

qemu-img info /var/lib/vz/import/ubuntu-24.04-server-cloudimg-amd64.qcow2
image: /var/lib/vz/import/ubuntu-24.04-server-cloudimg-amd64.qcow2
file format: qcow2
virtual size: 3.5 GiB (3758096384 bytes)
disk size: 596 MiB

Genuinely qcow2, 3.5 GiB virtual size. Good to build on.

Creating the VM

sudo qm create 103 --name wazuh-manager --memory 8192 --cores 4 --net0 virtio,bridge=vmbr1v100 --serial0 socket --vga serial0 --agent enabled=1
sudo qm disk import 103 /var/lib/vz/import/ubuntu-24.04-server-cloudimg-amd64.qcow2 local-lvm
sudo qm set 103 --scsi0 local-lvm:vm-103-disk-0
sudo qm set 103 --ide2 local-lvm:cloudinit
sudo qm set 103 --boot order=scsi0
sudo qm resize 103 scsi0 100G

VMID 103, on vmbr1v100 so it can reach the rest of the home LAN without the MTU issues vmbr0 carries on this host. Cloud-init identity and a static address:

sudo qm set 103 --ciuser michealg
sudo qm set 103 --sshkey ~/.ssh/wazuh.pub
sudo qm set 103 --ipconfig0 ip=192.168.1.4/24,gw=192.168.1.1

A dedicated keypair for this VM specifically, generated fresh rather than reused from anywhere else.

Booting into a rescue shell

sudo qm start 103
sudo qm status 103
status: running

“Running” turned out to mean less than I assumed. Nothing answered ping, SSH refused to connect at all, and dropping into the console with qm terminal 103 showed why:

(initramfs)
(initramfs)

The kernel had come up, but never found a root filesystem to mount, and dropped to a rescue shell instead. That’s a boot-stage failure, not a networking one, even though the visible symptom, no ping response, looks exactly like a networking problem at first glance.

The cause was a controller mismatch. qm create was never given an explicit --scsihw, so it defaulted to the older LSI 53C895A SCSI emulation. Ubuntu’s cloud image ships a trimmed-down initramfs that includes essentially just the virtio drivers a cloud image is expected to need, since that’s what almost every hypervisor it actually gets deployed on presents. It has no LSI driver built in, so it could see there was a kernel to boot but never found a disk to boot it from.

sudo qm stop 103
sudo qm set 103 --scsihw virtio-scsi-pci
sudo qm start 103

That booted straight into Ubuntu on the retry:

$ ping 192.168.1.4
64 bytes from 192.168.1.4: icmp_seq=1 ttl=64 time=0.385 ms
$ ssh -i ~/.ssh/wazuh michealg@192.168.1.4
Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-137-generic x86_64)
IPv4 address for eth0: 192.168.1.4
michealg@wazuh-manager:~$

Cloud-init had already set the hostname, static address, and resized the filesystem correctly in the background the whole time, it just needed a disk it could actually find.

Installing Wazuh

A pending kernel update from the fresh cloud image, cleared before installing anything on top of it:

sudo apt update && sudo apt upgrade -y
sudo reboot

Then the official all-in-one assisted installer, current version 4.14 at the time of writing:

curl -sO https://packages.wazuh.com/4.14/wazuh-install.sh && sudo bash ./wazuh-install.sh -a

The -a flag installs the indexer, manager, and dashboard together on one host, which is exactly the all-in-one profile the sizing in this post was based on. It ran cleanly first try, no further gotchas, a few minutes of output installing and configuring all three components in sequence.

Wazuh generates a random admin password during install rather than printing one in plain text, and stores it in an archive alongside the other component credentials:

sudo tar -O -xvf wazuh-install-files.tar wazuh-install-files/wazuh-passwords.txt

That file is genuinely sensitive, not a fingerprint or a device identifier but a live admin credential for a service that will end up holding security-relevant logs from this whole homelab, so it isn’t reproduced here. With it in hand, the dashboard is reachable at https://192.168.1.4, self-signed certificate warning and all, logging in as admin.

What’s next

There’s a Wazuh manager running and reachable, but it isn’t watching anything yet. Part 7 wires the Pi in as its first actual agent: log shipping, alerting on new SSH logins, and Wazuh’s SCA module doing continuous CIS-style auditing of the config this series spent Parts 3 and 4 building.