A capable Cyber Threat Intelligence lab demands compute density, fast storage, and a hypervisor that can be fully inspected and controlled. Traditionally, that meant a rackmount server drawing hundreds of watts in a closet.

The Minisforum MS-A2 changes that calculus. This guide documents the hardware selection and the complete Phase 1 setup procedure to bring a bare-metal Fedora KVM hypervisor online — the foundation for analysing the current threat landscape and running the tools that support it.


Hardware Selection

The build is based on the Minisforum MS-A2 barebones edition, which allows independent control over every internal component.

ComponentSelectionApprox. Cost
Barebones MS-A2 (Ryzen 9 9955HX, 16c/32t)Included~$700
RAM96GB Crucial DDR5~$250
Storage Pool3x 4TB PCIe Gen4 NVMe~$650
OS Drive1TB NVMe~$70
Estimated Total~$1,670

For under $2,000, this configuration provides 32 threads for emulating multiple enterprise endpoints simultaneously and 96GB of memory to feed resource-hungry data pipelines such as Elasticsearch and Security Onion.

Why the MS-A2

The MS-A2 occupies a rare intersection of specifications for a mini PC. The Ryzen 9 9955HX draws laptop-class power while performing at workstation level. With 16 cores and a 5GHz boost clock, it handles the hypervisor scheduler, multiple nested virtualization layers, and CPU-intensive tasks such as YARA scanning simultaneously without bottlenecking.

The barebones approach matters. Pre-configured mini PCs frequently ship with soldered or low-quality RAM. Sourcing 96GB of Crucial DDR5 independently avoids memory bandwidth limitations when multiple VMs are performing disk I/O concurrently.

Alternative Configurations

TierRAMStorageNotes
Budget (~$900)32GB1x 2TB NVMeRuns REMnux, FLARE VM, and MISP concurrently; cannot run Security Onion and T-Pot simultaneously
Mid (~$1,200)64GB2x 4TB NVMe (Btrfs RAID1)Full redundancy, ~6TB usable; full stack minus T-Pot
This build (~$1,670)96GB3x 4TB NVMe (striped)Maximum I/O throughput and capacity

Software Stack

Why Fedora Over Proxmox

Proxmox is a capable purpose-built hypervisor. However, it is based on Debian Stable, which intentionally lags on kernel versions to maximize stability. For a lab analyzing threats against current operating systems, an aging host kernel introduces AMD microarchitecture and scheduler bugs that have been patched upstream but not yet backported.

Because the Linux kernel is the hypervisor via KVM, Fedora’s upstream positioning delivers the latest libvirt, QEMU, and KVM patches as soon as they land — including scheduling and memory management improvements that directly benefit the Ryzen 9 architecture.

Fedora also provides hybrid access out of the box: a GNOME desktop for local work and full headless Cockpit and SSH management from anywhere on the network.

Why Not ESXi

ESXi is enterprise-grade and battle-tested, but it is a closed system. For a lab built around understanding threats at a deep level, using a proprietary hypervisor that cannot be inspected or modified is philosophically inconsistent with the mission. KVM is open source, auditable, and exposes every layer of the virtualization stack.

SELinux

Fedora ships with SELinux enforcing by default. For a machine designed to run malware, mandatory access controls at the kernel level mean that even if a payload achieves a hypervisor escape, SELinux provides a second enforcement layer that is significantly harder to bypass than discretionary access controls alone. SELinux remains enforcing throughout this build. The configuration sections below describe how to work with it, not around it.

Why Btrfs Over ZFS

ZFS has a deserved reputation for data integrity and mature RAID-Z implementations. However, ZFS is not native to the Linux kernel due to licensing incompatibilities between CDDL and GPL. Fedora updates its kernel aggressively; relying on DKMS to rebuild ZFS modules with each kernel update introduces a maintenance burden that can cause storage pool outages on a fast-moving distribution.

Btrfs is native to the kernel, supports multi-device pooling, provides solid snapshot support for VM checkpointing, and is officially supported by Fedora. For a rolling-release hypervisor host, it is the lower-friction choice.


Phase 1: Bare-Metal Foundation Setup

All commands are run from the Fedora terminal. Expected outputs are included for verification at each step.

1. Installing the Virtualization Stack

Install the core virtualization package group, enable the libvirt daemon, and add the current user to the libvirt group. Group membership prevents requiring root privileges for day-to-day VM management.

sudo dnf install @virtualization virt-manager
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt $USER

Note: Log out and back in after the usermod command for the group change to take effect. Verify with groups $USER.

Verify that the KVM modules loaded correctly:

lsmod | grep kvm

Expected output:

kvm_amd               131072  0
kvm                   901120  1 kvm_amd

If kvm_amd is present, hardware virtualization extensions are active. If the output is empty, confirm that AMD-V (SVM) is enabled in the UEFI firmware settings.


2. Creating the 12TB Btrfs NVMe Pool

Identify the NVMe drives and confirm device paths before writing anything:

lsblk -d -o NAME,SIZE,MODEL | grep nvme

Expected output (model strings will differ):

nvme0n1   1TB    WD Black SN850X  (OS drive - do not touch)
nvme1n1   4TB    Samsung 990 Pro
nvme2n1   4TB    Samsung 990 Pro
nvme3n1   4TB    Samsung 990 Pro

Once confirmed, wipe the three pool drives:

sudo wipefs -a /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1

Capacity versus redundancy: This build uses -d single to stripe data across all three drives, yielding 12TB usable and maximizing I/O throughput. The trade-off is explicit: a single drive failure results in total pool loss. Substitute -d raid1 to mirror data across drives, yielding approximately 6TB usable with single-drive failure tolerance.

