SELinuxシリーズ
本記事は、SELinuxシリーズの6記事目です。
- Linuxプロセスアクセス制御の概要
- SELinuxの概要
- SELinux Type Enforcement
- SELinuxの実践
- (参考) SELinuxのRBAC、UBAC、MLS、MCS
- (参考) SELinux Module Policyのソースコード読解、ビルド ←今ココ
- 参考URL
1〜3記事目は、4記事目を理解するための前提知識をカバーしています。
4記事目が最も重要で、SELinuxの具体的な操作方法やコマンド、トラブルシューティング手順を紹介しています。
5記事目以降は参考情報です。
SELinuxの関連記事は、SELinuxタグから探せます。
一連の記事はFedora環境を前提として書いています。
FedoraやRHELに類するディストリビューションであればほぼ同等の挙動になると思いますが、他のディストリビューションでは挙動に差異がある可能性があるのでご注意ください。
お伝えしたいこと
SELinuxのSecurity Policyは、ソースコード (Module Sources) をビルド・パッケージ化・インストールすることで使えるようになります。
SELinuxの運用でソースコードを扱うことはほとんどありません。
既存のルール把握であればsesearch
で、ちょっとした変更であればsemanage
で事足りるためです。
しかし、以下のような状況でソースコードを自力で書くことも稀にあります。
- 既に制御されているプロセス用にallowルールを追加する
- 全く制御されていないアプリケーションを新たに制御する
本記事では、上記のような状況に対処するためにModule Sourcesの理解に必要な基礎知識に触れます。
具体的には、以下のトピックを扱います。
- Base PolicyとModule Policy
- ソースコードリポジトリ
- Module Policyのビルドの流れ
- Module Sourcesの記述言語
- Module Sourcesの読み解き (簡単な例のみ)
最初の#Base PolicyとModule Policyは基礎知識として重要ですが、他のセクションについてはある程度順不同に読んで問題ありません。
興味のあるトピックを拾い読みいただければと思います。
- SELinuxシリーズ
- お伝えしたいこと
- Base PolicyとModule Policy
- Module Policyのビルドの流れ
- ソースコードリポジトリ
- Module Sourcesの記述言語
- m4 Macro
- Module Sourcesの読み解き
- 代表的なStatementとMacro
- まとめ
- 次の記事
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は、apacheやsshなどのModule Policy名が入ります。
より詳細な流れについては、次のセクションで説明します。
ビルド処理の詳細
本セクションでは、Module Sourcesの詳細なビルド手順を説明します。
SELinuxの運用において、ビルドの内部処理を理解する必要性はあまりありません。
以下のサマリの図を中心に確認の上、詳細な説明は気になる部分のみを拾い読みいただければと思います。
まずは、処理の全体像を示します。
下図は、ビルド手順中で実行されるシェルスクリプトやMakefileの処理内容を絵に描き起こしたものです。
ここから、上図の処理内容を文字ベースで詳細に説明します。
カッコ付き数字の部分は、図中の番号と合わせてあります。
実際に手を動かしてビルドする手順については、SELinuxの実践 - (参考) Custom Policyのビルドを参照してください。
まず、ビルドするために前提として必要なパッケージをインストールします。
sudo dnf install policycoreutils-devel
(1)
今回SELinuxでアクセス制御する対象となるアプリケーションを用意します。
実行ファイルは、予め最終的な配置場所に移動しておいてください。
例えば個人用なら~/.local/bin/X
、全体で共有するなら/usr/local/bin/X
などです。
実行ファイルのパスは、この後自動生成するX.sh
、X.fc
、X_selinux.spec
などのファイルの中身に影響します。
今回はお試しなので、ワーク用のディレクトリに格納します。
mkdir ~/work touch ~/work/X cd work
(※) Xという実行ファイルが存在しなくても、(2)のsepolicy generate --init X
コマンドは通ります。しかし、その後実行するシェルスクリプトはファイルXが存在しないと途中でエラーになるので、ここで作成しておきます
(2)
以下のコマンドで、Module Sourcesのテンプレートを自動生成します。
ファイルの内訳は、以下のとおりです。
X.fc
、X.if
、X.te
: m4 MacroとKernel Policy Languageで書かれたSecurity Policyのソースコード (→ #Module Sourcesの読み解き)X.sh
: Security Policyをビルドするためのコマンドが記述されたシェルスクリプトX_selinux.spec
: rpmパッケージを生成するためのメタデータ
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.fc
、X.if
、X.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.te
とtmp/all_interfaces.conf
をm4 Macroでマージしつつ評価し、tmp/X.tmp
ファイルを出力します。
ここでMacroが展開されることで、Kernel Policy Languageで書かれたソースコードになります。
m4 MacroとKernel Policy Languageの関係については、#Module Sourcesの記述言語にて説明します。
(7)
tmp/X.tmp
をcheckmodule
コマンドに渡し、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.mod
とtmp/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.te とX.if をcheck_module でコンパイルし、Policy Modules (tmp/X.mod ) を生成する |
(8) | X.fc をm4 で評価し、X.mod.fc を生成する |
(9) | Policy ModulesとX.mod.fc をsemodule_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種類あります。
Upstreamのリポジトリは、SELinuxコミュニティによって維持管理されています。
2のFedora版Reference Policyのリポジトリは、1のUpstreamをベースにカスタマイズして作られています。
FedoraやRHELなどのLinuxにデフォルトでインストールされているSecurity Policyは、Fedora版Reference Policyをビルドしたものです。
したがって、本記事においてもソースコードを参照する際は2のFedora版のリポジトリから引用します。
Reference Policyのカスタマイズは、ディストリビューションごとに独自に行われています。
例えば、Gentoo LinuxはFedoraとはまた別のReference Policyのリポジトリを独自に持っています (リンク)。
以下のリンクにReference Policyのファイル構成についての説明があるので、参考までに載せておきます。
- ディレクトリ構成の図解: The SELinux Notebook - The Reference Policy - #Reference Policy Overview
- ファイルごとの詳細情報: The SELinux Notebook - The Reference Policy - #Source Layout
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つです。
- m4 macro
- Kernel Policy Language Statement
- その他のStatements
※その他のStatementsは、構文の観点ではKernel Policy Language Statementと同列に考えても差し支えありません
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と比較すると、module
やrequire
がある分だけ記述量が増えています。
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の定義です。
第一引数のx
とy
がmacro名で、第2引数がmacroが展開された後の文字列を表しています。
例えば、x
は以下のように展開されます。
# x substr(ab
y
は以下のように展開されます。
# y cde, `1', `3')
ソースコードの3行目は、少々変わった表記になっています。
x
とy
を区切っているのは、 "空白" のリテラルです。
xy
というMacroではなく、x
とy
という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 ) |
|
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を定義しつつa1
とa2
という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.te
とssh.fc
はsshモジュールの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の特性のみ持ちます。
Interface Callの分類
Interface Callは、以下の2種類があります。
それぞれinterface()
、template()
というMacroによって呼ばれます。
名前 (Macro) | 説明 |
---|---|
Access Interfaces ( interface() ) |
|
Template Interfaces ( template() ) |
|
仕様上は、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.teやssh.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_daemonはBase 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_t
がsshd_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ファイルのどこで定義されているかを探すことは一見すると難しそうに見えるかもしれません。
しかし、以下の方法を使えば以外に簡単に見つかります。
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は以下のとおりです。
- policy_module Macro
- type Statement
- attribute Statement
- typeattribute Statement → 既存のAttributeに自分で作成したTypeを追加する
- allow Statement
- gen_require Macro
Module Sourcesを書くのであれば、他にも以下のルールを知っておいたほうが良いでしょう。
- type_transition Statement → TypeのSubject/Object Transitionルール
- gen_tunable Macro → Booleanの定義
- tunable_policy Macro → Booleanを使用した条件分岐の定義
- dontaudit Statement → アクセス拒否しても監査ログを出さないようにする
StatementとMacroの調べ方
SELinuxで使用されているStatementやMacroは、以下のリンクから調べることができます。
- The SELinux Notebook - Kernel Policy Language - #Policy Language Index
- The SELinux Notebook - Reference Policy - #Reference Policy Support Macros
- The SELinux Notebook - Modular Policy Support Statements
まとめ
SELinuxのModule Sourcesについて、ビルド処理のトレース、言語仕様の理解、ソースコードリーディングをやってみました。
紹介した構文は、基礎的なもののみにとどめています。
既存Moduleのソースコードを見ると、Attributeやm4 Macroを駆使して可能な限り読みやすく、効率よく書いています。
いきなり効率的に書くのは正直難しいと思いますが、慣れてきたらFedoraが提供しているModule Policyのソースコードを参考にしつつ、テクニックを真似しつつ書くのが良いのかもしれません。
ソースコードを読み書きする知識が必要になることはほぼないはずですが、「いざとなれば自分でも多少は書ける」ところまで行けば、より安心してSELinuxを運用できるのではないかと思います。
次の記事
SELinuxについて調べる上で役に立つ公式サイト・非公式サイトのURLを紹介します。
-
The SELinux Notebook - Types of SELinux Policy - #Monolithic Policy, #Loadable Module Policy↩
-
The SELinux Notebook - Reference Policy - #Installing and Building the Reference Policy Source↩
-
The SELinux Notebook - Reference Policy - #Reference Policy Module Files↩
-
RHEL8 - Using SELinux - 7.2. Creating and enforcing an SELinux policy for a custom application↩
-
RHEL8 - Using SELinux - 7.3. Creating a local SELinux policy module↩
-
The SELinux Notebook - Policy Store Configuration Files - #Building the File Labeling Support Files↩
-
The SELinux Notebook - Reference Policy - #gen_context Macro↩
-
The SELinux Notebook - Reference Policy - #policy_module Macro↩
-
The SELinux Notebook - Modular Policy Support Statements - #module↩
-
The SELinux Notebook - Modular Policy Support Statements - #require↩
-
SELinux by Example: Using Security Enhanced Linux - 12.3.2.1. Encapsulation ※書籍↩