DockerとSELinuxを組み合わせてDocker環境を強化しよう
こんにちは。SIOS OSSエバンジェリスト/セキュリティ担当の面です。
ここでは、かなり古くなってしまいましたが「DockerとSELinuxを組み合わせてDocker環境を強化しよう」を紹介します。
SELinuxについて
SELinuxはRed Hat Enterprise Linuxなどでデフォルトで取り入れられているセキュリティ機構で、Type Enforcemntモデルとリファレンスモニタのコンセプトを用いることにより、Linuxに強制アクセス制御を導入しています。
Dockerも1.3からセキュリティ強化に、このSELinuxを使うことが出来ます。
今回はこのSELinuxをDockerに使ったときの動きを見てみます。
以下、使用する言葉として
コンテキスト:プロセス・ファイルに付与されているラベル。ls -lZや、ps -axZなど、Zオプションをつけることで確認できる。形式は [user_u:role_r:type_t:s0]のようになっている。
ドメイン:コンテキストの中で、特にTypeの箇所を”ドメイン”と呼ぶ。SELinuxでは、この”ドメイン”を使って、どのドメインがどのドメインにどんなアクセスが出来るかを記述することにより、アクセス権を設定している。
となります。
CentOS 7でのDockerとSELinux
CentOS 7ではSELinuxはデフォルトで有効になっていますので、これを使用します。
また、CentOS 7ではDockerのインストールは、最初からEPELのリポジトリにパッケージがあるので、yumを用いて
“yum –enablerepo=epel -y install docker-io”
でインストールできます。
[root@cent7 ~]# yum --enablerepo=epel -y install docker-io --snip-- インストール: docker.x86_64 0:1.10.3-46.el7.centos.14 依存性関連をインストールしました: audit-libs-python.x86_64 0:2.4.1-5.el7 checkpolicy.x86_64 0:2.1.12-6.el7 docker-common.x86_64 0:1.10.3-46.el7.centos.14 docker-selinux.x86_64 0:1.10.3-46.el7.centos.14 libcgroup.x86_64 0:0.41-8.el7 libseccomp.x86_64 0:2.2.1-1.el7 libsemanage-python.x86_64 0:2.1.10-18.el7 oci-register-machine.x86_64 1:0-1.8.gitaf6c129.el7 oci-systemd-hook.x86_64 1:0.1.4-4.git41491a3.el7 policycoreutils-python.x86_64 0:2.2.5-20.el7 python-IPy.noarch 0:0.75-6.el7 setools-libs.x86_64 0:3.3.7-46.el7 yajl.x86_64 0:2.0.4-4.el7
この時、よく見てみると「docker-selinux」というパッケージも依存関係で一緒に入っていることがわかります。これはdocker用のSELinuxポリシが入っているパッケージで、これを入れることによりDockerでSELinuxオプションを使うことが可能になります。
最後にDockerをシステム起動時に有効になるようにします。
[root@cent7 ~]# systemctl start docker [root@cent7 ~]# systemctl enable docker
DockerのプロセスのSELinuxコンテキストを確認してみると
[root@cent7 ~]# ps -axZ|grep -i docker system_u:system_r:docker_t:s0 3133 ? Ssl 0:07 /usr/bin/docker-current daemon --exec-opt native.cgroupdriver=systemd --selinux-enabled --log-driver=journald
となっており、”system_u:system_r:docker_t:s0″のコンテキスト(docker_tドメイン)でDockerが動作していることがわかります。
また、よく見てみるとdockerプロセスに”–selinux-enabled”というオプションが付加されており、SELinuxが有効になった状態でDockerのプロセスが起動していることがわかります。
SELinuxを有効にしてDockerコンテナを立ち上げてみる
SELinuxを有効にした状態でDockerコンテナを立ち上げるとどうなるかを見てみましょう。
テストとして、CentOS7のDocker上でcentos:latestのDockerイメージを使用し、/bin/bashでシェルを起動するようにしてみます。
まず、”docker pull centos:latest”でCentOSの最新版をDockerで使えるようにします。
[root@cent7 ~]# docker pull centos:latest Trying to pull repository docker.io/library/centos ... latest: Pulling from docker.io/library/centos 8d30e94188e7: Downloading 32.43 MB/70.59 MB --snip-- 8d30e94188e7: Pull complete Digest: sha256:2ae0d2c881c7123870114fb9cc7afabd1e31f9888dac8286884f6cf59373ed9b Status: Downloaded newer image for docker.io/centos:latest
次に、CentOS7上でroot権限で、このイメージを用いた/bin/bashシェルを起動させます。
[root@cent7 ~]# docker run -it centos:latest /bin/bash [root@b35d6c5e945d /]#
Dockerコンテナ内でプロセスのコンテキストを確認してみる
このbashシェルの中で、”ps -axZ”として、プロセスのSELinuxコンテキストがどのようになっているかを確認してみます。
[root@b35d6c5e945d /]# ps -axZ LABEL PID TTY STAT TIME COMMAND system_u:system_r:svirt_lxc_net_t:s0:c162,c893 1 ? Ss 0:00 /bin/bash system_u:system_r:svirt_lxc_net_t:s0:c162,c893 15 ? R+ 0:00 ps -axZ [root@b35d6c5e945d /]#
このように、/bin/bashプロセスに”system_u:system_r:svirt_lxc_net_t:s0:c162,c893″というコンテキストが付与されていることがわかります。
Dockerホスト側でプロセスのコンテキストを確認してみる
ホスト側でプロセスのコンテキストを確認してみると
[root@cent7 ~]# ps -axZ --snip-- system_u:system_r:svirt_lxc_net_t:s0:c162,c893 3441 pts/1 Ss+ 0:00 /bin/bash
となっており、ホスト側で見ても”system_u:system_r:svirt_lxc_net_t:s0:c162,c893″のコンテキストが/bin/bashに付与されていることがわかります。
また、このコンテキストは”pstree -Z”を用いて
├─docker-current(`system_u:system_r:docker_t:s0') │ ├─bash(`system_u:system_r:svirt_lxc_net_t:s0:c162,c893')
となっており、Dockerのプロセス(docker-current)が子プロセスのbashを立ち上げる時に、”system_u:system_r:docker_t:s0″から”system_u:system_r:svirt_lxc_net_t:s0:c162,c893″へのコンテキストの遷移が行われていることがわかります。
色々な検証
次に、この”SElinuxを有効にしたDocker”がどのように振る舞うか、いくつか検証してみましょう。
1. 二つ以上のDockerコンテキストを上げる
Dockerコンテキストを二つ以上起動して、それぞれにどのようなSELinuxコンテキストが付与されるのかを見てみます。
幾つかのターミナルで、先ほどと同じく”docker run -it centos:latest /bin/bash”として/bin/bashシェルを複数起動してみます。
(1つめ) [root@cent7 ~]# docker run -it centos:latest /bin/bash [root@b35d6c5e945d /]# ps -axZ LABEL PID TTY STAT TIME COMMAND system_u:system_r:svirt_lxc_net_t:s0:c162,c893 1 ? Ss 0:00 /bin/bash system_u:system_r:svirt_lxc_net_t:s0:c162,c893 14 ? R+ 0:00 ps ax -Z (2つめ) [root@cent7 ~]# docker run -it centos:latest /bin/bash [root@6ba916e81932 /]# ps -axZ LABEL PID TTY STAT TIME COMMAND system_u:system_r:svirt_lxc_net_t:s0:c805,c1019 1 ? Ss 0:00 /bin/bash system_u:system_r:svirt_lxc_net_t:s0:c805,c1019 15 ? R+ 0:00 ps -axZ (3つめ) [root@cent7 ~]# docker run -it centos:latest /bin/bash [root@aed5484df9c6 /]# ps -axZ LABEL PID TTY STAT TIME COMMAND system_u:system_r:svirt_lxc_net_t:s0:c297,c417 1 ? Ss 0:00 /bin/bash system_u:system_r:svirt_lxc_net_t:s0:c297,c417 14 ? R+ 0:00 ps -axZ
ホスト側から見たそれぞれに/bin/bashのプロセスは、下記のように見えます。
[root@cent7 ~]# ps -axZ --snip-- system_u:system_r:svirt_lxc_net_t:s0:c162,c893 3441 pts/1 Ss+ 0:00 /bin/bash system_u:system_r:svirt_lxc_net_t:s0:c297,c417 5324 pts/3 Ss+ 0:00 /bin/bash system_u:system_r:svirt_lxc_net_t:s0:c805,c1019 5468 pts/5 Ss+ 0:00 /bin/bash
このように、それぞれの/bin/bashのコンテキストは”system_u:system_r:svirt_lxc_net_t:s0″までが共通で、MCSで用いられるc(カテゴリ)がそれぞれ固有に振られていることがわかります。
MCSは、過去に@ITの連載記事でも取り上げましたが、アクセス制御対象のオブジェクトにカテゴリを設定してカテゴリ別のアクセスを実現するものになります。
参照リンク:「Red Hat Enterprise Linux 5で始めるSELinux」 / 「第6回 新しく追加されたMulti Category Security」
それぞれのプロセスのカテゴリが異なっているため、万が一Dockerコンテナで脆弱性を付かれてroot権限を乗っ取られたとしても、他のコンテナに影響を及ぼすことが出来ず、結果として被害を局所化(脆弱性が発見されたコンテナのみに最小化)することが出来ます。
2. ホストのディレクトリをコンテナでマウントしてみる
ホスト上のディレクトリをコンテナでマウントして、アクセス権がどうなるかを見てみます。
Dockerコンテナ上でホストのディレクトリをマウントするには、Dockerコンテナを立ち上げる時に
[root@cent7poc /]# docker run -v [ホスト上の共有するディレクトリ]:[コンテナ上でマウントした際のディレクトリ]:[パーミッション] -it [Dockerイメージ] [プログラム]
などとします。今回はホスト上に/Testを(テストのために敢えて)777パーミッションで作成します。
[root@cent7poc /]# ls -lZ / --snip-- drwxrwxrwx. root root unconfined_u:object_r:default_t:s0 Test
となっています。
これを/Shareとしてマウントしますので、今までと同様にdockerコンテナを立ち上げる時に
[root@cent7poc ~]# docker run -v /Test:/Test:rw -it centos:latest /bin/bash
と実行することにより
[root@5d4919fd6235 /]# ps axZ LABEL PID TTY STAT TIME COMMAND system_u:system_r:svirt_lxc_net_t:s0:c1,c81 1 ? Ss 0:00 /bin/bash system_u:system_r:svirt_lxc_net_t:s0:c1,c81 14 ? R+ 0:00 ps axZ [root@5d4919fd6235 /]# ls -lZ / drwxrwxrwx. root root unconfined_u:object_r:default_t:s0 Test -rw-r--r--. root root system_u:object_r:svirt_sandbox_file_t:s0:c1,c81 anaconda-post.log --snip-- [root@5d4919fd6235 /]# [root@5d4919fd6235 /]# ls / Test bin etc lib lost+found mnt proc run srv tmp var anaconda-post.log dev home lib64 media opt root sbin sys usr [root@5d4919fd6235 /]#
と/Testとしてホスト上のディレクトリがマウントされます。
ここで、マウントする際にrw(Read/Write)をオプションとして指定したため、コンテナ上から/Testには書き込みが出来るはずです。しかし、実際に試してみると
[root@5d4919fd6235 /]# touch /Test/testfile touch: cannot touch '/Test/testfile': Permission denied
となり、書き込むことは出来ません。これは、上述の”ls -laZ”の結果からもわかる通りSELinuxの設定で、/Testには”unconfined_u:object_r:default_t:s0″というコンテキスト(default_tドメイン)が付加されており、/bin/bashのドメイン(svirt_lxc_net_t)がdefault_tドメインのディレクトリ・ファイルに書き込む権限がないため、書き込む行為が弾かれてしまうからです。
同様に、この状態では読み込みの権限もないため、/Testディレクトリの中をlsで覗こうとしても
[root@5d4919fd6235 /]# ls -lZ /Test ls: cannot open directory /Test: Permission denied
と、SELinuxのパーミッション設定で弾かれてしまいます。
ここで試しに、ホストOS上で
[root@cent7poc /]# setenforce 0 [root@cent7poc /]# getenforce Permissive
と、SELinuxによる保護が一時的に無い状態(Permissive)にすると、
[root@5d4919fd6235 /]# ls -lZ /Test -rw-r--r--. root root system_u:object_r:default_t:s0 testfile [root@5d4919fd6235 /]# getenforce Permissive
と、/Testディレクトリ内にファイル”testfile”を作成することが出来ます。この時、作成されたファイルには、親ディレクトリからコンテキストが継承され、system_u:object_r:default_t:s0(default_tドメイン)のコンテキストが付加されます。
ホストOS上のディレクトリをDocker上でマウントして読み込みや書き込みを行いたい場合には、svirt_lxc_net_tドメインがアクセスできるコンテキスト(ドメイン)を、ホストOS上のディレクトリに付与してあげることでアクセスが出来るようになります。例えば
[root@cent7poc /]# chcon -t "svirt_sandbox_file_t" /Test [root@cent7poc /]# setenforce 1 [root@cent7poc /]# getenforce Enforcing
のように、/Testに”svirt_sandbox_file_t”を与えてあげると、SELinuxを有効にしても
-rw-r--r--. root root system_u:object_r:svirt_sandbox_file_t:s0 testfile2
のように、/Testディレクトリ内にtestfile2を作成することが出来ます。この時、付加されるコンテキストは、親ディレクトリから継承して”system_u:object_r:svirt_sandbox_file_t:s0″になります。
ちなみに現在はSELinuxが有効(Enforcing)になっていますので、先程作成したtestfile(“system_u:object_r:default_t:s0″のコンテキスト)は、Dockerコンテナ内から/Testディレクトリを覗いた際には
[root@5d4919fd6235 /]# ls -lZ /Test ls: cannot access /Test/testfile: Permission denied ?--------- ? ? testfile -rw-r--r--. root root system_u:object_r:svirt_sandbox_file_t:s0 testfile2
のように、通常のパーミッション情報も取得できない状態になっています。
SELinuxをDockerと組み合わせて使用した場合には、このように大枠(ディレクトリ単位)でDocker側で見える・アクセスできる状態にしておいて、SELinuxを用いて細かいパーミッション設定を行うことが可能です。
今回の実験のまとめ
今回の実験のまとめになります。
SELinuxとDockerは、CentOS/RHELのデフォルトでは組み合わせて(有効になって)立ち上がります。
SELinuxとDockerを組み合わせることにより、Dockerコンテナごとに個別にSELinuxコンテキストを付与して、被害を局所化することが出来ます。
ホストOSのディレクトリをDockerコンテナごとに共有する際にも、SELinuxを用いて細かい単位でパーミッション設定を行うことが出来ます。