KVM 虚拟化安全加固实战:宿主机与虚机配置

KVM 虚拟化安全加固是自建虚拟化平台与云主机的基础防护工作。虚机逃逸、宿主机越权访问、镜像文件被篡改一旦发生,影响覆盖同一宿主机上的全部业务。本文以 libvirt + QEMU/KVM 为例,给出从 SELinux sVirt 隔离、libvirt 接口收敛、设备面裁剪到存储与网络隔离的完整加固配置,附可直接复制执行的命令与验证方法。

适用场景

以下情况建议按本文方案对 KVM 宿主机做一次完整加固:

  • 自建虚拟化平台承载对外业务,单台宿主机上运行多个租户或不同安全等级的业务虚机。
  • 云主机内做嵌套虚拟化,虚机中运行来源不完全可控的负载或第三方交付的镜像。
  • 合规检查需要提供”虚机间隔离””宿主机最小权限”的可验证证据。
  • 曾发生过宿主机被入侵后,攻击者通过 libvirt 接口或镜像文件横向访问其他虚机的事件。

需要明确一个前提:KVM 本身不是安全边界。真正决定隔离强度的是 SELinux 或 AppArmor 的强制访问控制、libvirt 的接口暴露面,以及虚机设备面被裁剪的范围。三者缺一,虚机逃逸与宿主机越权的攻击成本都会明显下降。

前置条件

  • 宿主机为 CentOS Stream / RHEL 8 以上或 Ubuntu 22.04 以上,已安装 qemu-kvm、libvirt、libvirt-client、virt-install。
  • 宿主机 SELinux 处于 Enforcing 状态;若为 Ubuntu 则启用 AppArmor 并加载 libvirt 配置。
  • 已有一台可用的测试虚机,且能通过 console 或 SSH 登录,用于逐项验证加固结果。
  • 具备 root 或 sudo 权限,能在维护窗口内重启 libvirtd 服务。
  • 镜像存放目录固定为 /var/lib/libvirt/images,避免散落在用户目录导致强制访问控制上下文缺失。

防护原理

KVM 架构下宿主机与虚机之间的界面主要有四类,加固也围绕这四类展开:

  • 进程与内核界面:每个虚机对应宿主机上一个 qemu-kvm 进程。若不施加强制访问控制,该进程以 libvirt 配置的用户身份运行,可读取宿主机上该用户可读的全部文件。sVirt 机制通过为每个虚机进程打上独立的 svirt_t 标签(含 s0:cXX,cYY 类别对),使不同虚机进程之间以及与宿主机进程之间互相不可访问。
  • 管理界面:libvirt 提供 unix socket 与 TCP/TLS 监听两种通道。TCP 通道一旦开启且鉴权配置不当,等于把虚机管理权暴露在网络上,是历史事故的高发点。
  • 设备界面:QEMU 模拟的设备越多,可被利用的模拟层代码路径越多。USB 重定向、9p 共享目录、SPICE 剪贴板、未限制的显卡设备都会显著扩大攻击面。
  • 存储与网络界面:镜像文件权限过宽意味着任意本地用户可读写虚机磁盘;默认 NAT 网络则让虚机之间可以互相扫描。

加固思路是逐层收敛:强制访问控制开启并验证生效,管理通道只保留本地 socket,设备面按业务需要最小化,存储与网络按最小连通原则配置。

操作步骤

步骤一:启用并验证 SELinux sVirt 隔离

# 确认 SELinux 状态与策略
getenforce
sestatus | head -5

# 如为 Permissive,临时切换并确认无异常后写入永久配置
setenforce 1
sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config

# 修正镜像目录的强制访问控制上下文
restorecon -R -v /var/lib/libvirt/images
ls -lZ /var/lib/libvirt/images/

若虚机磁盘放在 NFS 存储上,需要额外放行相关布尔值,否则 sVirt 会阻止虚机进程访问镜像:

setsebool -P virt_use_nfs on
setsebool -P virt_use_samba on   # 仅在使用 CIFS 存储时才需要
getsebool -a | grep virt_

步骤二:收敛 libvirt 管理接口