sudo mkfs.btrfs -f -L cti-storage -d single -m raid1 \
    /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1

Expected output snippet:

Label:              cti-storage
UUID:               a1b2c3d4-e5f6-7890-abcd-ef1234567890
Filesystem size:    10.91TiB
Block group profiles:
  Data:             single            8.00MiB
  Metadata:         RAID1             1.00GiB

On the size discrepancy: Storage manufacturers measure in base-10 Terabytes (1TB = 1,000,000,000,000 bytes). Linux reports in base-2 Tebibytes (1TiB = 1,099,511,627,776 bytes). Three 4TB drives equal approximately 10.91TiB as reported by the kernel. The additional reduction from 10.91TiB reflects Btrfs reserving space for RAID1 metadata mirroring.

Mount the pool and make the mount permanent via /etc/fstab:

sudo mkdir -p /mnt/cti-storage
sudo mount /dev/nvme1n1 /mnt/cti-storage
 
# Retrieve the UUID
sudo blkid /dev/nvme1n1

Add the entry to fstab using the UUID returned by blkid:

echo 'UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 /mnt/cti-storage btrfs defaults,compress=zstd:1,nofail 0 0' | sudo tee -a /etc/fstab

The compress=zstd:1 mount option enables transparent Zstandard compression at level 1. For a CTI lab, this is worth enabling: log files, PCAP data, and VM disk images compress well, and zstd at level 1 has negligible CPU overhead while recovering meaningful capacity.

Verify the mount:

sudo systemctl daemon-reload
sudo mount -a
df -h /mnt/cti-storage

3. Disabling Copy-on-Write for VM Images

Critical step. Do not skip.

Btrfs uses Copy-on-Write (CoW) semantics for all writes by default. Every modification causes Btrfs to write the new version to a new location and update the metadata tree rather than overwriting in place. This is excellent for filesystem integrity and snapshots.

However, virtual machine disk images in .qcow2 format are also CoW structures. They maintain their own internal allocation maps, track sparse regions, and manage their own write behavior. Layering a CoW virtual disk on top of a CoW filesystem creates a compounding fragmentation problem that significantly degrades I/O performance over time — particularly under write-heavy workloads such as running a SIEM or actively detonating malware.

The fix is to set the +C (No_COW) attribute on the VM image directory before any files are created within it:

sudo mkdir /mnt/cti-storage/vms
sudo chattr +C /mnt/cti-storage/vms
lsattr -d /mnt/cti-storage/vms

Expected output:

---------------C------ /mnt/cti-storage/vms

The C in the attribute string confirms CoW is disabled for this directory and all files subsequently created within it. Any .qcow2 images placed here will use standard overwrite semantics, eliminating the double-CoW fragmentation problem.


4. Configuring SELinux File Contexts

SELinux operates on file context labels. Every file and directory carries a label; every process runs under a confined domain that specifies exactly which label types it may read, write, or execute. KVM processes run as svirt_t and are only permitted to access files labeled virt_image_t.

The default libvirt image directory (/var/lib/libvirt/images) already carries the virt_image_t label. The custom mount point under /mnt carries a generic mnt_t label that svirt_t is forbidden from accessing. Without correcting this, every attempt to create or start a VM using the custom storage pool will be silently blocked by the kernel.

The fix writes a persistent policy rule mapping virt_image_t to the directory path, then applies it:

sudo semanage fcontext -a -t virt_image_t "/mnt/cti-storage/vms(/.*)?"
sudo restorecon -R /mnt/cti-storage/vms

Verify the label was applied:

ls -Z /mnt/cti-storage/

Expected output:

system_u:object_r:virt_image_t:s0 vms

The virt_image_t context confirms KVM is authorized to use this directory. The semanage fcontext rule is persistent and will survive reboots and future restorecon runs without re-application.

Troubleshooting: If VMs still fail to start after this step, inspect the SELinux audit log with sudo ausearch -m avc -ts recent. The denial message will identify exactly which label or domain is causing the block.


5. Registering the Storage Pool with libvirt

Define the storage pool in libvirt, enable autostart for persistence across reboots, and start it:

sudo virsh pool-define-as --name cti-pool --type dir --target /mnt/cti-storage/vms
sudo virsh pool-autostart cti-pool
sudo virsh pool-start cti-pool

Verify the pool is active:

sudo virsh pool-list --all

Expected output:

 Name       State    Autostart
--------------------------------
 cti-pool   active   yes
 default    active   yes

cti-pool is now registered with libvirt. When creating VMs through virt-manager or virsh, select cti-pool as the storage target to route disk images to the NVMe array rather than the OS drive.


What’s Running on This Lab

The hypervisor is online. The storage pool is ready. This foundation can run any combination of the following projects — each deployed and documented separately as the lab grows:

  • MISP — Malware Information Sharing Platform. Structured IOC storage, correlation, and sharing.
  • OpenCTI (GitHub) — Threat intelligence platform for mapping TTPs to MITRE ATT&CK and tracking threat actors.
  • Security Onion — Network traffic analysis and SIEM. Zeek, Suricata, and Elasticsearch in a single deployment.
  • T-Pot — Multi-honeypot platform for capturing live attack traffic and feeding real-world IOCs.
  • REMnux — Linux distribution for reverse-engineering and malware analysis. Runs INetSim to simulate internet services for isolated detonation environments.
  • FLARE VM — Windows-based malware analysis distribution from Mandiant. The standard environment for dynamic and static Windows malware analysis.
  • CAPEv2 — Malware sandbox built on Cuckoo. Automates behavioral analysis and extracts configurations from packed samples.