【図解/初心者向け】SELinuxとは?〜仕組みやメリット・効果の基礎入門解説〜

SELinuxとは ~概要と仕組み~

SELinux とは Secure-Enhanced Linux の略で、セキュリティ機能を強化するモジュールです。SELinux では『プロセスが OS 管理下のリソースへアクセスすることを監視・制御すること』ができます。

原理的にはプロセスは全ての OS 管理下リソースへのアクセスを制御されますが、実装としてはファイルシステムへのアクセス、および、TCP/UDP ポートの利用に関する制御が中心的に組み込み設定されています。

多くはアプリケーションからシステムコールを受ける際に、カーネル機能のLSM (Linux Security Module) でフックして許可ルールとの照合を行います。システムコールの量も許可ルールも多いため、照合は AVC というキャッシュで判断することで負荷を抑えています。

また、一部の SELinux を意識して作られたアプリケーション (SELinux-Aware なアプリ) ではシステムコール経由ではなくアプリ自身が SELinux のライブラリを使ってアクセス制御を行うこともできます。(ただしまだ実装は少ない。SE-postgreSQL 等がある。)

SELinux のアーキテクチャ概要を以下に示します。

SELinux では全てのプロセス (Source) と OS 管理リソース (Target) に ”コンテキスト(context)” というラベルを付けます。プロセスのコンテキストを Source Context (scontext)、ファイル等のリソースのコンテキストを Target Context (tcontext) と呼びます。

一方、SELinux の許可ルールは AV (Access Vector) ルールと呼ばれ、Source Context から Target Context へどのような操作 (Access Vector と言います) を許可するかを定めています。この AV ルールで許可されていない場合、プロセスはリソースへのアクセスを拒否されます。

コンテキストのフォーマットは [ User : Role : Type/Domain : Sensitivity range ] となっています。

3 つ目の属性は Source Context つまりプロセスのときはドメイン (Domain) と呼び、Target Context つまりファイル等のリソースのときはタイプ (Type) と呼びますが、基本的に呼び名が違うだけで扱い方は同じです。

本記事で扱う範囲

本記事では SELinux のデフォルト設定である「SELINUX=enforcing, SELINUXTYPE=targeted」を前提として解説します。

このモードでは、AV ルール (アクセス許可ルール) はコンテキストの中でも 3 つ目の属性である "タイプ/ドメイン" のみを取り扱います。(User や Role や Sensitivity range は無視します)

FAQ

Q. SELinux ってデフォルト設定でもいいの?

A. デフォルトでも十分効果があります。

アクセス制御に使われる Access Vector (AV) ルールは「httpd プロセスは具体的にどのようなディレクトリにアクセスしてよいか、どのような TCP ポートを使ってよいか」といった一般的な情報を元に作られています。

なので、プロセス (サービス) に関して特別な設定をしなければデフォルトでうまく動作するはずです。

ですが例えば httpd による公開ディレクトリを /var/www/html から変更したり、待受ポートを tcp:80,443 から変更するのであれば、それに対応した SELinux 設定が必要となります。

昔よりもトラブルになることは少なくなりましたが、今でもバグフィックスとして許可ルール (ポリシールール) がアップデートで追加されています。

Q. ポリシールールってどれくらいあるの?自分でも作らなきゃいけないの?

A. 20万以上あります。基本は自分で作らなくてもよいです

主要なポリシールールは大きく 2 種類あります。

1 つは Type Enforcement ルールで、前述の AV ルール (アクセス制御ルール) や後述の Type Transition ルール等を含むルールのことです。20 万以上のルールがあります。

Type Enforcement ルールは以下コマンドで確認できます。

# dnf -y install setools-console
# sesearch -A

AV の許可ルールだけ見たい場合は sesearch --allow を打ちます。

もう 1 つは Labeling ルールです。例えば『このディレクトリ配下に新規作成される file/dir はこのコンテキストを割り当てる』といったファイルやディレクトリへのコンテキスト割当ルールを行うファイルLabelingルールがあります。このLabelingルールは 6000 ルール程度です。例えば

/tmp(/.*)?  all files  fileB_u:fileB_r:fileB_t:s0

