systemd サービス起動時の SELinux コンテキストを変更する (サボリ編)

systemd では、ユーザー環境、依存関係などを設定したカスタムサービスを簡単に書くことができます。しかしデフォルトでは `root` 以外でカスタムサービスを動かしたとしても `init_t` や `unconfined_service_t` といったデフォルトの高い権限で動作してしまうため、比較的重要なカスタムサービスが攻略されたときに SELinux による保護に頼ることができません。

それだけでなく、systemd のデフォルト環境は SELinux との組み合わせで思わぬ副作用をもたらします。

私が遭遇した具体例においては……

1. 定期的にジョブを実行するスクリプトを作成し、rsync を用いてリモートネットワークのファイル (具体的には CentOS 7 の yum リポジトリ) を同期する
2. このスクリプトを実行するために、cron 代わりに systemd タイマーとそれに関連付けたサービスを使用する
3. rsync が正常実行できていない (以下はそのときの拒否ログ)

```text:/var/log/audit/audit.log
type=AVC msg=audit(1452236628.817:54229): avc:  denied  { name_connect } for  pid=18508 comm="rsync" dest=873 scontext=system_u:system_r:rsync_t:s0 tcontext=system_u:object_r:rsync_port_t:s0 tclass=tcp_socket
```

この原因は、systemd デフォルトの SELinux 環境から rsync を起動すると **rsync デーモンのためのドメインに** 自動遷移してしまい、デフォルト設定では外部ネットワーク接続の権限を失うためです。後述するように SELinux の boolean (動的に一部のポリシーを変更できるスイッチ) を変更することでもこの問題に対処することは可能ですが、この類型の問題を (rsync 以外でも) 避ける一般的な解法ではありません。

**一般的に**これを避けるためには、デフォルト以外のドメイン (コンテキスト) で systemd 用のサービスを動かす必要があります。この記事では "サボり編" として、SELinux に関する詳しい知識がなくとも比較的簡単に systemd カスタムサービスをカスタムコンテキストで動かす方法を紹介します。

# systemd ユニットを記述する

systemd は SELinux との統合に対応しており、実は `SELinuxContext` というディレクティブを `Service` セクションで定義すると、サービスを指定された SELinux コンテキストで起動させることができます。

次のファイルは、以下の (私が実際に使っている) 状況を想定しています。

* CentOS 7 の yum リポジトリは、SELinux ユーザーである `user_u` が関連付けられた[^1] `sync_centos7` ログインユーザーで同期する
* **サボった点** : このユーザーのログイン後デフォルトコンテキストである `user_u:user_r:user_t:s0` を起動に使用する

[^1]: **ログイン**ユーザーを新規作成する際に **SELinux** ユーザー `user_u` を関連付けるには、`useradd` コマンドにおいて `-Z user_u` というオプションを指定します。ふたつの "ユーザー" の違いに惑わされないようにしましょう (SELinux ユーザーはアクセス制御の大まかな範囲を指定するためにあり、ひとつの SELinux ユーザーを複数のログインユーザーに割り当てることができます)。

```text:/etc/systemd/system/sync_centos7.service
[Unit]
Description=CentOS 7 Repository Synchronization Service
After=network-online.target

[Service]
Type=simple
User=sync_centos7
ExecStart=/home/sync_centos7/sync-wrapper.sh
SELinuxContext=user_u:user_r:user_t:s0
```

これで systemd はサービスを指定されたコンテキスト `user_u:user_r:user_t:s0` で動作させようとします。これはユーザー `sync_centos7` のログイン時権限と同じなので、このユーザーで通常ログイン[^2] した後に同じことができるなら、systemd ユニットから起動したコマンドも同じことができるという寸法です。

[^2]: ここでの "通常ログイン" は、例えば SSH や TTY (サーバーなどの物理画面に表示されている端末) を使うなどしてログインすることを想定しています (OpenSSH は SELinux コンテキストをログインユーザーのものに設定する機能をサポートしています)。sudo などを用いて別ユーザーにログインした場合、通常ログインと違って SELinux コンテキストが変化しません。

# SELinux ポリシーを記述する

ただし、これだけではまだ動作しないことに注意してください。現状、次のような拒否ログとともにサービス開始に失敗します。

```text:/var/log/audit/audit.log
type=AVC msg=audit(1452237017.133:54239): avc:  denied  { transition } for  pid=19015 comm="(apper.sh)" path="/home/sync_centos7/sync-wrapper.sh" dev="sda3" ino=2281633452 scontext=system_u:system_r:init_t:s0 tcontext=user_u:user_r:user_t:s0 tclass=process
```

これは systemd (init デーモン) が動作するドメインである `init_t` から指定したドメインである `user_t` への遷移に失敗していることを意味します。これを避けるには、`init_t` から指定したドメインに遷移できるようにカスタム SELinux ポリシーを記述します。手順は次の通り。

