えんでぃの技術ブログ

えんでぃの技術ブログ

ネットワークエンジニアの視点で、IT系のお役立ち情報を提供する技術ブログです。

GNS3の概要

gns3_topology

お伝えしたいこと

GNS3とはどんなソフトかを簡単にご紹介します。
また、GNS3のインストール手順についても軽く触れます。

GNS3の具体的な操作方法については、本記事では紹介しません。

GNS3とは

GNS3を略さず言うと、Graphical Network Simulator 3です。
その名の通り、GUIでネットワークをシミュレーションできるソフトウェアです。
https://www.gns3.com/

ネットワーク機器やLinux/Windowsなどの仮想版を起動し、GUI上で配線して通信させることができます。
内部的にはKVMVirtualBoxVMware Workstation PlayerやDockerと連携することで、仮想マシンやコンテナ版のネットワーク機器を再生する仕組みになっています。

GNS3の画面イメージは以下のとおりです。

gns3_ui

GNS3は、商用インフラにおけるネットワークサービス提供を想定したツールではありません。
ハンズオンを通してネットワーク機器のコマンドを試したり、プロトコルの動作を理解するための学習ツールです。

GNS3のユースケース

GNS3は、以下のような場面で役に立ちます。

  • 手を動かしながらネットワーク技術を学習する
  • お試しで構成を組んでみて、実際に動くか確認する
  • プロトコル、コマンド、API、自動化の動作を確認する

GNS3の良いところ

仮想マシンを起動するだけであれば、VirtualBoxKVMなどで十分です。
ではなぜGNS3を使うのが良いかと言うと、理由は以下が挙げられると思います。

複雑な物理構成を再現できる

GNS3は1:1のケーブル結線を容易に再現できます。
またGUIが物理構成図の役割を果たしており、ネットワークの全体像を俯瞰しながら操作できるので快適です。

これらの特徴により、GNS3であれば複雑な物理構成も容易に再現できます。

VirtualBoxを直接操作して、UDP Tunnel機能で複雑な構成を組むのはとても大変です。

安定した動作

GNS3が初めてリリースされてから10年以上経っています。
製品不具合や動作の重さなどは初期のバージョンから改善されており、GNS3そのものは安定して動作します。

もちろん注意すべき点はいくつかあります。

  • GNS3 GUI上の設定変更やケーブル結線は、必ず両側の電源をOFFにしてから行う
    • この手のトラブルは、大抵VM再起動、GNS3再起動、ホストマシン再起動のいずれかで直る
  • 文字化けの原因になるので、VM名やディレクトリパスの日本語表記は極力避ける
  • Windowsファイアウォールに必要な通信が拒否された場合は、許可ルールの追加が必要

数々の便利機能との連携

以下の機能は仮想アプライアンスを建てずに利用可能です。
主役がルータやファイアウォールロードバランサーの場合、L2スイッチや疎通確認用端末にあまりメモリリソースを割けない場合もあります。
そんなとき、以下のL2スイッチや仮想パソコン機能が役に立ちます。

  • 組み込みL2スイッチ
    • Dynamipsの機能
    • VLAN設定可能なソフトウェアベースのL2スイッチ
    • メモリ消費が少ない
  • VPCS (Virtual PC Simulator) との連携

Wiresharkとの連携

GNS3上でケーブルを右クリックして、該当箇所のパケットキャプチャを取得することができます。
これにより、プロトコルの詳細な動作の確認や、トラブルの切り分け調査を手軽に行うことができます。

物理ネットワークではネットワーク機器にトラフィックミラーリングのための設定を入れ、パケットキャプチャ用の端末を接続したりします。
GNS3では、そのような煩わしい操作は一切不要です。

マルチベンダー

GNS3は、仮想マシンやコンテナであれば何でも追加可能です。
GNS3は特定のネットワーク機器OSに縛られません。
それどころか、LinuxWindowsサーバなども追加可能です。

GNS3は、ブリッジ接続にも対応します。
すなわち、GNS3内部で動作している仮想アプライアンスを、ホストPCとケーブル結線された物理ネットワークと通信させることも可能です。
物理ネットワーク機器が検証の主役である場合にも、仮想アプライアンスとルーティングプロトコルなどをやり取りさせるような検証でGNS3を役立てることができます。

Linked base VM

GNS3上にArista vEOSを3台追加する場合、事前にVMを3台構築しておく必要はありません。
linked base VM機能を有効化すれば、GNS3トポロジ上にVMを追加するたびに、テンプレートとして事前登録したVMが自動的にクローンされます。

ユーザーは、同じVMを何台もGNS3トポロジに追加することができます。
その際、クローン操作を意識する必要はありません。

GNS3と似たソフトウェア

GNS3と似たような機能を持つソフトウェアとしては、Cisco Packet TracerやCML (Cisco Modeling Lab)などが挙げられます。
これらの製品と比較したGNS3の強みは、無料かつマルチベンダーであることだと思います。
詳細はGNS3の公式ドキュメントにも説明があります。

GNS3 Documentation - What is GNS3? - #GNS3 Comparisons

GNS3のアーキテクチャ

GNS3は、内部的にはクライアントとサーバーに分かれます。
GNS3クライアントがGUI画面を、GNS3サーバーが内部処理を担当します。

名称 役割
GNS3クライアント
(gns3-gui)
  • GUI画面を提供する
  • ユーザーの操作をGNS3サーバに伝える
  • GNS3サーバの処理結果をGUI画面に反映する
GNS3サーバ
(gns3-server)
  • デフォルトではTCP3080をリッスンする
  • GNS3の内部処理全般を担当する
  • 各種外部ツールとの連携を担当する

gns3_summary

GNS3の構成パターン

基本的には、GNS3クライアントとGNS3サーバを同じマシンにインストールしてしまうのが最も簡単です。
WindowsMacにインストールすると、こちらの構成になります。
マニュアル通りに操作すれば、難なくインストールできる簡単さが特徴です。

別の使い方として、LinuxマシンにGNS3サーバと連携ソフト (KVMやVPCSなど) のみをインストールして、GNS3クライアント複数台からネットワーク越しにアクセスさせる構成も可能です。
この構成の組み方はマニュアルに全て書いてあるわけではないので、多少は手順の検討が必要になります。
この構成のメリットは、私の理解では2つあると思います。

  • GNS3クライアント側の要求スペックが低くなる
  • GNS3サーバに配置した仮想マシンのテンプレート、GNS3プロジェクト (≒GNS3上で作成したトポロジ)などを複数のユーザー間で共有できる

個人学習においてGNS3を使う場合は、基本的には前者の「GNS3クライアントとGNS3サーバを同じマシンに同居させる構成」のほうが快適だと思います。
パソコンのスペックが小さい場合には、別途共用の物理サーバにGNS3サーバをインストールする構成を取ることもあると思います。

GNS3の動作要件

GNS3の動作要件は、以下のURLに書いてあります。
https://www.gns3.com/software

OS

Windows, Mac, Linuxに対応しています。

動作要件という観点では、Linuxのどのディストリビューションも含まれます。
しかしGNS3公式でパッケージリポジトリを管理し、インストール方法も公式ドキュメントで案内されているのはUbuntuDebian系のディストリビューションのみです。

Fedoraの場合、Fedora公式のリポジトリからdnfでインストール可能です。
Arch系の場合はAUR (Arch User Repository) 経由でインストール可能です。

しかしいずれの場合も、一部ソースコードビルドによるインストールも伴います。
Fedoraについては、次の記事で詳細な手順を示します。

その他ハードウェアスペック

CPUの仮想化支援機能が有効化されていることというのが重要です。
具体的には、AMD AMD-V/RVI, またはIntel VT-X/EPIのことです。
今どきのAMD、またはIntel製のCPUであれば問題ないと思います。
BIOS/UEFIの設定で仮想化支援機能が無効化されていないかをご確認ください。

他にもRAMサイズやディスク空き容量などに言及がありますが、基本的には大きければ大きいほど良いです。
仮想マシンを動かしますので、RAMサイズは8GiB以上ないと結構厳しいのではないかと思います。

GNS3のインストール方法

以下にOSごとのGNS3のインストール方法の概略を示します。
どのOSでも共通で、VirtualBoxやDockerなどの仮想化ソフトは別途インストールする必要があります。

Windows

以下公式サイトからAll-in-oneインストーラをダウンロードして実行します。
GNS3 - Download GNS3

Solar PuttyやSolarwindsのツールセットもおすすめされますが、これらのツールを導入する必要はありません。

Solar PuttyTeratermなどで置き換え可能です。
Teratermを使う場合の手順は、以下のサイトが参考になります。

なおWindowsでGNS3を動かす場合、WindowsファイアウォールによってGNS3の機能がブロックされないようご注意ください。
Windowsファイアウォールの規則にない通信が発生した場合、デフォルトではポップアップが表示されるはずです。
そのポップアップにおいて、GNS3やDynamipsなどのプログラムの通信が出たときは許可すれば、問題を回避できると思います。

ポップアップの画面で通信を拒否してしまった場合はファイアウォールにdenyルールが追加されてしまいます。
その場合は、手動でそのdenyルールを削除する必要があります。

公式のインストール手順はこちらです。
GNS3 Windows Install

Mac

以下公式サイトからGNS3のインストーラをダウンロードします。
GNS3 - Download GNS3

インストーラをダブルクリックで起動し、GNS3のアイコンをApplicationsフォルダにドラッグします。
Macのお作法として、初回起動時はApplicationsフォルダ内のGNS3アイコンをダブルクリックして起動する必要があります。

GNS3の初回起動時、uBridgeのアクセス権限を付与する旨のポップアップが出るので、許可するようにしてください。

Macのバージョンによっては、OSのセキュリティ機能によって追加の操作が必要になる場合があります。
何かつまづいた場合は、以下の公式手順をご確認ください。
GNS3 Mac Install

Linux

Ubuntu/Debianについては公式サイトにインストール方法があります。
Ubuntuの場合は、PPAを追加してapt installする手順になっています。
GNS3 Linux Install

他のディストリビューションについては、公式の説明はありません。
しかし、以下の記事を参考にすればインストールできると思います。

Fedora35については私も動作確認しました。
Fedoraに対するGNS3のインストール手順については、FedoraへのGNS3インストール手順でも詳しく説明します。

(参考) GNS3 VM

GNS3 VMは、GNS3公式で配布されているUbuntuのイメージです。
このUbuntuにはGNS3サーバが入っています。

GNS3 VMVMware Workstation PlayerやVirtualBoxなどにインポートし、GNS3クライアントの設定でGNS3 VMを有効化することで使えるようになります。

WindowsMacなどの非LinuxマシンでQEMUを使いたい場合には使うかもしれません。
ただ、多くの場合GNS3 VMは使わなくて良いのでは...と思います。
QEMUを使わなくても、VirtualBoxで事足りるためです。

むしろ、GNS3クライアント起動のたびに毎回GNS3 VMが自動で立ち上がり、GNS3 VMの起動が完了するまで待たされるようになります。

GNS3 VM上で仮想マシンを動かすために、GNS3 VMにCPU/RAM/ディスク等のリソースを割り当てる必要があります。
非力なマシンでは、このリソース消費が結構大きいです。

したがって、私個人としてはGNS3 VMの利用をあまりおすすめはしません。
公式ではおすすめしていますが、これはGNS3 VMを利用者が使うことで環境差異をなくし、動作不備を防止するためだと理解しています。
GNS3をローカルでトラブルなく動かせている方に関しては、GNS3 VMはほぼ必要ありません。

まとめ

GNS3の概要を紹介しました。
GNS3のインストール方法や使い方については、GNS3タグの記事をご確認ください。

次の記事

Fedoraに対するGNS3のインストール手順を紹介します。

virt-builderによるLinuxインストールの「省略」

virt-builder-image
The cloud icon is created by Freepik - flaticon.com

お伝えしたいこと

今回はvirt-builderというLinuxコマンドの紹介です。
LinuxをMinimal構成でインストール済みのディスクファイルを簡単に生成するコマンドです。

Linuxインストールを自動化するのではなく、Linuxインストール済みのテンプレートから仮想ディスクを作成します。

KVM仮想マシンを構成している場合に相性が良いです。
コマンド1行で、なおかつ短時間でLinuxマシンが手に入ります。

前回お伝えしたKickstartファイルと比較すると、得意/不得意があります。
詳細は次のセクションで紹介します。

virt-builderとは

virt-builderとは、libguestfsというツール群に含まれるコマンドの1つです。
Linux環境であれば、OS標準のパッケージマネージャで手軽にインストールして使い始めることができます。

このコマンドによって、LinuxをMinimal構成でインストールしたデータを格納した仮想ディスクファイル (raw, またはqcow2形式) を手軽に生成できます。
具体的な動作は、下図のとおりです。

virt-builder

virt-builderコマンドを実行したときの動作は2工程です。

  1. オンラインリポジトリからxz圧縮されたテンプレートを~/.cache/virt-builder/配下にダウンロードする
  2. テンプレートを加工し、Linuxインストール済みのディスクファイルを生成する

virt-builder実行時のコマンドラインオプションによって、2の工程における加工の内容を制御できます。
具体的にはディスクサイズや、ログインパスワードの変更などが挙げられます。

virt-builderコマンドは、Linuxのインストール処理を行いません。
そのため、テンプレートをダウンロード済みであれば高速に動作します。

一方で、Linuxを手動/自動インストールする場合と比較して細かなカスタマイズは効きません。
virt-builderはMinimal構成の検証用VMを手軽に作成する方法と割り切り、より細かな制御が必要な場合はKickstartファイルによる自動インストールなど自力でインストールすることをおすすめします。

virt-builderの使い方

インストール

virt-builderは、guestfs-tools RPMパッケージに含まれます。

RHEL系のディストリビューションであれば、以下のコマンドでインストールできます。

sudo dnf install guestfs-tools

virt-builderの実行

virt-builderのSyntaxは非常にシンプルです。

virt-builder os-version [options...]

最小限のコマンド例は、以下のとおりです。
以下のコマンドにより、6 GiBのディスク容量を持つCentOS Stream9をインストール済みの仮想ディスクファイルが生成します。
rootのパスワードはランダム文字列が生成され、標準出力に表示されます。
今回はrFdQIE0WFI8AHTRyでした。

virt-builder centosstream-9

# (一部抜粋)
#   Output file: centosstream-9.img
#   Output size: 6.0G
# Output format: raw

今回の例では、os-versioncentosstream-9を指定しました。
指定可能なOSバージョンの一覧は、virt-builder -lで確認できます。

virt-builder -l

# (一部のみ抜粋)
# centosstream-9  x86_64  CentOS Stream 9

実際に使う際は、以下のように複数のオプションを指定するのが一般的です。

virt-builder centosstream-9 \
-o /home/shared/libvirt/images/test.qcow2 \
--size 10G \
--format qcow2 \
--hostname test \
--root-password 'password:mypassword' \
--ssh-inject root \
--selinux-relabel

# (一部抜粋)
#   Output file: /home/shared/libvirt/images/test.qcow2
#   Output size: 10.0G
# Output format: qcow2

今回使用したコマンドラインオプションの意味は、下表のとおりです。
完全なリストはman virt-builderを参照してください。

--selinux-relabelSELinuxが有効なディストリビューションをビルドする場合のみ指定します (FedoraCentOS Streamなど)
他のオプションは、どのディストリビューションにおいても指定することをおすすめします。

オプション 意味
-o
--output
  • 出力する仮想ディスクファイルパスを指定
  • デフォルト:
    • raw形式の場合: OSイメージ名.img
    • qcow2形式の場合: OSイメージ名.qcow2
--size
  • 仮想ディスクのサイズを指定する
  • 単位のいずれかの指定が必須
    • b: Bytes
    • K: KiB
    • M: MiB
    • G: GiB
    • (※) Tという単位は認識されなかった
  • 最低でも6.0G以上を指定する必要がある
  • デフォルト: テンプレートのサイズを変更せず使う
    (※) デフォルト値は6.0Gのことが多い
--format
  • ディスクファイルのフォーマットを指定する
  • rawqcow2のいずれかを指定可能
  • (※) ディスク容量の効率が良いqcow2がおすすめ
  • デフォルト: raw
--hostname
  • ホスト名を指定する
  • デフォルト: テンプレートファイルの構成に従う
--root-password
  • rootユーザーのログインパスワードを指定する
  • password:PASSWORD: 指定した文字列を設定する
  • file:FILENAME: 指定したファイルの1行目の文字列を設定する
  • random: ランダム文字列を設定し、標準出力に表示する
  • disabled: パスワードを無効化する
  • locked: rootユーザーをロックする
  • 詳細はman virt-builderを参照
  • デフォルト: random
--ssh-inject (※1)
  • SSH公開鍵を指定したユーザーの~/.ssh/authorized_keysに登録する
  • ユーザー名のみ指定した場合、~/.ssh/から自動選択される
  • SELinuxが有効なVMをビルドする場合、--selinux-relabelも指定するべき (※2)
  • 詳細はman virt-builderを参照
--selinux-relabel
  • ビルドの最終工程でSELinuxのrelabelを行う
  • 以下の2つの条件を両方満たす場合は指定必須 (※3)
    • RHELCentOS StreamなどSELinuxが有効なOSをビルドするとき
    • virt-builderのオプションでファイル生成するとき

(※1) sshdはデフォルトでrootユーザーによるパスワード認証を拒否する設定になっているので、--ssh-injectによってSSH鍵をアップロードすることをおすすめします

(※2) --ssh-injectオプションによって、~/.ssh/authorized_keysファイルを生成します。このファイル生成はSELinuxが起動していない状態で行われるため、生成されるファイルはラベル無し (no label) になります

(※3) SELinuxが起動している時にファイルを生成すると、SELinuxは全てのファイルにラベル付けし、ファイルシステムメタデータとして記録します。その後、SELinuxはラベルに基づいてアクセス制御を行います。--ssh-injectオプションを利用すると、マシンが起動しておらずSELinuxも動作していないところに~/.ssh/authorized_keysを生成します。このとき当然ラベル付けされないので、そのままだとSELinuxの制御が正しく行われません。具体的にはsshdauthorized_keysを読み取れず、公開鍵認証に失敗します。この問題を回避するには、--selinux-relabelをセットで指定することで、SELinuxのラベル付けを行います。この対応はRHEL, CentOS, CentOS Stream, Fedoraなど、SELinuxが有効なディストリビューション全てに必要です。SELinuxについてもっと知りたい方は、Linuxプロセスアクセス制御の概要を始めとするSELinuxシリーズもぜひお読みください