というファイルLabelingルールがあった場合、/tmp自体、およびその配下の全ての種類のファイル(シンボリックリンクやディレクトリ、ブロックデバイスファイル等)が新規に作成された場合にこのコンテキストが割り当たります。新規ではなくとも、restorecon -RFv コマンドをすることによりこのルール通りにコンテキストが修復されます。

ファイルLabelingルールは sesearch コマンドでは表示されませんが以下のsemanageコマンドで確認できます。

# dnf -y install policycoreutils-python-utils
# semanage fcontext -l

CentOS7 では『yum -y install policycoreutils-python』で semanage コマンドをインストールします。

Labeling ルールには他にも TCP/UDP ポート番号とコンテキストを紐付けるネットワーク Labeling ルールもあり、これは以下のコマンドで確認できます。

# semanage port -l

Labelingルールのイメージ図(紫の箇所)を以下に示します。

Q. SELinuxの実体はどこにあるの?

A. 設定は /etc/selinux および /var/lib/selinux 配下, ファイルコンテキストはファイルシステムの拡張属性, 動作中のバイナリは /sys/fs/selinux

定義やポリシー等のルールや設定は /etc/selinux および /var/lib/selinux 配下に存在しています。基本設定は /etc/selinux/config、中心となるポリシー (Reference Policy) は /etc/selinux/targeted/policy/policy.## になります。

また、ファイルやディレクトリのコンテキストはファイルシステムのメタ (i-node) 領域にある拡張属性の security.selinux という属性に存在しています。拡張属性は getfattr コマンドで見ることができ、setfattr コマンドで設定することもできます。

[root@localhost ~]# dnf -y install attr
[root@localhost ~]# touch test1.txt
[root@localhost ~]# getfattr -m security.selinux -d test1.txt
# file: test1.txt
security.selinux="unconfined_u:object_r:admin_home_t:s0"
[root@localhost ~]# setfattr -n security.selinux -v system_u:object_r:httpd_sys_content_t:s0 test1.txt
[root@localhost ~]# getfattr -m security.selinux -d test1.txt
# file: test1.txt
security.selinux="system_u:object_r:httpd_sys_content_t:s0"

ただし、通常は "getfattr" コマンドではなく "ls -Z" を使い、"setfattr" コマンドではなく chcon を使います。

[root@localhost ~]# ls -Z test1.txt
-rw-r--r--. root root system_u:object_r:httpd_sys_content_t:s0 test1.txt
[root@localhost ~]# chcon -t httpd_sys_rw_content_t test1.txt
[root@localhost ~]# ls -Z test1.txt
-rw-r--r--. root root system_u:object_r:httpd_sys_rw_content_t:s0 test1.txt

また、設定をファイルコンテキスト定義ルール通りに戻す場合は "restorecon -RFv" を使います。(R はディレクトリを再帰実行、F は強制リセット、v は詳細出力)

[root@localhost ~]# restorecon -RFv test1.txt
restorecon reset /root/test1.txt context system_u:object_r:httpd_sys_rw_content_t:s0->system_u:object_r:admin_home_t:s0

Q. パーミッションの種類ってどれくらいあるの?

A. たくさん

パーミッション一覧は seinfo コマンドで見れます。

[root@localhost ~]# yum -y install setools-console
[root@localhost ~]# seinfo -c -x

例えばファイルへの操作とネットワークに関する操作では、許可する内容は全然違います。ファイルには読み込みという操作がありますがネットワークにはありません。つまり操作対象によって具体的な許可する操作内容が異なるのです。

操作対象のことをオブジェクトクラス、許可する操作内容をパーミッションと言います。

オブジェクトクラス数は 100 程度あり、パーミッション数は 1000 を超えます。

詳細はこちら。http://www.selinuxproject.org/page/ObjectClassesPerms

Q. SELinuxでは結局具体的にどんな効果が期待できるの?

A. 次の章以降で詳しく見ていきます。

httpdでの例

httpd というプロセスは通常、httpd_t というドメインで起動しています。プロセスのコンテキストは ps の -Z オプションで確認できます。

