閉域網で DKMS モジュールを本当にビルドする
このラボは本物のVM上で動きます
Ubuntu 24.04のVM 1台です。カーネルをビルドしてモジュールを読み込むには、カーネルの権限が必要なので、PodではなくVMです。dkmsとビルドツールがインストールされていて、サンプルモジュールのソースが/opt/airgap-hello/(airgap_hello.c、Makefile)にあります。NVIDIAドライバーは再配布できないので、同じ経路(ヘッダーの持ち込み → DKMSビルド → modprobe)を、この20行のGPLモジュールでたどります。VMは、最初はインターネット(80/443)に届きます。ステップ2までが、接続された状態での持ち込み準備で、ステップ3で、外部への送信をふさぎます。採点は、外部からVMの8899ポートに入ってくるエージェントが行うので、すでに確立した接続をふさぐと、採点が切れます。
目標
実行中のカーネルと正確に同じヘッダーを持ち込んで、オフラインでインストールし、DKMSでモジュールをビルド・インストール・ロードしたあと、ヘッダーがずれたときのエラーを採取します。
なぜ重要なのか
エアギャップ環境でのドライバーのインストールで、最もよく壊れる場所は、ドライバーではなく、カーネルヘッダーです。DKMSは、実行中のカーネルのビルドツリー(/lib/modules/<커널>/build)で、モジュールをビルドしますが(プレースホルダーはカーネルです)、持ち込んだヘッダーが1バージョンでも違うと、ビルドが始まりすらしません。そして、カーネルの更新が入った瞬間に、同じことがもう一度起きます。このラボは、その経路を、本物のカーネルでたどります。
所要時間は50分ほどです。セッションが終わるとVMが消えるので、残したいファイルは、終了前に別に保管してください。
ステップ
running=(実行中のカーネル)、headers_pkg=(そのカーネルのヘッダーのパッケージ名)、build_dir=(yesまたはno。今/lib/modules/<커널>/buildがあるか)を書いてください(ファイル:/root/dkms/kernel.txt)。プレースホルダーはカーネルです。- 接続されている間に、そのカーネルのヘッダーのパッケージと、それが要求するヘッダーのパッケージを、インストールせずに
/root/dkms/bundle/に取得して、その中でSHA256SUMSを作成してください(ファイル:/root/dkms/bundle/SHA256SUMS)。 - 再実行しても、ルールが1セットだけ残るスクリプトで、外部への送信をふさいでください(スクリプト:
/root/dkms/egress.sh)。チェーンAIRGAP-EGRESSを使い、ループバックと確立済みの接続は通します。 - ハッシュを確認したあと、バンドルの.debでヘッダーをオフラインでインストールして、
/lib/modules/<커널>/buildができるようにしてください(プレースホルダーはカーネルです)。 - モジュールのソースを
/usr/src/airgap-hello-1.0/に置いて、dkms.confを書き(ファイル:/usr/src/airgap-hello-1.0/dkms.conf)、DKMSにaddしてください。 - 実行中のカーネルで、build・installして、
dkms statusにinstalledが出るようにしてください。 - モジュールを
site=labでロードして、vermagic=・signer=(なければnone)・sb_state=(mokutil --sb-stateの結果1行)を書いてください(ファイル:/root/dkms/load.txt)。 - ヘッダーがないカーネルバージョン
6.8.0-9999-genericでビルドさせて、DKMSが出すエラーを保存してください(保存先:/root/dkms/mismatch.txt)。
参考
- 実行中のカーネル:
uname -r・ヘッダーのパッケージ名:linux-headers-$(uname -r) - 取得だけ:
apt-get download <패키지...>(プレースホルダーはパッケージです)・要求するパッケージ:apt-cache depends <패키지>またはdpkg-deb -f <deb> Depends(プレースホルダーはパッケージです) - DKMS:
dkms add <소스디렉터리>・dkms build -m <이름> -v <버전> [-k <커널>]・dkms install ...・dkms status(プレースホルダーは、順に、ソースディレクトリ、名前、バージョン、カーネルです) - モジュール情報:
modinfo -F vermagic <모듈>、modinfo -F signer <모듈>(プレースホルダーはモジュールです)・ログ:dmesg | grep airgap_hello - よくある間違い1:
linux-headers-genericを取得してしまうこと。これは、最新のカーネルヘッダーを指すメタパッケージなので、実行中のカーネルと違うことがあります。 - よくある間違い2: ヘッダーのパッケージを1つだけ取得してしまうこと。カーネルヘッダーは、2つのパッケージ(フレーバーごと + 共通)に分かれています。
実行中のカーネルと、必要なヘッダー
running=・headers_pkg=・build_dir=の3行を書いてください(ファイル: /root/dkms/kernel.txt)。
DKMSが使うビルドツリーは、/lib/modules/<カーネル>/buildです。Ubuntuのヘッダーのパッケージ名は、実行中のカーネル文字列を、そのまま付けたものです。今そのディレクトリがあるかを、確認してください。
ヘッダーをインストールせずに取得して、ハッシュの一覧を作成する
実行中のカーネルのヘッダーのパッケージと、それが要求するヘッダーのパッケージを、/root/dkms/bundle/に取得して、SHA256SUMSを作成してください(ファイル: /root/dkms/bundle/SHA256SUMS)。
apt-get downloadは、インストールせずに、.debだけを取得します。取得したヘッダーパッケージのDependsを見ると、一緒に必要な共通のヘッダーパッケージが出てきます。ハッシュの一覧は、バンドルのディレクトリの中で、相対パスで作成してください。
外部への送信をふさいで、エアギャップ環境にする
再実行しても、ルールが1セットだけ残るスクリプトで、外部への送信をふさいでください(スクリプト: /root/dkms/egress.sh)。
新しいチェーンを作って、OUTPUTの先頭からそこへ送り、ループバック・確立済みの接続・採点エージェントを先に通してから、残りをふさぎます。これ以降は、aptが外部から取得できません。
持ち込んだヘッダーをオフラインでインストールする
ハッシュを確認したあと、バンドルの.debでヘッダーをインストールして、/lib/modules/<커널>/buildができるようにしてください(プレースホルダーはカーネルです)。
バンドルのディレクトリで、ハッシュの一覧で先に検査します。dpkgに2つの.debを一緒に渡すと、お互いの依存を一度に解決します。インストール後、buildがどこを指すリンクなのかを、見てください。
dkms.confを書いてadd
モジュールのソースを/usr/src/airgap-hello-1.0/に置いて、dkms.confを書き(ファイル: /usr/src/airgap-hello-1.0/dkms.conf)、DKMSにaddしてください。
DKMSは、/usr/src/<名前>-<バージョン>でソースを探します。dkms.confには、パッケージ名とバージョン、作られるモジュール名(.koなし)、インストール先、新しいカーネルで自動的にもう一度ビルドするかを書きます。
実行中のカーネルでbuild・install
実行中のカーネルで、build・installして、dkms statusにinstalledが出るようにしてください。
buildは、/var/lib/dkmsの下でモジュールを作成し、installが、それを/lib/modules/<カーネル>/の下に移して、depmodを実行します。失敗すると、make.logのパスが、出力に出ます。
ロードして、vermagic・署名・Secure Bootを記録する
モジュールをsite=labでロードして、vermagic=・signer=・sb_state=を書いてください(ファイル: /root/dkms/load.txt)。
modprobeは、depmodが作成した一覧からモジュールを探して、依存まで読み込み、パラメータを、名前=値で受け取ります。modinfo -Fで、フィールド1つだけを取り出せます。Secure Bootの状態は、mokutilが教えてくれます(EFI変数がないVMなら、そう答えます)。
ヘッダーがないカーネルでビルドさせる
カーネルバージョン6.8.0-9999-genericでビルドさせて、DKMSが出すエラーを保存してください(保存先: /root/dkms/mismatch.txt)。
dkms buildには、対象のカーネルを選ぶオプションがあります。そのカーネルのビルドツリーがなければ、DKMSは、コンパイルを始める前に止まります。出力(標準エラー出力を含む)を、そのまま保存してください。