注意,这是一篇由AI生成的问题分析报告。
背景
在一台Fedora 44的KVM宿主机(192.168.1.64, 使用cockpit管理)上创建了一个Fedora 44 Cloud版本的虚拟机master-0001。创建完成后发现:
- 不知道虚拟机的root密码,无法登录。
- 多次在Cockpit创建界面的"自动化"区域里指定root密码和SSH公钥,全部不生效。
虚拟机的IP为192.168.122.208(libvirt默认NAT网段192.168.122.0/24)。
排查过程
第一步:通过 guest agent 应急登录
virsh dumpxml master-0001 里发现virtio-serial通道的QEMU guest agent是state='connected',说明VM内agent正常运行,可以通过它直接在虚拟机内执行命令并重置密码。
验证agent在线:
[root@x-cluster ~]# virsh qemu-agent-command master-0001 '{"execute":"guest-ping"}'
{"return":{}}
用 guest-exec 执行任意命令(注意:agent运行在受限SELinux域中,读/etc/shadow、写/root/.ssh会被拒绝):
[root@x-cluster ~]# virsh qemu-agent-command master-0001 '{"execute":"guest-exec",
"arguments":{"path":"/bin/sh","arg":["-c","cat /etc/os-release"],"capture-output":true}}'
{"return":{"pid":1083}}
# 稍等后用 guest-exec-status 取回输出
NAME="Fedora Linux"
VERSION="44 (Cloud Edition)"
RELEASE_TYPE=stable
重置root密码(密码参数需要base64编码):
[root@x-cluster ~]# PW=$(echo -n 'vmpass1234' | base64)
[root@x-cluster ~]# virsh qemu-agent-command master-0001 '{"execute":"guest-set-user-password",
"arguments":{"username":"root","password":"'"$PW"'","crypted":false}}'
{"return":{}}
第二步:控制台自动登录注入SSH公钥
只设密码还不够:Fedora默认 PermitRootLogin prohibit-password,root不能通过SSH密码登录,密钥也不能直接写入(SELinux限制agent写/root/.ssh)。因此通过控制台登录获得完整root shell,再注入公钥:
- 用QEMU monitor截屏控制台画面:
[root@x-cluster ~]# virsh qemu-monitor-command master-0001 '{"execute":"human-monitor-command",
"arguments":{"command-line":"screendump /tmp/screen.ppm"}}'
- 把PPM拿到本机转PNG后用tesseract OCR,确认是tty1文本登录界面:
Fedora Linux 44 (Cloud Edition)
Kernel 6.19.10-300.fc44.x86_64 on x86_64 (tty1)
ens3: 192.168.122.208 ...
fedora login: _
- 用
virsh send-key逐个按键输入(要放慢速度、一次一个key,否则会丢键),登录后得到[root@fedora ~]#:
[root@x-cluster ~]# virsh send-key master-0001 KEY_R; sleep 0.5
[root@x-cluster ~]# virsh send-key master-0001 KEY_O; sleep 0.5
...
[root@x-cluster ~]# virsh send-key master-0001 KEY_ENTER
- 宿主机生成密钥对,用guest agent把公钥写到VM的
/tmp/pubkey,再在控制台shell里安装:
[root@x-cluster ~]# ssh-keygen -t ed25519 -f /root/.ssh/vm_key -N '' -C 'vm-master-0001'
# 在控制台shell中执行(通过send-key单字符键入)
[root@fedora ~]# cat /tmp/pubkey > /root/.ssh/authorized_keys
[root@fedora ~]# echo 'PermitRootLogin yes' > /etc/ssh/sshd_config.d/99-root.conf
[root@fedora ~]# systemctl restart sshd
至此三种登录方式全部可用:
| 方式 | 命令 |
|---|---|
| SSH密钥 | ssh -i /root/.ssh/vm_key root@192.168.122.208 |
| SSH密码 | sshpass -p 'vmpass1234' ssh root@192.168.122.208 |
| 控制台 | 用 vmpass1234 登录 |
第三步:定位Cockpit自动化失效的根因
静态线索
virsh dumpxml master-0001里没有任何cloud-init种子盘,只有一个vda系统盘;metadata标记为install_source_type=cloud。- VM内
cloud-init status显示disabled,/var/lib/cloud/data不存在,无任何运行日志;连镜像默认的fedora用户(uid 1000)都没有创建 → 种子从未被注入。 - Cockpit日志出现了一批报错:
内部错误:子进程报告(status=125):无法在 trusted.libvirt.security.ref_dac 上
获取 XATTR /var/lib/libvirt/boot/virtinst-hso34tm0-cloudinit.iso: 没有那个文件或目录
Unable to remove disk metadata on vm master-0001
from /var/lib/libvirt/boot/virtinst-hso34tm0-cloudinit.iso (disk target sda)
挖出Cockpit的调用方式
cockpit-machines的后端嵌入在/usr/share/cockpit/machines/index.js.gz里(内嵌一段python),cloud源+启动时实际执行的是:
virt-install --cloud-init user-data=/tmp/cockpit-machines-XXXX-user-data ...
种子ISO由virt-install生成在/var/lib/libvirt/boot/virtinst-<随机>-cloudinit.iso。
用inotify实测复现真相
[root@x-cluster ~]# inotifywait -m -e create,delete /var/lib/libvirt/boot
观测结果(同一个repro VM)揭示了完整生命周期:
23:40:23 virtinst-dv_rozuk-cloudinit.iso CREATE # ISO被创建
23:40:23 virtinst-dv_rozuk-cloudinit.iso OPEN # libvirt处理(定义+启动域)
23:40:24 virtinst-dv_rozuk-cloudinit.iso DELETE # 1秒后被virt-install删除!
23:40:24 virtinst-*-meta-data DELETE
23:40:24 virtinst-*-user-data DELETE
而域XML仍然引用这个已被删除的ISO路径:
<disk type='file' device='cdrom'>
<source file='/var/lib/libvirt/boot/virtinst-dv_rozuk-cloudinit.iso'/>
<target dev='sda' bus='sata'/>
</disk>
根因链条(用 audit 日志坐实):
以用户第二次创建(uuid 17a762e8)为例,journalctl 的 audit 记录是决定性的:
23:07:31 AUDIT2500 op=start reason=booted ... master-0001 <-- 第一次启动,带 cloud-init 种子 ISO
23:07:40 Machine qemu-21-master-0001 terminated <-- 仅 9 秒后,guest 自我重启
23:07:41 Unable to remove disk metadata ... virtinst-qwf1a4bk-cloudinit.iso (disk target sda)
23:07:41 AUDIT2500 op=stop reason=shutdown ...
23:07:41 AUDIT2500 op=start reason=booted ... master-0001 <-- 第二次启动,已不含种子盘
链条如下:
- virt-install的
Installer.start_install()先生成一个临时种子ISO放在/var/lib/libvirt/boot/,用它启动一个临时域;随后立刻defineXML()一个不含种子盘的持久postboot配置,并在_create_guest()之后执行_cleanup()把种子ISO、meta-data、user-data一并unlink()(installer.py:413-425)。 - Fedora Cloud 的 UEFI+SecureBoot(+TPM) 镜像首次启动时,shim 看到空的 EFI NVRAM,会调用 fallback.efi 填充启动项,并约 9 秒时触发一次自我重启(virt-manager 源码里专门有注释描述这个组合会"打乱安装流程")。
- 这次重启发生时 cloud-init 还没来得及运行(
ds-identify要到第 11 秒才跑)。重启后 guest 用的是持久配置——种子盘已经不在。 - 于是 cloud-init 在唯一一次带种子的启动里被跳过,之后每次启动都找不到数据源并被禁用;密码/公钥自然全部无效。
ref_dacXATTR 报错只是 libvirt 在重启/关机释放这个已被删除的临时ISO时发出的清理失败,与 SELinux/DAC 开关无关。
元凶是「UEFI+TPM 首次启动的 9 秒自重启」叠加「virt-install 两阶段(initial 带种子 / final 不带种子)」,而不是简单的"种子被删"。
为什么持久种子能救
把种子ISO放到 /var/lib/libvirt/boot 之外并写进持久XML后,它会跨重启一直存在:即使 shim 第 9 秒自重启,第二次启动时 sr0 仍在,第 11 秒的 ds-identify 就能找到 cidata,cloud-init 正常执行。这也是本次修复旧 master-0001 时验证过的方式(cloud-init status --long = done,DataSourceNoCloud [seed=/dev/sr0])。
guest 内的铁证:cloud-init 从未运行
通过可用的 guest agent 进入虚拟机取证(guest-exec 执行命令):
cat /var/log/cloud-init.log -> No such file(任何一次启动都没有产生日志)
/run/cloud-init/disabled -> 存在
cat /run/cloud-init/ds-identify.log -> ISO9660_DEVS= DSNAME= ... Disabled cloud-init [1]
lsblk -> 只有 vda,没有 sr0/cdrom
getent passwd (uid>=1000) -> 空(连镜像默认的 fedora 用户都没有)
ds-identify 在开机 11.45s 时找不到任何 ISO9660 数据源,于是把 cloud-init 整体禁用。结合主机侧持久XML里已经没有种子盘,可以确认 root 密码/公钥一次都没有到达 guest——不是 Cockpit 写错,而是种子从未被 cloud-init 消费。
隐藏的坑
云镜像放在了/root/下(权限700),qemu用户(uid 107)没有读权限,作为backing_store时virt-install直接报权限不够失败。镜像应放置到qemu可读的目录(如/var/lib/libvirt/images/)。
解决方案
应急手段(本次已生效)
- guest agent重置密码 + 控制台注入SSH公钥 + 放开
PermitRootLogin yes,三种登录方式均可用。
根因修复(正确做法:清掉悬空引用 + 持久种子)
纠错:security_driver = "none" 关不掉 DAC
qemu.conf 里的 security_driver 只能控制 MAC 驱动(SELinux/AppArmor),无法关闭 DAC 驱动。libvirt 自己的配置注释就写明了(/etc/libvirt/qemu.conf:523-525):
# Notes: The DAC security driver is always enabled; as a result, the
# value of security_driver cannot contain "dac". ...
而 trusted.libvirt.security.ref_dac 正是 DAC 驱动记录磁盘属主引用计数的 XATTR。所以改成 "none" 并重启后,日志里 DAC 照常工作:先打印 Configured security driver "none" disables default policy to create confined guests,随后 audit 里依然是 model=dac,并继续报同款 XATTR/status=125。报错与 SELinux/DAC 开关无关,它是"文件已删、引用仍在"的症状。
真正相关的开关是 dynamic_ownership / remember_owner(qemu.conf:577-584),它们只控制 DAC 是否改属主/记属主;但正确做法是清掉悬空引用,而不是关闭安全机制。
步骤1:清掉悬空种子引用
[root@x-cluster ~]# virsh dumpxml master-0001 | grep -n cloudinit.iso # 应无输出
# 若有,用 virsh edit 删除对应 <disk> 后 virsh define 重新定义
步骤2:改用持久种子 ISO(推荐)
种子 ISO 放在 /var/lib/libvirt/boot 之外,virt-install/Cockpit 不会删它;域 XML 引用的是真实存在的文件,DAC 驱动能正常读写其 XATTR:
[root@x-cluster ~]# mkdir -p /opt/data/cloud-seed
[root@x-cluster ~]# cat > /opt/data/cloud-seed/seed.yaml <<'EOF'
#cloud-config
disable_root: false
ssh_pwauth: true
users:
- name: root
ssh_authorized_keys:
- ssh-ed25519 AAAA...root_pub_key...
chpasswd:
list: |
root:你的密码
expire: false
EOF
[root@x-cluster ~]# dnf install -y cloud-utils
[root@x-cluster ~]# cloud-localds /opt/data/cloud-seed/master-ci.iso /opt/data/cloud-seed/seed.yaml
[root@x-cluster ~]# virsh attach-disk master-0001 /opt/data/cloud-seed/master-ci.iso \
sda --type cdrom --mode readonly --targetbus sata --config
[root@x-cluster ~]# virsh destroy master-0001 && virsh start master-0001
踩坑:cdrom 的目标总线必须是
sata。 直接virsh attach-disk ... sda不加--targetbus时 libvirt 会挂到scsi(lsilogic 控制器),而 Fedora Cloud 内核默认不加载mptspi,guest 里根本不出现sr0,cloud-init 依旧找不到数据源。用--targetbus sata后lsblk才会出现sr0。
步骤3:guest 内验证
lsblk -> 出现 sr0(如 370K rom)
cloud-init status --long -> status: done, DataSourceNoCloud [seed=/dev/sr0]
一劳永逸:cloudvm-seed 辅助脚本
既然 Cockpit 的自动化在当前 UEFI+SecureBoot 组合下不可靠,可在宿主机放一个脚本,给任意(包括 Cockpit 刚建的)cloud 虚拟机补持久种子:
[root@x-cluster ~]# cloudvm-seed <vmname> '你的root密码' [ssh公钥文件]
# 生成 /opt/data/cloud-seed/<vmname>-seed.iso(卷标 cidata),以 sata 挂到 sda 并重启该域
等 60~90 秒后即可 ssh -i /root/.ssh/vm_key root@<VM的IP>。
另一种让 Cockpit 自动化直接生效的思路
9 秒自重启由 SecureBoot/TPM 的首次启动流程触发。若新建 VM 时改用非 SecureBoot 的 UEFI(或关闭 TPM),首次启动即不再自重启,virt-install 的临时种子就能在这次启动中被 cloud-init 消费,Cockpit 的“自动化”可望正常生效(可在创建向导的固件选项里调整,或把 <loader secure='yes'> 改为非 secboot 的 OVMF 一试)。
可选:只想止血时的正确 DAC 开关
若暂时不想重建种子,只想消除 XATTR 报错,可关掉 DAC 的属主记忆(不是 security_driver):
[root@x-cluster ~]# cp -a /etc/libvirt/qemu.conf /etc/libvirt/qemu.conf.bak.$(date +%Y%m%d%H%M%S)
[root@x-cluster ~]# sed -i 's|^#remember_owner = 1|remember_owner = 0|' /etc/libvirt/qemu.conf
[root@x-cluster ~]# systemctl restart virtqemud
权衡:会放弃 libvirt 对磁盘属主的记录/还原。单机可用;多租户/高安全场景请用持久种子并把安全驱动恢复默认。
关键命令速查
| 用途 | 命令 |
|---|---|
| Guest agent在线检查 | virsh qemu-agent-command <域> '{"execute":"guest-ping"}' |
| 重置密码(密码需base64) | virsh qemu-agent-command <域> '{"execute":"guest-set-user-password",...}' |
| 转储域XML | virsh dumpxml [--inactive] <域> |
| 控制台截屏 | virsh qemu-monitor-command <域> '{"execute":"human-monitor-command","arguments":{"command-line":"screendump /tmp/x.ppm"}}' |
| 控制台按键 | virsh send-key <域> KEY_R(逐个发送+延时,避免丢键) |
| 监控种子ISO生命周期 | inotifywait -m -e create,delete /var/lib/libvirt/boot |
总结
- 原因不在cloud镜像,也不是简单的"密码设置失败",而是UEFI+SecureBoot(+TPM)镜像首次启动约9秒时的自我重启,恰好卡在
ds-identify(约11秒)之前,把这次唯一带种子的启动打断;而virt-install的种子ISO是临时的(_cleanup在域创建后立即unlink),持久XML又是不含种子的postboot配置——种子再也回不来,cloud-init于是永远找不到数据源。ref_dacXATTR报错只是libvirt始终启用的DAC驱动事后清理这个悬空文件时发出的症状,status=125掩盖了真实情况。 - 纠错:
security_driver="none"只能关 MAC(SELinux/AppArmor),关不掉 DAC;"单机关 DAC"是个无效方案,正确的止血开关是dynamic_ownership/remember_owner。 - 排查思路的关键是:先用guest agent拿到VM内的执行权(
guest-ping/guest-exec/guest-set-user-password)并用它读guest内证据,再用screendump+OCR+send-key打通控制台,最后用源码分析和inotify事件复现按下葫芦浮起瓢的完整生命周期。 - 修复上,先清掉悬空引用,再用持久种子ISO(放在
/var/lib/libvirt/boot之外)让cloud-init真正生效;把云镜像放到qemu可读的目录(如/var/lib/libvirt/images/)。