[root@localhost ~]# dnf -y install httpd
[root@localhost ~]# systemctl start httpd
[root@localhost ~]# ps auxZ | grep httpd_t
system_u:system_r:httpd_t:s0    root      1526  0.0  0.1 224024  5024 ?        Ss   23:51   0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0    apache    1527  0.0  0.0 224024  2956 ?        S    23:51   0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0    apache    1528  0.0  0.0 224024  2956 ?        S    23:51   0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0    apache    1529  0.0  0.0 224024  2956 ?        S    23:51   0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0    apache    1530  0.0  0.0 224024  2956 ?        S    23:51   0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0    apache    1531  0.0  0.0 224024  2956 ?        S    23:51   0:00 /usr/sbin/httpd -DFOREGROUND

一方、一般的な Web サーバのコンテンツの置き場所である /var/www/html にはどのようなルールが適用されるのでしょうか。

[root@localhost ~]# semanage fcontext -l | grep /var/www/html
/var/www/html(/.*)?/uploads(/.*)?                  all files          system_u:object_r:httpd_sys_rw_content_t:s0
/var/www/html(/.*)?/wp-content(/.*)?               all files          system_u:object_r:httpd_sys_rw_content_t:s0
/var/www/html(/.*)?/wp_backups(/.*)?               all files          system_u:object_r:httpd_sys_rw_content_t:s0
/var/www/html(/.*)?/sites/default/files(/.*)?      all files          system_u:object_r:httpd_sys_rw_content_t:s0
/var/www/html(/.*)?/sites/default/settings\.php    regular file       system_u:object_r:httpd_sys_rw_content_t:s0
/var/www/html/[^/]*/cgi-bin(/.*)?                  all files          system_u:object_r:httpd_sys_script_exec_t:s0
/var/www/html/munin(/.*)?                          all files          system_u:object_r:munin_content_t:s0
/var/www/html/cgi/munin.*                          all files          system_u:object_r:munin_script_exec_t:s0
/var/www/html/munin/cgi(/.*)?                      all files          system_u:object_r:munin_script_exec_t:s0
/var/www/html/owncloud/data(/.*)?                  all files          system_u:object_r:httpd_sys_rw_content_t:s0
/var/www/html/nextcloud/data(/.*)?                 all files          system_u:object_r:httpd_sys_rw_content_t:s0
/var/www/html/configuration\.php                   all files          system_u:object_r:httpd_sys_rw_content_t:s0

/var/www/html の 1 つ下のディレクトリにマッチするルールが表示されました。もう 1 つ上のディレクトリに関するルールを見てみると

[root@localhost ~]# semanage fcontext -l | grep /var/www | more
/var/www(/.*)?                                     all files          system_u:object_r:httpd_sys_content_t:s0
~~~省略~~~

このように表示されます。つまり、/var/www 配下のコンテンツには "httpd_sys_content_t" というタイプがラベルされます(/.*)? は 正規表現で、ここでの意味は /var/www もしくは /var/www/ 配下の全ての file/dir を指します。(.* は任意の文字列で、/ も含めることができますので何階層下のファイルであってもこの表現に含まれます。)

では httpd_t というドメインから httpd_sys_content_t というタイプにはどのような操作を許可されているのでしょうか。

[root@localhost ~]# sesearch --allow | grep "allow httpd_t httpd_sys_content_t "
   allow httpd_t httpd_sys_content_t : dir { ioctl read getattr lock search open } ;
   allow httpd_t httpd_sys_content_t : lnk_file { read getattr } ;
   allow httpd_t httpd_sys_content_t : file { ioctl read getattr lock open } ;
   allow httpd_t httpd_sys_content_t : dir { ioctl read write getattr lock add_name remove_name search open } ;

4 つの許可ルール (AVルール) が表示されました。

1 番目のルールは 4 番目のルールに内包されています。許可ルールは色々なモジュールからインストールされるため、このように重複することは頻繁に起こります。(だからルールの行数が増えるわけですが)

lnk_file はシンボリックリンクファイルのことです。read は汎用的なファイルアクセス許可、getattr は属性情報へのアクセス許可を意味します。シンボリックリンクはリンク先情報がメタ (属性) 情報にありますので getattr が必要です。