# /etc/libvirt/libvirtd.conf 关键项
listen_tls = 0
listen_tcp = 0
auth_tcp = "sasl"
auth_unix_ro = "polkit"
auth_unix_rw = "polkit"
unix_sock_group = "libvirt"
unix_sock_ro_perms = "0770"
unix_sock_rw_perms = "0770"
unix_sock_admin_perms = "0700"
audit_level = 2
log_level = 2
# 仅接本地 socket,不监听任何 TCP 端口
ss -lntp | grep -E '16509|16514'   # 加固后应无输出
systemctl restart libvirtd

listen_tcp = 0 与 listen_tls = 0 是底线配置。远程管理需求应通过 SSH 隧道转发本地 unix socket 实现,或启用 TLS 并要求客户端证书,禁止直接开放明文 TCP。

步骤三:限制 QEMU 进程能力与权限

# /etc/libvirt/qemu.conf 关键项
security_driver = "selinux"
user = "qemu"
group = "qemu"
dynamic_ownership = 1
remember_owner = 0
clear_emulator_capabilities = 1
seccomp_sandbox = 1
max_processes = 20
max_files = 64
vnc_listen = "127.0.0.1"
spice_listen = "127.0.0.1"
cgroup_controllers = [ "cpu", "devices", "memory", "blkio" ]

clear_emulator_capabilities = 1 会剥离 QEMU 进程的多余 Linux capabilities,seccomp_sandbox = 1 开启系统调用过滤。vnc_listen 与 spice_listen 限制为回环地址,控制台一律通过 SSH 隧道访问,不允许直接暴露 5900 等端口。

步骤四:裁剪虚机设备面

virsh edit web01

在 XML 中按以下原则删减(保留业务真正需要的设备):

  • 删除 <sound>、<usb> 控制器与全部 USB 重定向 <hostdev>。
  • 删除 9p 共享目录,即 <filesystem type='mount'> 整段;确实需要共享文件时改用网络文件服务并单独加固。
  • 删除 SPICE 的剪贴板与文件传输通道 <channel> 段落。
  • 控制台图形设备改为仅回环监听并设置口令,例如 <graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1' passwd='随机强口令'/>。
  • 为虚机显式添加安全标签:<seclabel type='dynamic' model='selinux' relabel='yes'/>。
  • 关闭内存气球回收,或仅在非敏感虚机保留:<memballoon model='none'/>。

修改后需重启虚机生效,并确认启动正常:

virsh destroy web01 && virsh start web01
virsh dumpxml web01 | grep -E "graphics|seclabel|hostdev|filesystem|memballoon"

步骤五:存储隔离与镜像加密

# 镜像所有权收敛到 qemu 用户,权限 600
chown qemu:qemu /var/lib/libvirt/images/web01.qcow2
chmod 600 /var/lib/libvirt/images/web01.qcow2
chmod 700 /var/lib/libvirt/images
qemu-img info /var/lib/libvirt/images/web01.qcow2

对承载敏感数据的虚机,建议使用 LUKS 加密的 qcow2 卷,防止镜像文件被复制后离线读取:

# 生成密钥(保存到 root 可读的独立文件,勿随镜像一同备份到同一介质)
head -c 32 /dev/urandom > /etc/libvirt/secrets/web01.key

qemu-img create \
  --object secret,id=sec0,file=/etc/libvirt/secrets/web01.key \
  -f qcow2 \
  -o encrypt.format=luks,encrypt.key-secret=sec0 \
  /var/lib/libvirt/images/web01-enc.qcow2 40G

步骤六:网络隔离

默认 default 网络使用 NAT,虚机之间可直接互访。建议为不同业务等级建立独立网络,并限制虚机之间的横向流量:

# 创建隔离网络 isolated.xml(无 forward 段即无 NAT 转发)
cat > /tmp/isolated.xml <<'EOF'
<network>
  <name>isolated</name>
  <bridge name='virbr1' stp='on' delay='0'/>
  <ip address='192.168.100.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='192.168.100.10' end='192.168.100.200'/>
    </dhcp>
  </ip>
</network>
EOF
virsh net-define /tmp/isolated.xml
virsh net-start isolated
virsh net-autostart isolated
# 用 nftables 限制虚机间横向访问,仅放行管理端口
nft add table inet vmisolate
nft add chain inet vmisolate forward '{ type filter hook forward priority 0; policy accept; }'
nft add rule inet vmisolate forward iifname "virbr1" oifname "virbr1" \
  tcp dport != 22 drop