1. [ポリシー開発機] `selinux-policy-devel` パッケージ (RHEL7 / CentOS 7 の場合) をインストールする
2. [ポリシー開発機] 下に示す `Makefile` と `systemd_customjobs.te` を記述する (systemd_customjobs という名前は重複がない限り好きに変えて良い)
3. [ポリシー開発機] `make` を実行する
4. 生成された systemd_customjobs.pp を運用機にコピーする (運用機とポリシー開発機が別の場合)
5. [運用機] systemd_customjobs.pp と同じディレクトリで、`semodule -i systemd_customjobs.pp` を実行する

```mf:Makefile
all:
	make -f /usr/share/selinux/devel/Makefile
.PHONY: all
```

```text:systemd_customjobs.te
dnl SELinux ポリシーを記述する .te ファイル (M4 言語) において、dnl 以降はコメント (記述する必要はない)

dnl モジュール名とバージョン情報
policy_module(systemd_customjobs.te,1.0.0)

gen_require(`
	type init_t;
	dnl user_t 以外に遷移させる場合はここと……
	type user_t;
')

dnl ここを変更する。
allow init_t user_t:process { transition };
```

SELinux ポリシーのビルドにエラーが発生せず、`semodule` コマンドでポリシーを読み込めたら、作業は完了です。これであなたのカスタムサービスを `user_t` ドメインの下で動かすことが可能になりました。

# さらにサボる - 特に制限されていない環境でサービスを動かす

特に SELinux による保護を求めておらず、SELinux と systemd のデフォルトコンテキストによる問題を避けたいだけなら、unconfined な (権限が狭められていない) ドメインで動かすのも良いかもしれません。その場合、上記の節における一部の文字列を適宜読み替えてください。

* `SELinuxContext` は、`unconfined_u:unconfined_r:unconfined_t:s0` (MLS ポリシーを使っていないデフォルト環境の場合)
* ドメイン/タイプ として、`user_t` の代わりに `unconfined_t` を記述する (一般論としては、上記 `SELinuxContext` のコロンで句切られた 3 番目の項目)

# もっと知りたい人のために

## systemd のデフォルトコンテキストと SELinux の自動遷移

systemd の基本的なプロセスは、`init_t` というドメイン/タイプの下で動作しています。これは非常に強い権限を持つ一方、ポリシーにおいては各種のデーモンを適切に制限された環境で起動するために、複数の "自動遷移" ルールが記述されています。

この自動遷移は、もっと身近な例ではユーザーが `passwd` コマンドを起動するときに `/etc/shadow` ファイルへの書き込みアクセス権を持つ `passwd_t` ドメインに遷移するようなときに用いられ、RHEL7 / CentOS 7 を含む複数の SELinux ベース環境においては systemd の `SELinuxContext` に頼ることのないドメイン遷移を行うために使用されます。

rsync の場合、デーモン版である `rsyncd.service` のためか `init_t` から `rsync_t` というドメイン/タイプに (rsync 実行ファイルに付けられたラベルのタイプである `rsync_exec_t` を経由して) 自動遷移するルールが記述されており、systemd のデフォルト権限で起動されたサービスから rsync を起動すると、場合によっては `rsync_t` に遷移してしまいます。前述の通りこのドメイン/タイプにはデフォルトで外部ネットワーク接続のための権限がないため、普通にシェルスクリプトと同様に rsync を用いたリモート同期ができなくなる、というわけです。

## SELinux の boolean

SELinux のポリシーにおいては、必要に応じてオンオフすることが可能な機能が boolean としてまとめられています。rsync ドメイン (`rsync_t`) に対してクライアント機能を有効にする `rsync_client` がそのような例で、その他にもメジャーなソフトウェアのマイナーな機能などは boolean で有効/無効を切り替えることが可能となっています。

例えば `rsync_client` boolean をオンにするには、次のようなコマンドを実行します。

	setsebool -P rsync_client=1

ここで `-P` オプションは設定した内容を次回起動時にも引き継ぐオプション (これを指定しない場合再起動すると値が戻る) です。

さて、これを上記の記事で使わなかったのには幾つか理由があるのですが、ひとつの理由に、これを有効にすると rsync デーモン `rsyncd.service` においてもクライアント機能のための権限が有効になってしまい、rsync デーモンを併用するときに穴を自ら広げることになるからです [^3]。

[^3]: あとの理由は、最初に書いた通り SELinux boolean が一般的な解決策ではないことと、systemd 側の `SELinuxContext` という、記事にすると面白そうな解法を見つけたため。

# 参考文献

1. [systemd.exec(5)](http://www.freedesktop.org/software/systemd/man/systemd.exec.html) - FreeDesktop.org (Man Pages)
2. [rsync_selinux(8)](http://linux.die.net/man/8/rsync_selinux) - Linux man page