systemd では、ユーザー環境、依存関係などを設定したカスタムサービスを簡単に書くことができます。しかしデフォルトでは root
以外でカスタムサービスを動かしたとしても init_t
や unconfined_service_t
といったデフォルトの高い権限で動作してしまうため、比較的重要なカスタムサービスが攻略されたときに SELinux による保護に頼ることができません。
それだけでなく、systemd のデフォルト環境は SELinux との組み合わせで思わぬ副作用をもたらします。
私が遭遇した具体例においては……
- 定期的にジョブを実行するスクリプトを作成し、rsync を用いてリモートネットワークのファイル (具体的には CentOS 7 の yum リポジトリ) を同期する
- このスクリプトを実行するために、cron 代わりに systemd タイマーとそれに関連付けたサービスを使用する
- rsync が正常実行できていない (以下はそのときの拒否ログ)
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
が関連付けられた1sync_centos7
ログインユーザーで同期する -
サボった点 : このユーザーのログイン後デフォルトコンテキストである
user_u:user_r:user_t:s0
を起動に使用する
[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 ユニットから起動したコマンドも同じことができるという寸法です。
SELinux ポリシーを記述する
ただし、これだけではまだ動作しないことに注意してください。現状、次のような拒否ログとともにサービス開始に失敗します。
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 ポリシーを記述します。手順は次の通り。
- [ポリシー開発機]
selinux-policy-devel
パッケージ (RHEL7 / CentOS 7 の場合) をインストールする - [ポリシー開発機] 下に示す
Makefile
とsystemd_customjobs.te
を記述する (systemd_customjobs という名前は重複がない限り好きに変えて良い) - [ポリシー開発機]
make
を実行する - 生成された systemd_customjobs.pp を運用機にコピーする (運用機とポリシー開発機が別の場合)
- [運用機] systemd_customjobs.pp と同じディレクトリで、
semodule -i systemd_customjobs.pp
を実行する
all:
make -f /usr/share/selinux/devel/Makefile
.PHONY: all
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。
参考文献
- systemd.exec(5) - FreeDesktop.org (Man Pages)
- rsync_selinux(8) - Linux man page
-
ログインユーザーを新規作成する際に SELinux ユーザー
user_u
を関連付けるには、useradd
コマンドにおいて-Z user_u
というオプションを指定します。ふたつの "ユーザー" の違いに惑わされないようにしましょう (SELinux ユーザーはアクセス制御の大まかな範囲を指定するためにあり、ひとつの SELinux ユーザーを複数のログインユーザーに割り当てることができます)。 ↩ -
ここでの "通常ログイン" は、例えば SSH や TTY (サーバーなどの物理画面に表示されている端末) を使うなどしてログインすることを想定しています (OpenSSH は SELinux コンテキストをログインユーザーのものに設定する機能をサポートしています)。sudo などを用いて別ユーザーにログインした場合、通常ログインと違って SELinux コンテキストが変化しません。 ↩
-
あとの理由は、最初に書いた通り SELinux boolean が一般的な解決策ではないことと、systemd 側の
SELinuxContext
という、記事にすると面白そうな解法を見つけたため。 ↩