步骤七:关闭 KSM 内存页合并

KSM 会让不同虚机共享内容相同的内存页,可能造成跨虚机的侧信道信息泄露。承载多租户业务的宿主机建议关闭:

systemctl disable --now ksmtuned
echo 0 > /sys/kernel/mm/ksm/run
# 持久化:写入本地自启脚本或 systemd unit
echo 'echo 0 > /sys/kernel/mm/ksm/run' >> /etc/rc.local

配置验证

检查项 验证命令 期望结果
强制访问控制生效 getenforce Enforcing
sVirt 标签已隔离 ps -eZ \| grep qemu-kvm 进程标签为 svirt_t,且不同虚机类别对 s0:cXX,cYY 不同
管理端口未暴露 ss -lntp \| grep -E '16509\|16514' 无输出
设备面已裁剪 virsh dumpxml web01 \| grep -cE 'hostdev\|filesystem' 输出 0
控制台仅回环 virsh dumpxml web01 \| grep graphics listen=’127.0.0.1′ 且带 passwd
镜像权限收敛 ls -lZ /var/lib/libvirt/images/web01.qcow2 权限 600,属主 qemu:qemu
KSM 已关闭 cat /sys/kernel/mm/ksm/run 0

建议把上述检查写成一个巡检脚本定期执行,任何一项偏离即触发告警,避免加固成果在后续变更中被静默回退。

常见问题

FAQ 1:开启 SELinux Enforcing 后虚机启动失败,日志报 Permission denied,如何排查?

绝大多数情况是镜像文件的强制访问控制标签不正确,而不是策略过严。按以下顺序排查:

# 1. 查看审计日志中的拒绝事件
ausearch -m AVC -ts recent

# 2. 修正镜像标签
restorecon -R -v /var/lib/libvirt/images

# 3. 若镜像位于 NFS/CIFS,需要放行布尔值
setsebool -P virt_use_nfs on

# 4. 重新启动虚机
virsh start web01

若 AVC 记录显示 denied { read } 且对象是镜像文件,使用 restorecon 即可解决。若仍失败,可临时 setenforce 0 验证是否为策略问题,但不要把 Permissive 状态长期留在宿主机上。

FAQ 2:关闭 KSM 后宿主机内存占用升高、超分能力下降,如何取舍?

KSM 能显著降低同类虚机的内存开销,代价是潜在的侧信道风险。可行的折中方案:

  • 宿主机上运行多个租户时保持 KSM 关闭,用内存超分比例控制容量,不要用 KSM 换取密度。
  • 宿主机上只运行同一租户的内部业务时,可以单独开启 KSM,并把这一项在资产台账中标注为已接受风险。
  • 改用大页内存(hugepages)配比分配,既提升性能又避免页合并,代价是内存不可超分。

FAQ 3:virsh 连接到 libvirtd 报鉴权失败,或提示权限不足怎么办?

# 确认用户已加入 libvirt 组
id
usermod -aG libvirt 运维账号
# 组权限变更需重新登录会话后生效
newgrp libvirt
virsh list --all

# polkit 策略被改动导致读写鉴权异常时,确认配置未被覆盖
grep -E 'auth_unix_rw' /etc/libvirt/libvirtd.conf
systemctl restart libvirtd

加固后的正常表现是:普通用户只能通过 virsh 只读方式查看信息,管理操作需要 libvirt 组权限或 root。若出现”任何本地用户都能管理虚机”的情况,说明 unix_sock_rw_perms 配置过宽,应立即收紧到 0770。

总结

KVM 虚拟化安全加固的核心是把隔离能力真正打开并逐层收敛暴露面,而不是堆砌安全软件。落地顺序建议为:先确认 SELinux/AppArmor 处于强制模式且 sVirt 标签生效,再关闭 libvirt 的 TCP 监听、限制控制台到回环地址,接着裁剪 USB、9p、剪贴板等非必要设备,最后收敛镜像权限、按业务等级拆分网络并关闭多租户环境下的 KSM。加固完成后把上表的验证命令固化为巡检脚本,每次变更后重跑一遍,才能保证隔离强度不随时间衰减。