晴耕雨読

working in the fields on fine days and reading books on rainy days

SELinuxでDockerのセキュリティを強化する

この記事では、Rocky LinuxでSELinuxが有効化されたDokcerを構築する方法について、実際にコマンドを使いながら演習をします。

公式の手順「Install Docker Engine on CentOS | Docker Docs」に従って、Dockerのインストールをします。 インストール手順は、公式の手順に従って行います。

[def-root]# yum install -y yum-utils
[def-root]# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
[def-root]# yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

ここまでで Docker をインストールすることができたので、サービスを起動していきます。

[def-root]# systemctl start docker

動作確認のために、公式が用意しているテスト用のコンテナをダウンロードして起動してみましょう。

[def-root]# docker run hello-world

問題なく起動させることができました。 さて、ここまででDockerのセットアップは完了したのですが、SELinuxの設定が完了していません。 公式の手順に従ってインストールしたDockerはデフォルトでSELinuxが有効になっていないため、万が一 Docker コンテナが特権で動作しているとホスト側の操作ができてしまう、いわゆる「コンテナエスケープ」をされたときに適切に攻撃を防ぐことができなくなります。

そこで、Dockerの起動オプションを編集して、dockerコンテナのプロセスがSELinuxのドメインの下で動作し、適切にアクセス制御がされるように設定してみましょう。 まず、dockerのサービスは systemd から dockerd コマンドが実行されることで、Dockerのサービスが起動します。 そのため、systemd のユニットファイルを編集することで、docker の起動オプションを変更することができます。

[def-root]# cd /usr/lib/systemd/system
[def-root]# cp -p docker.service{,.bak}

ユニットファイルを編集する前に、タイムスタンプを変えずに、バックアップを作成しておきました。 そしたら、ユニットファイルを編集していきます。

[def-root]# vi docker.service
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --selinux-enabled

サービス起動時に実行するコマンドを指定する ExecStart の部分で、/usr/bin/dockerd を起動しているので、そのオプションに –selinux-enabled を指定してあげます。 これで起動すると、SELinuxで適切なラベルが付与された状態でプロセスが起動してきます。

dockerのサービスを再起動する前に、まずは、ユニットファイルを編集したので、ユニットファイルの再読み込みを行います。

[def-root]# systemctl daemon-reload

再読み込みしたら、dockerのサービスを起動してみましょう。

[def-root]# systemctl restart docker
[def-root]# systemctl status docker
● docker.service - Docker Application Container Engine
    Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; preset: disabled)
    CGroup: /system.slice/docker.service
            └─20333 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --selinux-enabled

systemctl の stasus で確認したときに、コマンドのオプションに –selinux-enabled が追加されたことで、SELinuxが有効化されたモードで起動されました。 このときの dockerd のプロセスは container_runtime_t ドメインで動作しています。

[def-root]# ps auxZ | grep docker
system_u:system_r:container_runtime_t:s0 root 20333 0.0  5.0 1928868 91592 ?     Ssl   4月19   1:28 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --selinux-enabled

さて、この状態でコンテナを起動してみましょう。 このとき、後で検証できるように root ディレクトリを起動するコンテナにマウントして、コンテナの中からホスト側の root ディレクトリにアクセスできるようにして、コンテナを起動します。

[def-root]# docker run -it --rm -v /root:/hostroot centos:latest /bin/bash
[root@4dd9367681d8 ~]# id
[root@4dd9367681d8 ~]# ps -Z

コンテナの中に入ることができました。 ps コマンドでプロセスのコンテキストを見ると、bash のプロセスは container_t のドメインで動作していることが確認できます。 そして、現在のカレントディレクトリのコンテキストも一応確認してみます。

[def-root]# pwd
/root
[def-root]# ls -dZ .
system_u:object_r:container_file_t:s0:c126,c245 .

カレントディレクトリは、container_file タイプなので、ファイルの読み書きが許可されていそうです。 実際に、vim コマンドを使って書き込みすることは可能です。

[def-root]# vi test.txt
hello
[def-root]# cat test.txt

次に、カレントディレクトリをホスト側からマウントした hostroot ディレクトリに移動します。 ここのディレクトリがホスト側のディレクトリと共通なので、ここに書き込めるかどうかでDockerのプロセスに対するSELinuxが有効かどうかを検証することができます。

[root@4dd9367681d8 /]# cd /hostroot/
[root@4dd9367681d8 hostroot]# ls -d -Z .
system_u:object_r:admin_home_t:s0 .

まず、hostroot のコンテキストを確認すると、admin_home タイプになっています。 これは、一般的に root ディレクトリの下にラベル付けされるタイプで、rootユーザのみが書き込めることを表すものです。 この hostroot に対して、ファイルを作成してみましょう。

[root@4dd9367681d8 hostroot]# echo hello > test.txt
bash: test.txt: Permission denied
[root@4dd9367681d8 hostroot]# ls
ls: cannot open directory '.': Permission denied

ファイルを書き込めないどころか、そもそも ls コマンドでディレクトリの中を確認することすらできません。 それでは、ホスト側でSELinuxが何を拒否したのかを確認してみましょう。

[def-root]# grep denied /var/log/audit/audit.log
type=AVC msg=audit(1713603292.240:1088): avc:  denied  { write } for  pid=21476 comm="bash" name="root" dev="dm-0" ino=16777346 scontext=system_u:system_r:container_t:s0:c170,c393 tcontext=system_u:object_r:admin_home_t:s0 tclass=dir permissive=0

監査ログの拒否ログには、containerドメインで動作するbashコマンドが admin_home タイプのディレクトリに書き込みを試みようとしたのをSELinuxが拒否したことが記録されています。 つまり、コンテナの内部からコンテナの外へのアクセスがSELinuxによって拒否されており、より高いセキュリティを維持することができます。

