Ubuntu Weekly Recipe

第746回update-grubの仕組みを使ってUbuntuのGRUBをさらにカスタマイズする

第743回のUbuntuの標準ブートローダーであるGRUBを改めて見直すでは、GRUBのUbuntuらしい使い方を紹介しました。今回はUbuntuに最初からインストールされている温室育ちのGRUBを、もうちょっとだけからかってみます。

前回と同じく「UEFIシステム上にインストールされたgrub-efi」のみを想定した記述となっています。また、基本的なスタンスは前回と同じで、 「トラブルに遭遇したくなければ、GRUBには手を出すな」 です。進んだところでふたつも手に入りません。緊急ボタンがあると思わず平手で押しつぶしてしまう、そんな破滅的な人生を歩むあなたに向けた記事です。

update-grubコマンドがやっていること

改めて第743回の簡単なおさらいです。GRUBにはおおよそ次の4種類の設定方法が存在します。

  • /boot/grub/grub.cfg:GRUBが起動時に参照する設定ファイル。他のファイルはこれを生成するために存在する
  • /boot/grub/grubenvgrub.cfgの挙動を一時的・恒久的に変更できる環境変数ブロックで、grub-editenv等で設定する
  • /etc/default/grubupdate-grub実行時に生成するgrub.cfgの内容をコントロールするための環境変数セット
  • /etc/grub.d/update-grub実行時に生成するgrub.cfgの断片を出力するスクリプト群

第743回では、/etc/default/grubを使った設定方法(Simple Configuration)を前提にいくつかの例を提示しました。Simple Configurationは、たとえばカーネルの起動パラメーターなどの既存の設定を少し変えたい場合に有効な方法です。また、grub-editenvの簡単な使い方も説明しています。

今回は残りのgrub.cfg/etc/grub.dの内容についての話になります。まずはgrub.cfgについてです。Ubuntuの/boot/grub/grub.cfgは冒頭に次のように書かれています。

#
# DO NOT EDIT THIS FILE
#
# It is automatically generated by grub-mkconfig using templates
# from /etc/grub.d and settings from /etc/default/grub
#

Ubuntuを使う限りにおいて、grub.cfgを直接編集することは推奨されません。これはカーネルやGRUBのパッケージが更新されたときに、update-grubコマンドによって上書き再生成されることがあるからです。grub.cfgの中身を変更したい場合は、/etc/grub.d以下にある生成用の断片ファイルを編集・追加してupdate-grubを実行することが正しい手順となります。

ところで上記のメッセージにはgrub-mkconfigを使えと書いてあります。実は、普段使うupdate-grubは、単なるgrub-mkconfigのラッパーになっています。

$ cat $(command -v update-grub)
#!/bin/sh
set -e
exec grub-mkconfig -o /boot/grub/grub.cfg "$@"

要するに出力ファイルを指定しなくても良いようにしただけですね。言い換えると、試しにシステムのgrub.cfg以外に出力したいならgrub-mkconfigを直接実行して、/boot/grub/grub.cfgと差分をとって確認すれば良いということになります。

grub-mkconfigコマンド自体は、いくつかの事前設定はありますが、基本的に/etc/grub.dディレクトリにある個々の実行可能なプログラムを、ファイル名の順番に実行し、その標準出力を指定したgrub.cfgファイルに保存しているだけです。Ubuntuなら、次のようなプログラムがあります。いずれもシェルスクリプトですが、シェルスクリプトである必要はありません。

$ ls /etc/grub.d/
00_header        10_linux_zfs   30_os-prober      40_custom
05_debian_theme  20_linux_xen   30_uefi-firmware  41_custom
10_linux         20_memtest86+  35_fwupd          README

READMEと、あとチルダ付きのバックアップファイル、拡張子にdpkとかrpmが付くパッケージマネージャーが退避したファイルなどは無視されます。実行した結果はgrub.cfgの中に次のような感じで記録されていきます。

### BEGIN (ファイル名) ###
出力結果
### END (ファイル名) ###