file は一般的なファイルのことです。read や getattr は lnk_file と同様です。ioctl は IO ドライバ (ここでは Disk IO ドライバ) が提供する機能へのアクセス許可を意味します。lock は排他、open はファイルディスクリプタを紐付けてファイル入出力 (読み書き) が出来る状態にすることです。

dir はディレクトリのことです。file のアクセス権に加え、ディレクトリへの汎用的な書き込みを意味する write、ディレクトリ配下へのファイル作成する add_name、ファイル削除する remove_name が許可されています。

これらが httpd に与えられた、/var/www/html 配下のファイルへの操作です (ただしそれよりも下位ディレクトリで、別の定義ルールに該当する場合は、別のコンテキストが割り当たるため、操作許可内容は異なります。)

プロセスのドメインを決めるType Transition ルール

ところで、httpd のドメインが "httpd_t" である、というのはどのように決まっているのでしょうか。

実はこのドメインを決める要素が TypeEnforcement (TE) ルールにある Type Transition ルールです。httpd の実行ファイルは /usr/sbin/httpd ですが、このファイルのタイプは httpd_exec_t です。

[root@localhost ~]# ls -Z /usr/sbin/httpd
-rwxr-xr-x. root root system_u:object_r:httpd_exec_t:s0 /usr/sbin/httpd

一方、Type Transition ルールには以下のようなルールがあります。

[root@localhost ~]# sesearch --type | grep httpd_exec_t
~~~
type_transition init_t httpd_exec_t : process httpd_t;
~~~

これは『init_t というドメインにいるプロセスが httpd_exec_t のタイプの実行ファイルを起動するとそのプロセスは httpd_t のドメインに遷移する』という意味です。

systemd のプロセスのドメインが init_t であるため、起動時および systemctl コマンドでの httpd サービス開始時は (systemd による操作で起動するため) このルールに合致する訳です。

[root@localhost ~]# ps auxZ | more
LABEL                           USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
system_u:system_r:init_t:s0     root          1  0.0  0.1 193512  6612 ?        Ss   11:30   0:04 /usr/lib/systemd/systemd --switched-root --system --deserialize 22

この Type Transition ルールに該当しないケースでは、起動を行ったプロセスのドメインを継承します。

例えば root ユーザがログインや su - により bash にいるときのタイプは "unconfined_t" です。これは「制限なし」という意味で、SELinux としてはかなり自由に操作ができるタイプで、ログイン定義ルールによって予め決まっています。(後述しますが、権限を落とすように設定変更することも可能です。)

[root@localhost ~]# ps -Z
LABEL                              PID TTY          TIME CMD
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 4152 pts/0 00:00:00 bash

この状態で (systemctl ではなく) 直接 httpd を実行すると、httpd のコンテキストも "unconfined_t" になります。

[root@localhost ~]# /usr/sbin/httpd
[root@localhost ~]# ps auxZ | grep httpd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 root 4315 0.0  0.0 224020 3488 ? Ss 22:27   0:00 /usr/sbin/httpd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 apache 4316 0.0  0.0 224020 2956 ? S 22:27   0:00 /usr/sbin/httpd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 apache 4317 0.0  0.0 224020 2956 ? S 22:27   0:00 /usr/sbin/httpd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 apache 4318 0.0  0.0 224020 2956 ? S 22:27   0:00 /usr/sbin/httpd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 apache 4319 0.0  0.0 224020 2956 ? S 22:27   0:00 /usr/sbin/httpd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 apache 4320 0.0  0.0 224020 2956 ? S 22:27   0:00 /usr/sbin/httpd

このあたりに SELinux の目指したものが透けて見えます。つまり、一度 "httpd_t" というドメインで起動した httpd プロセスは、例えこのプロセスが乗っ取られ、root 権限に昇格され、シェルを起動されようが、子プロセスを fork しようが、(Type Transition ルールに無い限り、) ドメインは "httpd_t" から抜け出せないので、root にも関わらず、出来ることは "httpd_t" に与えられた AV ルール内に限られるのです。

ただし、"unconfined_t" のプロセスを乗っ取られたら SELinux を有効にした意味が無くなりますので、絶対にログインユーザのシェルから httpd 等のプロセスを直接起動してはいけません。必ず systemctl コマンド経由にします。