もし、SELinuxが無効化されている場合は、通常通り、マウントしたrootのディレクトリに書き込むことができます。 試しに、SELinuxを無効化して検証してみましょう。

[def-root]# setenforce 0
[def-root]# getenforce
[root@4dd9367681d8 hostroot]# ls
[root@4dd9367681d8 hostroot]# echo hello > test.txt
[root@4dd9367681d8 hostroot]# cat test.txt
hello
[def-root]# cat test.txt
hello

SELinuxを無効化したことで、ホスト側のrootのホームディレクトリにファイルが書き込めることが確認できました。 ですが、SELinuxを無効化することはセキュリティレベルの低下につながるため、できれば無効化しないで設定だけでなんとかしたいです。 一旦、SELinuxは有効に戻しておきましょう。

[def-root]# setenforce 1

では、SELinuxを有効化したまま、ホスト側で指定した特定のディレクトリにのみアクセスできるように設定するにはどうすればいいでしょうか。 1つの案としては、ホスト側のディレクトリのコンテキストを修正することです。 まず、sesearch コマンドで、container_t ドメインがアクセスできるタイプについてSELinuxの設定を調査してみます。

[def-root]# sesearch --allow --source container_t
allow svirt_sandbox_domain container_file_t:dir { add_name create execmod ioctl link lock read relabelfrom relabelto remove_name rename reparent rmdir setattr unlink watch watch_reads write }
allow svirt_sandbox_domain container_ro_file_t:dir { ioctl lock read }

container ドメインがアクセスする対象は container_file_t や container_ro_file_t などのラベルをつけておくと、SELinuxによるアクセスが許可されるようです。 試しに、ホスト側の root のホームディレクトリの下に、container_file タイプを持つディレクトリを作成して、それをマウントして、コンテナからホスト側に書き込みできるか検証してみましょう。

[def-root]# pwd
[def-root]# mkdir rw_dir
[def-root]# ll -Z
[def-root]# chcon -t container_file_t rw_dir
[def-root]# ll -Z

コンテナから読み書きできるディレクトリを用意したので、このディレクトリをマウントしてコンテナを起動します。

[def-root]# docker run -it --rm -v /root/rw_dir:/hostroot centos:latest /bin/bash
[root@28dfb8df0a10 /]# cd /hostroot/
[root@28dfb8df0a10 hostroot]# ls -dZ .
unconfined_u:object_r:container_file_t:s0 .
[root@28dfb8df0a10 hostroot]# echo hello2 > hello2.txt
[root@28dfb8df0a10 hostroot]# ls
[def-root]# ls rw_dir/

マウントした container_file タイプのディレクトリに書き込みできることができました。

もし、マウントしたディレクトリを読み取り専用にしたい場合は、container_ro_file タイプをラベル付けします。 試しにコマンドで検証してみましょう。

[def-root]# cp -r rw_dir/ ro_dir
[def-root]# chcon -R -t container_ro_file_t ro_dir
[def-root]# ll -Z

[def-root]# docker run -it --rm -v /root/ro_dir:/hostroot centos:latest /bin/bash
[def-root]# cd /hostroot
[def-root hostroot]# ls -dZ .
unconfined_u:object_r:container_ro_file_t:s0 .
[def-root hostroot]# ls
hello2.txt
[root@b1e3967e8a69 hostroot]# cat hello2.txt
hello
[root@b1e3967e8a69 hostroot]# echo aaa >> hello2.txt
bash: hello2.txt: Permission denied
[root@b1e3967e8a69 hostroot]# mkdir hoge
mkdir: cannot create directory 'hoge': Permission denied

マウントした container_ro_file タイプのディレクトリの中身やファイルの中身を見ることはできますが、ファイルを書き込もうとしたりディレクトリを作ろうとしたりすると、SELinuxによって拒否されます。

今回は、わざとrootのホームディレクトリをマウントして、コンテナの内部がらホスト側への書き込みがしやすい環境を用意していますが、実際にはDockerの脆弱性を利用してコンテナのサンドボックスの枠を超えてホスト側を操作するコンテナエスケープと呼ばれる攻撃によって、ホスト側へ攻撃される場合があります。 そのような場合でも、SELinuxによってDockerのプロセスを適切なドメインの下で動作させていることで、被害を最小化することができるようになります。

このように、サンドボックスとしても利用されるDockerコンテナですが、クラウド上のコンテナはセキュリティ管理の不備により重要なデータが露出する可能性があり、攻撃者にとって格好の的となる可能性があります。 例えば、Docker Hub上でコミュニティが共有するコンテナイメージの中に悪意のあるスクリプトを入れておいたり、設定に不備のあるコンテナを起動したりすることで、コンテナエスケープと呼ばれる、コンテナ側からホスト側に脱出して、ホスト側のサーバを操作できるような攻撃にさらされてしまう可能性があります。

Dockerのような特権で動作するサービスの子プロセスであるコンテナは、適切なセキュリティの設定を行わないと、攻撃者による被害にあってしまいます。 もし、コンテナが被害にあっても、ホスト側への被害を最小限に食い止めることができる技術の1つに SELinux があります。 SELinuxを無効化することは、システム開発者にとっては運用しやすく便利な面もありますが、それは攻撃者においても同じことが言えます。 攻撃者にとっても攻撃しやすく、踏み台として使いやすく便利だと思われることでしょう。 ファイアウォールの設定がめんどくさいから、ファイアウォールを無効化する、というのと同じくらいSELinuxを無効化することは危険な行為です。

SELinuxは、RedHat系でデフォルトで搭載されている機能です。 SELinuxがDockerのコンテナも含めたプロセスを制限してくれるため、より高いセキュリティを維持することができるようになります。

以上です。