軽く内容を確認していきましょう。

  • 00_header/etc/default/grubの内容に基づいて、全体の設定や関数を生成する部分
  • 05_debian_theme:GRUBのテーマ設定。現在のUbuntuはほぼ何も設定していない
  • 10_linux:Linux起動用のメニューエントリー。リカバリーモードもここに入る
  • 10_linux_zfs:ZFS環境向けのLinux起動用のメニューエントリー
  • 20_linux_xen:Xen環境向けのLinux起動用のメニューエントリー
  • 20_memtest86+:memtest86+を起動するメニューエントリー。EFI環境だと何も出力しない
  • 30_os-prober:Windowsなど他のOSを検索し、メニューに追加する。Ubuntu 22.04 LTSからは無効化されている
  • 30_uefi-firmware:次回起動時にUEFIの設定画面を表示し再起動するfwsetupコマンドを実行するだけのメニューエントリー
  • 35_fwupd:UEFI BIOSを更新するメニューエントリー。ただしサポートしている環境のみ
  • 40_custom:カスタマイズ用のサンプルスクリプト。単にスクリプトの3行目以降をそのまま表示するだけ
  • 41_custom:同上。こちらはgrub.cfgと同じ場所にcustom.cfgがあれば、それを読み込んで利用する

ちなみにGRUBは国際化に対応しています。これはgettextを利用して、update-grub実行時にgrub.cfgに出力する文字列を、実行時のロケールに合わせて翻訳するというものです。この場合、GRUBで表示されたメニューは翻訳された言語の文字列が使われるわけですが、正しく表示されるためにはフォントが対応している必要があります。

GRUBの場合は、PFF2フォーマットのunicode.pf2ファイルを利用しています。ただし日本語グリフの品質は若干微妙なところです。よって英語で表示するか、grub-mkfontで任意のフォントを変換し/boot/grub/以下に配置しgrub.cfgの中でloadfont フォントファイル名を実行するかの二択になります。

以降は、既存の設定をベースに、Ubuntuではサポートしていない機能を追加してみましょう。

Windowsを検索対象にする

UbuntuはもともとWindowsとのデュアルブートをサポートしていました。初期のUbuntuだとインストール時にWindowsパーティションが見つかれば、そこからブックマークや連絡先を抽出して、UbuntuのFirefoxやThunderbird等に反映する仕組みが存在しましたし、WubiというWindowsのパーティションに直接Ubuntu用のイメージファイルを作成し、そこからデュアルブートする仕組みもありました。もちろんWindowsマシンにUbuntuをインストールした時は、GRUBのメニューにWindowsが表示されるような調整もされていました。

