0. はじめに
気が付けば桜の花は舞い散り、葉桜の緑があざやかな季節が早くもやって参りました。巷では研修の一環として構築したWebサーバに権限もファイアウォールも設定したにも関わらず、なぜか意図した動作にならない事に頭を抱えた末、
# setenforce 0
# getenforce
Permissive
に辿り着いた新入エンジニアの方もいらっしゃる頃ではないでしょうか。かく言う私もデフォルトで有効になっているセキュリティをこんなにも気軽に無効にしていいものかと、悶々とした一人です。
SELinux はいわばシステムへの侵入を許した後を想定した、事後防衛のための仕組みです。近年コンテナ技術への注目度が高まっていく中で、このようなカーネルレベルのセキュリティの重要性が高まっています。本稿では Linux 初学者の方や、Dockerのセキュリティについて目を向け出した方向けに、 SELinux について知っておくべきことを紹介します。
目次
1. Docker 特有のセキュリティリスクについて
1-1. コンテナブレイクアウト
1-2. 実際の事例
1-3. Docker における MAC の必要性
2. SELinux と Docker の関係
2-1. SELinux のおさらい
2-1-1. SELinuxのアクセス制御の仕組み
2-1-2. 実際のファイルコンテキストとアクセスベクタルール
2-2. SELinux オフで Docker を動作させた場合
2-2-1. dmesg 問題
2-3. SELinux がどのように Docker を保護するか
2-3-1. container-selinux の概要
2-3-2. コンテナ内のコンテキスト
3. bind-Mount 時に想定されるエラー
4. おわりに
1. Docker 特有のセキュリティリスクについて
1-1. コンテナブレイクアウト
デフォルト設定では、 Docker コンテナ内の実行ユーザは root になり、ユーザ固有の識別子 (UID) は 0 番になります。通常であればコンテナからはホスト上のプロセスもファイルも見えない隔離状態にあるため、コンテナがホストに対して悪さをすることはありません。しかし、もしこの隔離状態が破られた場合、コンテナ内の root ユーザは正規の root ユーザと何ら変わりない権限を持ちます。なぜならば、ホストは UID でユーザを識別するため、コンテナ内 root の UID も 0 番である以上ホスト側の root ユーザとの区別がつかないためです。
1-2. 実際の事例
実際に近年発生したコンテナブレイクアウトの危険を伴う Docker の脆弱性には以下のようなものがありました。
- docker cp コマンドの脆弱性(CVE-2019-14271, 2019/7/25 修正済み)
ライブラリ libnss_*.so が不正な内容に書き換えられたコンテナを対象にdocker cp コマンドを実行すると、攻撃者が任意のコードを実行可能になる脆弱性が発覚しました。
- runc の脆弱性(CVE-2019-5736, 2019/2/11 修正済み)
Docker のコンテナ作成を実際に担っているのは、runc というランタイムです。
悪意あるコンテナを実行した場合に runc を書き換えられ、任意のコマンドをホスト上で実行される恐れがありました。
1-3. Docker における MAC の必要性
上記の通り、未知の脆弱性が存在する可能性はいかなるソフトウェアにも潜んでいます。特に比較的新しい技術を採用する場合、そのリスクは一段と高いものとなります。また、カーネルをホストと共有するというコンテナ技術の特性上、OS上のユーザやプロセスが利用可能なリソースや、アクセス可能なファイルについて仕切りをつけることは予期せぬ動作を防止する意味でも重要な役割を持ちます。
境界型セキュリティによる完全な侵入防止の実現の難しさと、ホストとコンテナ間の分離を強化という2点を考慮した時、白羽の矢が立つのは SELinux を代表とする MAC(強制アクセス制御)の仕組みです。
以降の章では、SELinux がどのように Docker を保護するのかについて、より具体的な部分について掘り下げていきます。
2. SELinux と Docker の関係
2-1. SELinux のおさらい
前章でも述べましたが、 SELinux とは Linux カーネルに MAC (強制アクセス制御)を追加する仕組みです。管理者を含む全てのユーザは、SELinux によって明示的に許可されていない動作は制限を受けるようになります。つまり SELinux はホワイトリスト形式で OS 管理下の動作を監視し、制御しています。ただ日頃 Linux を触っていて、SElinux による制御を感じる機会は多くないと思います。なぜかというと、RHEL 系の OS では デフォルトで targeted と呼ばれるポリシー(複数のルールの集合)が適用されており、 RHEL にサポートされている OSS に関してはユーザ自身でポリシーを定義しなくても SELinux の庇護を受けた状態で利用できるからです。
では具体的に、どのような仕組みでルールを定義しているでしょうか。
2-1-1. SELinuxのアクセス制御の仕組み
まず、SELinux は全てのファイルやプロセスに対してコンテキストと呼ばれるラベルを付与します。そして、あるラベルからあるラベルへの操作をどの程度許可するかというルールを定義します。これをアクセスベクタルールと呼びます。動作主体と動作受態は何者であるかをコンテキストによって定めた後、両者の関係はどうあるべきかを記述するのが SELinuxのルール定義です。
2-1-2. 実際のファイルコンテキストとアクセスベクタルール
Apache を例に、実際に OS 上でユーザ、ファイル、プロセスがどのようなコンテキストを持っているか確認します。ファイルのコンテキストは ls コマンドに Z オプションを付与することで確認できます。
ユーザが web 上に公開したいコンテンツを置くディレクトリ、 /var/www/html が持つコンテキストはsystem_u:object_r:httpd_sys_content_t:s0
です。
# ls -lZ /var/www | grep html
drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 html
SELinux のコンテキストは : 記号で区切られた4つのフィールドから成ります。
左から順に、 ユーザ:ロール:タイプ(動作主体の場合はドメインと呼ばれる):MLSレベル を示しています。
続いて、id コマンドに Z オプションを付与し、ログイン中のユーザのコンテキストを確認します。root ユーザのデフォルトのコンテキストは以下です。
# id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
unconfined は SELinux から一切の制限を受けない特別なドメインの一つです。デフォルト状態の SELinux では、一般ユーザも含め unconfined ドメインが割り当てられるようになっており、ユーザ情報を基にした制御を行いません。
httpd のプロセスが持つコンテキストも確認してみましょう。例によって、ps コマンドに Z オプションを付与します。
# ps -auxZ | grep httpd
system_u:system_r:httpd_t:s0 root 7116 0.8 0.0 230420 5212 ? Ss 14:23 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 apache 7117 0.0 0.0 230420 3008 ? S 14:23 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 apache 7118 0.0 0.0 230420 3008 ? S 14:23 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 apache 7119 0.0 0.0 230420 3008 ? S 14:23 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 apache 7120 0.0 0.0 230420 3008 ? S 14:23 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 apache 7121 0.0 0.0 230420 3008 ? S 14:23 0:00 /usr/sbin/httpd -DFOREGROUND
httpd プロセスが持つタイプは httpd_t であることがわかりました。ところで、httpd の DocumentRoot の初期値である /var/www/html ディレクトリのタイプは httpd_sys_content_t でした。SELinux 管理用のユーティリティコマンドを使って、両者の関係がどのように定義されているのか確認してみましょう。
SELinux のユーティリティはパッケージ名とコマンド名が一致していないことが多いため、*bin/<パッケージ名> で指定してインストールを行います。
# yum -y install *bin/sesearch
SELinux のアクセスベクタルールの中から、動作主体が httpd_t, 動作受態は httpd_sys_content_t を持つファイルへの動作許可に関するルールを検索します。
# sesearch --allow -s httpd_t -t httpd_sys_content_t --class file -d
Found 1 semantic av rules:
allow httpd_t httpd_sys_content_t : file { ioctl read getattr lock map open } ;
ファイルの読み込み(read)、占有(lock)を始めとする、5つの動作(ioctl read getattr lock map open)が許可されていることを読み取れました。
2-2. SELinux オフで Docker を動作させた場合
2-1 節では、SELinux のアクセス制御の仕組みと実例について触れました。
SELinux がどのように コンテナからホストを保護するのか着目する前に、MAC を導入せずにコンテナを動作させた場合の問題点について確認します。
2-2-1. dmesg 問題
SELinux が 無効になっている状態で、コンテナから dmesg コマンドを実行するとカーネルが出力したメッセージが見えてしまいます。
# getenforce
Disabled
# docker run -it --rm centos:7 bash
# dmesg
コンテナとホストの隔離が完全ではないことは、コンテナを糸口にしたホストへの攻撃の原因となる恐れがあり危険です。
ホスト側で SELinux が有効な状態だと読み取りが拒否されます。
# getenforce
enforcing
# docker run -it --rm centos:7 bash
# dmesg
dmesg: read kernel buffer failed: Operation not permitted
2-3. SELinux がどのように Docker を保護するか
2-3-1. container-selinux の概要
SELinux はポリシー・モジュールという形で、定義したルール群を rpm パッケージとして共有できます。container-selinux は、SELinux が有効な環境下で Docker が制限にかかることなく利用できるよう定義されたポリシーです。Docker Community Edition (docker-ce) の 依存パッケージとしてインストールされます。
2-3-2. コンテナ内のコンテキスト
仮想マシン上で Apache をインストールした時、 /var/www/html のコンテキストは以下でした。(2-1-2 参照)
# ls -lZ /var/www | grep html
drwxr-xr-x. root root system_u: object_r:httpd_sys_content_t:s0 html
さて、コンテナ内で Apache のインストールを行った場合、仮想マシンと同じコンテキストが同ディレクトリに付与されるでしょうか。実際に確かめてみます。
# ls -lZ /var/www | grep html
drwxr-xr-x. root root system_u: object_r:container_share_t:s0 html
仮想マシン上の場合とは異なり、 container_share_t:s0 タイプが付与されていることがわかりました。コンテナ内でのファイルコンテキスト付与の規則は container-selinux に従うためです。
新しくディレクトリを作成した場合も比較してみます。Document Root を、 /var/www/html より一段上の階層に作り変更したという想定で、/var/html ディレクトリと testpage ファイルを作成してコンテキストを確認します。
# mkdir /var/html
# touch /var/html/testfile
# ls -lZ /var/html
-rw-r--r--. root root unconfined_u: object_r:var_t:s0 testpage
httpd_sys_content_t タイプではなく、 var_t タイプが付与されました。 /var/www 内と /var/ 内では付与されるコンテキストが異なるため、SELinux が有効な状態だと web サーバにアクセスしても権限エラーで拒否されてしまいます。
一方、コンテナ内では /var/ww/html 下と同様、 container_share_t が付与されます。仮想マシンと同様の権限エラーはコンテナの場合発生しません。
# mkdir /var/
# cd /root/testdir
# touch testfile
# ls -lZ
-rw-r--r--. root root system_u: object_r:container_share_t:s0 testpage
プロセスもまた、コンテナ内では独自のコンテキストが付与されます。
# ps axZ | grep httpd
system_u:system_r:spc_t:s0 2140 ? Ss 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:spc_t:s0 2169 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:spc_t:s0 2170 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:spc_t:s0 2171 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:spc_t:s0 2172 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:spc_t:s0 2173 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
httpd に付与されている spc_t は、 unconfined_t 同様ポリシーに制限されることのない特別なタイプです。ホスト側で SELinux が有効な状態かつ、Docker デーモンの起動時に –selinux-enabled オプションを指定しなかった場合、コンテナアプリのプロセスにはこのタイプが付与されます。このタイプは他のコンテナが持つファイル等にもアクセスが可能なため、複数のコンテナによりサービスを構築する場合問題になる恐れがあります。spc_t タイプを付与せずにコンテナを起動するには、以下の手順を実行します。
(1) /usr/lib/systemd/system/docker.service を開き、ExecStart の値に –selinux-enabled を追記します。
# vi /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --selinux-enabled
(2) 変更を反映し、Docker デーモンをリスタートします。
# systemctl daemon-reload
# systemctl restart docker
プロセスのタイプが、 container_t:s0:c500,c676 に変更されます。
# ps axZ | grep httpd
system_u:system_r:container_t:s0:c500,c676 1 ? Ss 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:container_t:s0:c500,c676 6 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:container_t:s0:c500,c676 7 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:container_t:s0:c500,c676 8 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:container_t:s0:c500,c676 9 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:container_t:s0:c500,c676 10 ? S 0:00 /usr/sbin/httpd -DFOREGROUND
MLS レベルを表す s0 の後に、新しいフィールドが追加されました。これは MCS カテゴリ番号です。同じカテゴリ番号を持つリソースへのアクセスに限定し、部署ごとに権限を隔てるといった利用ができるラベルです。Docker では、コンテナとコンテナ同士の不要な干渉を防止するために MCS カテゴリ番号を使ったアクセス制御を行っています。
3. bind-Mount 時に想定されるエラー
2章では、SELinux がどのように Docker に許可を与え、ホストとコンテナ間の分離に貢献しているかについて焦点を当てました。最後に、SELinux によって意図した動作を妨げられた場合の対応について、bind-Mount 時を例に説明します。
Docker でホスト上のディレクトリをコンテナに共有したい場合は、コンテナ実行時に bind-Mount タイプでマウントを行います。しかし、マウントされたディレクトリのコンテキストは、コンテナ内のプロセスからの操作を許可するものとなっていないため、コンテキストを書き換える必要があります。解決手段の一つとして、docker run コマンドのオプションを利用し、コンテキストの書き換えを自動で行う方法があります。コンテナ実行時にコンテキストの自動書き換えを行うには、以下のオプションのいずれかを、目的に合わせて選択します。
docker run -v <マウントするディレクトリの絶対パス>:<マウント先の絶対パス>:z
docker run -v <マウントするディレクトリの絶対パス>:<マウント先の絶対パス>:Z
docker run -v <マウントするディレクトリの絶対パス>:<マウント先の絶対パス>:ro
各オプションの仕様の違いは以下の通りです。
- 小文字 :z オプション
複数のコンテナから読み書き可能な状態でマウントします。具体的には、container_file_t タイプが一時的に対象ディレクトリへ付与されます。 小文字 :z オプションによる設定は一時的なものであるため、ホストの再起動や restorecon コマンドによるリセットによって消滅します。 - 大文字 :Z オプション
最後にマウントしたコンテナのみ読み書き可能でマウントします。具体的には、container_file_t タイプに加え、MCS カテゴリ番号が対象ディレクトリに付与されます。 対象ディレクトリは、最後にマウントされたコンテナと一致するカテゴリ番号を持つことになるため、複数のコンテナから対象ディレクトリがマウントされた場合、最後にマウントされたコンテナからのアクセスのみ許可されます。 - :ro オプション
複数のコンテナから読み取りのみ可能な状態でマウントします。 具体的には、container_share_t タイプを対象ディレクトリに付与します。
4. おわりに
Docker を使うなら SELinux をオンにすべき理由とは、ゼロデイ攻撃への防衛強化及び、カーネルを共有するというコンテナ技術の特性上切っては切り離せないシステム内部の問題への対策のためでした。今回は SELinux のポリシーの管理ユーティリティとして sesearch のみ使用しましたが、エラーログを基にしたポリシーの自動作成を行ってくれるツール allow2audit やコンテキストとルールの確認を行える semanage など、まだまだたくさんの便利なユーティリティが存在します。良い SELinux ライフを。
参考文献
- 須田瑛大, 五十嵐綾, 宇佐美友也 “Docker / Kubernates 開発・運用のためのセキュリティ実践ガイド” マイナビ出版 2020年
- Dan Walsh “Dan Walsh’s Blog — LiveJournal” https://danwalsh.livejournal.com/74754.html (参照 2020年4月20日)
- Maciej “Maciej – Medium” https://medium.com/@iced_burn/docker-selinux-312457062179 (参照 2020年4月20日)
- RedHat “RedHat Customer Portal” https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/6/html/security-enhanced_linux/sect-security-enhanced_linux-working_with_selinux-selinux_contexts_labeling_files (参照 2020年4月20日)