(参考) 生成したディスクイメージファイルでVMを起動する

virt-builderで生成したディスクファイルをセットしてVMを起動すれば、インストール済みのLinuxを直ちに使うことができます。

例えば、KVM環境の場合は、以下のようなvirt-installコマンドでVMを作成・起動します。
今回はLinuxのインストールを行わないので--cdromのような起動順位を変更するオプションを指定せず、代わりに--importを指定します。

virt-install \
--name test \
--memory 1024 \
--vcpus 2 \
--os-variant centos-stream9 \
--disk path=/home/shared/libvirt/images/test.qcow2 \
--import
--noautoconsole

作成したVMを強制終了して削除するには、以下のコマンドを実行します。

virsh destroy test ; virsh undefine test --storage vda

詳細はKVMの基本操作集にあるVMの作成VMの削除を参考にしてください。

(参考) オンラインリポジトリのURL

virt-builderコマンドが参照するオンラインリポジトリのURLは、デフォルトで以下のとおりです。

  1. https://builder.libguestfs.org
  2. http://archive.libguestfs.org/builder
  3. https://download.opensuse.org/repositories/Virtualization:/virt-builder-images/images/

リポジトリのURLは、/etc/virt-builder/repos.d/*.confにて管理されています。

(参考) virt-builderに対応しているディストリビューション

2022/4現在で以下のとおりです。

virt-builderの引数に指定できるディストリビューション名は、virt-builder -lで確認できます。

virt-builder -l
# (一部のみ抜粋)

# opensuse-13.1        x86_64     openSUSE 13.1
# opensuse-42.1        x86_64     openSUSE Leap 42.1
# opensuse-tumbleweed  x86_64     openSUSE Tumbleweed
# alma-8.5             x86_64     AlmaLinux 8.5
# centos-6             x86_64     CentOS 6.6
# centos-7.0           x86_64     CentOS 7.0
# centos-8.2           x86_64     CentOS 8.2
# centosstream-8       x86_64     CentOS Stream 8
# centosstream-9       x86_64     CentOS Stream 9
# cirros-0.3.5         x86_64     CirrOS 0.3.5
# debian-6             x86_64     Debian 6 (Squeeze)
# debian-11            x86_64     Debian 11 (bullseye)
# fedora-33            x86_64     Fedora® 33 Server
# fedora-35            x86_64     Fedora® 35 Server
# fedora-35            aarch64    Fedora® 35 Server (aarch64)
# freebsd-11.1         x86_64     FreeBSD 11.1
# scientificlinux-6    x86_64     Scientific Linux 6.5
# ubuntu-10.04         x86_64     Ubuntu 10.04 (Lucid)
# ubuntu-20.04         x86_64     Ubuntu 20.04 (focal)

まとめ

Minimal構成でLinuxをインストール済みの仮想ディスクファイルをお手軽に入手する方法としてvirt-builderを紹介しました。
Kickstartよりも更に簡単に検証用マシンを作成することができます。

virt-builderで作成したLinuxは、構成がほぼ固定されます。
デフォルトの構成をカスタマイズしたい場合は、やはり自分でLinuxをインストールする必要があります。
Linuxインストールを自動化したいときは、Kickstartが便利です。

virt-builderVMクローン、Kickstart、またはその他の自動化をうまく使い分け、効率よくLinux検証環境を整備しましょう。

KickstartによるLinuxインストール自動化 (自動トリガー・PXEブート連携)

featured_image

Kickstartシリーズ

Kickstartとは、Linuxのインストールを自動化する機能です。
本シリーズではRHEL, Fedora, CentOS Streamをモデルに紹介します。

一連の記事でKickstartの構成を3パターン紹介します。
今回の記事では、3つ目の構成を紹介します。

  1. KickstartによるLinuxインストール自動化 (手動トリガー・HTTP連携)
  2. KickstartによるLinuxインストール自動化 (自動トリガー・ローカルディスク接続)
  3. KickstartによるLinuxインストール自動化 (自動トリガー・PXEブート連携) ← 今ココ

今回の記事については私が構成の内容を理解しきれておりません。
したがって、RHEL8のマニュアル通りの操作を抜粋して紹介するにとどめます。
詳しい原理や、使い勝手やセキュリティを踏まえた設計の吟味などはできませんが、ご了承ください。

今回の構成では、PXE BootやKickstartに必要なファイルをアップロードするサーバ (※) は、他にDHCPが動作していないネットワーク上で起動する必要があります。
(※) 本記事ではPXEサーバと呼びます

"他にDHCPが動作していないネットワーク" というのは、実はかなり大きな制約です。
VirtualBoxのようなローカル検証環境においてはDHCPを活用しているケースが多いと思うので、個人的にはローカル検証環境とPXE Bootはあまり相性が良くないと思います。

一方でPXE Boot用のネットワークが設計上分離されており、PXEサーバが常時起動しているようなインフラが整備されていればうまく活用できると思います。

構成概要

PXE (Preboot eXecution Environment) とは、ネットワークブートの一方式です。
ピクシー (/ˈpɪksiː/) と読まれることが多いようです1

ネットワーク越しにブートローダーやOSイメージ等の情報を取得して起動を開始します。
ブートローダーのパラメータにLinuxインストーラKickstartファイルのURLを指定することで、インストール作業の完全自動化まで可能となります。

kickstart3

環境構築は以下の手順で行います。
詳細は#環境構築にて説明します。

  1. DHCPサーバをインストールする
  2. DHCPサーバにTFTPサーバのIPアドレスなどを設定する
  3. TFTPサーバをインストールする
  4. ブートローダーの設定ファイルやバイナリファイルをTFTPサーバにアップロードする
  5. HTTP、FTP、またはNFSサーバを構築し、以下ファイルをアップロードする
    1. Kickstartファイル
    2. パッケージリポジトリ

Kickstartは以下の手順で開始します。
この構成では、イメージファイルの準備も不要です。
詳細は#Kickstartの実行にて説明します。

  1. インストール対象のマシンに空のディスクをセットする
  2. ネットワーク起動の優先順位を高くする
  3. マシンを起動する

環境構築

今回は、DHCPサーバ、TFTPサーバ、HTTPサーバを同じマシンに全て同居させた "PXEサーバ" を構築します。
今回の構成では、サーバは192.168.101.2/24というIPアドレスを持ちます。
デフォルトゲートウェイは、192.168.101.1であるとします。

手順はRHEL8 - Performing an advanced RHEL installationをベースとしています。
以降、各セクションに参照したセクションへのURLを記載します。

本記事の手順はCentOS Stream 9 (RHEL9相当) で動作を確認しました。

冒頭に記載しましたとおり、本記事では各設定内容の詳細までは吟味しません。
ただ、気になるポイントがあればSYSLINUX Wiki - PXELINUXが参考になると思います。

DHCPが動作していない仮想ネットワークを作成する

PXEサーバが動作するネットワークでは、他のDHCP機能が動作しないようにしてください。
VirtualBoxlibvirtなどの仮想ネットワークを使っている場合は、組み込みのDHCP機能がデフォルトで有効になっているので注意してください。

(参考) DHCPサーバが動作しない仮想ネットワークを作る理由

同じネットワーク内にDHCPサーバを複数台構築すると、DHCPサーバの動作が競合してしまいます。

同じネットワークにDHCPサーバが複数台存在すると、以下のような動きになってしまいます。
(※) DHCPサーバに冗長化機能があり、Active-Standby構成で動作しているようなケースは例外です

  • クライアントがブロードキャストでDHCPDISCOVERを出す
  • 複数台のDHCPサーバがDHCPDISCOVERを受け取り、DHCPOFFERを返す
  • クライアントは複数のDHCPサーバからDHCPOFFERを受け取るが、最初に届いたDHCPOFFERで後続の処理を進める

つまり、DHCPOFFERを先に届けたもの勝ちという動きになり、意図しないDHCPサーバがクライアントにIPアドレスを渡してしまいます。

今回の構成では、確実にPXEサーバが起動しているDHCPサーバからIPアドレスを受け取る必要があります。
PXEサーバ上で動作するDHCPサーバは、後続処理のためにTFTPサーバのIPやファイルパスを伝える特殊な役割を持つためです。

したがって、VirtualBoxKVM (libvirt) が起動している仮想ネットワークのDHCP機能と、PXEサーバのDHCP機能が競合しないように構成を組む必要があります。

(参考) KVMの場合の手順

KVM環境において、DHCP機能を無効化したネットワークを作る手順を紹介します。
操作方法はKVMの基本操作集 - #仮想ネットワークの作成が参考になります。

Cockpitの場合は以下のようなパラメータを指定します。

cockpit_add_nodhcp_network1

仮想ネットワーク作成後、ネットワーク起動し、自動起動も有効化しておきましょう。

cockpit_add_nodhcp_network2

コマンドで実行する場合は、やや煩雑な手順になります。
今回は少々手抜きですが、上記Cockpitで作成したXML形式の定義情報を貼っておきます。
ただし、MACアドレス、UUID情報、ブリッジ名は自動生成に任せたいので削除しておきました。

<network>
  <name>nodhcp</name>
  <forward mode='nat'>
    <nat>
      <port start='1024' end='65535'/>
    </nat>
  </forward>
  <domain name='nodhcp' localOnly='yes'/>
  <dns>
    <host ip='192.168.101.1'>
      <hostname>gateway</hostname>
    </host>
  </dns>
  <ip address='192.168.101.1' netmask='255.255.255.0' localPtr='yes'>
  </ip>
</network>

以下のコマンドにより、作成したtmp.xmlを読み込んで新しい仮想ネットワークを作成します。

virsh net-define tmp.xml

ネットワークを起動し、自動起動を有効化します。

virsh net-start nodhcp
virsh net-autostart nodhcp

VMのネットワーク設定

KVMの構成におけるネットワーク設定を参考までに残しておきます。
以下の構成です。

sudo nmcli connection add ifname enp1s0 con-name enp1s0 type ethernet ipv4.method manual ipv4.addresses 192.168.101.2/24 ipv4.gateway 192.168.101.1 ipv4.dns 192.168.101.1 autoconnect yes

sudo nmcli connection up enp1s0

Linuxイメージファイルのマウント

今回のPXEサーバの構築にはLinuxインストール用のイメージファイルを使用します。

Linuxインストール用のISOファイルを接続した上でサーバを起動します。
Cockpitから仮想マシンに対してISOファイルを追加する際、BusをSCSIではなくSATAを指定しないとLinux上で/dev/cdromが生成しませんでした。

サーバ起動後、以下のコマンドでISOファイルをマウントします2

sudo mkdir /media/cdrom
sudo mount -t iso9660 -o ro,loop /dev/cdrom /media/cdrom

DHCPサーバの構築

参考元: 14. Preparing to install from the network using PXE
(14.2と14.3にそれぞれBIOSUEFI向けの設定方法が説明されていますが、DHCPの設定に関してはどちらも共通です)

DHCPサーバのインストール

以下のコマンドでDHCPサーバをインストールします。

sudo dnf install dhcp-server

DHCPサーバの設定

続いてDHCPサーバの設定を書き換えます。

sudo vi /etc/dhcp/dhcpd.conf

dhcpd.confの中身を以下のように編集します。
IPアドレスの値は環境によって調整してください。

option space pxelinux;
option pxelinux.magic code 208 = string;
option pxelinux.configfile code 209 = text;
option pxelinux.pathprefix code 210 = text;
option pxelinux.reboottime code 211 = unsigned integer 32;
option architecture-type code 93 = unsigned integer 16;

subnet 192.168.101.0 netmask 255.255.255.0 {
    option routers 192.168.101.1;
    range 192.168.101.101 192.168.101.254;

    class "pxeclients" {
      match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
      next-server 192.168.101.2;

      if option architecture-type = 00:07 {
        filename "BOOTX64.efi";
        } else {
        filename "pxelinux/pxelinux.0";
      }
    }
}

中身について補足します。

DHCPサーバの起動

DHCPサーバを起動します。

# sudo systemctl enable dhcpd.service; sudo systemctl start dhcpd.service と同じ
sudo systemctl enable dhcpd.service --now

TFTPサーバの構築

参考元: 14. Preparing to install from the network using PXE

TFTPサーバのインストール

以下のコマンドでTFTPサーバをインストールします。

sudo dnf install tftp-server

firewalldによるTFTP通信許可

firewalldが有効な場合は、以下のコマンドでTFTP通信を許可しておきます。
重要なコマンドは「変更」と「設定反映」です。

# 事前確認。serviceにtftpがないこと
sudo firewall-cmd --list-all

# 変更
sudo firewall-cmd --add-service tftp --permanent

# 差分比較
diff -u <(sudo firewall-cmd --list-all) <(sudo firewall-cmd --list-all --permanent)
# -public (active)
# +public
#    target: default
#    icmp-block-inversion: no
# -  interfaces: enp1s0
# +  interfaces: 
#    sources: 
# -  services: cockpit dhcpv6-client ssh
# +  services: cockpit dhcpv6-client ssh tftp
#    ports: 
#    protocols: 
#    forward: yes

# 設定反映
sudo firewall-cmd --reload

# 事後確認。serviceにtftpがあること
sudo firewall-cmd --list-all

BIOS利用クライアント向けのブートローダー格納

参考元: 14.2. Configuring a TFTP server for BIOS-based clients

PXEBOOT用のブートローダーや設定ファイルをTFTPサーバに格納します。
本セクションで紹介するのは、BIOSベースのクライアント向けの手順です。
UEFIベースのクライアント向けの手順は、#UEFI利用クライアント向けのブートローダー格納にて別途紹介します。
理由がなければ、両方のセクションを対応しましょう。

作業ディレクトリに移動し、ブートローダRPMをISOイメージファイルから取り出します。
RPMファイルを展開し、中にあるブートローダーファイル群を/var/lib/tftpboot/pxelinux/ディレクトリに格納します。

mkdir ~/tmp/
cd ~/tmp/

# RHEL8の公式手順ではISOファイルから取り出している
  # RHEL8の場合: cp -p /media/cdrom/BaseOS/Packages/syslinux-tftpboot*.rpm .
  # CentOS Stream9場合: cp -p /media/cdrom/AppStream/Packages/syslinux-tftpboot-*.rpm .
dnf download syslinux-tftpboot

rpm2cpio syslinux-tftpboot*.rpm | cpio -dimv

sudo mkdir /var/lib/tftpboot/pxelinux
sudo cp -p tftpboot/* /var/lib/tftpboot/pxelinux/

作業用ディレクトリは不要になったので、このタイミングで消しておきます。

cd
rm -rf ~/tmp/

PXELINUXの設定ファイルを格納するディレクトリを作成します。

sudo mkdir /var/lib/tftpboot/pxelinux/pxelinux.cfg

PXELINUXの設定ファイルを作成します。

sudo vi /var/lib/tftpboot/pxelinux/pxelinux.cfg/default

defaultファイルには以下の内容を記述します。
IPアドレスや文言は環境に合わせてアレンジしてください。

default vesamenu.c32
timeout 50

display boot.msg

label linux_cs9
  menu label ^Install system
  menu default
  kernel images/centos_stream9/vmlinuz
  append initrd=images/centos_stream9/initrd.img ip=dhcp inst.repo=http://192.168.101.2/repositories/centos_stream9 inst.ks=http://192.168.101.2/kickstarts/centos_stream9
label vesa_cs9
  menu label Install system with ^basic video driver
  kernel images/centos_stream9/vmlinuz
  append initrd=images/centos_stream9/initrd.img ip=dhcp inst.xdriver=vesa nomodeset inst.repo=http://192.168.101.2/repositories/centos_stream9 inst.ks=http://192.168.101.2/kickstarts/centos_stream9
label rescue_cs9
  menu label ^Rescue installed system
  kernel images/centos_stream9/vmlinuz
  append initrd=images/centos_stream9/initrd.img rescue
label local
  menu label Boot from ^local drive
  localboot 0xffff

BIOS画面からPXE Bootを試みるとき、このdefaultファイルを参照してBoot Entryを選択します。
ファイル名は他にもUUIDやMACアドレスIPアドレスを元にした文字列も指定可能です。
上記のファイル名の条件に一致するようなクライアントから接続があった場合、defaultファイルよりも優先してそれらのファイルが読み込まれます。
詳細はSYSLINUX Wiki - PXELINUX - #Configuration filenameを参照してください。

設定ファイルの文法については、SYSLINUX Wiki - Configを参照してください。

今回はシンプルにdefaultファイルのみ作成しました。
MACアドレスIPアドレスごとのファイルを作成するかは設計思想に依存すると思いますが、手間がかかるので基本defaultのみで良いのではないかと思います。

defaultファイルは、RHEL8のマニュアルから以下の部分をアレンジしました。

変更前 変更後 詳細
prompt 1 行削除
  • prompt 1の場合、常にboot:プロンプトを表示する
  • prompt 0の場合、boot:プロンプトを表示しない
    (デフォルト)
  • boot:の表示によって起動が遅くなるので省いた
  • prompt
timeout 600 timeout 50
label linux label linux_cs9
  • 今後別OSの選択肢が増えたときのために名称変更
  • label
label vesa label vesa_cs9 同上
label rescue label rescue_cs9 同上

vesa_cs9, rescue_cs9, local_cs9については、不要であれば#コメントアウトしても問題ないと思います。

上表以外にも、各labelの配下に指定しているkernelinitrdinst.repoIPアドレスとファイルパスを適切な値に変更しました。

kernelinitrdについては、/var/lib/tftpboot/をrootとしたときの相対パスを指定します。
後続の#kernel, initrdファイルの格納において、ここで指定したパスにkernelinitrdを格納する必要があります。

inst.repoは、RPMパッケージリポジトリのURLです。
IPアドレスはHTTPサーバのものを指定します。
今回はDHCP/TFTP/HTTPサーバがすべて同じマシンに同居する構成のため、共通のIPアドレスである192.168.101.2を指定します。
後続の#RPMパッケージリポジトリの格納において、RPMパッケージリポジトリをここで指定したファイルパスに格納する必要があります。
inst.repoと同等のオプションはKickstartファイルのuriオプションでも指定しますが、これらはどちらも指定する必要があります。

inst.repoと同じ行にinst.ksを追記し、KickstartファイルのURLを指定しました。
後続の#Kickstartファイルの格納において、Kickstartファイルをここで指定したファイルパスに格納する必要があります。

UEFI利用クライアント向けのブートローダー格納

参考元: 14.3. Configuring a TFTP server for UEFI-based clients

前セクションではBIOSベースのクライアント用のブートローダーをTFTPサーバに格納しました。
本セクションでは、UEFIベースのクライアント用のブートローダーを格納します。

異なるバイナリファイルや設定ファイルを用意する必要がありますが、設定のポイントは基本的にBIOS向けの対応手順と変わりません。

まず、作業用ディレクトリを作成して移動します。

mkdir ~/tmp
cd ~/tmp

shim、grub2-efi RPMパッケージをISOファイルから取り出します。
これらのRPMはオンラインリポジトリには存在しなかったので、ISOファイルから取り出す必要がありました。

cp -p /media/cdrom/BaseOS/Packages/shim-x64-[0-9]*.rpm .
cp -p /media/cdrom/BaseOS/Packages/grub2-efi-x64-[0-9]*.rpm .

RPMファイルを展開して中身のファイルを取り出します。

rpm2cpio shim-x64-[0-9]*.rpm | cpio -dimv
rpm2cpio grub2-efi-x64-[0-9]*.rpm | cpio -dimv

EFIブートイメージファイルを取り出し、TFTPからアクセス可能な領域に格納します。

mkdir /var/lib/tftpboot/uefi

# RHELの場合は...
# sudo cp -p boot/efi/EFI/redhat/grubx64.efi
# sudo cp -p boot/efi/EFI/redhat/shimx64.efi
sudo cp -p boot/efi/EFI/centos/grubx64.efi
sudo cp -p boot/efi/EFI/centos/shimx64.efi

EFI用のBoot Entryを追記します。

sudo vi /var/lib/tftpboot/grub.cfg

以下のように記載します。

set timeout=5
menuentry 'CentOS Stream 9' {
  linuxefi images/centos_stream9/vmlinuz ip=dhcp inst.repo=http://192.168.101.2/repositories/centos_stream9/ inst.ks=http://192.168.101.2/kickstarts/centos_stream9
  initrdefi images/centos_stream9/initrd.img
}

grub.cfg環境変数やコマンドについては、GNU GRUB Manual - #16 The list of available commandsを参照してください。

grub.cfgファイルは、RHEL8のマニュアルから以下の部分をアレンジしました。

変更前 変更後 詳細
set timeout=60 set timeout=5
menuentry 'RHEL 8' menuentry 'CentOS Stream 9'

上表の他にも各種ファイルのパスやURLを環境に合わせて変更しました。

vmlinuzinitrd.imgについては、/var/lib/tftpboot/をrootとしたときの相対パスを指定します。
後続の#kernel, initrdファイルの格納において、ここで指定したパスにkernelinitrdを格納する必要があります。

inst.repoは、RPMパッケージリポジトリのURLです。
IPアドレスはHTTPサーバのものを指定します。
今回はDHCP/TFTP/HTTPサーバがすべて同じマシンに同居する構成のため、共通のIPアドレスである192.168.101.2を指定します。
後続の#RPMパッケージリポジトリの格納において、RPMパッケージリポジトリをここで指定したファイルパスに格納する必要があります。
inst.repoと同等のオプションはKickstartファイルのuriオプションでも指定しますが、これらはどちらも指定する必要があります。

inst.repoと同じ行にinst.ksを追記し、KickstartファイルのURLを指定しました。
後続の#Kickstartファイルの格納において、Kickstartファイルをここで指定したファイルパスに格納する必要があります。

kernel, initrdファイルの格納

PXE Bootで起動する際に必要なkernelイメージファイル、及びInit RAM DiskをISOファイルからコピーして格納します。
以下のコマンドを実行します。

mkdir -p /var/lib/tftpboot/pxelinux/images/centos_stream9
sudo cp -p /media/cdrom/images/pxeboot/{vmlinuz,initrd.img} /var/lib/tftpboot/pxelinux/images/centos_stream9/

TFTPサーバの起動

# sudo systemctl enable tftp.socket; sudo systemctl start tftp.socket と同じ
sudo systemctl enable tftp.socket --now

HTTPサーバの構築

参考元: RHEL8 - Performing an advanced RHEL installation - 6.4. Creating an installation source using HTTP or HTTPS

HTTPサーバにはKickstartファイルと、RPMパッケージリポジトリをアップロードします。

HTTPサーバのインストール

以下のコマンドでApache httpdをインストールします。

sudo dnf install httpd

firewalldによるHTTP通信許可

firewalldが有効な場合は、以下のコマンドでHTTP通信を許可しておきます。
重要なコマンドは「変更」と「設定反映」です。

# 事前確認。serviceにhttpがないこと
sudo firewall-cmd --list-all

# 変更
sudo firewall-cmd --add-service http --permanent

# 差分比較
diff -u <(sudo firewall-cmd --list-all) <(sudo firewall-cmd --list-all --permanent)
# -public (active)
# +public
#    target: default
#    icmp-block-inversion: no
# -  interfaces: enp1s0
# +  interfaces: 
#    sources: 
# -  services: cockpit dhcpv6-client ssh tftp
# +  services: cockpit dhcpv6-client http ssh tftp
#    ports: 
#    protocols: 
#    forward: yes

# 設定反映
sudo firewall-cmd --reload

# 事後確認。serviceにhttpがあること
sudo firewall-cmd --list-all

Kickstartファイルの格納

参考元: 5.3. Making a Kickstart file available on an HTTP or HTTPS server

KickstartファイルをHTTPサーバにアップロードします。
使用するファイルは、KickstartによるLinuxインストール自動化 (手動トリガー・HTTP連携) - #書き換え後のKickstartファイルとほぼ同じです。

ただし、cdromrepoの行のみ以下のように書き換えます。
RPMパッケージリポジトリをCDROMではなく、HTTPサーバ上のリポジトリに変更します。

# cdrom
url --url=http://192.168.101.2/repositories/centos_stream9

# repo --name="AppStream" --baseurl=file:///run/install/sources/mount-0000-cdrom/AppStream
repo --name="AppStream" --baseurl=http://192.168.101.2/repositories/centos_stream9/AppStream

cdromRPMパッケージリポジトリとしてISOイメージファイルを指定するコマンドです。
今回はRPMパッケージリポジトリとしてHTTPサーバを指定するので、これをurlコマンドに置き換えます。

repoは、baseなどデフォルトのRPMリポジトリに追加のリポジトリを指定するオプションです。
RHEL8以降は、インストール時にAppStreamというリポジトリを追加で指定する必要があります3

用意したKickstartファイルをHTTPサーバにアップロードします。

sudo mkdir /var/www/html/kickstarts
sudo vi /var/www/html/kickstarts/centos_stream9

今回使用するKickstartファイルは以下のとおりです。

# Generated by Anaconda 34.25.0.25
# Generated by pykickstart v3.32
#version=RHEL9
# Use graphical install
graphical
repo --name="AppStream" --baseurl=file:///run/install/sources/mount-0000-cdrom/AppStream

%addon com_redhat_kdump --enable --reserve-mb='auto'

%end

# Keyboard layouts
keyboard --vckeymap=jp --xlayouts='jp'
# System language
lang en_US.UTF-8

# Network information
network  --bootproto=dhcp --device=enp1s0 --ipv6=auto --activate
network  --hostname=cs

url --url=http://192.168.101.2/repositories/centos_stream9

%packages
@^minimal-environment

%end

# Run the Setup Agent on first boot
firstboot --enable

# Generated using Blivet version 3.4.0
ignoredisk --only-use=vda
# Partition clearing information
clearpart --none --initlabel
# Disk partitioning information
part pv.111 --fstype="lvmpv" --ondisk=vda --grow
part /boot --fstype="ext4" --ondisk=vda --size=1024
volgroup cs --pesize=4096 pv.111
logvol / --fstype="ext4" --percent=100 --name=root --vgname=cs
logvol swap --fstype="swap" --size=1024 --name=swap --vgname=cs

# System timezone
timezone Asia/Tokyo --utc

# Root password
rootpw --iscrypted $6$GMEsgHeNZwjJWb.d$BuP/sn9FX6xKEFGYo8KzNmLB5jPl7RhFQyHnklgrK2CkQntcsT2aOCz4Ozzcefc5J7gXO7IXVvj7KdQOddpv81
user --groups=wheel --name=endy --password=$6$qtY89ARpZJNqk8ap$1u6e3CplnSGpqBppAyd/.f9fheOt74TA.fqMdPsROa3kmSSlHuOyXurBWSr5EHksAemR.3HjDjkI1n6JIeJIk1 --iscrypted --gecos="endy"

過去記事と同様に、お好みでシンボリックリンクを作っておくとCentOS Streamのバージョン差異を抽象化できて便利になります。
よろしければ、以下のコマンドでcentos_streamというファイル名のシンボリックリンクを作成しておきましょう。

ln -s centos_stream9 centos_stream

RPMパッケージリポジトリの格納

参考元: 6.4. Creating an installation source using HTTP or HTTPS

ISOファイルからRPMパッケージリポジトリ情報をコピーします。
この部分の手順は、いわゆるリポジトリサーバの構築手順に近い内容かなと思います。

今回はLinuxの初期インストール用のパッケージを調達したいので、オンラインリポジトリではなくISOファイルからリポジトリデータをコピーします。

以下のコマンドでコピーします。
コピー元ディレクトリにある.treeinfoという隠しファイルが重要です。
*のようなワイルドカードを使うと隠しファイルのコピーが漏れてしまうので、以下の例のようにフォルダごとコピーしましょう。

sudo mkdir /var/www/html/repositories/
sudo cp -pr /media/cdrom/ /var/www/html/repositories/
sudo mv /var/www/html/repositories/cdrom /var/www/html/repositories/centos_stream9

HTTPサーバの起動

以下のコマンドでHTTPサーバを起動します。

# sudo systemctl enable httpd.service; sudo systemctl start httpd.service と同じ
sudo systemctl enable httpd.service --now

Kickstartの実行

ここまでの操作により、Kickstartに必要なPXEサーバの構築は完了しました。

では、いよいよ空のVMPXE Bootし、KickstartによってLinuxを自動インストールします。

(参考) 仮想マシンの作成

KVM配下に仮想マシンを作成する場合を例に、パラメータの例を示します。

詳細はKVMの基本操作集 - #VMの作成をご覧ください。

CockpitからVMを作成する際は、下図のようにパラメータを指定します。
今回はPXE Bootをするため、通常のCDROMからの起動ではなく、ネットワークからの起動を選択します。
また、PXEサーバと同じネットワークに接続するようにします。
今回の場合は、#DHCPが動作していない仮想ネットワークを作成するで作成したnodhcpというネットワークに接続します。

cockpit_vm_creation_with_network_boot_enabled

virt-installコマンドで仮想マシンを作成する場合は、例えば以下のようなコマンドを実行します。

Network bootを優先するため、普段指定している--cdromの代わりに--pxeを指定します。
また、今回は仮想ネットワークにdefault以外のものを指定するので、明示的に--network network=nodhcpによって指定します。

コンソール画面の自動起動を無効化したい場合は、--no-autoconsoleオプションも付けましょう。
--no-autoconsoleを指定する場合は、末尾の&は削除します。

virt-install \
--name test \
--memory 2048 \
--vcpus 2 \
--disk size=10 \
--graphics vnc \
--graphics spice \
--os-variant centos-stream9 \
--network network=nodhcp \
--pxe &

インストール手順を何度か試したいとも思いますので、作成したVMを停止して削除するコマンドも添えておきます。
--os-variantの指定を省いた場合は、--storage vdaの部分が--storage hdaになることもあります。
詳細はKVMの基本操作集 - #VMの削除を参照してください。

virsh destroy test ; virsh undefine test --storage vda --snapshots-metadata --managed-save

Kickstartによる自動インストール

VMを起動すると、以下の処理が自動的に走ります。

正しく動作すれば、以下のブートローダーの画面が表示されます。
無操作のまま5秒待つか、一番上のInstall systemを選択するとこの先のインストールに進みます。

pxe_bootloader

正しく処理が進めば、このままインストールが完了します。

(参考) トラブルのパターン

全てではありませんが、過去に踏んだことのあるトラブルを紹介します。

inst.repoの指定ミス

参考: 23.1. Configuring the Installation System at the Boot Menu - #Specifying the Installation Source

inst.repoに指定したパス配下の.treeinfoにHTTPアクセスできなかった場合、ブートローダーのグレーの画面を通過後、以下のようなエラー画面に遷移します。

.treeinfoの読み取りに失敗し、inst.repoinst.ksのパスに/images/install.imgを付与したパスから読み取る処理にフォールバックします。そのパスにもファイルが存在しないため、HTTP通信で404エラー応答が返り、処理に失敗します。

error_repository1

↓しばらく待つと、以下のようなエラーメッセージがしばらくループし続けた後、完全に処理を停止します。

error_repository2

このエラーが出た場合、inst.repoに指定したパスを中心に、HTTP通信がエラーになった原因を探って修正する必要があります。
確認するファイルは以下の2つです。

  • /var/lib/tftpboot/pxelinux/pxelinux.cfg/default
  • /var/lib/tftpboot/grub.cfg

(参考) Kickstartでエラーが発生したときの対処法

1つ目の記事で紹介した(参考) Kickstartでエラーが発生したときの対処法を参照してください。

まとめ

PXE bootとKickstartを組み合わせて、ネットワーク越しに各種リソースにアクセスしてLinuxインストールを完全自動化する構成を紹介しました。

構成がなかなか複雑ですが、ネットワークに繋いでマシンの電源を入れるだけでLinuxのインストールが完了するというのは面白いと思います。

しかし、ローカル検証環境において活用するにはDHCPサーバの競合を回避する必要があります。
本番用途で活用するには、構築したサーバに何らかの方法で固定IPアドレスを割り振る工程も自動化するところで頭を使うと思います。

実際のユースケースと結びつけて正しく設計しようと考えると、今回紹介した構成は難易度が高いと思います。
他のKickstart構成やVMクローン、AnsibleなどのDevOpsツールなどの代替手段があるだけに、PXE bootの構成は中々採用されないかもしれません。

私も今のところ良いユースケースを思いつかない状況ですが、せっかく構築できたので本記事で紹介した次第です。
うまい使い方をご存知の方がいれば、ぜひコメントいただけますと幸いです。

関連記事

KickstartによるLinuxインストール自動化 (自動トリガー・ローカルディスク接続)

featured_image

Kickstartシリーズ

Kickstartとは、Linuxのインストールを自動化する機能です。
本シリーズではRHEL, Fedora, CentOS Streamをモデルに紹介します。

一連の記事でKickstartの構成を3パターン紹介します。
今回の記事では、2つ目の構成を紹介します。

私が普段Linuxをインストールするとき、この方法を使うことが一番多いです。

  1. KickstartによるLinuxインストール自動化 (手動トリガー・HTTP連携)
  2. KickstartによるLinuxインストール自動化 (自動トリガー・ローカルディスク接続) ← 今ココ
  3. KickstartによるLinuxインストール自動化 (自動トリガー・PXEブート連携)

構成概要

今回の構成は、Kickstartファイルを格納した専用の記憶デバイスをセットするだけでインストールを自動化します。

前回記事の構成と比較すると、初回起動時にinst.ks=...KickstartファイルのURLを手動入力しなくて良い点がメリットとして挙げられます。
一方でKickstart用ディスクを最初に接続し、インストール完了後に必要に応じて接続解除するという一手間が発生する点はデメリットとも言えます。

しかしながらコンソール上の文字入力が不要となるため、シェルスクリプトやAnsibleなどの外部ツールと連携して完全自動化したい場合は、今回の構成の方が実現が容易になります。

kickstart2

環境構築は以下の手順で行います。
詳細は#環境構築にて説明します。

  1. ファイルシステムOEMDRVとラベル付けする
  2. 上記ファイルシステムKickstartファイルを/ks.cfgというファイルパスで保存する

Kickstartは以下の手順で開始します。
詳細は#Kickstartの実行にて説明します。

  1. ISOファイル、インストール先ディスク、上記のキックスタート用ディスクをセットする
  2. ISOファイルから起動する
  3. 自動的にインストールが完了する

この構成は、特にLinuxを扱える環境においては採用しやすいと思います。
Linux環境であれば、mkisofsコマンドによって、Kickstartファイルだけを格納した専用のKickstartトリガー用ファイルをISO形式で簡単に作成できます。
(※) 本ブログでは、このKickstartトリガー用ファイルを「Kickstartイメージファイル」と呼びます

Windows環境でKickstartイメージファイルを作成するには、恐らくフリーソフトに頼る必要があります。
Macの場合はわかりません。

環境構築

Kickstartファイルの作成

前回記事で作成したファイルをそのまま使います。

ファイル名をks.cfgにリネームし、作業用ディレクトリに格納しておきます。

tree tmp
# tmp
# └── ks.cfg

Kickstart用ディスクの作成

mkisofsのインストール

Kickstart用ディスクの作成にはmkisofsコマンドを使います。

RHELディストリビューションであれば以下のコマンドでインストールできます。

sudo dnf install xorriso

Kickstartイメージファイルの作成

Kickstartイメージファイルの要件は以下のとおりです。

では、Kickstartイメージファイルを作成します。

まず、事前にks.cfgという名前のKickstartファイルをディレクトリに配置します。
サブディレクトリなどに含めず、必ずディレクトリ直下に配置するようにしてください。

ls tmp/
# ks.cfg

続いて、tmp/ディレクトリを引数にmkisofsによってISOファイルを作成します。

mkisofs -V OEMDRV -o ks_centos_stream9.iso tmp/

今回使用したmkisofsコマンドラインオプション、及び引数を下表に示します。

オプション / 引数 意味
-V OEMDRV
-o ks_centos_stream9.iso 出力ファイルパスをks_centos_stream9.isoと指定
tmp/ tmp/ディレクトリの中身をISOファイルシステムに含める

最後に、Kickstart用ディスクファイルをお好みの場所に格納します。
今回の例では、/home/shared/libvirt/images/kickstarts/ks_centos_stream9.isoに格納しました。

前回記事と同様に、お好みでシンボリックリンクを作っておくとCentOS Streamのバージョン差異を抽象化できて便利になります。
よろしければ、以下のコマンドでks_centos_stream.isoというファイル名のシンボリックリンクを作成しておきましょう。

ln -s ks_centos_stream9.iso ks_centos_stream.iso

Kickstartの実行

ここまでの作業で準備は整いました。
いよいよ初期状態の仮想マシンを用意してKickstartを開始します。

(参考) 仮想マシンの作成

KVM配下に仮想マシンを作成する場合を例に、パラメータの例を示します。
詳細はKVMの基本操作集 - #VMの作成をご覧ください。
VM作成時、Kickstart用ディスクファイルもセットするのがポイントです。

(参考) Cockpitの場合

CockpitでVMを作成する場合、以下の手順で行います。

まずはKickstartファイルを含むディレクトリにStorage Poolが紐付いていない場合は、作成しておきます。
プールを作成する理由は後ほど説明します。
作成手順はKVMの基本操作集 - #ストレージプールの作成が参考になります。

続いて、VMを作成します。
VM作成はKVMの基本操作集 - #VMの作成を参考にしてください。
VM作成時のパラメータは以下を指定します。
ポイントは下図の(3)にあるとおり、VM起動前に追加設定を行うよう指定することです。

cockpit_ksfile1

続いて作成したVMの詳細画面へ移動し、DisksセクションのAdd disksを選択します。
続いて、下図のようにCustom pathからISOファイルへのフルパスを指定します (※)

cockpit_ksfile2

(※) ちょっとした小技ですが、"Use existing"からPool名とKickstartイメージファイル名のみを指定することで、ISOファイルの指定をすることができます。この方法で指定するためには、事前にKickstartイメージファイルを格納するフォルダと紐づくPoolを作成する必要があります。Poolの作成手順は、KVMの基本操作集 - ストレージプールの作成を参照してください。このやり方は癖が強いので、やるとしたら商用環境ではなくご自身の個人環境で行うようにしましょう。

(参考) virt-installの場合

以下のコマンドを実行することで、Kickstartイメージファイルを搭載したVMを作成・起動できます。
基本的には前回記事と同じコマンドですが、以下の部分が差分です。

  • 末尾に--diskを追加指定することでKickstartイメージファイルを追加している
  • Kickstartイメージファイルの追加時、readonly=onを指定することでドライブを読み取り専用にしている (※)
  • --check path_in_use=offにより、Kickstartイメージファイルを搭載したVMが他に存在してもエラーにならないようにする
virt-install \
--name test \
--memory 2048 \
--vcpus 2 \
--disk size=10 \
--graphics vnc \
--graphics spice \
--os-variant centos-stream9 \
--cdrom /home/shared/libvirt/images/isos/centos/CentOS-Stream-9-latest-x86_64-dvd1.iso \
--disk path=/home/shared/libvirt/images/kickstarts/ks_centos_stream9.iso \
--check path_in_use=off &

VMを強制終了して削除するには、以下のコマンドを実行します。

virsh destroy test ; virsh undefine test --storage vda --snapshots-metadata --managed-save

Kickstartによる自動インストール

VMを起動し、インストールを開始すれば自動的にKickstartが始まります。

前回記事とは異なり、コンソール画面からinst.ks=...を指定する必要はありません。
これはOEMDRVラベルのついたファイルシステム/ks.cfgファイルがあるとき、ks.cfgファイルを自動的にKickstartファイルとして読み込む仕様があるためです1

(参考) Kickstartでエラーが発生したときの対処法

1つ目の記事で紹介した(参考) Kickstartでエラーが発生したときの対処法を参照してください。

(参考) ISO以外の形式でKickstartイメージファイルを作成する

本セクションで紹介する手順は非推奨です。
KickstartイメージファイルはISOファイルで作成するべきです。

Kickstartイメージファイルは、ISO以外のファイルシステムでも作成できます。
例えば、軽量なファイルシステムであるFATやEXT2でフォーマットすることも技術的には可能です。

例えば、以下のような手順でFATファイルシステムでフォーマットされたqcow2形式のディスクファイルを作成できます。

sudo dnf install guestfs-tools
virt-make-fs -t fat -F qcow2 --label OEMDRV tmp/ ks_centos_stream9.qcow2

しかしqcow2でKickstartイメージファイルを作成すると、紐づくVMを削除するときに巻き添えでKickstartイメージファイルも誤って削除するリスクが上がります。
したがって、冒頭のとおりKickstartイメージファイルをqcow2で作成することは推奨しません。

具体的なメカニズムを以下に補足します。

  • qcow2ディスクファイルは、デフォルトで読み書き可能ディスクとして/dev/vdXにマウントされる
    • (一方で、ISOファイルはデフォルトで読み取り専用かつ/dev/sdXにマウントされる)
  • CockpitでVMを削除するとき、読み取り専用ではないディスクはデフォルトで削除対象のチェックボックスにチェックが入る
  • virsh undefine --storage vdaを指定したとき、Kickstartイメージファイルが読み書き可能ディスクだった場合一緒に削除されてしまう
  • Cockpitやvirshによる誤削除を防止するには、qcow2形式のKickstartイメージファイル追加時に、明示的に読み取り専用オプションを指定する必要がある
    • Cockpit画面では、VMの詳細画面で追加済みのディスクをEditすることで読み取り専用に変更できる
    • virt-installVMを作成する際は、--disks path=xxx,readonly=onのように読み取り専用オプションを追加できる

ISO形式でKickstartイメージファイルを作成すれば、上記一連の面倒な仕様すべてから開放されます。

上述の理由から、KickstartイメージファイルはISOイメージファイルで作成すべきです。

参考URL

本記事に関連する参考URLをまとめて掲載します。

Kickstart用ディスクによるKickstartの自動開始については、Red Hat社のマニュアルが参考になります。
RHEL8 - Performing an advanced RHEL installation - 7.3. Starting a Kickstart installation automatically using a local volume

その他、Kickstart自体の概要やKickstartファイルの書き方については、前回記事で紹介した内容を前提としております。

まとめ

Kickstart用ディスクをセットしたマシンを起動するだけで、Kickstartを自動トリガーしてLinuxのインストールを自動化する方法を紹介しました。

前回記事と比較して、Kickstartのトリガーが自動化されたことで更に便利になりました。
しかし、mkisofsコマンドが使えるLinux環境にアクセスできない場合はKickstart用ディスクの作成・変更が少々面倒かもしれません。

KVMでローカル検証環境を組んでいる方は、この構成を便利に扱えると思います。
Linuxのインストールが面倒になったら、ぜひご活用ください。

関連記事

KickstartによるLinuxインストール自動化 (手動トリガー・HTTP連携)

featured_image

Kickstartシリーズ

Kickstartとは、Linuxのインストールを自動化する機能です。
本シリーズではRHEL, Fedora, CentOS Streamをモデルに紹介します。

一連の記事でKickstartの構成を3パターン紹介します。
今回の記事では、1つ目の構成を紹介します。

本記事は初回のため、Kickstartの概要、及びKickstartファイルの書き方にも言及します。

  1. KickstartによるLinuxインストール自動化 (手動トリガー・HTTP連携) ← 今ココ
  2. KickstartによるLinuxインストール自動化 (自動トリガー・ローカルディスク接続)
  3. KickstartによるLinuxインストール自動化 (自動トリガー・PXEブート連携)

Kickstartの概要

Kickstartとは

Kickstartとは、上述のようなLinuxのインストール作業を自動化する仕組みです。

事前準備として、インストール用の設定値をテキストファイル (Kickstartファイル) に予め記述しておきます。
そしてインストール対象のマシンからそのファイルを読み込むことで、人間の操作を介することなくインストール作業を完了できます。

主なユースケース

Kickstartは、設定の書き方次第ではディスクサイズの違いを吸収して単一の定義ファイルで表現できます。
例えば、/bootに1GiB、/残り全てといったパーティションサイズの指定が可能です。

絶対ではありませんが、書き方を工夫することで異なるバージョンのディストリビューションを同じKickstartファイルで管理できるケースもあります。
とはいえ、少なくともメジャーバージョンが異なる場合はKickstartファイルを分けたほうが無難です。

VMクローンだとこのようにはいきません。
ディスクサイズやディストリビューションのバージョンなど、ほんの少しでも差分があれば新たなテンプレートVMを用意する必要が出てきます。

テンプレートVMはファイルサイズが大きい上に、UIによっては既存のVMと表示が入り混じって目障りになることもあります。
少ないファイルで多くの構成パターンをカバーできるKickstartは、VMクローンよりも管理しやすさの点で優秀です。

構成概要

今回の構成では、Kickstartファイルのみをネットワーク上に配置します。
インストール対象のVMのローカルには、インストーラのイメージファイルと空のディスクをセットしておきます。

Kickstartのトリガーは手動で行い、KickstartファイルはHTTP(S)、FTPNFSのいずれかによってダウンロードする構成です。

kickstart1

環境構築は以下の手順で行います。
詳細は#環境構築にて説明します。

  1. Kickstartファイルを作成する
  2. 作成したKickstartファイルをサーバにアップロードする (今回はHTTPサーバの例を示します)

Kickstartは以下の手順で開始します。
詳細は#Kickstartの実行にて説明します。

  1. LinuxをインストールしたいマシンにISOファイルとディスクファイルをセットする
  2. ISOファイルから起動する
  3. インストーラの画面においてinst.ks=http://...という形式でKickstartファイルのURLを入力してKickstartを開始する

この構成のメリットは以下の2点です。

  • Kickstartの構成を組むのがトップクラスに簡単であること
  • どのような構成でも容易に実現できる

一方でデメリットは、以下の2点です。

  • Kickstart実行時、HTTP/FTP/NFSサーバが起動している必要がある
  • inst.ks=...というURLの指定に多少の手間がかかる

環境構築

#構成概要の環境を構築する手順を紹介します。

ここで紹介する手順はRHEL8のマニュアルに準じています。
関係するURLは、#参考URLにてまとめて紹介します。

ISOファイルの入手

CentOS Stream限定のお話となりますが(※)、本記事の手順ではDVD ISOファイルを使用します。
Boot ISOファイルは使用しません。
(※) そして恐らくRHELも同様

ダウンロードするファイル名のイメージは以下のとおりです。

以下のBoot ISOファイルではないのでご注意ください。

DVD ISOファイルが必要な理由については#(参考) DVD ISOファイルが必要な理由にて補足しますので、興味のある方はご確認ください。

Kickstartファイルの雛形の入手

Kickstartファイルは、Linuxを手動でインストールしたときに/root/anaconda-ks.cfgに自動生成します。
このファイルをベースに書き換えて、自分の望む形のKickstartファイルを作成することにします。

今回使うKickstartファイルのベースを以下に示します。
以下のファイルはCentOS Stream9のインストール時に生成したので、centos_stream9というファイル名で保存しておきます。

(※) 末尾のパスワードハッシュ文字列は、平文ではmypasswordです

# Generated by Anaconda 34.25.0.25
# Generated by pykickstart v3.32
#version=RHEL9
# Use graphical install
graphical
repo --name="AppStream" --baseurl=file:///run/install/sources/mount-0000-cdrom/AppStream

%addon com_redhat_kdump --enable --reserve-mb='auto'

%end

# Keyboard layouts
keyboard --vckeymap=jp --xlayouts='jp'
# System language
lang en_US.UTF-8

# Network information
network  --bootproto=dhcp --device=enp1s0 --ipv6=auto --activate
network  --hostname=cs

# Use CDROM installation media
cdrom

%packages
@^minimal-environment

%end

# Run the Setup Agent on first boot
firstboot --enable

# Generated using Blivet version 3.4.0
ignoredisk --only-use=vda
# Partition clearing information
clearpart --none --initlabel
# Disk partitioning information
part pv.111 --fstype="lvmpv" --ondisk=vda --size=39935
part /boot --fstype="ext4" --ondisk=vda --size=1024
volgroup cs --pesize=4096 pv.111
logvol / --fstype="ext4" --size=38908 --name=root --vgname=cs
logvol swap --fstype="swap" --size=1024 --name=swap --vgname=cs

# System timezone
timezone Asia/Tokyo --utc

# Root password
rootpw --iscrypted $6$GMEsgHeNZwjJWb.d$BuP/sn9FX6xKEFGYo8KzNmLB5jPl7RhFQyHnklgrK2CkQntcsT2aOCz4Ozzcefc5J7gXO7IXVvj7KdQOddpv81
user --groups=wheel --name=endy --password=$6$qtY89ARpZJNqk8ap$1u6e3CplnSGpqBppAyd/.f9fheOt74TA.fqMdPsROa3kmSSlHuOyXurBWSr5EHksAemR.3HjDjkI1n6JIeJIk1 --iscrypted --gecos="endy"

この先の操作はお好みで実施ください。

(任意) シンボリックリンクの作成

以下のコマンドで、centos_streamというシンボリックリンクを作成します。

今後何らかの事情でバージョン違いのCentOS Streamをインストールする機会があるかもしれません。
その際、バージョンごとにKickstartファイルを個別に作成して管理するのは手間がかかります。
実際、CentOS Stream9で動作するKickstartファイルは、CentOS Stream8でもほぼ動作します。
そこで、今後CentOS StreamをKickstartでインストールする際は、バージョンに関わらずcentos_streamを参照する運用とします。
これによりKickstartファイルの管理が楽になるだけでなく、centos_streamが実際にどのバージョンを参照しているのか後から追うこともできます。

ln -s centos_stream9 centos_stream

(任意) 元ファイルのバックアップ

この後のセクションで、上記Kickstartファイルを一部書き換えます。
書き換える前に、上記書き換え前のファイルをoriginals/などの別フォルダにバックアップしておきましょう。

こうすることで、Kickstartをどう書き換えたか後から追うことができます。

mkdir originals
cp centos_stream9 originals/centos_stream9

(参考) Kickstartファイルの読み方

ファイルの各行にはインストール時に選択したオプション値が記述されていますが、全てを完璧に理解する必要はありません。
どういった内容かはなんとなく想像できると思いますので、気になったコマンドの仕様を調べて書き換える程度で一旦は良いと思います。

重要なコマンドについては、この後のセクションで紹介します。
コマンドの仕様をより詳細に知りたい場合は、以下のマニュアルをご確認ください。

Kickstartファイルの書き換え

本セクションでは、自動生成したKickstartファイルのうち、パーティションやLVMのサイズを書き換えます。

他にもカスタマイズすることでもっと便利にできますが、こちらは任意でご反映ください。
その他のカスタマイズについては、#(参考) その他のKickstartコマンドにて紹介します。

パーティションサイズの書き換え

自動生成したKickstartファイルには、パーティションやLogical Volumeのサイズが固定値で記述されています。
この部分を動的な表現に書き換えることで、ディスクサイズに応じたサイズの指定が可能となります。

パーティションサイズはpart、LVMのLogical Volumeのサイズはlogvolコマンドで設定されています。
該当箇所を以下に抜き出します。

# Disk partitioning information
part pv.111 --fstype="lvmpv" --ondisk=vda --size=39935
part /boot --fstype="ext4" --ondisk=vda --size=1024
volgroup cs --pesize=4096 pv.111
logvol / --fstype="ext4" --size=38908 --name=root --vgname=cs
logvol swap --fstype="swap" --size=1024 --name=swap --vgname=cs

今回はルートファイルシステムの設定行について、part--size=xxx--growに、logvol--size=xxx--percent=100に変更します。
これによって、インストール対象のVMのディスクサイズがどのようなサイズであっても、空き容量を全てルートファイルシステムに割り当てることができます。

/以外の/bootswapのサイズは1GiBだとすると、全体のディスクサイズとルートファイルシステム (/) のサイズの関係は以下のようになります。

ディスクサイズ /のサイズ
20GiB 18GiB
40GiB 38GiB

上記の実装とするには、以下の2行を変更します。
それぞれの行で--sizeが動的な表現に置き換わっています。

# part pv.111 --fstype="lvmpv" --ondisk=vda --size=39935
part pv.111 --fstype="lvmpv" --ondisk=vda --grow

# logvol / --fstype="ext4" --size=38908 --name=root --vgname=cs
logvol / --fstype="ext4" --percent=100 --name=root --vgname=cs

書き換え後のKickstartファイル

partlogvolの2行を書き換えた後のKickstartファイルを以下に示します。

# Generated by Anaconda 34.25.0.25
# Generated by pykickstart v3.32
#version=RHEL9
# Use graphical install
graphical
repo --name="AppStream" --baseurl=file:///run/install/sources/mount-0000-cdrom/AppStream

%addon com_redhat_kdump --enable --reserve-mb='auto'

%end

# Keyboard layouts
keyboard --vckeymap=jp --xlayouts='jp'
# System language
lang en_US.UTF-8

# Network information
network  --bootproto=dhcp --device=enp1s0 --ipv6=auto --activate
network  --hostname=cs

# Use CDROM installation media
cdrom

%packages
@^minimal-environment

%end

# Run the Setup Agent on first boot
firstboot --enable

# Generated using Blivet version 3.4.0
ignoredisk --only-use=vda
# Partition clearing information
clearpart --none --initlabel
# Disk partitioning information
part pv.111 --fstype="lvmpv" --ondisk=vda --grow
part /boot --fstype="ext4" --ondisk=vda --size=1024
volgroup cs --pesize=4096 pv.111
logvol / --fstype="ext4" --percent=100 --name=root --vgname=cs
logvol swap --fstype="swap" --size=1024 --name=swap --vgname=cs

# System timezone
timezone Asia/Tokyo --utc

# Root password
rootpw --iscrypted $6$GMEsgHeNZwjJWb.d$BuP/sn9FX6xKEFGYo8KzNmLB5jPl7RhFQyHnklgrK2CkQntcsT2aOCz4Ozzcefc5J7gXO7IXVvj7KdQOddpv81
user --groups=wheel --name=endy --password=$6$qtY89ARpZJNqk8ap$1u6e3CplnSGpqBppAyd/.f9fheOt74TA.fqMdPsROa3kmSSlHuOyXurBWSr5EHksAemR.3HjDjkI1n6JIeJIk1 --iscrypted --gecos="endy"

Kickstartファイルのアップロード

#書き換え後のKickstartファイルで準備したKickstartファイルをHTTP、FTP、またはNFSサーバにアップロードします。

今回はHTTPサーバを建てて、そこにKickstartファイルをアップロードする手順を紹介します。
今回の手順は初期状態のRHELディストリビューション (RHEL/CentOS Stream/Fedora) を想定しています。

まずはHTTPサーバをインストールします。
今回はApache httpdを使用します。

sudo dnf install httpd

HTTPサーバ上にKickstartファイルをアップロードします。
アップロード先のパスは、わかりやすい名称であれば何でも良いと思います。

今回はCentOS Stream用のKickstartファイルを作成するため、/var/www/html/kickstarts/centos_stream9というパスにてファイル作成します。
今後他のディストリビューション用のKickstartファイルを作成したときは、同じ階層にrhel8fedora35などのファイル名で格納する想定です。

sudo mkdir /var/www/html/kickstarts
sudo vi /var/www/html/kickstarts/centos_stream9

centos_stream9ファイルの中身は、#書き換え後のKickstartファイルの内容を貼り付けます。

firewalldが有効な場合は、HTTP通信を許可しておきます。
デフォルトではcockpitやssh通信は許可されますが、http (TCP80) は拒否されます。

HTTP通信を許可するには、以下のコマンドを実行します。
重要なのは「変更」と「設定反映」の部分です。

# 事前確認。serviceにhttpがないこと
sudo firewall-cmd --list-all

# 変更
sudo firewall-cmd --add-service http --permanent

# 差分比較
diff -u <(sudo firewall-cmd --list-all) <(sudo firewall-cmd --list-all --permanent)
# -public (active)
# +public
#    target: default
#    icmp-block-inversion: no
# -  interfaces: enp1s0
# +  interfaces: 
#    sources: 
# -  services: cockpit dhcpv6-client ssh
# +  services: cockpit dhcpv6-client http ssh
#    ports: 
#    protocols: 
#    forward: yes

# 設定反映
sudo firewall-cmd --reload

# 事後確認
sudo firewall-cmd --list-all

HTTPサーバを起動、及び有効化します。

# sudo systemctl enable httpd; sudo systemctl start httpd と同じ
sudo systemctl enable httpd --now

Kickstartの実行

ここまでの作業で準備は整いました。
いよいよ初期状態の仮想マシンを用意してKickstartを開始します。

(参考) 仮想マシンの作成

KVM配下に仮想マシンを作成する場合を例に、パラメータの例を示します。
詳細はKVMの基本操作集 - #VMの作成をご覧ください。

CockpitからVMを作成する際は、下図のようにパラメータを指定します。
通常のインストール時と同様に、ISOファイルと仮想ディスクをセットします。

vm_creation_with_cockpit

virt-installコマンドで仮想マシンを作成する場合は、例えば以下のようなコマンドを実行します。
今回は直ちにコンソール画面を操作したいので、--noautoconsoleを省略しつつ、コマンド末尾に&をつけることでバックグラウンド実行にしています。

virt-install \
--name test \
--memory 2048 \
--vcpus 2 \
--disk size=10 \
--graphics vnc \
--graphics spice \
--os-variant centos-stream9 \
--cdrom /home/shared/libvirt/images/isos/centos/CentOS-Stream-9-latest-x86_64-dvd1.iso &

インストール手順を何度か試したいとも思いますので、作成したVMを停止して削除するコマンドも添えておきます。
--os-variantの指定を省いた場合は、--storage vdaの部分が--storage hdaになることもあります。
詳細はKVMの基本操作集 - #VMの削除を参照してください。

virsh destroy test ; virsh undefine test --storage vda --snapshots-metadata --managed-save

Kickstartによる自動インストール

初回起動時はISOファイルから起動することで、Linuxのインストール画面が表示されます。

ここで "↑" キーを押してxxxにカーソルを合わせ、Tabキーを押下します。
するとブートパラメータが表示されるので、末尾にinst.ks=http://x.x.x.x/kickstarts/centos_stream9を追記します。
x.x.x.xには、構築したWEBサーバのIPアドレスを入力してください。
このパラメータを指定することで、インストール処理の冒頭でKickstartファイルをcurlコマンドでダウンロードします。

パラメータ指定の際は、以下の点をご留意ください。

  • キーボードは英字配列
  • URL部分は自身の環境に合わせて適切なIPアドレスとファイルパスを指定すること
  • inst.ksの部分は、既存のパラメータと半角スペースで区切ること

boot_parameter

うまく行けばインストールが自動的に開始され、再起動を促す画面まで一気に進みます。
一部のパラメータ指定に不備があった場合は、その部分のみ手動操作を求められます。

以上でKickstartによる自動インストールは完了です。

残りは参考情報ですので、興味がある部分のみお読みいただければと思います。

(参考) Kickstartでエラーが発生したときの対処法

Kickstartの処理途中でエラーがあった場合、エラーのない箇所のみインストールを進めたところで停止します。

以下の画像のように、追加の操作が必要な箇所には目印が付きます。
問題のある箇所のみ手動でパラメータ指定することでインストール処理が継続します。

error_kickstart

Kickstartのエラーが発生した場合は、以下のように対処します。

  • GUI画面からエラーの原因を読み解く
  • Kickstartファイルを修正する
  • Kickstartをリトライする

(参考) その他のKickstartコマンド

#パーティションサイズの書き換え以外にも、Kickstartファイルには色々なカスタマイズが可能です。
全ては紹介しきれませんが、特に興味深いコマンドのみピックアップして説明します。

EULAとfirstbootの無効化

デスクトップ環境などのGUI付きでインストールする場合のみに影響します。
CLI環境には影響ありません。

デフォルトでは、Linuxを初回起動したときにEULA (End User License Agreement) やFirstboot (ウィザード形式の初期設定画面) が表示されます。
これらの画面を無効化するには、eula --agreedを追記し、firstboot --enableを書き換えます。

# Run the Setup Agent on first boot
# firstboot --enable
firstboot --disable
eula --agreed

EULAとFirstbootの画面がどういったものかは、以下のサイトから確認できます。

私の環境ではLinuxGUI付きで初期インストールすることは滅多にありませんが、これらのオプションは指定しています。

SSH鍵の登録

Kickstartファイル内にSSH公開鍵の情報を記載することで、SSH公開鍵を自動登録できます
この設定を活用することで、量産したVMssh-copy-idを実行することなく鍵認証でSSHログインできるので非常に便利です。

鍵の指定は以下のように行います。
sshkeyコマンドに対してSSH鍵登録先のユーザー名、そして登録したい公開鍵の文字列を指定します。
SSH鍵ペアを作成済みであれば、Linuxマシンの場合は~/.ssh/配下にid_rsa.pubなどのファイル名で公開鍵ファイルが存在すると思います。
SSH鍵ペアを未作成の場合は、ssh-keygenコマンドで作成できます。

(※) 以下の公開鍵は既に破棄済みですのでご心配なく...

sshkey --username=root "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCmVHbllSO6gpk4KmzZgWFnPfxiiW2v8zYg+FssH4WRWAg1BZrroKWdsjdMLq1WjJvBdbxEvqD6x6tjSQ1nT4+QIVqLGztNK1A39KOzTDENPSdhVK5EfJMWviquG3u0BonZnBSdn3/mv8ociQ1aZFFAijncNSeZQdkgqzhL5cpched25eyMEjdh3XbRHbbb6LGpIPf/R+V9BVsUYtONOR4YPs0b5RF8K/N09L3yQ3YWmiPAP+3ywsW7U5k9Y81gQyf2yyk3Ci6ZuEHwzdepsFCpAJBSh9wgzR8TufxZTqs2c5kf9hMgw29+id9mwhfj4xc5cnSdCZu9qVByjYEjrT16SwCQQlvDytezMv4Hv9g3/q3nkXuDRmEy0KpvlRLGQeBbrL8m4fGGD9WQ2yMTPJ0BsahfcCHXbTozXODi0a15Eqf1nhslHXAotA+rGomH78wB4PRrZixzKfv0ZtgCVs91BaOrOiwwbGRaTVkio3WaApfUXUWRIwjWgd3HI+EiV1k= endy@pc"

sshkey --username=endy "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCmVHbllSO6gpk4KmzZgWFnPfxiiW2v8zYg+FssH4WRWAg1BZrroKWdsjdMLq1WjJvBdbxEvqD6x6tjSQ1nT4+QIVqLGztNK1A39KOzTDENPSdhVK5EfJMWviquG3u0BonZnBSdn3/mv8ociQ1aZFFAijncNSeZQdkgqzhL5cpched25eyMEjdh3XbRHbbb6LGpIPf/R+V9BVsUYtONOR4YPs0b5RF8K/N09L3yQ3YWmiPAP+3ywsW7U5k9Y81gQyf2yyk3Ci6ZuEHwzdepsFCpAJBSh9wgzR8TufxZTqs2c5kf9hMgw29+id9mwhfj4xc5cnSdCZu9qVByjYEjrT16SwCQQlvDytezMv4Hv9g3/q3nkXuDRmEy0KpvlRLGQeBbrL8m4fGGD9WQ2yMTPJ0BsahfcCHXbTozXODi0a15Eqf1nhslHXAotA+rGomH78wB4PRrZixzKfv0ZtgCVs91BaOrOiwwbGRaTVkio3WaApfUXUWRIwjWgd3HI+EiV1k= endy@pc"

私の環境でも検証環境へのログインを簡略化するため、このオプションを活用しています。

パスワードの変更

ここでは、Kickstartによって自動作成するユーザーのパスワード文字列を変更する方法をご説明します。
rootpwuserコマンドの後に指定されているパスワード文字列を変更します。
元々は以下のようにハッシュ文字列でパスワードが記述されています。

# Root password
rootpw --iscrypted $6$GMEsgHeNZwjJWb.d$BuP/sn9FX6xKEFGYo8KzNmLB5jPl7RhFQyHnklgrK2CkQntcsT2aOCz4Ozzcefc5J7gXO7IXVvj7KdQOddpv81
user --groups=wheel --name=endy --password=$6$qtY89ARpZJNqk8ap$1u6e3CplnSGpqBppAyd/.f9fheOt74TA.fqMdPsROa3kmSSlHuOyXurBWSr5EHksAemR.3HjDjkI1n6JIeJIk1 --iscrypted --gecos="endy"

パスワード文字列は--isencrypted--plaintextオプションに書き換えることで平文で表現できます。
しかし、セキュリティの観点からハッシュ文字列でパスワード表記することをおすすめします。
平文に対応するハッシュ文字列を調べるには、以下のコマンドを実行します。
対話式でパスワード入力を2度求められるので入力すると、入力した文字列に対応するハッシュ値が得られます。
以下はパスワード文字列としてmypasswordと入力したときの結果です。

python -c 'import crypt,getpass;pw=getpass.getpass();print(crypt.crypt(pw) if (pw==getpass.getpass("Confirm: ")) else exit())'
# python -c 'import crypt,getpass;pw=getpass.getpass();print(crypt.crypt(pw) if (pw==getpass.getpass("Confirm: ")) else exit())'
# Password: 
# Confirm: 
# $6$roXC8Mf0XMhtK5k5$4a5V/qBhNAoFKDXnWbbMNy4jfCYUmg3cTo0FxpIespWhzEQ/be9AxzgO8tF.hDK89X7LI9b0ZHd8iwywBijzL0

上記スクリプトにはSaltを加えてからハッシュ計算するため、同じ入力値を渡して複数回実行した場合も異なるハッシュ値を得られます。
実際は2つのユーザーに対して同じパスワードを設定する場合も、異なるハッシュ値を渡すことで見かけ上はセキュアにできます。
攻撃者があるユーザーのパスワード文字列を特定した場合、同じパスワード文字列を別のユーザーにも試すと思うので結局意味はありませんが...。

もう一度スクリプトを実行してmypasswordを入力し、以下の文字列を得られたとします。

# $6$D3K4t69AP7mf2VSH$A/7neqAqFNusKz.4LOEUscfDlua.mavpRUgM9/c6kjhKw1XI8MqZjFY9FcBwUjjlcbBIjElyzvDwxpmcfGNAq1

これらの文字列をパスワードとして指定することで、rootユーザーとendyユーザーのパスワードをそれぞれmypasswordに変更します。

rootpw --iscrypted $6$roXC8Mf0XMhtK5k5$4a5V/qBhNAoFKDXnWbbMNy4jfCYUmg3cTo0FxpIespWhzEQ/be9AxzgO8tF.hDK89X7LI9b0ZHd8iwywBijzL0

user --groups=wheel --name=endy --iscrypted --gecos="endy" --password=$6$D3K4t69AP7mf2VSH$A/7neqAqFNusKz.4LOEUscfDlua.mavpRUgM9/c6kjhKw1XI8MqZjFY9FcBwUjjlcbBIjElyzvDwxpmcfGNAq1

最後に補足ですが、userコマンドに指定されている--gecosは、いわゆるfull nameのことです。
認証動作には直接影響しないパラメータですので、わかりやすい名前を設定すると良いでしょう。

インストール完了後の電源オフ、再起動

以下のキーワードによって、インストール完了後の動作を変更することができます。
いずれか一つのみ指定してください。

  • halt: 手動インストールの時と同じく、"Reboot"ボタンを表示して停止する
  • shutdown: シャットダウンする (poweroffよりも時間がかかる)
  • poweroff: 電源を落とす
  • reboot: 再起動する

デフォルトはhaltです。
別のパラメータを指定することで、インストールをより完全自動化に近づけることができます。

rebootを指定する場合は注意してください。
起動の優先順位の設定はデフォルトで "disk" がトップになっていると思いますが、CDROMなどがトップに設定されている場合は再起動ループになる可能性があります。
再起動ループになったら気付ける仕組みがあり、その後手動でも良いので強制的に電源OFFできる環境がある場合はrebootにしても問題ないのかなと思います。
そうではない場合は、poweroffshutdownが便利だと思います。

ただ私のKVM環境では、poweroffを指定した場合も何故かVMが直ちに起動することがありました。
つまり、rebootと同じ挙動になりました。
インストール完了後にshutdown -P nowpoweroff相当のコマンドを実行した場合は直ちに電源が切れ、再起動することはありませんでした。
poweroffの挙動はハードウェア (仮想マシンの場合はエミュレートされた仮想ハードウェア) に依存するとのことなので、たまに不思議な挙動をしても「そういったものだ」と割り切ることにしています。
参考: poweroff

逆に不思議な挙動が絶対に嫌な場合は、他の選択肢を取るのが良いと思います。
つまり電源を落としたいならshutdown、再起動したいならreboot、ユーザーの操作をはさみたいならhaltということになります。
お使いの環境でpoweroffが安定するかどうかは、実機で何度か検証してご確認ください。

私の環境ではrebootを使用しています。

Post install scriptの追加

Kickstartの処理の最後にスクリプトによる自動処理を混ぜることも可能です。
デフォルトのインタープリタ/bin/shですが、%post --interpreter=/bin/pythonのようにインタープリタを指定することが可能です。
参考: %postセクション

指定方法は簡単で、以下のように%postセクション内にスクリプトを記述するだけです。
以下に例を示します。

最初の2行のtouchコマンドでそれぞれファイルを生成します。
その後、3行目のコマンドで/interpreterファイルを生成し、中に%postセクション内の処理を実行中のシェルコマンドを記述します。

%post
touch /hello
touch /its_a_beautiful_day
echo `ps -p $$ -o comm | tail -1` > /interpreter
%end

上記の処理をKickstartファイルに追記してインストールしたLinuxには、スクリプトに記述したとおりファイルが生成しています。

ls /hello 
# /hello

ls /its_a_beautiful_day 
# /its_a_beautiful_day

cat /interpreter 
# /bin/sh /tmp/ks-script-1rh4gjta

Ansibleのようなオーケストレーションツールを使えば、初期インストール後の処理を記述することは容易です。
ですがKickstartファイルでも同じ処理ができることを知っておくと、検証環境を即席で量産するときに便利なこともあるかもしれませんね。

RHELの場合、%postセクションにsubscription-managerコマンドを記述することでシステムの登録まで自動化する使い方が便利そうです。

私はまっさらなMinimum Installの環境がほしいので、Post install scriptはあまり利用していません。

(参考) DVD ISOファイルが必要な理由

#ISOファイルの入手の補足です。

DVD ISOファイルの場合、ISOファイルの内部にRPMパッケージリポジトリを含みます。
そのため、ISOファイル単体でインストール処理全てを完結できます。

一方で、Boot ISOファイルにはRPMパッケージリポジトリを含みません。
CentOS Stream8/RHEL8以降はAppStreamリポジトリからRPMパッケージをインストールします。
Boot ISOの場合は、オンラインリポジトリを指定する必要があります。
手動インストールの場合は "Closest Repository" を選択することでオンラインリポジトリのURLを意識することなくインストール処理を進めることができます。
ところが、Kickstartファイルには "Closest Repository" 相当のオプションが存在しません。

"Closest Repository" を選択した場合のanaconda-ks.cfgファイルのリポジトリ指定がどのように記載されるかと言うと、現状は「何も記載されません」。
具体的には、repoによるAppStreamリポジトリの指定や、urlcdromによるRPMインストールソースの指定が何も書いていない状態になります。

通常であればanaconda-ks.cfgファイルをそのままKickstart処理に利用してもKickstartは成功するはずですが、 "Closest Repository" オプションを利用した場合は例外的に失敗します。

"Closest Repository" 相当のKickstartの指定が存在しないことは2013年から指摘されていますが、2022年現在も実装されていません。
したがって、この挙動は今後も変わることはないと思います。
参考: Bugzilla #972265

以上の理由から、Kickstartを実行する際にはRPMパッケージを内包しているDVD ISOファイルを使用してインストールを行います。

参考URL

Red Hat社のマニュアルに一通りの情報が書いてあります。
RHEL8 - Performing an advanced RHEL installation

上記マニュアルの中でも、本記事に特に関連するセクションを抜粋して下表にリンクをまとめました。
一次情報を確認したい方は、以下からご覧ください。

"B. Kickstart commands and options reference" にはKickstartファイルに出てくるコマンドが全て説明されています。
このセクションは、Kickstartファイルを書く方はにとって特に役に立つでしょう。

URL 内容
4. Creating
Kickstart files
  • Kickstartファイルの作り方
  • 今回は4.2のanaconda-ks.cfgを書き換える手法を選択
5. Making Kickstart files
available to the
installation program
  • Kickstartファイルの配置方法
  • 今回は5.3のHTTPサーバを利用
7. Starting Kickstart installations
  • 初期状態のサーバを起動後、Kickstartを開始する手順
  • 今回は7.1の手動起動の手順を実施
9. Maintaining
Kickstart files
  • ksvalidatorコマンドのインストール方法、及び使い方
16. Boot options
  • 初期状態のサーバを起動後、Boot Optionを指定する方法
  • 今回は "Editing the > prompt" の手順を実施
A. Kickstart script
file format reference
  • Kickstartファイルの構造
  • %がセクション、#がコメント
  • 他は知らなくてもほぼ困らない
B. Kickstart commands
and options reference
  • Kickstartファイルに出てくるCommandの意味
  • 本記事で取り扱わなかったコマンドも全て書いてある

まとめ

Kickstartを手動でトリガーし、HTTPを介して外部サーバと連携してインストールを自動化する構成を紹介しました。

Kickstartを手動トリガーするために、インストーラを起動した後にinst.ks=http://xxxという文字列をコンソール画面で入力するのが若干の手間です。
しかし、この構成はどのような環境においても簡単に実現できます。
構築も簡単です。

Kickstartに始めて挑戦するときのお試し構成として、こちらの構成をまずは組んでみるのはいかがでしょうか?
Linuxの手動インストールに比べると、この構成でも十分便利です。
ぜひご活用ください。

関連記事

SELinux 参考URL

SELinux_logo

SELinuxシリーズ

本記事は、SELinuxシリーズの7記事目です。

  1. Linuxプロセスアクセス制御の概要
  2. SELinuxの概要
  3. SELinux Type Enforcement
  4. SELinuxの実践
  5. (参考) SELinuxのRBAC、UBAC、MLS、MCS
  6. (参考) SELinux Module Policyのソースコード読解、ビルド
  7. 参考URL ←今ココ

1〜3記事目は、4記事目を理解するための前提知識をカバーしています。
4記事目が最も重要で、SELinuxの具体的な操作方法やコマンド、トラブルシューティング手順を紹介しています。

5記事目以降は参考情報です。

SELinuxの関連記事は、SELinuxタグから探せます。

一連の記事はFedora環境を前提として書いています。
FedoraRHELに類するディストリビューションであればほぼ同等の挙動になると思いますが、他のディストリビューションでは挙動に差異がある可能性があるのでご注意ください。

お伝えしたいこと

私がSELinuxの概要を理解するにあたり、特に役に立った参考URLを列挙します。

これらのURLは、本ブログのSELinuxタグの記事において、参考URLとして随所に登場します。

The SELinux Notebook

SELinuxの開発者コミュニティが更新しているドキュメントです。
つまり、こちらが公式ドキュメントです。
非常に丁寧に書かれている上、内容もしっかり更新されています。

本ブログで扱っている情報の大半は、こちらのドキュメントから引用しています。
こちらに書いてある内容を全て理解できれば、SELinuxの知識量としては問題ないと思います。

開発者目線の記述もあるので私達が読む上では若干取捨選択が必要となるものの、十分に理解しやすいです。
一部機能のユースケースが書いてないため理解が難しい箇所や、本文中で使われている単語の定義が不明な部分もあるので、そういった部分は他のドキュメントから補完すると良いと思います。

GitHubMarkdownファイルが最新版です。
MakefileをビルドしてPDF出力したものもアップロードされているので、お好みの方をご利用ください。

参考URLを指すときに特定のセクションにジャンプさせるため、本ブログではMarkdown版の方を参考URLとして貼っています。
ただ、手元で読む分にはCtrl+F全文検索できるのでPDF版も便利かなと思います。

Security-Enhanced Linux for mere mortals

Red Hat Summit 2018で発表されていたプレゼンテーションです。
YouTubeの字幕機能が使えるので、英語リスニングが苦手でも大丈夫です。

こちらのプレゼンテーションは、以下の点でおすすめです。

  • 前提知識不要で、非常にわかりやすい
  • SELinuxの中でも使いそうな機能に絞り、実用的なテクニックとともに教えてくれる
  • ジョークが面白い

私は一番最初にこの動画を見てSELinuxの概要を理解しました。

念のため発表資料のURLも貼りましたが、YouTubeだけ見れば十分です。

SEの道標

日本語のブログで、SELinuxに限らずインフラ全般について詳しく、わかりやすく、図解付きで解説してくれています。
過去の日付に投稿された記事についてもこまめに最新化されています。

SELinuxについても、概要から詳細に至るまで図解付きで丁寧に解説されています。
英語に抵抗のある方は特におすすめです。

このブログは、xotakiさんからTwitterで教えていただいたことで知りました。
教えてくださり、ありがとうございます。

SEの道標 - SELinux

とほほのSELinux入門

日本語のブログで、SELinuxの情報が簡潔かつ詳細に書いてあります。
短い文書で情報をわかりやすく、網羅性高く表現されています。

非常に濃い内容を短い時間で学べますので、SELinuxを学ぶ方にはぜひチェックいただきたいと思います。

SELinux以外にもプログラミング言語やWEB系のフレームワークLinuxの関連情報も充実しています。
私も最近知ったばかりのサイトですが、このサイトでいっぱい勉強させていただきたいと思います。

とほほのSELinux入門

Gentoo Wiki

Gentoo WikiSELinux記事は情報量が多く、綺麗にまとまっていてわかりやすいです。
まずはQuick introductionでSELinuxの動きをなんとなく掴み、Tutorialsで段階的に学習できる作りになっています。

Gentoo Wikiは、わかりやすさと詳しさの面で一番おすすめできる記事です
英語かつ多少文字数が多いかもしれませんが、読み進める価値はあります。
特にSELinux Tutorialsは予備知識なく理解できる上に、The SELinux Notebookと比較しても実用的な内容に絞って詳細に説明されています。
SEの道標やThe SELinux Notebookと並行して読み進めれば、SELinuxを詳しく理解できると思います。

GentooRHELとは異なるディストリビューションのため、SELinuxの一般論を参考にする読み方をおすすめします。
例えばコマンドの出力は具体例として理解の助けにしても問題ないですが、自分の環境ではどのような出力になるかは別途確認する必要があるでしょう。

RHEL8 - Using SELinux

RHEL8の公式マニュアルです。
Red Hat社公式という安心感の他に、以下の点でおすすめです。

  • RHELの環境を前提に書かれている
  • 実用的かつ具体的な実装手順が複数紹介されている

このドキュメントにしか書かれていないわかりやすい表現もいくつかあります。
ただこれらはいずれも応用的な知識なので、上述のドキュメントでSELinuxの概要を理解した後に読むと良いかもしれません。

このドキュメントならではのおすすめは、主に以下の部分です。
他にもPodmanやAnsibleに特化したセクションもありました。

↓トップページはこちらです。

まとめ

SELinuxの参考URLを紹介しました。
SELinuxに興味のある方は、ぜひチェックしてみてくださいね。

一連のSELinuxシリーズの記事はこれで一段落です。
2020年10月中旬から2021年1月下旬までSELinuxを調べてまとめ続けていたので、実に3ヶ月もの間SELinuxにかかりきりでした。
年末年始や有給取っているときもほぼず〜っとSELinuxを調べていました。

大変な労力がかかりましたが、一連の記事が読者の皆様や私自身の将来に役に立つ内容であることを願っています。

SELinux Module Policyのソースコード読解、ビルド

SELinux_logo

SELinuxシリーズ

本記事は、SELinuxシリーズの6記事目です。

  1. Linuxプロセスアクセス制御の概要
  2. SELinuxの概要
  3. SELinux Type Enforcement
  4. SELinuxの実践
  5. (参考) SELinuxのRBAC、UBAC、MLS、MCS
  6. (参考) SELinux Module Policyのソースコード読解、ビルド ←今ココ
  7. 参考URL

1〜3記事目は、4記事目を理解するための前提知識をカバーしています。
4記事目が最も重要で、SELinuxの具体的な操作方法やコマンド、トラブルシューティング手順を紹介しています。

5記事目以降は参考情報です。

SELinuxの関連記事は、SELinuxタグから探せます。

一連の記事はFedora環境を前提として書いています。
FedoraRHELに類するディストリビューションであればほぼ同等の挙動になると思いますが、他のディストリビューションでは挙動に差異がある可能性があるのでご注意ください。

お伝えしたいこと

SELinuxのSecurity Policyは、ソースコード (Module Sources) をビルド・パッケージ化・インストールすることで使えるようになります。

SELinuxの運用でソースコードを扱うことはほとんどありません。
既存のルール把握であればsesearchで、ちょっとした変更であればsemanageで事足りるためです。
しかし、以下のような状況でソースコードを自力で書くことも稀にあります。

  1. 既に制御されているプロセス用にallowルールを追加する
  2. 全く制御されていないアプリケーションを新たに制御する

本記事では、上記のような状況に対処するためにModule Sourcesの理解に必要な基礎知識に触れます。

具体的には、以下のトピックを扱います。

  • Base PolicyとModule Policy
  • ソースコードリポジトリ
  • Module Policyのビルドの流れ
  • Module Sourcesの記述言語
  • Module Sourcesの読み解き (簡単な例のみ)

最初の#Base PolicyとModule Policyは基礎知識として重要ですが、他のセクションについてはある程度順不同に読んで問題ありません。
興味のあるトピックを拾い読みいただければと思います。

Base PolicyとModule Policy

/etc/selinux/SELINUXTYPE/policy/policy.NNに格納されているSecurity Policy Fileは、アクセス制御ルール全てを内包する単一のバイナリファイルです。
このファイルは、内部的には以下の2つに分かれています。

  • Base Policy
  • Module Policy

Base Policyは、SELinuxのコアとなる共通ルールが含まれたバイナリファイルです。
具体的には、UserとRoleの定義、SID (Security ID) の定義、Linux kernel関連のType定義やアクセス制御ルールなどが含まれています。

Module Policyは、アプリケーションごとのアクセス制御ルールが含まれたバイナリファイルです。
httpdのModule PolicyやsshdのModule Policyなど、アプリケーションごとにファイルが分かれています。

SELinuxは、1つのBase Policyに多数のModule Policyをインストールすることで全体のSecurity Policyを構成する、Loadable Module Policy構成を取っています (※)

(※) Security Policyには2種類の構成があります。BaseとModuleが分離しているLoadable Module Policy構成と、Module Policyを全てBase Policyに組み込んでビルドするMonolithic Policyです。Loadable Module Policyは、semoduleコマンドによってLinuxを起動したままModule Policyのインストール、アンインストールが可能な構成です。Monolithic Policy構成では、そのような制御は不可能です。Monolithic Policyは、Androidなどリソースの限られた環境にSELinuxをインストールする際に使われる構成です。Base Policyも含めてSecurity Policy全体をビルドするときに、build.confファイルのMONOLITHICというオプションでどちらの構成にするかを指定できます。通常のLinuxでは、Loadable Module Policy構成でビルドしたものが使われています。1

Module Policyはアプリケーションごとに細分化されており、semoduleコマンドによってLinuxを再起動することなくインストール、アンインストールできます。

ただし、実機で試してみたところアンインストールできるのは後から自分で追加したModule Policyのみのようでした。
初期インストールされているModule Policyは、同名かつ異なるPriority値を持つ別のModule Policyを代わりにインストールしない限り、アンインストールには失敗しました。

sudo semodule -lfull | grep apache
# 100 apache            pp

sudo semodule -X 100 -r apache
# libsemanage.semanage_direct_remove_key: Removing last apache module (no other apache module exists at another priority).
# Failed to resolve booleanif statement at /var/lib/selinux/targeted/tmp/modules/100/awstats/cil:232
# Failed to resolve AST
# semodule:  Failed!

Module Policyのビルドの流れ

Module Policyの記述言語は、以下の2種類があります。

  • Kernel Policy Language
  • CIL (Common Intermediate Language)

本セクションではそれぞれの言語のビルド手順を紹介していますが、重要なのはKernel Policy Languageのビルド手順です。
RHEL8のマニュアルで主にカバーされているのは、Kernel Policy Languageのビルド手順であるためです (リンク)。

各言語の詳細については、#Module Sourcesの記述言語を参照してください。

Kernel Policy Languageのビルド

ビルド処理の概要

ビルドの手順は複数存在します。
例えば、全てのBase PolicyとModule Policyをソースコードからビルドする手順2もありますが、私達が使うことはもちろんありません。

今回行うのはModule Policyのビルドです。
既存のReference Policyに新たなModule Policyを追加し、インストールします。

主に以下のようなユースケースでModule Policyを作成します。

  • 既存のルールではアクセス許可設定が足りない場合
  • 既存ではアクセス制御対象外のプロセスをアクセス制御対象に追加する場合

Module Policyのビルドは、ざっくり言うと以下の流れで進みます。
図中のXは、apachesshなどのModule Policy名が入ります。

  • テキストファイルとしてソースコードを書く (X.te, X.fc, X.if)
  • ソースコードをPolicy Packageに加工する (X.pp)
  • Policy Packageをインストールする

how_security_policies_are_built

より詳細な流れについては、次のセクションで説明します。

ビルド処理の詳細

本セクションでは、Module Sourcesの詳細なビルド手順を説明します。
SELinuxの運用において、ビルドの内部処理を理解する必要性はあまりありません。
以下のサマリの図を中心に確認の上、詳細な説明は気になる部分のみを拾い読みいただければと思います。

まずは、処理の全体像を示します。
下図は、ビルド手順中で実行されるシェルスクリプトMakefileの処理内容を絵に描き起こしたものです。

module_policy_build_and_installation_flow

ここから、上図の処理内容を文字ベースで詳細に説明します。
カッコ付き数字の部分は、図中の番号と合わせてあります。
実際に手を動かしてビルドする手順については、SELinuxの実践 - (参考) Custom Policyのビルドを参照してください。

まず、ビルドするために前提として必要なパッケージをインストールします。

sudo dnf install policycoreutils-devel

(1)
今回SELinuxでアクセス制御する対象となるアプリケーションを用意します。
実行ファイルは、予め最終的な配置場所に移動しておいてください。
例えば個人用なら~/.local/bin/X、全体で共有するなら/usr/local/bin/Xなどです。
実行ファイルのパスは、この後自動生成するX.shX.fcX_selinux.specなどのファイルの中身に影響します。
今回はお試しなので、ワーク用のディレクトリに格納します。

mkdir ~/work
touch ~/work/X
cd work

(※) Xという実行ファイルが存在しなくても、(2)のsepolicy generate --init Xコマンドは通ります。しかし、その後実行するシェルスクリプトはファイルXが存在しないと途中でエラーになるので、ここで作成しておきます

(2)
以下のコマンドで、Module Sourcesのテンプレートを自動生成します。
ファイルの内訳は、以下のとおりです。

X_selinux.specは、X.shの末尾に書かれているrpmbuildコマンドによってrpmを自動生成するためのファイルです。
Security Policyのビルド・インストール処理とは直接関係ありません。

sepolicy generate --init X
# Created the following files:
# /home/endy/work/X.te # Type Enforcement file
# /home/endy/work/X.if # Interface file
# /home/endy/work/X.fc # File Contexts file
# /home/endy/work/X_selinux.spec # Spec file
# /home/endy/work/X.sh # Setup Script

ls -l
# -rw-r--r--. 1 endy endy    0 Nov 30 01:43 X
# -rw-r--r--. 1 endy endy   65 Nov 30 01:44 X.fc
# -rw-r--r--. 1 endy endy  675 Nov 30 01:44 X.if
# -rw-r--r--. 1 endy endy 1623 Nov 30 01:44 X_selinux.spec
# -rwxr-x---. 1 endy endy 1407 Nov 30 01:44 X.sh
# -rw-r--r--. 1 endy endy  426 Nov 30 01:44 X.te

実際の開発であれば、ここでX.fcX.ifX.teに必要なルール定義を記述します。
今回はビルドの内部処理の紹介なので、特に何も追記せずそのまま進めます。

(3)
以下のコマンドで、シェルスクリプトを起動します。
(10)で実行しているsemoduleコマンドの実行にroot権限が必要なので、sudoをつけて実行します。

sudo ./X.sh

このコマンドにより、(4)〜(12)の処理が全て実行されます。

モジュールのビルドとインストールの部分だけ抜き出すと、X.shは以下の2コマンドと同等の処理を行っています。

# make -f /usr/share/selinux/devel/Makefile X.pp
# sudo semodule -i X.pp

(4)
X.shの中でmake -f /usr/share/selinux/devel/Makefile X.ppが実行され、Makefile内の処理が実行されます。
Makefile内で更にinclude文が実行され、/usr/share/selinux/devel/include/Makefileが呼び出されます。
このMakefileの処理により、(5)〜(7)の処理が実行されます。
makeコマンドのターゲットにX.ppが指定されているので、Makefileの中で%.pp:で始まる行の処理が実行されます。
そして、依存関係を解決するために各種処理が実行されます。

ご参考までに、Makefileの文法はGNU makeの公式ドキュメントから調べることができます。

(5)
X.if/usr/share/selinux/devel/include/配下の全ての.ifファイル (つまりLinux Distributionに付属する全ての.ifファイル) に対してm4コマンドを実行し、Macroを評価しつつマージしたtmp/all_interfaces.confを生成します。
tmp/all_interfaces.confは、全てのInterface Callが含まれた長大なテキストファイルです。
Interface Callとは、簡単に言うと「関数」のようなものです。
Interface Callについて、詳細は#External Interface Call File (.if)にて説明します。

(6)
X.tetmp/all_interfaces.confをm4 Macroでマージしつつ評価し、tmp/X.tmpファイルを出力します。
ここでMacroが展開されることで、Kernel Policy Languageで書かれたソースコードになります。

m4 MacroとKernel Policy Languageの関係については、#Module Sourcesの記述言語にて説明します。

(7)
tmp/X.tmpcheckmoduleコマンドに渡し、Kernel Policy Languageをコンパイルします。
コンパイルの結果、tmp/X.modというバイナリファイル (※) が生成します。
(※) このファイルをPolicy Moduleといいます

(8)
X.fcをm4 Macroで評価し、X.mod.fcファイルを生成します。
X.mod.fcは、File Contextの定義をKernel Policy Languageで記述したテキストファイルです。

(9)
semodule_packageコマンドにtmp/X.modtmp/X.mod.fcを渡し、Policy Packageファイル (X.pp) を生成します。
Policy Packageは、semodule_packageコマンドでインストール可能なバイナリファイルです。
Makefileの処理はここでおしまいです。

(10)
ここでX.shの処理に戻ります。
semodule -i X.ppというコマンドを発行し、Module Xをインストールします。
これにより、Security Policyファイルが書き換わります。

ここまでの流れを要約すると、以下のようになります。

項番 処理内容
〜(2) Module Sources (X.te, X.if, X.fc) を用意する
(3)〜(7) X.teX.ifcheck_moduleコンパイルし、Policy Modules (tmp/X.mod) を生成する
(8) X.fcm4で評価し、X.mod.fcを生成する
(9) Policy ModulesとX.mod.fcsemodule_packageでパッケージ化し、Policy Package (X.pp) を生成する
(10) semoduleでPolicy Packageをインストールする

以降の(11)、(12)はおまけの処理です。

(11)
sepolicy manpage -p . -d X_tというコマンドにより、X_tドメインに関するmanページファイルを生成します (X_selinux.8)
生成したmanファイルはカレントディレクトリに配置されます。
この時点では、mandbに登録されるフォルダパスには配置されません。

(12)
X.shの最後の処理で、rpmbuild -ba X_selinux.specコマンドによってrpmファイルが作成されます。
rpmファイルの中身は、X_selinux_specに定義されています。

rpmファイルの使い方については、SELinuxの実践 - (参考) Custom Policyのビルドを参照してください。

以上で、図に表現したSecurity Policyのビルド・インストール処理の説明はおしまいです。

後片付け

Module Xをアンインストールし、workフォルダを削除することで構成を元に戻します。

sudo semodule -r X
# libsemanage.semanage_direct_remove_key: Removing last X module (no other X module exists at another priority).

cd
sudo rm -rf ~/work/

(参考) CILのビルド

CILファイルを作成したときのインストールは簡単です。
CILファイルを記述して、semodule -iでインストールするだけで完了します。

RHEL8 - Using SELinux - 7.3 Creating a local SELinux policy moduleに掲載されている具体例を使って試してみましょう。

以下の内容を持つY.cilファイルを作成します。
ファイル名に使われているYがModule名として扱われるので、実際にインストールするときはご注意ください。

(allow cupsd_lpd_t cupsd_var_run_t (sock_file (read)))

後は、以下のコマンドでY.cilをインストールすればおしまいです。

sudo semodule -i Y.cil

インストールされたことを確認します。
その後、アンインストールしてもとに戻しておきましょう。

sudo semodule -lfull | grep Y
# 400 Y                 cil         

sudo semodule -r Y
# libsemanage.semanage_direct_remove_key: Removing last Y module (no other Y module exists at another priority).

sudo semodule -lfull | grep Y
# (出力なし)

ソースコードリポジトリ

SELinuxのSecurity Policyのソースコードリポジトリは2種類あります。

  1. Reference Policy (Upstream)
  2. Reference Policy (Fedora)

Upstreamのリポジトリは、SELinuxコミュニティによって維持管理されています。
2のFedora版Reference Policyのリポジトリは、1のUpstreamをベースにカスタマイズして作られています。

FedoraRHELなどのLinuxにデフォルトでインストールされているSecurity Policyは、Fedora版Reference Policyをビルドしたものです。
したがって、本記事においてもソースコードを参照する際は2のFedora版のリポジトリから引用します。

Reference Policyのカスタマイズは、ディストリビューションごとに独自に行われています。
例えば、Gentoo LinuxFedoraとはまた別のReference Policyのリポジトリを独自に持っています (リンク)。

以下のリンクにReference Policyのファイル構成についての説明があるので、参考までに載せておきます。

Module Sourcesの記述言語

Security Policyのソースコードを記述する言語は、2種類あります3

  • Kernel Policy Language
  • CIL (Common Intermediate Language)

2022年現在、主に使われているのはKernel Policy Languageです。
本記事でもKernel Policy Languageを中心的に扱います。

Kernel Policy Language

Kernel Policy LanguageによってSecurity Policyの定義を記述する際、複数の仕組みをごちゃ混ぜにして利用します4
具体的には以下の3つです。

Module Policyのソースコード (Module Sources) は、m4 Macro processor > Kernel Policy Language Compilerの順に評価されます。
したがって、人間が最初に書くコードは、Kernel Policy Languageというよりもm4 Macroです。

Module Sourcesをm4コマンド (m4 Macro processor) に渡すと、Macroが展開されてKernel Policy Languageが生成するという流れです。

もちろんKernel Policy Languageを直接書いても良いのですが、m4 Macroを使って記述することで同じ記述を繰り返しコピー&ペーストせずに表現でき、ソースコードの見通しが良くなるというメリットがあります。

m4 Macroで書いているといっても、m4 Macroの文字列リテラルとしてKernel Policy Languageをほぼそのまま書いている部分も多数存在します。
リテラルではない部分がMacroです。

Kernel Policy Languageとm4 Macroの見分け方は、私の経験上以下のような見分け方があります。

  • Macro名()のように、引数を取る関数やクラスのような見た目の行はm4 Macroを呼び出している
  • 行末に;がつくものは、ほぼKernel Policy Statement
  • 行末に;がつかないものは、ほぼm4 Macro

とはいえ、m4 MacroとKernel Policy LanguageのStatementはごっちゃになりやすいので、本記事においてはKernel Policy LanguageについてはStatement、m4 MacroについてはMacroという表現に統一します

上記3種類の言語の具体的な使われ方については、#Module Sourcesの読み解きにて紹介します。

(参考) CIL (Common Intermediate Language)

CILの概要

CIL (Common Intermediate Language) は、Kernel Policy Languageよりも後にできた言語です5
Kernel Policy Languageとは異なり、m4 Macroのような外部の仕組みを使わず、少ない記述量で高度な機能を持たせることができます。

CILの仕様は、以下のGitHubのドキュメントに書いてあります。
https://github.com/SELinuxProject/selinux/tree/master/secilc/docs

現状、以下の理由からCILを自分で書くことはありません。
私達が書くのは、基本的にm4 macroとKernel Policy Languageです。

  • 現状、Red Hat社がサポートしているSecurity Policyの作成方法はKernel Policy Languageによる記述6
  • 現状、自分で書いたCILによるSecurity Policyの追加はRed Hat社によってサポートされない7

CILで記述されたModule Policy

現状、CILで書かれているModule Policyは1つしかありませんでした。

sudo semodule -lfull | grep cil
# 100 permissivedomains cil

他は全てKernel Policy Languageで書かれています。
ppとはPolicy Packageの略で、Kernel Policy Languageをコンパイル・パッケージ化することで生成するPolicy Packageファイルの拡張子でもあります。

sudo semodule -lfull | grep pp
# 200 cockpit           pp
# (以下略)

CILの例

以下にCILで書いたアクセスallowルールの例を示します。
参考元: RHEL8 - Using SELinux - 7.3. Creating a local SELinux policy module

(allow cupsd_lpd_t cupsd_var_run_t (sock_file (read)))

同じ定義をKernel Policy Languageで記述すると、以下のようになります。
CILと比較すると、modulerequireがある分だけ記述量が増えています。

module local_cupslpd-read-cupssock 1.0;

require {
    type cupsd_var_run_t;
    type cupsd_lpd_t;
    class sock_file read;
}

#============= cupsd_lpd_t ==============
allow cupsd_lpd_t cupsd_var_run_t:sock_file read;

m4 Macro

m4 Macro自体は、SELinuxとは独立した言語です。
このMacroは、文字列やMacroの引数を解析して別の文字列を生成しています。

SELinuxにおいては、m4 MacroからKernel Policy Languageのソースコードを生成する使い方をしています。
そしてKernel Policy Languageをコンパイル、インストールすることでSecurity Policyのバイナリファイルが生成するという流れです。

m4 Macroを利用することで、Kernel Policy Languageを楽に記述することができます。
特に#External Interface Call File (.if)において、繰り返し記述する処理をマクロ化 (≒関数化) するのに役立っています。

各Macroの意味については、以下を参照してください。
The SELinux Notebook - Reference Policy - #Reference Policy Support Macros

define文自体の仕様が気になる方は、m4 Macroのドキュメントを参照してください。
GNU M4 macro processor - #Define a macro

m4 Macroの概要

m4についてはあまり深く触れませんが、この先に出てくるSource Moduleで使われている表現を理解するために最低限必要そうな部分のみ触れます。

  • m4は標準済のMacroを使い、文字列置換してファイル生成したり、シェルコマンドを実行したりできる
  • 上から1行ずつ「評価」される
  • Macroは、他の言語で言うところの関数のようなもの
  • リテラルは、以下★1のように表記する (バックティックとシングルクォーテーションで囲う)
  • xのようにリテラルではない文字列は、Macroとして扱われる
  • Macroが評価されると、展開されて別の文字列に変化する
  • #以降はコメントになる
  • define()でMacroを作成できる。第一引数がMacro名、第二引数が処理内容
  • define()の中で$1, $2, ...は、Macroを読んだときの第N引数の値として評価される8
# ★1
`XXX'

具体的な例を3つ示します。
Module Sourcesを書く方はお読みいただければと思いますが、そうでない方はスキップして問題ありません。

(参考) 例1. 文字列リテラルとMacroの評価

m4 macroのソースコードが以下のように書かれていたとします9

define(`x', `substr(ab')
define(`y', `cde, `1', `3')')
x`'y

1行目と2行目は、それぞれmacroの定義です。
第一引数のxyがmacro名で、第2引数がmacroが展開された後の文字列を表しています。

例えば、xは以下のように展開されます。

# x
substr(ab

yは以下のように展開されます。

# y
cde, `1', `3')

ソースコードの3行目は、少々変わった表記になっています。
xyを区切っているのは、 "空白" のリテラルです。
xyというMacroではなく、xyという2つのMacroであることを示すために、このような表記になっています。

したがって、3行目の部分は以下のように展開されます。

# x`'y
substr(abcde, `1', `3')

上述のsubstr(...)リテラルではないので、再帰的にMacro展開されます。
substrは組み込みのMacroで、第一引数の文字列の一部を取り出します10
上記のコードは、「0-orientedで数えて1文字目から3文字目までを取り出す」という意味になります。
言い換えると、「abcdeの2文字目から4文字目を取り出す」ことになります。
すなわち、評価後の文字列は以下のようになります。

# substr(abcde, `1', `3')
bcd

m4 macroは、リテラルでないものは再帰的にmacroとして展開されます。
やや癖の強い言語です。

(参考) 例2. m4 macroファイルの実行

viなどで、以下のファイルを作ります。
ファイル名は1.m4とします。

define(`x', `substr(ab')
define(`y', `cde, `1', `3')')
x`'y

以下のm4コマンドで実行します。
実行すると、2行の空行と、bcdと書かれた行が返ります。

m4 1.m4
# 
# 
# bcd

m4は1行ずつ評価して、評価文字列を返します。
最初の2行は、define(...)の部分が "空白" とみなされ、行の末尾にある改行コード (LF)がそのまま評価された結果として、空行として出力されています。

(参考) 例3. dnlとMacroの引数

例2では、define()行の末尾の改行コードが評価されて、実行結果に空行ができてしまいました。

dnlというMacroを使うことで、dnlマクロ以降の同一行の文字を評価対象外にできます。
これには改行コードも含みます。
例3では、このdnlを使って空行を抑止します。

2.m4というファイルを作成し、以下のように記述します。

define(`x', `$1_$2.$3')dnl
x(`ONE', `TWO', `THREE')

このファイルを実行してみます。

m4 2.m4 
# ONE_TWO.THREE

今度は1行目に出力されました。
dnlによって、define()の行末にある改行コードが評価されなかったことがわかります。

また、ソースコード2行目のx(...)の部分がONE_TWO.THREEという評価結果になっています。
これはxに渡した3つの引数がdefine()の中の$1$3に代入されて評価された結果です。

なかなか興味深いですが、m4 Macroの仕様理解はこのぐらいにしておきます。

Module Sourcesの読み解き

本セクションでは、.te、 .if、 .fc という拡張子を持つ3種類のModule Sourcesファイルの意味を読み解いてみます。
目的は、以下のとおりです。

  • .te、.if、.fcに何を書けば良いか理解する
  • m4 Macro独特の記法を理解する
  • 基本的なStatementとして、type, attribute, allowを理解する

Module Sourcesのファイル構成

先に、Module Sourcesの3ファイルの意味について、概要を示します。

ファイル (拡張子) 内容
Private Policy File
(.te)
  • ローカルスコープのルール定義
  • Object定義やアクセス制御ルールなどを書く
  • 具体的には、Typeの定義、allowルールの記述、Interface Callの呼び出しなど
External Interface Call File
(.if)
グローバルスコープを持つInterface Call (≒関数) の定義
File Labeling Policy File
(.fc)
File Contextの定義

これらのファイルは、m4 MacroとKernel Policy Languageの組み合わせで書かれています。

以降のセクションで、それぞれのModule Sourcesファイルについて更に詳しく説明します。

File Labeling Policy File (.fc)

.fcファイルの基本構造

File Labeling Policy File (.fc) の理解は簡単です。
なぜなら、File Contextの定義そのものであるためです。

File Contextについては、SELinux Type Enforcement - #File Contextとrelabelを参照してください。

File Labeling Policy File (*.fc) は、以下のフォーマットで記述します11

pathname_regexp [file_type] security_context

各要素の意味は、以下のとおりです。
見た目は若干異なるものの、sudo semanage fcontext -lで表示されるFile Contextと全く同じ情報です。

要素名 意味
pathname_regexp ファイルパス。
PCRE (Perl Compatible Regular Expression) 形式の正規表現対応。
先頭と末尾に^$が自動挿入されるため、完全一致で記載する
file_type ファイル形式の指定。
省略した場合は任意 (all) の扱いとなる。
フラグの意味は以下の通り。

-b: Block Device
-c: Character Device
-d: Directory
-p Named Pipe (FIFO)
-l: Symbolic Link
-s: Socket File
--: Regular File
security_context ファイルに割り当てるSecurity Context。
<<none>>を記載すると、Security Contextを割り当てないという意味になる。
gen_context() Macroを使って記述する

gen_context() Macro (※) の定義は、以下の通りです12, 13
第一引数にSecurity Context (user:role:type)、第二引数にmls sensitivity、第三引数にmcs categoryを指定します。
第三引数は省略可能です。
(※) SELinuxで使われるm4 Macroは、Reference Policy Support Macroと呼ばれることもあります

gen_context(context,mls_sensitivity,[mcs_categories])

files.fcの例

具体例として、GitHubのfiles.fcから一部抜粋します。

/.*                gen_context(system_u:object_r:default_t,s0)
/           -d  gen_context(system_u:object_r:root_t,s0)
/\.journal          <<none>>
# (以下略)

比較のために、Linux実機のFile Contextも掲載します。

sudo semanage fcontext -l
# SELinux fcontext  type          Context

# /                 directory     system_u:object_r:root_t:s0 
# /.*               all files     system_u:object_r:default_t:s0 
# (以下略)

ssh.fcの例

ssh.fcのサンプルも貼ります。
relabel時、実行ファイルを*_exec_tでラベル付けするよう定義しています。

/usr/sbin/sshd         --  gen_context(system_u:object_r:sshd_exec_t,s0)

semanageでは以下のように表示されます。

sudo semanage fcontext -l
# (一部抜粋)

# SELinux fcontext       type          Context
# /usr/sbin/sshd         regular file  system_u:object_r:sshd_exec_t:s0 

Private Policy File (.te)

.teファイルの基本構造

Private Policy File (.te) には、File Contextを除く諸々のアクセス制御ルールを記述します。
具体的には以下を含みます。

  • Type/Attributeの定義
  • allow Statementによるアクセス許可
  • Type Transition
  • Boolean
  • .ifファイルで作成したInterface Callの呼び出し

.teファイルの先頭には、必ずpolicy_module Macroを記載します。
このMacroはモジュール名とバージョン番号を宣言し、なおかつBase Policyに含まれる基本的な定義をrequire Statementによって自動的に読み込みます。
Base Policyには、kernel関連のTypeやUser、Role、Constraint定義などを含みます。

2行目以降には、上に箇条書きで示したようなtype, attribute, allow Statementによるルール記述が続きます。

以降のセクションで具体例を示しつつ、必要に応じて構文を補足します。

files.teの例

前述の#files.fcの例より続きます。

files.fcでは/というパスに対してroot_tというFile Contextを紐付けていました。
このroot_t Typeは、同じモジュール内のfiles.te (※) で定義されます。
(※) Type Statement自体は、External Interface Call File (.if) で書き、files.te内でInterface Callを呼び出すことでTypeを宣言するような書き方もあります。Interface Callについては、#External Interface Call File (.if)にて説明します

今回の場合は、files.teにroot_t Typeが定義されていました。

さて、今回のソースコード例を見てみましょう。
以下にfiles.teの冒頭から途中行までを抜粋したサンプルを掲載します。

policy_module(files, 1.18.1)
# (中略)

attribute base_file_type;

#
# root_t is the type for rootfs and the root directory.
#
type root_t;
files_base_file(root_t)
files_mountpoint(root_t)

policy_module(files, 1.18.1)についてですが、全ての.teファイルの先頭には必ずpolicy_module Macroが記述されます14
このMacroが展開すると基本的にはmodule Statementになります。
module Statementは、モジュール名 (今回はfiles) とモジュールのバージョン (今回は1.18.1) を宣言するもので、全てのteファイルに必要な宣言です15

しかし、policy_module Macroが行うのはそれだけではありません。
Macro展開時に、UserやRole、Constraint、そしてKernel関連のTypeなどの基本的な宣言をrequire Statementも記述されるようになっています。
require Statementは、Moduleの外部で宣言された定義情報を読み込むために必要な宣言です16
例えば、以下のように書くとfiles.teには書かれていないvar_lock_t Typeの宣言を外部のファイルから読み込み、後続の処理で使えるようにします。

require {
    type var_lock_t;
}

実際のソースコードでは、以下のgen_require Macroを使って記述することのほうが多いです。
gen_require Macroは、マクロ展開後にrequire Statementになります (つまり同じ意味です)17
わざわざrequire Statementではなく、gen_require Macroを使って書く理由は、正直よくわかりません。

gen_require(`type var_lock_t;')

policy_module(files, 1.18.1)は、上記に記載したとおりです。
必要なrequire Statementを裏で実行しつつ、モジュール名 (files) とバージョン番号 (1.18.1) を宣言しています。

attribute base_file_type;は、Attributeを宣言しています。
ここでは宣言のみで、Typeとの紐付けは特に行いません。
Attributeに対する紐付けは複数モジュールをまたいで何度も行われることが多いので、大抵Interface Call Macroで処理をまとめます。
したがって、.ifファイルで定義したMacroを使ってTypeと紐付けるので、わかりやすくtype Statementを使ったAttributeとの紐付けを.teファイル上で見ることは滅多にありません。
AttributeについてはSELinux Type Enforcement - #Attributeで説明しているので、必要に応じて参照してください。

Attributeに関連し、type StatementのSyntaxを掲載します18
type t1_t;と書けばt1_tというtypeを定義して終わりですが、type t1_t a1,a2;のように定義すると、t1_t Typeを定義しつつa1a2という2つのAttributeを紐付けます。

type type_id [alias alias_id] [, attribute_id];

type root_t;は、root_tというTypeを宣言しています。
root_tの具体的な役割は、file.fcのFile Context情報 (relabel時に使われる)、Object Transitionルール、allowルールなどでroot_tを使ったルールを記述することで決まります。

以下の2行は、.ifファイルで定義されたInterface Call Macroを呼び出しています。

files_base_file(root_t)
files_mountpoint(root_t)

このMacroを引数付きで呼び出すことで、1行または複数行のKernel Policy Languageやm4 Macroに展開されます。
いわゆる関数のようなものです。

Interface Callの意味については次のssh.teを例に説明するので、これら2行のインターフェースについてはあまり深く扱いません。
ちなみに、この2行ではroot_tをAttributeに紐付ける処理を行っています。

ssh.teの例

#ssh.fcの例から続きます。
ssh.fcでは、sshd_exec_tに対応するFile Contextを定義していました。

ssh.teの行頭から途中までを一部抜粋します。

policy_module(ssh, 2.4.2)

ssh_server_template(sshd)
init_daemon_domain(sshd_t, sshd_exec_t)

type sshd_exec_t;
corecmd_executable_file(sshd_exec_t)

allow sshd_t self:process setcurrent;

policy_module(ssh, 2.4.2)はお決まりの宣言なので、ssh.teにおいても先頭に書かれています。

以下の2行については、引数を取っていることと、行末にセミコロン (;) がついていないことからMacroの呼び出しと判断できます。
policy_module()やgen_require()のようなお決まりのMacroでなければ、他は全てInterface Callです。
これらInterface Callの中身については.ifのセクションで触れるとして、ひとまず先に進みます。

ssh_server_template(sshd)
init_daemon_domain(sshd_t, sshd_exec_t)

type sshd_exec_t;で、File Contextに登場したsshd_exec_t Typeを宣言しています。
その直後でinit_daemon_domain(sshd_t, sshd_exec_t)の引数に渡されていますが、これもInterface Callなので今は置いておきます。

allow sshd_t self:process setcurrent;では、sshd_t (sshdデーモンプロセス) から自分自身 (self = Subject = sshd_t) へのsetcurrentの実行を許可しています。
allow Statementについては、過去記事のSELinux Type Enforcement - #allow Statementに詳細が書いてありますので、必要に応じて参照してください。

Interface Callの説明のみ残りましたが、この続きは#Access Interfaceの例 (ssh.teの続き)で説明します。

External Interface Call File (.if)

Encapsulation

Source Moduleには、Encapsulationという考え方があります19
この考え方はReference Policyの各モジュールの独立性を保つための考え方です。

Encapsulationの考え方においては、特定Moduleにおいて定義したTypeやAttributeは、他のModuleからは参照できません。
「プログラミング用語であるスコープ (オブジェクトの有効範囲) がモジュール内に限定されている」と言い換えることもできます。

例えば、ssh.tessh.fcsshモジュールのModule Sourcesです。
ssh.teで定義したTypeであるssh_keygen_tは、別モジュールのrsync.teでは直接参照できません。

では、「rsyncプロセスがSSH通信を利用して遠隔にファイルをコピーする」ことを許可したいとき、rsync.teにはどのようにallowコマンドを記載すれば良いのでしょうか。
ssh.teと全く同じTypeとAttributeの定義をコピー&ペーストして、rsync.teにも書くなんてことはしたくないはずです (※)
(※) プログラミングにおいて、ソースコードのコピペは避けるべきとよく言われます

ここで、モジュール間でTypeやAttributeなどのオブジェクト情報を共有するための仕組みとしてinterfaceが登場します。

Interface Call

Interface Callとは、異なるモジュール間でTypeやAttributeなどの定義情報を共有するための仕組みです。
ただ単に定義情報を共有するだけでなく、Macroの形で共有できます
詳細は#m4 Macroを参照いただきたいのですが、要するに引数を取って、内部に複数のコマンドを含めることができます。
Macroは、文字列を返す関数と似たような動きと理解しても問題ありません。

Interface Callの主な使い方は、以下の2つです。
詳細は#Interface Callの分類で説明しますが、Interface CallにはAccess InterfaceとTemplate Interfaceの2種類があります。
Access Interfaceは1と2の両方の特性を持ちますが、Template Interfaceは1の特性のみ持ちます。

  1. 一連の処理をMacroの形でグループ化して再利用する。引数も取れる (関数的な役割)
  2. Access Interfaceは、外部モジュールからも参照できる (Macroがグローバルスコープを持つ)

Interface Callの分類

Interface Callは、以下の2種類があります。
それぞれinterface()template()というMacroによって呼ばれます。

名前 (Macro) 説明
Access Interfaces
(interface())
  • 外部モジュールと定義情報を共有するための繋ぎとしてのInterfaceを定義する
  • Macro形式で記述するため、まとまった処理を引数付きで表現できる
Template Interfaces
(template())
  • 同一モジュール内でのみ使うMacroを定義する
  • 似た処理をMacroとして切り出すことで、ソースコードを見やすくする目的で使う
  • user/role/type/attributeをまとめて定義し、allowルールの許可までセットで含めた処理を記述することが多い
  • Macro形式で記述するため、まとまった処理を引数付きで表現できる

仕様上は、interface()template()は作成したInterfaceのスコープ (有効範囲) が異なる点が唯一の相違点です。

interface()はグローバルスコープを持ち、外部モジュールの.teや.ifファイルなどからも呼び出せます。
(※) 例えばssh.ifにおいて、interface()で定義したssh_sigchld()xserver.teで呼び出すといった使い方をします。もちろん、ssh_sigchld()ssh.teやssh.ifで呼び出すことも文法上は可能です

template()はローカルスコープを持ち、同一モジュールの.teや.ifからしか呼び出せません。
(※) 例えばssh.ifにおいて、template()で定義したssh_dyntransition_domain_template()は、ssh.tessh.if自身で呼び出すことができます。interface()で定義した場合とは異なり、kernel.teのような他モジュールのファイルから呼び出すことはできません

全てのTemplate InterfaceをAccess Interfaceで代用することは可能ですが、バグを減らしたり、ソースコードを見やすくするためにも、うまく使い分けるのが良いと思います。
最低限のスコープを利用することは、プログラミングの一般論として重要です。

自作モジュールを開発する場合は、作成したIntefaceを他モジュールと共有する必要性がほぼないので、Template Interfaceを使うことが多くなるのではないかと思います。

次のセクションで、Access InterfacesとTemplate Interfaceの具体的な使い方を説明します。

Access Interfaceの例 (ssh.teの続き)

前述の#ssh.teの例より続きます。
まず、ssh.teのソースコードの抜粋を再掲します。

policy_module(ssh, 2.4.2)

ssh_server_template(sshd)
init_daemon_domain(sshd_t, sshd_exec_t)

type sshd_exec_t;
corecmd_executable_file(sshd_exec_t)

allow sshd_t self:process setcurrent;

この中で、以下の部分がInterface Callでした。
Interface Callの名前は_で区切られていますが、原則として先頭部分は定義元のモジュール名が入ります。
つまり、init_daemon_domain()corecmd_executable_file()ssh_で始まっていないので、これはssh Moduleの外部で作成されたInterfaceと推測できます。
現にssh.ifファイルでこれらのInterface名を検索してもヒットしません。
すなわち、これらのInterfaceはinterface()で定義されたAccess Interfaceであると推測がつきます。

それぞれGitHub全文検索して探すと、それぞれの定義が以下のように見つかります。

init_daemon_domain()の定義は、init.ifにありました。

interface(`init_daemon_domain',`
  gen_require(`
      attribute direct_run_init, direct_init, direct_init_entry;
      type init_t;
      role system_r;
      attribute daemon;
      attribute initrc_transition_domain;
      attribute initrc_domain;
  ')

    typeattribute $1 daemon;
    typeattribute $2 direct_init_entry;

    domain_type($1)
    domain_entry_file($1, $2)

    type_transition initrc_domain $2:process $1;

    ifdef(`direct_sysadm_daemon',`
      type_transition direct_run_init $2:process $1;
      typeattribute $1 direct_init;
  ')
')

非常に定義が長いですが、m4 macroの目線で見ると実は非常にシンプルな構造です。
以下にinterface MacroのSyntaxを記載します20

interface(`name',`interface_rules')

第一引数のnameにはInterface名が入ります。
今回の場合、init_daemon_domainです。

そして第二引数にはInterfaceを呼び出されたときの内部処理を書きます。
内部処理は文字列リテラルでそのまま書くだけですが、$1はInterface呼び出し時の第一引数が、$2には第二引数が代入されることに注意してください。
Interface呼び出し時の処理は、以下の部分です。
m4 macroの文法において、文字列リテラルは `xxx' のように記載する (バックティックとシングルクォーテーションで囲う) ことを思い出してください。
非常に長いですが、m4 macro的には以下の塊が単一の文字列リテラルとして扱われます。

`
   gen_require(`
        attribute direct_run_init, direct_init, direct_init_entry;
        type init_t;
        role system_r;
        attribute daemon;
        attribute initrc_transition_domain;
        attribute initrc_domain;
    ')

  typeattribute $1 daemon;
  typeattribute $2 direct_init_entry;

  domain_type($1)
  domain_entry_file($1, $2)

  type_transition initrc_domain $2:process $1;

  ifdef(`direct_sysadm_daemon',`
        type_transition direct_run_init $2:process $1;
        typeattribute $1 direct_init;
    ')
'

中身をざっと読むと以下の処理を行っているようです。

  • gen_require()によって、後続の処理で使うtype/attribute/roleを外部モジュールから読み込む
  • typeattributeによって、第一引数のTypeをdaemon Attributeに、第二引数のTypeをdirect_init_entry Attributeに後から紐付けている (※)
  • domain_type()、domain_entry_file() というInterfaceの処理を呼び出している。これらの定義はdomain.ifにあり、domain Attributeへの紐付け処理などを行うことで基本的なアクセス許可ルールを一括実装している
  • type_transitionにより、第二引数へのDomain Transitionを実装
  • ifdefは、対象の変数が定義されていた場合に処理される。direct_sysadm_daemonBase Policyビルド時に指定されるパラメータ

(※) type StatementはTypeの定義と同時にAttributeと紐付けます。既に定義済みのTypeに対してAttributeを紐付けたい時はtypeattribute Statementを使います21

まとめると、このInterface Macroは必要なTypeやRoleを外部から読み込み、Attributeとの紐付けを行い、ルール定義しています。
init_daemon_domain(sshd_t, sshd_exec_t)のように呼び出すことで、sshdのDomain Transitionを許可しつつ、デーモンとして基本的な権限を許可しています。

続いて、ssh.teのcorecmd_executable_file(sshd_exec_t)の解釈に進みます。
まずはcorecommands.ifに記載のあるcorecmd_executable_file()の定義を貼ります。

interface(`corecmd_executable_file',`
  gen_require(`
      attribute exec_type;
  ')

    typeattribute $1 exec_type;

    files_type($1)
')

今度は非常にシンプルです。
$1にはsshd_exec_tが入るので、ここに書いてある処理はAttributeへの紐付けを行っています。
files_type() Interfaceの定義の確認は省略しますが、要するにファイルとして基本的な権限を付与しているのだと思います。

ssh.ifを例にしたAccess Interfaceの説明は、こちらで以上です。

Template Interfaceの例 (ssh.if)

Template Interfaceは、template() Macroによって定義します22
Syntaxと使い方は基本的にAccess Interfaceと同様ですが、唯一スコープのみが異なります。

Access Interfaceは外部モジュールからも呼び出せますが、Template Interfaceは同一モジュール内からしか呼び出せません。
例えば、ssh.ifで定義したTemplate Interfaceは、ssh.ifかssh.teからは呼び出せますが、kernel.ifやdomain.teからは呼び出せません。

以下にtemplate()のSyntaxを示します。
interface()と同じく、第一引数にTemplate名、第二引数に中身の処理を書きます。
Template Interfaceは、慣例としてInterface名の末尾を_templateとすることが多いです。

template(`name',`template_rules')

では、具体例に入ります。
以下にssh.ifにおけるTemplateの定義例を示します。
定義が非常に長いので、一部のみ抜粋します。

template(`ssh_server_template',`
    gen_require(`
        type sshd_t;
    ')

    type $1_t, ssh_server;
    type $1_devpts_t;
    type $1_tmpfs_t;
    type $1_var_run_t;

    allow $1_t self:tcp_socket create_stream_socket_perms;
    allow $1_t self:udp_socket create_socket_perms;
    allow $1_t self:tun_socket { create_socket_perms relabelfrom relabelto };

処理内容はこんな内容です。
sshプロトコルデーモンとして必要なアクセス制御を一括で実装しているようです。

  • 第一引数に渡した文字列を元にType名を作り、定義する ($1_t)
  • ssh_server Attributeに紐付ける
  • allow Statementにより各種Permissionを付与

ssh_server_template()は、以下のようにssh.teで呼び出されています。

ssh_server_template(sshd)

sshdという文字列を引数として渡すことで、$1_tsshd_tといったように評価されます。

Template InterfaceもAccess Interfaceと同じ使い方でしたが、ssh.ifかssh.teでしか使われない部分のみ差分でした。

以上でTemplate Interfaceの具体例の紹介もおしまいです。
以降のセクションで、ややこしい部分についていくつか補足します。

(参考) .teと.ifの違い

一見すると、TypeやAttributeの定義、allowルールの追加などが.teファイルにも.ifファイルにも書かれているように見えるので、両者のファイルの区別がつきにくいかもしれません。

一つ明確にしておくと、直接的に定義やルールを書き換える処理を書くのは.teファイルのみです。

.ifには、Interface Callの定義のみ書きます。
Interface Callは定義するだけでは何の効果もなく、.teファイルなどで呼び出すことで初めて効果を発揮します。

(参考) Interface Callの探し方

Interface Callはグローバルスコープを持つ場合があるので、数十ある.ifファイルのどこで定義されているかを探すことは一見すると難しそうに見えるかもしれません。
しかし、以下の方法を使えば以外に簡単に見つかります。

  1. Interface名から判断する
  2. GitHub全文検索する

ssh_server_template()を例に試してみましょう。

まずは1のアプローチですが、Interface名がssh_で始まるので、この時点でssh.ifに書かれているとあたりが付きます。
Interface名の先頭や、Module Sourcesのファイル名には命名規則上Module名を使うことになっているので、このような探し方が可能です。

次に2のアプローチですが、以下のようにGitHub全文検索する方法があります。
interface()template()のどちらで定義されているかによって検索ワードを若干変えています。
Template Interfaceは_templateで終わるので、今回の検索対象であるssh_server_template()はTemplate Interfaceであると予想がつきますね。

検索の結果、今回はtemplate()ssh.ifに定義されていることがわかりました。

代表的なStatementとMacro

本記事で紹介したStatement/Macroは以下のとおりです。

Module Sourcesを書くのであれば、他にも以下のルールを知っておいたほうが良いでしょう。

StatementとMacroの調べ方

SELinuxで使用されているStatementやMacroは、以下のリンクから調べることができます。

まとめ

SELinuxのModule Sourcesについて、ビルド処理のトレース、言語仕様の理解、ソースコードリーディングをやってみました。

紹介した構文は、基礎的なもののみにとどめています。
既存Moduleのソースコードを見ると、Attributeやm4 Macroを駆使して可能な限り読みやすく、効率よく書いています。
いきなり効率的に書くのは正直難しいと思いますが、慣れてきたらFedoraが提供しているModule Policyのソースコードを参考にしつつ、テクニックを真似しつつ書くのが良いのかもしれません。

ソースコードを読み書きする知識が必要になることはほぼないはずですが、「いざとなれば自分でも多少は書ける」ところまで行けば、より安心してSELinuxを運用できるのではないかと思います。

次の記事

SELinuxについて調べる上で役に立つ公式サイト・非公式サイトのURLを紹介します。

endy-tech.hatenablog.jp