SELinux ってプロセスが乗っ取られて root に昇格されても大丈夫っていうけど、プロセスが乗っ取られたら SELinux を無効にされないの?』という疑問に対する答えがこれです。

SELinuxの思想 ~DACとMAC~

既に答えの根拠は出していますが、SELinux は攻撃そのものを防ぐことを目指しているわけではありません。

攻撃された結果、プロセスが乗っ取られたり root 権限を奪取された場合においても、そのプロセスを MAC (Mandatory Access Controll) による強制アクセス制御下に置き、行動を制限させることを目的としています。

この SELinux による MAC (強制アクセス制御) は、今までのファイルシステムの DAC (任意アクセス制御) とは軸が異なります。

ファイルシステムの DAC は各ユーザによりアクセス権を変更できるためこのように呼ばれますが、それ以外に root への権限付与を行いすぎた、という反省があります。つまり、root になればそのシステムを自由に操り法律を決める神様になれるのですが、プログラムの脆弱性によりリモートから root へ昇格するクラッキング行為が幾度と行われてきました。

SELinux によるアクセス制御は単に root であるだけでは変更できません。システムの神様になるには、root 権限かつ "unconfined_t" のような無制限のドメインである必要があるのです。

なのでポイントとして、攻撃者がパスワードクラックして ssh によるログインをされた場合は攻撃を制限できません。効果があるのはあくまで httpd 等で動かしているプロセスをバッファオーバーフロー等で乗っ取られた時の話です。

ログインユーザの操作をSELinuxで制限する

前述の通り、デフォルトではシェルから起動すると "unconfined_t" という無制限のドメインで動作させてしまう危険性があります。これは一般ユーザであってもそうなります。

これを制限するには、ログイン時のドメインを "unconfined_t" から変更する必要があります。これを実現するためには、『ローカルアカウントユーザ』を『SELinux ユーザ (コンテキストの一番最初の属性)』と紐付けることが必要です。

Linux ログイン時のデフォルト SELinuix ユーザ (__default__) は "unconfind_u" というユーザです。

[root@localhost ~]# semanage login -l
ログイン名                SELinux ユーザー         MLS/MCS 範囲           サービス
__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
system_u             system_u             s0-s0:c0.c1023       *

__default__ となっているのがデフォルト値で、全ユーザが "unconfind_u" に紐付けられています。そしてこの SELinux ユーザのロールは "unconfined_r" に紐付けられています。

[root@localhost ~]# semanage user -l
                ラベリング      MLS/       MLS/
SELinux ユーザー    プレフィックス    MCS レベル    MCS 範囲                         SELinux ロール
guest_u         user       s0         s0                             guest_r
root            user       s0         s0-s0:c0.c1023                 staff_r sysadm_r system_r unconfined_r
staff_u         user       s0         s0-s0:c0.c1023                 staff_r sysadm_r system_r unconfined_r
sysadm_u        user       s0         s0-s0:c0.c1023                 sysadm_r
system_u        user       s0         s0-s0:c0.c1023                 system_r unconfined_r
unconfined_u    user       s0         s0-s0:c0.c1023                 system_r unconfined_r
user_u          user       s0         s0                             user_r
xguest_u        user       s0         s0                             xguest_r

そしてログイン時のドメインはロールの _r を _t に変更したものになります。

では実際に testuser を unconfined_u ではないユーザに紐づけて見ましょう。

[root@localhost ~]# semanage login -a -s staff_u testuser
[root@localhost ~]# semanage login -l

ログイン名                SELinux ユーザー         MLS/MCS 範囲           サービス

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
system_u             system_u             s0-s0:c0.c1023       *
testuser             staff_u              s0-s0:c0.c1023       *

この状態で su - testuser をしてもドメインは変わりません。なぜならもともと unconfined_t で動作しているシェルだからです。

[root@localhost ~]# su - testuser
最終ログイン: 2018/07/01 (日) 21:58:14 JST日時 pts/0
[testuser@localhost ~]$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

そこで新たな SSH セッションで接続し、testuser でログインします。すると無事、ドメインが staff_t に変わりました。

[testuser@localhost ~]$ id -Z
staff_u:staff_r:staff_t:s0-s0:c0.c1023