はじめに

注意事項

この記事は何らかの理由でSELinuxを利用しなければならない時に発生する、意図せずプログラムが動かなくなる問題を解決するための手段を書いたものである。

作業対象のOSは作業中いつでも停止可能であるものとする。SELinuxの設定作業中に停止不可能とか無茶なので。
また、すべての操作はrootユーザで行っている。SELinuxは「管理者による強制的なアクセス制御」なのでrootユーザが操作しなければならない。

内容は主にCentOS 7で確認し、CentOS 6やFedora 22も一部確認に使用している。
SELinuxの管理で使用する各種のコマンドは初期からインストールされているものは少なく、またコマンド名がそれを含むrpmパッケージ名と一致しないものが多い。
このような場合はyum install *bin/<コマンド名>でインストールすることができる。Fedora 22ではdnf install /usr/*/<コマンド名>とする。SELinuxを利用できる状態にする

SELinuxの状態の確認

とりあえずSELinuxが利用できる状態になっていないとこの記事の意味がないので、まずはSELinuxの状態の確認方法と、利用できる状態にする方法を説明する。
もっともこの辺りは各種媒体で良く見かけるものであり(主にSELinuxを無効化するために、だが)、良く知っているならば読み飛ばしてもらって問題ないだろう。

SELinuxの状態には次の3つある。

disabled
無効化されている
permissive
有効化されているが監査ログに記録する以外の働きをしない
enforcing
有効化されており設定通りに働く

SELinuxの状態を確認するコマンドはいくつかある。

  • 一番よく知られているのはgetenforceである。これを実行するとDisabledかPermissiveかEnforcingかが標準出力に出力される。
  • selinuxenabledを実行するとdisabled以外の場合リターンコードが0になり、disabledの場合1になる。リターンコードなので実行後echo $?で取得できる。
  • sestatusは一番詳細な状態を表示してくれるコマンドである。SELinux status項にdisabledかenabledが、Current mode項にenforcingかpermissiveかが出力される。

SELinuxの状態変更:disabledからpermissiveへ

前記の通り、状態がenforcingの時だけSELinuxが働いてくれるので、まずはenforcingにしよう。
すでにenforcingなら先に読み進めて構わない。

現在の状態がdisabledの場合、直接disabledからenforcingに変更するとシステムが起動しないことがあり得るので、enforcingにする前にまずは一旦permissiveにする。

permissiveに状態を変化させるには /etc/selinux/config のSELINUX=行が”SELINUX=permissive”となるように書き換え、その後OSを再起動する。

SELinuxの状態変更:permissiveからenforcingへ

permissiveからenforcingにするには次を実行する。

# setenforce 1

逆にenforcingからpermissiveにする場合はsetenforce 0である。

このようにSELinuxの状態をpermissiveからenforcing、あるいはその逆にenforcingからpermissiveにする場合はOS再起動は不要だが、OS再起動をすると /etc/selinux/config のSELINUX=行の状態に戻ってしまうので、OS再起動後もenforcingのままにするには /etc/selinux/config が”SELINUX=enforcing”となるように書き換える。

/etc/selinux/configを書き換える際の注意

/etc/selinux/config ではなく /etc/sysconfig/selinux を書き換えろ、としている文書もあるが、実際には /etc/sysconfig/selinux は /etc/selinux/config のシンボリックリンクである。
シンボリックリンクである /etc/sysconfig/selinux に対してsedを–follow-symlinksオプションや–copy(-C)オプションを付けずに実行する、あるいはcp -aで上書きするなどすると、 /etc/sysconfig/selinux はシンボリックリンクから実ファイルに変化し /etc/selinux/config には反映されないため設定を変更したことにはならない。

また、 /etc/selinux/config のSELINUX=行以外を誤って変更してはならない。
誤って変更した場合に起こることの例は
http://qiita.com/MurabitoK/items/d44fc01b9e1ac2fdcfab のようなことで、ここにはその復旧方法も書かれている。

なぜSELinuxによってプログラムは意図せず動かなくなるのか

Type Enforcement

さて、ここからが本題である。

なぜSELinuxによってプログラムがこちらの意図に反して動かなくなることがあるのだろうか。
それはSELinuxがenforcingの場合、プロセスからファイルやディレクトリ、ソケットなど(SELinuxの用語ではこれらをまとめてリソース、種類1つ1つをクラスと呼ぶ)にアクセスできるのが許可された場合だけになるからである。
この仕組みをType Enforcement(以下、TE)と呼ぶ。

TEはchmodで設定するファイルのパーミッションとは別に行われるもう一つのアクセス制御であり、SELinuxがenforcingの場合、ファイルのパーミッションによるアクセス制御とTEによるアクセス制御との両方を通過しないとアクセスは成功しないというわけだ。

TEによるアクセス制御はrootユーザにも適用される。あるプロセスの脆弱性を突かれてrootユーザへの権限昇格を許すなどしても、そのプロセスのドメインに許可されていないリソースへのアクセスを防ぐ、というのがTEの狙いである。

ドメインとタイプ

SELinuxの世界では、すべてのプロセスとリソースはSELinuxコンテキストと呼ばれるものでラベル付けされている。

psコマンドに-Zオプションを付ければプロセスのSELinuxコンテキストを見ることができる。

# ps -eZ
LABEL                             PID TTY          TIME CMD
system_u:system_r:init_t:s0         1 ?        00:00:02 systemd
system_u:system_r:kernel_t:s0       2 ?        00:00:00 kthreadd
(長いので中略)
system_u:system_r:sshd_t:s0-s0:c0.c1023 4840 ? 00:00:00 sshd
system_u:system_r:sshd_t:s0-s0:c0.c1023 4844 ? 00:00:00 sshd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 4846 ? 00:00:00 sshd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 4849 pts/0 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 4852 ? 00:00:00 sshd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 4860 ? 00:00:00 sftp-server
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 4898 pts/0 00:00:00 sudo
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 4899 pts/0 00:00:00 bash
system_u:system_r:kernel_t:s0   31494 ?        00:00:00 bioset

lsコマンドに-Zオプションを付ければファイルなどのSELinuxコンテキストを見ることができる。

# ls -Z /tmp
-rwx------. root    root    system_u:object_r:initrc_tmp_t:s0 ks-script-6Tm7iL
-rwx------. root    root    system_u:object_r:initrc_tmp_t:s0 ks-script-wKtOJm
-rwxrwxrwx. vagrant vagrant unconfined_u:object_r:user_tmp_t:s0 script.sh
-rwxrwxr-x. vagrant vagrant unconfined_u:object_r:user_tmp_t:s0 vagrant-shell
-rw-r--r--. root    root    unconfined_u:object_r:user_tmp_t:s0 vboxguest-Module.symvers
-rw-------. root    root    system_u:object_r:initrc_tmp_t:s0 yum.log

SELinuxコンテキストは:で区切られた「(_uで終わるもの):(_rで終わるもの):(_tで終わるもの):(s数字で始まるもの):(c数字で始まるもの。一部のみ)」の部分である。
この中で_tで終わるものがプロセスやリソースのタイプである。それぞれのプロセスやリソースはタイプを1つだけ持つ。
プロセスのタイプは特にドメインと呼び、リソースのタイプとは区別するので、ここでも以下そう呼ぶことにする。

この記事で語る範囲内の話ではSELinuxコンテキストのうち_tで終わるものしか使わないし使われないしなので他は忘れて良い。

TEによるアクセス制御

TEはホワイトリスト方式であり、ルールが定義されていないすべてのアクセスは「アクセスを許可せず、アクセスが試みられた場合監査ログに記録する」ことになっている。
ルールの設定は「どのドメインが、どのタイプの、どのクラスに、どの操作を」という単位ごとに次の3種類の指定を行うことができる。2

allow
アクセスを許可し、アクセスが試みられた場合監査ログに記録しない
auditallow
アクセスを許可し、アクセスが試みられた場合監査ログに記録する
dontaudit
アクセスを許可せず、アクセスが試みられた場合監査ログに記録しない

アクセスを許可する・許可しないという観点と監査ログに記録する・記録しないという観点で、2×2で指定なしも含めて合わせて4種類というわけだ。

個々のルールをSELinuxではアクセスベクタルールと呼んでいる。
アクセスベクタルールが1つも定義されていない状態ではまったく何も動かないので、selinux-policy-xxパッケージ(xxは /etc/selinux/config のSELINUXTYPE=行の値)によって提供されるルールによってシステムを動かすことになる。

つまりSELinuxによってプログラムがこちらの意図に反して動かなくなる状態とは、selinux-policy-xxパッケージによってallowあるいはauditallowされたアクセスベクタルール以外のことを実行しようとした状態、というわけだ。

SELinuxのせいで動かないと思った時に何をすれば良いか

その問題がSELinuxのせいかどうかを見分ける

プログラムがこちらの意図に反して動かない場合、まずはそれがSELinuxによってアクセスが拒否されたせいかどうかを見極める必要がある。
そのためにはSELinuxの状態をsetenforce 0で一時的にpermissiveにし、それで動くようになればSELinuxが原因であると言える。

ファイルのパーミッションによるアクセス制御はTEによるアクセス制御より先に判定される。
従ってファイルのパーミッションを解決した後になってSELinuxによる問題が顕在化することもある。

permissiveに変更するのにはもう一つ意味がある。
enforcingの状態ではSELinuxによる複数の問題があった場合、最初に当たった問題の時点で処理が終了し、監査ログにそれしか残らないのでその分を解決してもまたすぐに次の問題に当たることになる。
permissiveにした場合、複数の問題が監査ログに残るので一括して解決することができる。

もちろんpermissiveにするのは一時的なことであり、問題を解決するためのアクションを取った後setenforce 1でenforcingに戻して実際に問題が解決しているかどうかを確認する必要がある。

監査ログを確認する

SELinuxによって問題が発生した場合、監査ログを確認することによってその問題がどのようなものかを知ることができる。
監査ログはauditdサービスによってデフォルトでは /var/log/audit/audit.log に出力されている。

type=AVC msg=audit(1436807141.396:399): avc:  denied  { open } for  pid=2363 comm="httpd" path="/var/www/html/1.html" dev="dm-4" ino=19923324 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_tmp_t:s0 tclass=file
type=SYSCALL msg=audit(1436807141.396:399): arch=c000003e syscall=2 success=no exit=-13 a0=7fbf95c62290 a1=80000 a2=0 a3=7ffe31bbf530 items=0 ppid=1277 pid=2363 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm="httpd" exe="/usr/sbin/httpd" subj=system_u:system_r:httpd_t:s0 key=(null)

見るべきはtype=AVCで始まる行である。次のような意味になり、どのような問題が発生したのかを判別できる。

denied { }
{ }内の操作が拒否された
comm
プロセスのコマンド名
pathなどいろいろ
アクセスされたリソース
scontext
プロセスのSELinuxコンテキスト
tcontext
アクセスされたリソースのSELinuxコンテキスト
tclass
アクセスされたリソースのクラス

監査ログはausearch -m avcで見ることもできる。発生日時がわかりやすいフォーマットで表示される分こちらの方が良いと思う。

time->Tue Jul 14 02:05:41 2015
type=SYSCALL msg=audit(1436807141.396:399): arch=c000003e syscall=2 success=no exit=-13 a0=7fbf95c62290 a1=80000 a2=0 a3=7ffe31bbf530 items=0 ppid=1277 pid=2363 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm="httpd" exe="/usr/sbin/httpd" subj=system_u:system_r:httpd_t:s0 key=(null)
type=AVC msg=audit(1436807141.396:399): avc:  denied  { open } for  pid=2363 comm="httpd" path="/var/www/html/1.html" dev="dm-4" ino=19923324 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_tmp_t:s0 tclass=file

またausearchには豊富な絞り込みオプションがあり、例えば-tsオプションは指定した日時以降の監査ログだけを出力する。
この日時はmm/dd/YYYY HH:MM:SSのフォーマットで指定する。これらをクォートで囲んではならない。日付を省略した場合は「今日の指定した時刻」、時刻を省略した場合は「指定日の0時ちょうど3」として扱われる。日付の代わりにtodayとかyesterdayとかで指定もできる。

ところで、アクセスが拒否されているのに監査ログには残らないdontauditというアクセスベクタルールの種類があることを説明した。
監査ログに記録されていないことには対処できないので、enforcing時にアクセス拒否されるのに監査ログには事象が記録されていないようならdontauditルールを無効化してしまうのが良い。
次のコマンドですべてのdontauditルールを無効化することができる。

# semodule -DB

しかしdontauditルールが無効化されていると、次の項で説明するsetroubleshootが働かなくなるようなので使い分けが必要になる。
すべてのdontauditルールを再度有効化するには次のコマンドを実行する。

# semodule -B

setroubleshoot

監査ログを見て問題の原因が判明すれば、次のようなアクションを行い解決を目指すことになる。

  • SELinuxブール値を変更し、アクセスベクタルールを追加適用する
  • リソースのタイプを正しいものに直す
  • 監査ログからアクセスベクタルールを生成し適用する

個々の説明は後程行うが、これらのうちどのアクションを選択すれば良いかの指針を与えてくれるsetroubleshoot(実行ファイル名はsetroubleshootd)というものがある。
setroubleshootは監査ログが出力されるタイミングでその対応方法を導き出し /var/log/messages に出力してくれるものである。

setroubleshootを利用するにはsetroubleshoot-serverパッケージをインストールする。4
インストール後、TEのアクセス制御に引っかかり監査ログが出力された時に /var/log/messages (Fedora 22ではjournalctlを見た方が良い)の中に1つのタイムスタンプに対する出力として次のようなログが現れる。

Jul 14 22:06:36 localhost python: SELinux is preventing /usr/sbin/httpd from name_connect access on the tcp_socket port 25151.

***** Plugin catchall_boolean (47.5 confidence) suggests ******************

If you want to allow httpd to can network connect cobbler
Then you must tell SELinux about this by enabling the 'httpd_can_network_connect_cobbler' boolean.

Do
setsebool -P httpd_can_network_connect_cobbler 1

***** Plugin catchall_boolean (47.5 confidence) suggests ******************

If you want to allow httpd to can network connect
Then you must tell SELinux about this by enabling the 'httpd_can_network_connect' boolean.

Do
setsebool -P httpd_can_network_connect 1

***** Plugin catchall (6.38 confidence) suggests **************************

If you believe that httpd should be allowed name_connect access on the port 25151 tcp_socket by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# grep httpd /var/log/audit/audit.log | audit2allow -M mypol
# semodule -i mypol.pp

permissive状態にしてあれば問題が複数あったなら、このようなログも複数現れるはずだ。

1つも現れない場合、auditdサービスをreloadしてからもう一度監査ログを出力させてみる。それでも現れなければOS再起動してみる。
また、CentOS 6だと上記のような行は現れず次のような文字列を含む行が現れる。

For complete SELinux messages. run sealert -l 7d8bffe6-d07f-4971-84a5-9fe70ebeba76

この場合、書かれているようにsealert -l 7d8bffe6-d07f-4971-84a5-9fe70ebeba76を実行すれば先程示したようなログが表示される。

このログの中で一番最初に書かれている*****に囲まれて書かれているプラグイン名を調べる。
上記ログの例ならそのプラグイン名は「catchall_boolean」である。

このプラグイン名から次に行うアクションを決定する。
次項でプラグイン名ごとに何をすべきかを示す。

問題解決のために実行すべきアクション

catchall_boolean → SELinuxブール値の変更

SELinuxブール値とは、アクセスベクタルールをいくつかまとめて管理者が有効化、あるいは無効化できるようにしたものである。
あるプログラムにおいてすべての機能を使わない場合、使わない機能のためのアクセスベクタルールは許可しないまま、使う機能のためのアクセスベクタルールだけを許可する、というようにできるだけ少ないアクセス権限で運用することを可能にするために設定されている。

あるSELinuxブール値をオンにするためにはsetsebool -P <SELinuxブール値名> 1を、オフにするためにはsetsebool -P <SELinuxブール値名> 0を実行する。
この-PオプションはOS再起動後も変更を永続化するためのオプションである。

先程のsetroubleshootのログでは、1つ目のcatchall_booleanにsetsebool -P httpd_can_network_connect_cobbler 1を、2つ目のcatchall_booleanにsetsebool -P httpd_can_network_connect 1を実行しろと書かれている。

つまりこの問題はhttpd_can_network_connect_cobblerかhttpd_can_network_connectかどちらか1つのSELinuxブール値を有効にすれば解決できる、ということだ。

何故どちらか1つなのか。
あるSELinuxブール値で有効化/無効化できるアクセスベクタルールのリストはsesearch --allow --auditallow -C -b <SELinuxブール値名>で取得できる。
–allowオプションはallowルールを表示、–auditallowオプションはauditallowルールを表示、-Cオプションは後で説明する、-bオプションはSELinuxブール値名で絞り込むというオプションである。

次はhttpd_can_network_connect_cobblerブール値の内容である。

# sesearch --allow --auditallow -C -b httpd_can_network_connect_cobbler
Found 1 semantic av rules:
DT allow httpd_t cobbler_port_t : tcp_socket name_connect ; [ httpd_can_network_connect_cobbler ]

また次はhttpd_can_network_connectブール値の内容である。

# sesearch --allow --auditallow -C -b httpd_can_network_connect
Found 17 semantic av rules:
DT allow httpd_t port_type : tcp_socket name_connect ; [ httpd_can_network_connect ]
DT allow httpd_sys_script_t netif_t : netif { tcp_recv tcp_send udp_recv udp_send ingress egress } ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_suexec_t client_packet_type : packet { send recv } ; [ httpd_can_network_connect ]
DT allow httpd_suexec_t node_t : node { tcp_recv tcp_send udp_recv udp_send recvfrom sendto } ; [ httpd_can_network_connect ]
DT allow httpd_sys_script_t port_type : tcp_socket { recv_msg send_msg name_connect } ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_sys_script_t port_type : udp_socket { recv_msg send_msg } ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_suexec_t httpd_suexec_t : tcp_socket { ioctl read write create getattr setattr lock append bind connect listen accept getopt setopt shutdown } ; [ httpd_can_network_connect ]
DT allow httpd_suexec_t httpd_suexec_t : udp_socket { ioctl read write create getattr setattr lock append bind connect getopt setopt shutdown } ; [ httpd_can_network_connect ]
DT allow httpd_suexec_t netif_t : netif { tcp_recv tcp_send udp_recv udp_send ingress egress } ; [ httpd_can_network_connect ]
DT allow httpd_sys_script_t client_packet_type : packet { send recv } ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_sys_script_t httpd_sys_script_t : tcp_socket { ioctl read write create getattr setattr lock append bind connect listen accept getopt setopt shutdown } ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_sys_script_t httpd_sys_script_t : udp_socket { ioctl read write create getattr setattr lock append bind connect getopt setopt shutdown } ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_sys_script_t node_t : tcp_socket node_bind ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_sys_script_t node_t : udp_socket node_bind ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_sys_script_t node_t : node { tcp_recv tcp_send udp_recv udp_send recvfrom sendto } ; [ httpd_enable_cgi httpd_can_network_connect && ]
DT allow httpd_suexec_t port_type : tcp_socket { recv_msg send_msg name_connect } ; [ httpd_can_network_connect ]
DT allow httpd_suexec_t port_type : udp_socket { recv_msg send_msg } ; [ httpd_can_network_connect ]

sesearchの出力結果の読み方は次の通りである。

  • 最初の「DT」は1文字目がD→現在無効、E→現在有効という意味で、2文字目はT→オンの時に有効、F→オフの時に有効という意味である。-Cオプションを指定した時のみ表示される。
  • 次の「allow」はアクセスベクタルールの種類が表示される。
  • その後は”ドメイン”、”タイプ”、コロンを挟んで”クラス”、”操作の種類”である。”操作の種類”は複数が対象になっている場合はまとめて{ }に囲まれて表示される。
  • 最後にセミコロンを挟んで[ ]に囲まれてSELinuxブール値名である。-Cオプションを指定した時のみ表示される。複数のブール値名が表示されている場合、そのすべてを変更しないと有効化/無効化できない。5

httpd_can_network_connectブール値の内容のうちの1行はこうなっている。

DT allow httpd_t port_type : tcp_socket name_connect ; [ httpd_can_network_connect ]

これはhttpd_can_network_connect_cobblerブール値の唯一の1行の内容と比べると、”タイプ”の項がcobbler_port_tではなく「port_type」となっているところだけが相違点である。
このように”タイプ”や”ドメイン”の項にありながら_tで終わっていないものはいくつかのタイプやドメインをひとまとめにしてグループ化したものであり、アトリビュートと呼ばれる。
1つのアトリビュートに含まれるタイプやドメインはseinfo -a<アトリビュート名> -xで取得できる。
seinfo -aport_type -x | grep -w cobbler_port_tを実行すると、port_typeアトリビュートにはcobbler_port_tタイプも含まれていることがわかる。

つまり、httpd_can_network_connectブール値のうちの1行であるDT allow httpd_t port_type : tcp_socket name_connect ; [ httpd_can_network_connect ]はhttpd_can_network_connect_cobblerブール値の唯一の1行DT allow httpd_t cobbler_port_t : tcp_socket name_connect ; [ httpd_can_network_connect_cobbler ]を包含することがわかる。

従ってhttpd_can_network_connect_cobblerブール値を有効にしてできることはhttpd_can_network_connectブール値を有効にすることでも達成できる、というわけである。

で、どちらを有効にすべきかとなると、権限が少ない方とかより相応しい方とかで判断することになるだろう。
semanage boolean -lでSELinuxブール値の一覧を取得できるので、その最後に書かれている簡単な説明を読んで決めても良い。

restorecon → アクセスされたファイルのタイプがデフォルト設定とは異なる

「リソースのタイプを正しいものに直す」編の1つ目として、アクセスされたファイルのタイプがデフォルト設定とは異なるパターンがある。

次はhttpdをstartし、/tmp/1.htmlを作ってcp -a /tmp/1.html /var/www/htmlを実行した後curl http://localhost/1.htmlを実行したがTEのアクセス制御に引っ掛かりエラーになった時のsetroubleshootの出力内容である。

*****  Plugin restorecon (92.2 confidence) suggests   ************************

If you want to fix the label.
/var/www/html/1.html default label should be httpd_sys_content_t.
Then you can run restorecon.
Do
# /sbin/restorecon -v /var/www/html/1.html

これは/var/www/html/1.htmlの規定のタイプがhttpd_sys_content_tであるにもかかわらず別のものになっているのが問題の原因であり、/sbin/restorecon -v /var/www/html/1.htmlを実行すると解決する、と言っている。

もっとも解決にはディレクトリに対してrestorecon -RFv /var/www/html/としてしまった方が良いだろう。
restoreconはディレクトリやファイルのSELinuxコンテキストを永続的に規定のものに変更するコマンドである。
-Rオプションはディレクトリ内を再帰的に実行する、-Fオプションは強制的に変更する、-vオプションはSELinuxコンテキストを変更したディレクトリやファイルを表示する。

あるディレクトリやファイルの規定のSELinuxコンテキストはmatchpathcon <パス>で取得できる。パスには*などを使用できる。
semanage fcontext -lで規定のSELinuxコンテキストの一覧が取得できるが、こっちはちょっと読みづらい。

この問題はファイルをcp -a 6 やmvで別のディレクトリに持っていくと元のSELinuxコンテキストのままになることが原因になって起こることが多い。

対策として /etc/selinux/restorecond.conf に列記されたパスを自動的に規定のSELinuxコンテキストに修正してくれるrestorecondサービスを使用するのも良い。
修正してくれるタイミングはおそらくinotifyイベントが発生した時、つまりコピーや移動などを含むファイル作成時や変更時、後はサービス起動時である。
ただし、ハードリンクに対しては働かないことに注意。

catchall_labels → 参照ディレクトリを標準以外のものにした場合の対応

「リソースのタイプを正しいものに直す」編の2つ目は1つ目と似ているが、こちらはファイルの既定のタイプすらプロセスのドメインからアクセス禁止となっている場合である。

次は/var/www/htmlではなく/space/1をドキュメントルートにしてhttpdをstartし、curl http://localhost/a.htmlを実行したがTEのアクセス制御に引っ掛かりエラーになった時のsetroubleshootの出力内容である。

*****  Plugin catchall_labels (83.8 confidence) suggests   *******************

If you want to allow httpd to have getattr access on the a.html file
Then you need to change the label on /space/1/a.html
Do
# semanage fcontext -a -t FILE_TYPE '/space/1/a.html'
where FILE_TYPE is one of the following: NetworkManager_exec_t, (長すぎるので省略).
Then execute:
restorecon -v '/space/1/a.html'

これはsemanage fcontext -a -t FILE_TYPE '/space/1/a.htmlを実行して規定のSELinuxコンテキストを変更してからrestorecon -v '/space/1/a.htmlを実行しろ、と言っている。
FILE_TYPEはhttpd_tドメインがアクセスを許可されたタイプであるNetworkManager_exec_t以降から選べば良いのだが大量にありすぎてこの中から選ぶのは困難である。

そこで元々のドキュメントルートである /var/www/html のタイプを調べて、同じものを今のドキュメントルートに適用すれば良いだろう。

matchpathcon /var/www/htmlを実行すると、そのタイプがhttpd_sys_content_tであることがわかる。
なので今のドキュメントルートの規定のタイプをhttpd_sys_content_tにすれば良いが、ディレクトリだけではなくsemanage fcontext -a -t httpd_sys_content_t "/space/1(/.*)?"としてディレクトリ内のファイルの規定のタイプにも適用されるようにする。この変更は永続的である。

その後、restorecon -RFv /space/1で今のドキュメントルートディレクトリとその中のファイルのタイプを規定のものに変更すれば完了である。

なお、自分で追加した規定のタイプはsemanage fcontext -l -Cで見ることができる。
-lオプションは一覧を表示、-Cオプションは管理者が追加したものだけを表示するオプションである。

ちなみにファイルが作られる時、そのファイルのSELinuxコンテキストは規定のものではなく、そのファイルを置いたディレクトリと同じものになる。

bind_ports → 利用するポートを標準的でないポート番号に変更した場合の対応

「リソースのタイプを正しいものに直す」編の最後として、利用するポートを標準的ではないポート番号に変更した場合の対応を紹介する。
(ソケットもリソースの一種である)

次は公開ポートを8888にしてhttpdをstartしようとしたがTEのアクセス制御に引っ掛かり失敗した時のsetroubleshootの出力内容である。

*****  Plugin bind_ports (92.2 confidence) suggests   ************************

If you want to allow /usr/sbin/httpd to bind to network port 8888
Then you need to modify the port type.
Do
# semanage port -a -t PORT_TYPE -p tcp 8888
    where PORT_TYPE is one of the following: http_cache_port_t, http_port_t, jboss_management_port_t, jboss_messaging_port_t, ntop_port_t, puppet_port_t.

対応方法は書かれている通り、semanage port -a -t PORT_TYPE -p tcp 8888を実行すれば良い。この変更は永続的である。
PORT_TYPEはhttpd_tドメインがアクセスを許可されたタイプであるhttp_cache_port_t, http_port_t, jboss_management_port_t, jboss_messaging_port_t, ntop_port_t, puppet_port_tのうちの1つを選べば良いことを示している。
これらの中で意味合いが合っているタイプを選べば良いだろう。各タイプをラベルにしているポートは例えばsemanage port -l | grep -w http_cache_port_tで得ることができるので、それを参考にするのも良いだろう。

catchall → 監査ログからアクセスベクタルールを生成し適用する

最後に紹介するのは最終手段とすべき、監査ログからアクセスベクタルールを生成し適用する方法となる。
最終手段とすべき、というのは最初から自作しているドメインならともかく、selinux-policy-xxパッケージから提供されているドメインでこれをしないとならないことは本来ないはずでSELinuxブール値などで対処すべきだからである。
とはいえselinux-policy-xxパッケージから提供されているものにも結構不備はあるのである。

次はreposyncというコマンドを実行した時のsetroubleshootのログ出力 7 だが、「Plugin catchall (100. confidence) suggests」を返してきた場合他に手はないのでこの方法を採ることになる。

***** Plugin catchall (100. confidence) suggests **************************

If you believe that python2.7 should be allowed read access on the history directory by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# grep reposync /var/log/audit/audit.log | audit2allow -M mypol
# semodule -i mypol.pp

書かれているようにgrep reposync /var/log/audit/audit.log | audit2allow -M mypolでアクセスベクタルールのコンパイル済みファイル(これをポリシーモジュールと呼ぶ)であるmypol.ppを生成し、semodule -i mypol.ppを実行すれば監査ログファイル /var/log/audit/audit.log をgrep reposyncで抽出したものをすべて永続的にallowするようになる。

ただmypolという名前では目的がわかりづらいため適当な名前を付けた方が良いだろう。同じ名前のポリシーモジュールが複数あってもすべてを生かすようには取り込めない、という理由もある。

audit2allowは監査ログファイル /var/log/audit/audit.log をパイプで受け取る以外にもausearchの結果もパイプで受け取ることができる。
例えば2015-07-14 12:00:00以降の実行ファイル名がreposyncである監査ログだけをポリシーモジュールにしたい場合、ausearch -m avc -ts 07/14/2015 12:00:00 -c reposync | audit2allow -M reposyncとすれば良い。

audit2allow -M (ポリシーモジュール名)はコンパイル済みの(ポリシーモジュール名).pp以外にソースである(ポリシーモジュール名).teも生成する。
selinux-policy-develパッケージがインストールされていれば、teファイルを必要に応じて手で修正した後、make -f /usr/share/selinux/devel/Makefile (ポリシーモジュール名).ppで別途コンパイルを行うこともできる。コンパイル後にsemodule -i (ポリシーモジュール名).ppで取り込むことになる。
これはまだ監査ログには現れていないがいずれ登場するのがわかっている許可したい動作を、先んじて許可するためなどに使用できる。

設定可能なクラスと操作の種類は次を見れば書かれている。
http://selinuxproject.org/page/ObjectClassesPerms

なお、audit2allowで生成されたteファイルには、ブール値で対応可能なアクセスベクタルールがあった場合それを教えてくれるので、そのような場合はそれに従うと良いだろう。

対応のまとめ

ここまで見てきた通りSELinuxを使っていて問題が発生した場合、基本的な対応パターンは次のようなものになる。

  1. setenforce 0でpermissiveにする
  2. /var/log/messages に出力されたsetroubleshootのログを確認し、適切な対応を実施する
  3. setenforce 1でenforcingに戻して問題が解消しているかを確認する

経験上、permissiveの時は発生しないのにenforcingの時には発生する問題もあったりするが、その場合もsetroubleshootのログを確認して問題解決するまで対応を続けるだけである。

これで問題が解決すれば実は監査ログからアクセスベクタルールを生成する場合以外は監査ログファイル /var/log/audit/audit.log を確認する必要はあまりなかったりする。

ただ、これだけで問題が解決しない場合がある。

dontauditルールは「アクセスを許可せず、アクセスが試みられた場合監査ログに記録しない」ルールである。
つまり監査ログファイルを見ることによっても、setroubleshootのログを見ることによっても問題解決ができないのである。
(監査ログに残らなければsetroubleshootも働かない)

その場合はsemodule -DBでdontauditルールを無効にして対応することになる。
ただしdontauditルールを無効にした場合もsetroubleshootが働かなくなるので、setroubleshootに頼らない方法で問題解決をしなければならない。

setroubleshootに頼らずに問題解決を行う

setroubleshootに頼らない場合、次のような方法で対応できる。

監査ログにはプロセスのドメイン、リソースのタイプ、リソースのクラス、アクセス拒否された操作が記録されている。
sesearch --allow --auditallow -C -s <ドメイン> -t <タイプ> -c <クラス> -p <操作>を実行することにより、対応するSELinuxブール値があるかどうかを知ることができる。
対応するSELinuxブール値が見つかればそれを使って解決すれば良いだろう。

監査ログのpathにはアクセスされたパスが書かれているので、それに対してrestoreconして解決すればタイプの誤りだったことがわかる。

標準的なディレクトリやポート以外を使っているために問題が発生している場合は、管理者は標準外を使っていることを大抵知っているだろうからそれに応じた対応ができる。

どれにも当てはまらない場合は監査ログからアクセスベクタルールを生成する。

対応が完了したらsemodule -Bでdontauditルールを再度有効化する。

SELinuxを諦める……が、敗戦処理にもやり方がある

最後に。

これまで示してきたやり方のほとんどは正しく対症療法であり、未知の問題の発生が不定期で多種多様に渡るなど、頑張ってSELinuxに対応しようしても現実的に無理、ということもあるだろう。
もちろんsetenforce 0を実行し、 /etc/selinux/config を書き換えてpermissiveにしてしまえばSELinuxをすぐに諦めることができる。

ただ、問題を起こすのがどのドメインかがわかっていればそのドメインだけをpermissiveにし、他のドメインについては引き続きenforcingでSELinuxを使用し続ける、という対応を取ることもできる。

次のコマンドで特定のドメインをpermissiveにすることができる。この変更は永続的でありOS再起動をしても解除されない。

# semanage permissive -a <ドメイン>

permissiveになっているドメインは次で確認できる。

# semanage permissive -l

ドメインをpermissiveでなくすには次のコマンドを実行する。

# semanage permissive -d <ドメイン>

参考文献



  1. 古いdnfではdnf install *bin/<コマンド名>でも通ったがFedora 22のdnfでは通らない。バグっぽいが https://github.com/rpm-software-management/dnf/pull/238/files のコメント見ると仕様扱いかなあ。 

  2. 実際にはもう1つ、「他のルールによるallow, auditallowを許さない」、neverallowという特別なルールもある。 

  3. manを読む限り正確には00:00:01である可能性が高い。なお、指定した時刻ちょうどは検索結果に含まれる。 

  4. setroubleshoot-serverはパッケージ名にserverという文字列を含むが、デスクトップ環境用のGUIアプリケーションを含まない、程度の意味合いだと推測される。デスクトップ環境用のGUIアプリケーションを含んだパッケージはsetroubleshoot。 

  5. 調べがつかなかったが「&&」は逆ポーランド記法の位置に書かれたAND演算子と思われる。 

  6. 正確には-aオプションが含有する「–preserve=context」の影響である。 

  7. reposyncを実行した時はいつでもこうなるわけではない。プロセスのドメインは親プロセスと通常は同じになるので、親プロセスのドメインにアクセスベクタルールの不備があればこうなる、というだけである。 

TOP