このあたりの設定は時代とともに徐々に廃止されていきます。セキュリティ的な都合やメンテナンスの難しさ、Windowsの高速ブートとの兼ね合いなど理由はさまざまです。そしてUbuntu 22.04 LTSでは、とうとう「Windowsの起動メニューエントリーをGRUBに表示する」機能が無効化されました(Ubuntu Weekly Topcisの2021年12月24日号⁠。これはWindowsを含む他のOSがインストールされているかどうかの検出部分が、セキュアブート等の抜け穴になりかねないという判断が行われたためです。

Ubuntu 22.04 LTSでも、Windowsインストール済みの環境にインストールする際はメニューエントリーが作られます。22.04以前のバージョンで作った環境からアップグレードした場合も、この機能は残っています。それ以外のケースについては/etc/grub.d/30_os-proberの実装に合わせて動きます。具体的には次の部分です。

if [ "x${GRUB_DISABLE_OS_PROBER}" = "xtrue" ]; then
  grub_warn "$(gettext_printf "os-prober will not be executed to detect other bootable partitions.\nSystems on them will not be added to the GRUB boot configuration.\nCheck GRUB_DISABLE_OS_PROBER documentation entry.")"
  exit 0
elif [ "x${GRUB_DISABLE_OS_PROBER}" = "xauto" ]; then
  # UBUNTU: We do not want to disable os-prober on upgrades if we found items before.
  if test -e /boot/grub/grub.cfg && ! grep -q osprober /boot/grub/grub.cfg; then
    grub_warn "$(gettext_printf "os-prober will not be executed to detect other bootable partitions.\nSystems on them will not be added to the GRUB boot configuration.\nCheck GRUB_DISABLE_OS_PROBER documentation entry.")"
    exit 0
  fi
fi

まとめると、次のような挙動になっています。

  • GRUB_DISABLE_OS_PROBER="true"なら、Windows等のOS検知は行わない(GRUB 2.06で追加された仕組み)
  • GRUB_DISABLE_OS_PROBER="auto"なら、grub.cfgで過去に検知した結果が残っていたら検知する、なければ何もしない
  • GRUB_DISABLE_OS_PROBERが上記以外の値なら、Windows等のOS検知を行う

一般的なGRUBはgrub-mkconfigの中でtrueを初期値に設定していますが、UbuntuのGRUBはauto機能を追加してそれを初期値として扱っています。もし、常にOS検知を行いたい場合は、次のように/etc/default/grubを変更しておくと良いでしょう。

$ echo 'GRUB_DISABLE_OS_PROBER="false"' | sudo tee -a /etc/default/grub
$ sudo update-grub

もし自動検知は不要で、固定的なメニューエントリーがあれば良いということであれば、検知した結果のgrub.cfgの中身を抽出し、/etc/grub.d/xx_windowsなどを作って次のようにしてしまうという手もあります。

#!/bin/sh
cat <<EOF
(grub.cfgの中身をここに記述する)
EOF

ファイル名のxx_の部分は、メニューエントリーの順番をどこにするかで決めてください。Ubuntuのエントリー(リカバリーも含む)の次に表示したければ、25_とかにしておくと良いでしょう。あとは、次のように実行権限を付けて、update-grubを実行するだけです。

$ sudo chmod +x /etc/grub.d/xx_windows
$ sudo update-grub

実際に再起動する前にgrub.cfgの中身を見て、正しい結果になるか確認しておきましょう。

古き良きmemtestを復活させる

/etc/grub.d/20_memtest86+はmemtest86+を起動するメニューエントリーです。memtest86+はメモリテストを行うオープンソースのプログラムで、新しいPCやメモリモジュールを購入した時に初期不良が存在しないかや、PCでよくわからない不具合が発生した時にメモリに問題がないかなどを調べるのに便利なツールです。

しかしながらUEFI環境だと次のように無効化されています。

# We need 16-bit boot, which isn't available on EFI.
if [ -d /sys/firmware/efi ]; then
  echo "Memtest86+ needs a 16-bit boot, that is not available on EFI, exiting" >&2
  exit 0
fi

これはオープンソース版のmemtest86+がLegacy BIOSのみサポートしており、UEFI BIOSでは動かなかったことによるものです。

しかしながら昨年リリースされたMemtest86+ V6では、念願のUEF BIOS対応が行われました。これはmemtest86+ V5からフォークしたPCMemTestが先祖返りしたものです。その際、従来のいくつかの機能が消えてしまっているようですが、単にテストするだけなら特に問題ないでしょう。また、セキュアブート対応については未対応で、将来リリースされる6.10以降で対応される雰囲気です。

memtest86+のバイナリは、ブータブルISOイメージとして配布されています。よってまずはここからmemtest86+の本体となるバイナリだけを取り出してしまいましょう。

$ unzip ../mt86plus_6.01_64.iso.zip
$ mkdir temp
$ sudo mount -o loop,ro mt86plus64.iso temp
$ sudo cp temp/EFI/BOOT/bootx64.efi /boot/memtest.efi
$ sudo umount temp
$ file /boot/memtest.efi
/boot/memtest.efi: Linux kernel x86 boot executable bzImage, version \353fHdrS\014\002, RW-rootFS,

fileコマンドでは「Linux kernel」という風に表示されてしまっていますが、実際はEFIから直接実行可能なファイル形式になっています。ただ、memtest86+だとlinuxコマンドで起動しているようなので、今回はそれにあわせてメニューエントリーを作成しておきます。

$ cat <<'EOD' | sudo tee /etc/grub.d/30_memtest
#!/bin/sh
cat <<EOF
menuentry "Memory test" {
    linux /boot/memtest.efi
}
EOF
EOD
$ sudo chmod +x /etc/grub.d/30_memtest
$ sudo update-grub

最後に次回起動時に「Memory test」が選ばれるようにしておきましょう。

$ sudo grub-reboot "Memory test"

この状態で再起動します。ただしUEFIセキュアブートは一旦オフにしておいてください。もしオフでないなら、上記コマンドは先にsudo grub-reboot "UEFI Firmware Settings"と実行したほうが良いかもしれません。オフにしていない場合は、⁠bad shim signature」と表示されます。

図1 セキュアブートがオフされていないとこのようにエラーになる。何かキーを押すと再起動する
図1
図2 起動時にESCキーを連打していると、次のようなGRUBメニューが表示される。「Memory test」が今回追加した項目
図2
図3 無事にmemtest86+が動作した
図3

これでメモリテスト用のエントリーが追加されました。いつなんどき「暇だし、とりあえずメモリテストの画面を眺めておくか」という気分なっても安心です。

GRUBにパスワードを設定する

Ubuntuは、基本的に 「マシンに物理的にアクセスできる人は全面的に信頼する」 セキュリティモデルになっています。

たとえばUbuntuのrootアカウントはロックされていてログインできませんし、sudoグループに所属したユーザーしかsudoコマンドを実行できません。

しかしながら、マシンの前にいる人であれば誰でも、そのマシンの電源を切って無理やり再起動し、キーボードでGRUBメニューを表示させ、そこから「recovory mode」を選択してしまえば、管理者権限を取得可能です[1]。他にもUSBメモリーでブートしてしまうという手もあります。もちろんストレージ自体を暗号化することで、簡単にはデータを奪われないようにする方法はあるのですが、それでも限界はあります。Ubuntuのセキュリティ対策は、原則として「リモートから簡単にアクセスできないようにする」ものに限られることに留意しておきましょう。

とは言え、ちょっとした手間をかけることで「ローカルのマシンから攻撃しにくくする」ことは可能です。前述のストレージの暗号化もそうですし、rootアカウントにパスワードをセットしておくと、知識のない相手であればちょっとした嫌がらせになるかもしれません。そんな対応のひとつが「GRUBのアクセス制限」です。

GRUBのアクセス制限を有効化すると、次のような制約がかけられます。

  • GRUBのCLIへのアクセスを管理者のみに限定する
  • GRUBのメニューエントリーの編集を管理者のみに限定する
  • GRUBの特定のメニューエントリーを、管理者ないし指定したユーザーのみがアクセスできるようにする

実際の手順を見ていきましょう。まずはアカウントとパスワードの記述方法ですが、passwordコマンドとpassword_pbkdf2コマンドの2種類が存在します。passwordgrub.cfgにアカウント名とパスワードをそのまま書く方法です。シンプルですがセキュアではありません。それに対してpassword_pbkdf2のほうは、パスワードを別途生成したハッシュ値のみ記述する形になります。今回はpassword_pbkdf2を使うことにします。password_pbkdf2用のハッシュ値の生成は、grub-mkpasswd-pbkdf2コマンドを利用します。

$ grub-mkpasswd-pbkdf2
パスワードを入力してください:
Reenter password:
PBKDF2 hash of your password is grub.pbkdf2.sha512.10000.(長いハッシュ値)

上記のgrub.pbkdf2.sha512.10000.(長いハッシュ値)grub.cfgに記述するので控えておいてください。

次に/etc/grub.d/40_customを次のように編集します。

#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.
set superusers="admin"
password_pbkdf2 admin grub.pbkdf2.sha512.10000.(長いハッシュ値)

追加したのは最後の2行だけです。もちろん/etc/grub.d/40_custom以外を使ってもかまいません。superusersは管理者アカウントとして指定する任意のアカウント名前を設定します。カンマなどで区切ると複数の指定が可能です。password_pbkdf2に指定するのはアカウントの名前とそれに対応するパスワードハッシュです。passwordないしpassword_pbkdf2で作ったアカウントで、superusersにないアカウントは通常のアカウント扱いとなります。

ただしこの設定だけだと、⁠起動のたびに毎回ユーザー名とパスワードの入力」が必要になります。アカウントを作成すると、その時点でメニューエーントリーの選択が「管理者だけ許可」になるためです。個々のメニューエントリーの設定には、次のいずれかのオプションが必要です。

  • 無制限に許可したい:--unrestrictedオプションを付ける
  • 特定のユーザーにのみ許可したい:--usersオプションを付ける
  • それ以外は管理者のみが選択可能

--usersオプションもカンマ等の区切りで複数の指定が可能になっています。

Ubuntuの場合はメニューエントリーが/etc/grub.d/10_linuxで自動生成しているため、メニューエントリーにオプションを追加するためには、このスクリプトの修正が必要です。具体的には次のように変更します。

      echo "menuentry '$(echo "$title" | grub_quote)' ${CLASS} \$menuentry_id_option 'gnulinux-$version-$type-$boot_device_id' {" | sed "s/^/$submenu_indentation/"
  else
      echo "menuentry '$(echo "$os" | grub_quote)' ${CLASS} \$menuentry_id_option 'gnulinux-simple-$boot_device_id' --unrestricted {" | sed "s/^/$submenu_indentation/"

menuentryを出力している部分は上記の2箇所になります。このうち後ろのほうに--unrestrictedを追加しています。後者はGRUBメニューで言うところの「Ubuntu」と表示される部分で、前者は「Advanced options for Ubuntu」の中のサブメニューを出力する部分です。

通常は「Ubuntu」だけ選択するようにしておいて、⁠recovery mode」などを選択できるほうはパスワードを要求する、そんな設定になります。もちろんすべてのメニューに--unrestritedを付けておいてもかまいません。この場合はsuperusersにないユーザーは、メニューエントリーの選択は可能なものの編集等は不可になります。また、--users ユーザー名と指定すると、特定のユーザーのみ許可できます。

図4 GRUBでメニューエントリー選択時のパスワード入力画面
図4

起動が失敗した時のリカバリー機能

Ubuntuのgrub.cfgには、起動が失敗したことを検知する2種類の仕組みが実装されています。

ひとつめはrecordfailで、これは前回の起動が完了したかを検知するものです。まず最初に、GRUBからUbuntuを起動する際にGRUBの変数領域/boot/grub/grubenvに、recordfail=1を記録します。その後、システムが無事に起動したら/lib/systemd/system/grub-common.serviceが起動したら⁠⁠、grub-editenv /boot/grub/grubenv unset recordfailを実行して、recordfail変数を削除します。その上で、次回起動時に次のように判断します。

  • recordfailに1が設定されている:前回は何らかの理由で起動途中で止まっていたと疑われるので、GRBUのメニューを表示し、10秒間のタイムアウトを設定する
  • recordfail変数が存在しないか1以外:前回は無事に起動したので、通常の起動シーケンスに入る

これにより起動失敗したときはGRUBメニューから別のカーネルを選択したり、起動オプションを変更しやすくなっています。ただし、自動的に別のカーネルを選ぶようにはなっていないので、手動での対応が必要です。

もうひとつはinitrdfailです。これはクラウド向けのinitrd/initramfsなしでブートするモードの際に使われるもので、通常のUbuntuインストール環境では使われません。こちらもrecordfailと似たような感じで、GRUB側で変数をセットし、起動後に変数を削除、変数の内容によって次回起動時に挙動を変更することになります。

今回はシンプルに、recordfaileが1だったら=前回の起動途中で何か問題が発生したら、Advanced optionの1個前のカーネルを選択する」という形で対応してみましょう。recordfailを判定しているところに処理を追加します。具体的には/etc/grub.d/00_headerの次の部分です。

make_timeout ()
{
    cat << EOF
if [ "\${recordfail}" = 1 ] ; then
  set timeout=${GRUB_RECORDFAIL_TIMEOUT:-30}
  default="1>2"
  fallback=0
else

追加したのはdefault="1>2"fallbackの2行となります。defaultの指定方法は、第743回のGRUB_DEFAULTの指定方法と同じです。今回の場合は「Advanced options(2番目⁠⁠」のメニューエントリーの、⁠古いカーネル(3番目⁠⁠」を選びたいため、0から数えて「1の2」という形になります。

fallback「メニューで指定されたカーネルやinitramfsがロードできなかった時にどこにフォールバックするか」を示します。今回のケースだと指定しなくても良い(ロードされる前にrecordfailが1に設定されるのでロード失敗すると次は失敗扱いになる)のですが、念のために設定しておきました。もちろん0以外に「確実に起動できるメニューエントリー」を用意してそこに設定しておくのも良いでしょう。Live USBを常に接続しておくのも良いかもしれません。

これまでと同様にsudo update-grubして反映します。動作確認には「Ubuntuの起動途中で停止する」対応が必要です。方法はいくつかありますが、ここはシンプルに起動途中で停止したつもりで、recordfail=1を設定してしまいましょう。

$ sudo grub-editenv - set recordfail=1

これで再起動したら、自動的にリカバリー側に遷移します。GRUBのメニューエントリーが表示されて、30秒のタイムアウト設定になっていて、さらに「Advanced options」が選択されていることを確認しましょう。また30秒後には自動的に起動するはずです。uname -aなどでカーネルが指定したメニューエントリーのそれになっているか確認してください。

ちなみにgrub-common.serviceは、あまり厳密な依存関係を設定していません。よって「このサービスが立ち上がれば起動成功とし、そうでない場合は失敗とする」といったユーザー側の事情で、systemdの依存関係を設定するのもありでしょう。

GRUBには他にもロードするイメージのハッシュ値をチェックする機能や、機能が制限されたロックダウンモード、TPMを利用したMeasuring Bootなどにも対応しています。実際に公式のマニュアルを参照すると、思いの外いろいろなことができることに気づくでしょう。ぜひ、自分なりのGRUBメニューを模索してみてください。

ただし大事なことなので、最後にもう一度伝えておきます。基本的には 「トラブルに遭遇したくなければ、GRUBには手を出すな」 です。

おすすめ記事

記事・ニュース一覧