えんでぃの技術ブログ

えんでぃの技術ブログ

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

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