QEMU/kvm (Libvirt)仮想マシンでUEFI HTTPブートを試す
目次
- 概要
- 知識の整理
- 作業の流れ
- 実行内容
- 今後試してみたいこと
- 参考サイト
libvirt(virsh)やvirt-installによるVM作成、管理の経験がある方を対象読者として想定。
概要
LinuxホストでLibvirtからQEMU/KVM仮想マシンに対してUEFI HTTPブートによる
ネットワークブートでOSインストールを行う。
仮想マシンでのネットワークブートはVMWare ESXiでPXEブートしかやったことがなかった。
そもそもネットワークブート=PXEブートというイメージだった。
調べる過程でUEFI HTTPブートの存在を知り、これを利用することにした。
今回の調査ではdnsmasqを使う。
Libvirtではdnsmasqを利用しているが、既存のLibvirt用ネットワーク環境に
影響を与えないようにするため、ブートサーバも別途VMを用意して
そこでDHCPやHTTPサーバを立て、VMホストで作成したブリッジを通じて
クライアントとサーバが通信できるようにする。
知識の整理
- PXEブート
- UEFI HTTPブート
- DHCPからブートファイル名の伝達
- UEFIでの設定内容
PXEブート
Preboot eXecution Environmentの略で、インテルが策定した。
OSをローカルストレージに持たずにブートするネットワークブートの一つ。
クライアントに対してDHCP、TFTP、HTTPという順にブートに必要な情報やファイルを
渡していく。やったことはないがHTTPの代わりにFTPを使うこともできるらしい。
OSが起動する前の状態なので、インストーラのvmlinuz等カーネルが立ち上がるまでは
簡単なプロトコルしか使えない。
DHCPが簡単なプロトコルなのかというツッコミがありそうだが、もともとDHCPは
BOOTPというネットワークブートのためのプロトコルの後継として作られたという経緯がある。
ネットワークブートは他にUEFI HTTPブートやiSCSIブート、iPXEブートなどが存在するらしい。
UEFI HTTPブート
UEFI2.5から実装され、PXEブートに取って代わることを目的とされた。PXEブートとよく似ている。
DHCP、HTTPとブートに必要な情報、ファイルを渡していきTFTPサーバを必要としない。
またPXEブートでは基本的にLAN内でしか使用できないのに対し、UEFI HTTPブートでは
オプションでDNSを利用できるのでドメインをまたいでインストーラを利用することができる。
DHCPサーバとしてdnsmasqではTFTPも設定が可能だが、
1つレイヤがなくなるためPXEブートよりも構成が若干シンプルになる。
トラブルシューティングのときにcdする回数がちょっと減る。
またPXEよりもUEFI HTTPブートの方がよりモダンと言える。
DHCPからブートファイル名の伝達
前述の通りもともとDHCPはBOOTPという、ハードディスクを持たないマシンを
ネットワークブートさせるためのプロトコルの後継として作られた。
BOOTPからの変更点としてDHCPパケットのメッセージ部の最後には可変長の「オプション領域」がある。
オプション領域は次の3つの情報が含まれる。
- タグ: 1~255の情報の分類
- 長さ: データサイズ
- 値: 具体的な内容
ネットワークブートクライアントはDHCP DISCOVER/REQUESTでタグに60を指定し
値の中でPXEClientやhttpclientを指定することでpxeクライアントや
httpブートクライアントとしてDHCPサーバに伝達する。
DHCPサーバではクライアントが
リクエストしてきたときにブートローダとして渡すファイル名を指定しておく必要がある。
UEFIでの設定内容
クライアントがレガシーなBIOSではなくUEFIに対応している必要がある点には注意が必要。
2017年以降のビジネス用PCやサーバであればおおよそUEFI2.5がサポートされているとされる。
自分の環境の場合はvirt-installで仮想マシンをインストールさせる際には
オプションでUEFIを使用することを明示する必要があった。
また以下のようにUEFIでの設定が必要な項目もいくつかある。
- Network Stackの有効化: HTTP通信ではURLの解釈や、HTTPSなど場合によっては
証明書の検証など、従来のPXEブートに比べて高度な通信を行う。
Network Stackのような項目でEnableが設定されていないと本来OSが立ち上がった後に
使うプロトコルであるHTTPをUEFIで使うことができない。 - Boot Mode: Legacy(旧来のBIOS)モードやCSM(Compatibility Supported Mode,
UEFIでBIOS互換の機能を使う。Windows7などのレガシーなOSのインストールの際に
使う)ではなくUEFIを使う。 - HTTP Boot (または URL Boot): 「Advanced」や「Boot」、「Network」といった
タブの中に、直接「HTTP Boot」という項目があるか確認する。 - Boot Priority (起動順序): 起動デバイスの選択肢の中に「UEFI HTTP: ...」や、
特定のネットワークカード名にプロトコル名(IPv4 HTTPなど)が含まれていれば対応していることになる。
(参考)iPXEブート
PXEから拡張する形で作られたオープンソースのファームウェア。
iPXE機能を備えたNICを使用するか、
PXEをサポートするNIC上のファームウェアをiPXEに置き換えるか、
PXEで起動してiPXEをチェーンロードする形で利用する。
PXEではデータ転送にTFTPを利用するが、iPXEではHTTP、iSCSI、ATA over Ethernet
(AoE)、ファイバチャネルover Ethernet (FCoE) でデータを転送できる。
作業の流れ
以下のような手順で確認作業を進めていく。
- DHCPサーバとHTTPサーバを動作させるブートサーバVMを作成する。
- HTTPサーバにインストーラカーネル、ブートローダを配置する。
- DHCPサーバを構築する。
- クライアントとなるVMを作成し、HTTPブートが動作することを確認する。
資材
VMホスト
ホスト名: YSPX-14IAH7
タイプ: ノートPC (Yoga Slim Pro X, Lenovo)
CPUモデル: 12th Gen Intel(R) Core(TM) i7-12700H (Pコア6, Eコア8, 20スレッド)
メモリ: 32GB
ローカルディスク: 2TB (購入時の1TBから独自に入れ替え済み)
OS: Linux (Ubuntu 24.04)
カーネル: 6.14.0-37-generic
libvirt バージョン: 10.0.0
virt-install バージョン: 4.1.0
NW: ブートサーバが各種パッケージをインストールするためにインターネットに出られる
NAT ネットワークと、ブートサーバとクライアントが通信するためのブリッジインタフェースを作成する。
ブートサーバ
コア数: 2
メモリ: 3072MB
ストレージ: 50GB
OS: Linux (Ubuntu24.04)
NW: 前述のNATネットワークとブリッジインタフェースに接続する。
ストレージはもっと小さくてもいいとは思うがインストーラをダウンロードした後に
vmlinuz等をコピーするので程々に。
apache2: 2.4.58
dnsmasq: 2.90
今回は1つのマシンにDHCPサーバとHTTPサーバを同居させているがもちろん分離しても良い。
ブートクライアント
コア数: 2
メモリ: 3072MB または 8192MB
ストレージ: 15GB
OS: Linux (Ubuntu24.04)
NW: ブリッジインタフェースと接続。
クライアントのメモリサイズについて、Ubuntuのネットワークブートでは
インストーラのisoファイルをクライントのinitramfsマウント後にダウンロードする必要がある。
これはインストールに必要なソフトウェアを使うのにisoファイルを
ルートファイルシステムとしてマウントするため。
isoファイルのダウンロードの際は、ディスク上ではなくメモリにダウンロード時の
データを保持する。インストール先のマシンのメモリが少ないと
no space left on deviceと表示され、インストールが中断されてしまう。
例えばUbuntu24.04 liveserverのisoファイルのサイズは約2.6GBだが、
3072MBのメモリのVMにインストールしようとしたところ上記のエラーが表示された。
870MB(isoファイル全体の約30%)をダウンロードした時点でこうなった。
自分はメモリを8192MBに設定することでダウンロードを完了させることができた。
isoファイルを渡すにはNFSから渡す方法もある。
NFSから渡す場合はダウンロードではなくディスクをNFSマウントするので
クライアントのメモリが小さくても動作する。
3072MB(や試していないがそれ以下のメモリ容量)のVMでもインストールが可能になる。
NFSでエクスポートしているディレクトリにそのままisoファイルをマウントさせるだけで
動作する。
実行内容
ホスト側設定
NATネットワークについては作成済みのlibvirt管理下のmgmtネットワークを利用。
virsh net-dumpxml mgmt実行結果
<network>
<name>mgmt</name>
<uuid>2e324dfb-d804-4684-b3f6-7e04832dd159</uuid>
<title>management network for VMs</title>
<description>management network for VMs</description>
<forward mode='nat'>
<nat>
<port start='1024' end='65535'/>
</nat>
</forward>
<bridge name='virbr-mgmt' stp='on' delay='0'/>
<mac address='52:54:00:c4:7d:62'/>
<ip address='192.168.120.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.120.2' end='192.168.120.254'/>
</dhcp>
</ip>
</network>
ブリッジ名はbr-isolated2とする。
(2としているのはbr-isolatedはすでに作成済みだったため)
VMホストはUbuntu24.04デスクトップなのでnetplanで行う。
/etc/netplan/99-br-isolated2.yamlの内容
network:
version: 2
bridges:
br-isolated2:
dhcp4: true
addresses:
- 192.168.123.2/24
ブートサーバでDHCPを動かしそこからブリッジにIP割り当てを行う想定だったが、うまく行かず。
ブートサーバ側でIP設定をした後にブリッジに向けてpingを実行したタイミングで
ブリッジインタフェースがUpになりDHCPによるIP割り当ても可能になった。
ブートサーバ作成
下記virt-installコマンドでVM作成
virt-install --name nwboot-server01 \
--vcpus 2 \
--memory 3072 \
--os-variant ubuntu24.04 \
--disk size=50 \
--location /var/lib/libvirt/iso/ubuntu-24.04-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
--console pty \
--serial pty \
--extra-args "console=tty0 console=ttyS0,115200n8" \
--network network=mgmt \
--network bridge=br-isolated2
webサーバ
debian系なのでapache2パッケージをインストール
試していないがpythonでpython3 -m http.serverのようにしてwebサーバにしてもいいはず。
インストーラのisoをwgetなりcurlなりでダウンロード、適当なところにマウントして
まず次のものをDocumentRootに配置。
- EFI/boot/grubx64.efi or bootx64.efi
(ブートローダ。セキュアブートに対応させるか否かで使い分ける) - casper/vmlinuz
- casper/initrd
インストーラのディレクトリ構造を真似て/var/www/html/casper/vmlinuzと配置してもいいし、
単に/var/www/html/vmlinuzのように資材をDocumentRoot直下に置いてもいい。
インストーラのisoファイルもDocumentRootに配置しておく。
公式やクローンサイトからインターネット経由でisoファイルを
ダウンロードさせるようにしてもよいが、
インストール環境によっては閉じたNW内で実施しなければならない場合もある。
今回はNATネットワークではなく閉じたブリッジなのでVMはインターネットに出られない。
vmlinuz等他の資材と同様にインストーラisoファイルもHTTPサーバに配置しておく。
これらのあとにブートローダ設定ファイルとしてgrub.cfg、autoinstallで使う
user-data, meta-dataもwebサーバ内に配置する。
grub.cfgの内容は以下の通り。
set default=0
set timeout=5
menuentry "Install Ubuntu (HTTP Boot)" {
set gfxpayload=keep
linux (http,192.168.123.1)/vmlinuz ip=dhcp url=http://192.168.123.1/ubuntu-24.04-live-server-amd64.iso autoinstall cloud-config-url=/dev/null --- "ds=nocloud-net;s=http://192.168.123.1/"
initrd (http,192.168.123.1)/initrd
}
"ds=...;s=...の部分について、セミコロンが値の途中で入っており、
これがgrub.cfgでの行末を表す終端文字として認識されないよう二重引用符で囲っている。
autoinstallで使用するファイルについてs=http://192.168.123.1/としていて、
最後に/までしかいれていないのは指定したURLの階層からuser-dataやmeta-dataという
名前のファイルを自動的に探しに行くため。
この最後のスラッシュが入っていないと自動インストールが動作しないことがあるので注意。
user-dataは下記の通り。
#cloud-config
autoinstall:
version: 1
storage:
layout:
name: direct
identity:
hostname: nwboot-client01
password: $6$rounds=500000$7jS0xwmA.Sb4Ctm6$cABh.OrmF8PwtjuOgfuvf8d6Sk2q0PuBzpnyk26SIPxxRVBncwDcoigLRjUlvVZekcfaGe/hBv/RSLrtqcS2r1
username: root
ssh:
install-server: true
authorized-keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOmgyD1akSav2kMlJ87Ak9K6bEYp1BQz68JbZxPsUsoy yasu@YSPX-14IAH7
timezone: Asia/Tokyo
user-data:
runcmd:
- echo 'Hello, World!' > /var/tmp/hello-world.txt
- systemctl enable ssh
users:
- name: ubuntu
shell: /bin/bash
passwd: $6$rounds=500000$7jS0xwmA.Sb4Ctm6$cABh.OrmF8PwtjuOgfuvf8d6Sk2q0PuBzpnyk26SIPxxRVBncwDcoigLRjUlvVZekcfaGe/hBv/RSLrtqcS2r1
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOmgyD1akSav2kMlJ87Ak9K6bEYp1BQz68JbZxPsUsoy yasu@YSPX-14IAH7
lock_passwd: false
sudo: ALL=(ALL) ALL
ユーザ名ubuntuでパスワードubuntuでログインできるようにしている。
またssh公開鍵の登録も行っている。
autoinstallでのOSインストールを行ったことを確認できるよう、/var/tmp/hello-world.txt
というテキストファイルも配置している。
これらを配置したらブラウザなりcurlなりで資材にアクセスできることを確認する。
ファイアウォールやApparmor/SELinuxなどの仕組みが有効のままだとうまく行かない
場合もあるのでルール変更を行っておくか機能を無効化しておく。
/var/www/htmlの中身については下記の通りになる。
/var/www/html/
├── bootx64.efi
├── grubx64.efi
├── grub.cfg
├── user-data
├── meta-data
├── initrd
├── vmlinuz
└── ubuntu-24.04-live-server-amd64.iso
DHCPサーバ
今回はdnsmasqでDHCPを構成する。
aptでdnsmasqパッケージをインストール後、/etc/dnsmasq.d/confというファイルを
作成し、下記のように記載。
interface=enp1s0
bind-dynamic
listen-address=127.0.0.1,192.168.123.1
dhcp-authoritative
dhcp-range=192.168.123.2,192.168.123.254,255.255.255.0,1h
dhcp-option=option:dns-server,192.168.123.1
dhcp-vendorclass=set:httpboot,HTTPClient
dhcp-option=tag:httpboot,option:vendor-class,"HTTPClient"
dhcp-option-force=tag:httpboot,67,"http://192.168.123.1/grubx64.efi"
bind-dynamic
dnsmasqでは3つのnetworking modeがある。
- wildcard
- bind-interface
- bind-dynamic
基本的に設定ファイルではbind-interfaceかbind-dynamicを使い、
デフォルト設定のwildcardはこのいずれもない場合に適用される。
wildcardはマシンのすべてのIP(0.0.0.0、すなわちワイルドカード)の53番ポートに来た
通信をdnsmasqのプロセスに割り当てる(bindする)。
dnsmasqのプロセスを1つしか起動しない場合にはこれを使う。
インタフェースのアップ・ダウンしてもすべてインタフェースの53番ポート宛の通信を処理するので
プロセスを再起動する必要がない。
bind-interfaceでは特定のインタフェースの53番ポートに来たときだけ
その通信をdnsmasqのプロセスに割り当てる。
特定のポートからのDHCPリクエストにのみ応答する場合や、複数のdnsmasqの
プロセスを起動する場合にはこちらを使う。
プロセス起動時に指定したインタフェースがダウンしていると、アップしても
dnsmasqを再起動しないと認識されない場合があるらしい。
bind-dynamicは両者のいいとこ取りの存在となる。
特定のインタフェースに対して割り当てを行うが、インタフェースのアップ・ダウンを
検知して動的に割り当てる。
特にVMやコンテナをホストするようなマシンではブリッジインタフェースに
接続しているVM、コンテナがないとブリッジがダウンするのでこの設定が有効になる。
libvirtなどのデフォルト設定でも使われている。
(/var/lib/libvirt/dnsmasq/*.confなどのファイルを参照)
dnsmasqにおけるタグ
dnsmasqでは同一NW内でもクラアントにタグを付与し、
そのタグを持つクライアントにだけ設定を適用して処理を分けることが可能になっている。
dhcp-vendorclass=set:httpboot,HTTPClientではDHCP拡張オプションの
Vendorclassの値が文字列HTTPClientとマッチするクライアントにタグhttpbootを
付与する、という設定を行っている。
そしてtag:httpbootのある行ではこのタグが付与されたクライアントに対してだけ
設定を適用することになる。
ブートクライアントへのブートファイルの伝達
オプション67を使ってブートファイルのURLを指定している。
dhcp-option=tag:httpboot,option:vendor-class,"HTTPClient"ではクライアントへの
応答となるDHCP OFFERやDHCP ACKにも拡張オプションでVendorclassにHTTPClientを
設定している。
UEFIファームウェアの中には、自分が送ったVendorclassと同じ値がサーバーからの
応答に含まれているかを確認することで、「このDHCPサーバーは単にIPを貸すだけでなく、
HTTP Bootのシーケンスをサポートしているか?」を判断する。
含まれていない場合は応答したDHCPサーバがネットワークブート用のものではなく
通常のDHCPサーバと判断し、HTTPブートのプロセスを中断する場合がある。
この行はこうした挙動を防ぐ目的で入れている。
特にエンタープライズ向けの製品ではこのようなチェックが厳密に行われる傾向があるらしい。
ブートファイルの伝達ではdhcp-option-forceを使っている。
UEFIの実装のなかにはオプション67でブートファイル名を要求していないのに
それが送られてこないとブートに失敗するという挙動をするものがあるらしい。
HTTPブートではこのforceを使うことでブートファイル名を強制的に送りつけるやり方で、
ブートファイルを指定するのが定石らしい。
bootx64.efiとgrubx64.efiの使い分け
bootx64.efiはセキュアブートを有効にしているマシンで必要になる。
libvirtをパッケージからインストールした場合のVMのデフォルトのUEFIでは
セキュアブートが有効になっているらしく、virt-installコマンドで無効化した状態で
VMを作成できる。
セキュアブートは起動時に署名済みのUEFIファームウェアやブートローダの署名を確認することで
ルートキットのような不正なソフトウェアが実行されるのを防ぐ仕組み。
ルートキットとはコンピュータへの不正侵入を隠蔽し、
攻撃者が管理者権限で自由に操作し続けるためのツール群。
ウイルスのようにファイルを壊すのではなく、自身の存在が見つからないように
隠蔽しバックドアを維持することが特徴。
psコマンドを叩いてもルートキットの存在を表示することはできず、カーネルや
ブートローダ、最終的にはBIOS/UEFIのファームウェアやハイパーバイザも改ざんして
OS再インストールでも除去できなくなる。
2010年にイランの各施設でUSBストレージを介して感染し、
遠心分離機の破壊に使われたstuxnetなどが有名。
またAndroid端末でいわゆるroot化するツールもやっていることも同じ。
ルートキットには悪意と隠蔽というニュアンスが含まれているためroot化ツールは
厳密にはルートキットとは区別される。
セキュアブートが無効な場合はどちらのブートローダでも問題ないが、
有効の場合はbootx64.efiしか使うことができない。
bootx64.efiにはマイクロソフトの署名が入っていることを確認できる。
自分の環境ではsbsigntoolパッケージがデフォルトでインストールされており、
sbverify --list bootx64.efiのように実行することでブートローダへの署名を確認できる。
OSが立ち上がって来たあとでセキュアブートが有効になっているか確認するには
mokutil --sb-stateと実行する。
設定ファイルを更新したらサービスを再起動しておく。
ブートクライアント作成
以下のコマンドでVMを作成。
virt-install \
--name nwboot-client01 \
--vcpus 2 \
--memory 8192 \
--disk size=15 \
--os-variant ubuntu24.04 \
--network bridge=br-isolated2 \
--boot=uefi,loader_secure=no \
--install bootdev=hd,bootdev=network
-
--boot=efiでUEFIによる仮想マシンを作成。loader_secure=noでセキュアブートを無効化。
セキュアブートが有効なVMを作成したい場合はloader_secure=noを指定しなければいい。 -
--install bootdev=…で起動デバイスの順序を指定。
今回は初回起動時にインストール先のディスクにOSは入っていないので、
ディスクをチェックした後にPXEブート、PXE (IPv6)ブート、HTTPブート、
HTTP (IPv6)ブートの順に試そうとする。
PXEから順にDHCP REQUESTを送るのでHTTPブートが始まるまで数分かかる。
VMのUEFIファームウェアはOVMFというオープンソースのもので、自分でカスタムビルドすることで
HTTPブートを優先するように設定することもできるらしい。
コマンドを実行してVMを起動後、Start PXE over IPv4というメッセージが表示され、
順にネットワークブートを実行しようとする。
Start HTTP Boot over IPv4というメニューが表示され、成功するとgrubのメニュー画面が表示され、
ジャーナル画面、ubuntuのインストーラ画面へと移行する。
NFSサーバの利用
isoファイルを渡すのにNFSを使用する場合、grub.cfgではlinuxの行に次のように記載する
linux (http,192.168.123.1)/vmlinuz ip=dhcp boot=casper netboot=nfs nfsroot=192.168.123.1:/export/ubuntu autoinstall cloud-config-url=/dev/null --- "ds=nocloud-net;s=http://192.168.123.1/"
今後試してみたいこと
- DebianやFedoraなど異なるディストリビューションのネットワークブートでのOSインストール
- dnsmasqやhttpサーバのdocker compose化