SELinux と Docker と OpenShift v3

Dockerは、SELinuxのタイプに`svirt_lxc_net_t`ラベルを設定し、コンテナ内のプロセスからホスト上のリソース間のアクセス制御を強固なものにしています。
また、KVM仮想マシンプロセスのsvirtの仕組みと同じく、コンテナ間の同一タイプ(svirt_lxc_net_t)を持つプロセスに対しては、MCS(Multi-Category Security)のカテゴリラベルによりアクセスの制限をしています。

今回のエントリーでは、SELinuxによるDockerリソースのアクセス制御と、内部でDockerを利用するOpenShift v3が、そのSELinuxをどう取り扱ってるのかを見ていきます。

MEMO: Fedora 22上の Docker1.8.2とRHEL7上のDocker1.7.1で確認しています。

~~~
$ docker -v
Docker version 1.7.1, build 446ad9b/1.7.1
$ docker -v
Docker version 1.8.2-fc22, build f1db8f2/1.8.2
~~~

## Docker と SELinux

dockerコンテナを稼働させると、デフォルトで `system_u:system_r:svirt_lxc_net_t:s0` と MCS(以下の例では、`c467,c468`) のラベルがコンテナ内のプロセスに付与されます。

~~~
$ docker run -it centos:latest /bin/bash
[root@0fd221016245 /]# ps -efZ
LABEL                           UID        PID  PPID  C STIME TTY          TIME CMD
system_u:system_r:svirt_lxc_net_t:s0:c467,c468 root 1 0  0 17:06 ?     00:00:00 /bin/bash
system_u:system_r:svirt_lxc_net_t:s0:c467,c468 root 35 1  0 17:07 ?    00:00:00 ps -efZ
~~~

(私のラベル`unconfined_u`です・・という人は、dockerのデーモンプロセスに`--selinux-enabled`オプションを付けて確認してください。サービスで起動している場合には、`/etc/sysconfig/docker`等の`OPTIONS`行に付与してください。`--selinux-enabled`オプションなしで起動させている場合には、SELinuxはDocker側で無効化されています。)

冒頭で述べたように、このラベル付けは次のようになっています。

コンテキスト | ラベル
---|---
user       |   system_u
role       |   role system_r
type       |   svirt_lxc_net_t
level      |   s0
categories |   c467,c468

最後のcategories(c467,c468)というのが、MCS(Multi-Category Security)で、それ以外のラベルは一般的なSELinuxと同様です。それでは、このラベルがDockerコンテナでどのように働いているのか見ていきましょう。

### SELinuxラベルによるホストリソースへのアクセス制御

まずは、SELinuxの基本で、MCS以外のラベルによるアクセス制御の話です。ホスト上のファイルをコンテナ内から利用する例で見てみましょう。

~~~
sudo mkdir /voltest  # ホスト上で共有するディスクを作成
docker run -ti -v /voltest:/foo --name v1 --rm centos:latest /bin/bash
[root@c83e2e9cd9bf /]# cd foo/
[root@c83e2e9cd9bf foo]# touch bar
touch: cannot touch 'bar': Permission denied
~~~

ホスト上の`/voltest`をコンテナと共有し、コンテナ内から`bar`というファイルを作成してみたのですが、`Permission denied`となりました。ここで、`Permission denied`だからといって、即座に`chmod 777 /voltest`をしてしまうのは間違いです。`/var/log/audit/audit.log`を確認してください。以下のメッセージに出力されているように、scontext(`svirt_lxc_net_t`)からtcontext(`root_t`)へのwriteをSELinuxが禁止していたことが確認できます。

~~~
type=AVC msg=audit(1444584620.637:339): avc:  denied  { write } for  pid=5811 comm="touch" name="voltest" dev="dm-1" ino=540248458 scontext=system_u:system_r:svirt_lxc_net_t:s0:c201,c215 tcontext=system_u:object_r:root_t:s0 tclass=dir permissive=0
~~~

`svirt_lxc_net_t`は、最初に見たdockerプロセスのtypeラベルですので、ホスト上の`/voltest`に付いていた`root_t`ラベルへのアクセスが制限が適用されていたのです。
正しい対処法は、`man docker-run` にありますが、`chcon -Rt svirt_sandbox_file /voltest`を実行し、ホスト側のディレクトリにラベルを付与することです。

~~~
# chcon -Rt svirt_sandbox_file_t /voltest   # 別端末を使ってホスト上の/voltestのラベルを変更

[root@c83e2e9cd9bf foo]# touch bar
[root@c83e2e9cd9bf foo]# ls bar
bar

# ls -lZ /voltest
total 0
-rw-r--r--. 1 root root system_u:object_r:svirt_sandbox_file_t:s0 0 Oct 13 22:08 bar
~~~

問題なくファイルが作成できました。ちなみに、このSELinuxのラベル付けは、docker 1.7以降(くらい)から、

~~~
docker run -ti -v /voltest:/foo:z --rm centos:latest /bin/bash
~~~

と、小文字の`z`をオプション末尾に付与することで、`chcon`をしなくとも、共有するディレクトリに自動でラベルが付与されるようになっています。

~~~~
$ sudo mkdir /voltest2
$ ls -alZ /voltest2/
total 4
drwxr-xr-x.  2 root root unconfined_u:object_r:default_t:s0    6 Oct 12 02:37 .
dr-xr-xr-x. 20 root root system_u:object_r:root_t:s0        4096 Oct 12 02:37 ..

(別端末でコンテナを起動)
$ docker run -it -v /voltest2:/foo:z --rm centos:latest /bin/bash
[root@126692b9a171 /]#

$ ls -alZ /voltest2/
total 4
drwxr-xr-x.  2 root root system_u:object_r:svirt_sandbox_file_t:s0    6 Oct 12 02:37 .
dr-xr-xr-x. 20 root root system_u:object_r:root_t:s0               4096 Oct 12 02:37 ..
~~~

ただ、このやり方には注意が必要で、ホスト上の既存のディレクトリを共有する場合、ディレクトリ以下すべてに`system_u:object_r:svirt_sandbox_file_t`のラベルが付与されてしまいます。既存のラベルを破壊してしまわないように注意してください。

#### MCS(Multi-Category Security) と マルチテナント

ここで、コンテナプロセスが共有したディレクトリを利用する、別のコンテナプロセスを立ち上げてみます。

~~~
(1つ目のコンテナ)
$ docker run -ti -v /voltest:/foo:z --name v1 --rm centos:latest /bin/bash  #最初のdockerプロセスを停止させてしまった人は、再度起動してください。

(2つ目のコンテナ)
$ docker run -ti --volumes-from v1 centos:latest /bin/bash
[root@126692b9a171 ~]# touch /foo/bar
[root@710f0f475a35 /]# ls foo/bar
foo/bar
~~~

ファイルの共有する意図して起動させたにしても、予想以上に簡単にホスト上のディレクトリを共有してファイルが作成できてしまいました。これは、自分の環境だけでDockerを起動させる場合には問題がないのですが、例えばマルチテナントでDockerを動作させる場合、他人(別のDockerコンテナプロセス)から容易にアクセスされてしまう可能性があります。
そんな時、活躍するのがSELinuxのMCS(Multi-Category Security)の仕組みです。すでに上の例で気づいたかもしれませんが、これまでの共有ディレクトリのラベルには、`c467,c468`といったMCSラベルは付与されていませんでした。次の例で、このMCSラベルを使った、別のDockerプロセスからのアクセス制御を見ていきます。

~~~
$ docker run -ti -v /voltest:/foo:Z --name v1 --rm centos:latest /bin/bash
[root@06d02540c1cf /]# ps -efZ
LABEL                           UID        PID  PPID  C STIME TTY          TIME CMD
system_u:system_r:svirt_lxc_net_t:s0:c427,c606 root 1 0  0 18:17 ?     00:00:00 /bin/bash
system_u:system_r:svirt_lxc_net_t:s0:c427,c606 root 18 1  0 18:17 ?    00:00:00 ps -efZ

$ docker run -ti --volumes-from v1 --rm centos:latest /bin/bash
[root@b7b8b8b239b1 /]# touch foo/bar
touch: cannot touch 'foo/bar': Permission denied
~~~~

この例では、docker起動オプションの`-v /voltest:/foo`に、**大文字**の`Z`を付与しています。これを付与することで、共有したディレクトリにはMCSラベルまで付与され、同じラベル(`system_u:system_r:svirt_lxc_net_t:s0`)を持った別のDockerプロセスからも、アクセスができないようにしています。

~~~
$ ls -laZ /voltest
total 4
drwxrwxrwx.  2 root root system_u:object_r:svirt_sandbox_file_t:s0:c427,c606    6 Oct 12 03:19 .
dr-xr-xr-x. 20 root root system_u:object_r:root_t:s0                         4096 Oct 12 02:37 ..
~~~

大文字の`Z`を付与したMCSラベル付きのコンテナと、リソースを共有したい場合には、それ以降に起動させるコンテナプロセスにも、同じMCSラベルを付与することで実現可能になります。別端末を使い、起動コマンドに`--security-opt`を付与してdockerプロセスを起動させてください。

~~~
$ docker run --security-opt label:level:s0:c427,c606 -ti --volumes-from v1 --rm centos:latest /bin/bash
[root@baef30c451bd /]# ps -efZ
LABEL                           UID        PID  PPID  C STIME TTY          TIME CMD
system_u:system_r:svirt_lxc_net_t:s0:c427,c606 root 1 0  0 18:24 ?     00:00:00 /bin/bash
system_u:system_r:svirt_lxc_net_t:s0:c427,c606 root 19 1  0 18:24 ?    00:00:00 ps -efZ
[root@baef30c451bd /]# touch foo/bar
[root@baef30c451bd /]# ls foo/bar
foo/bar
~~~

予想通り、同じカテゴリラベル(`c427,c606`)が付与されたコンテナからは、アクセスができました。これで、同じラベル`system_u:system_r:svirt_lxc_net_t`を持ったDockerプロセスからも、アクセス制御と許可を設定できるようになりました。
ちなみに、脱線になりますがこのMCSラベルにも注意点があります。インターネット上のSELinuxのトラブルシューティング手順で、`restorecon -RF <ファイルパス>` と `F`(強制変更) オプションを付けて変更するようにしている記事が多いのですが、最近のソフトウェアは、MCSラベルを動的に付与しているケースがあり、迂闊に`restorecon -RF`でコンテキストを変更していると、誤ってMCSラベルをリセットしていしまうケースがあります。もちろん、`F`オプションを付与する必要がある場面もあるのですが、MCSを利用しているソフトウェアにはご注意ください。

さて、ここまでSELinuxの変更を見てきましたが、例えば、DockerコンテナをPaaS基盤として提供しているプラットフォームでは、SELinuxをどう扱っているのでしょうか？MCSラベルは、リソースの隔離には便利ですが、同じユーザーやプロジェクト内などアクセスを許可したい場面もあります。最後は、DockerをPaaSとして提供している、OpenShift v3がどのようにそれを扱っているか見てみます。

### OpenShift v3とDockerセキュリティ

結論から言ってしまうと、OpenShift v3(v3.0.x)では、各プロジェクトで同じMCSラベルを、OpenShift上で稼働させるコンテナプロセスに割り当てています。つまり、同一プロジェクト以外で稼働させるコンテナプロセス間のリソースアクセスは許可し、それ以外からのアクセスは禁止しているのです。実際に、設定されていることを確認してみます。

まずは、「project-a」プロジェクト内で、新しいアプリケーションを2つ作成してみましょう。

~~~
$ oc project project-a
Now using project "project-a" on server "https://ose3-master.example.com:8443".

$ oc new-app https://github.com/nak3/helloworld-v3.git -l app=php
$ oc new-app https://github.com/openshift/ruby-hello-world.git -l app=ruby

$ oc get pod
NAME                       READY     STATUS       RESTARTS   AGE
helloworld-v3-1-bmwo6      1/1       Running      0          13m
helloworld-v3-1-build      0/1       ExitCode:0   0          31m
ruby-hello-world-1-45sru   1/1       Running      0          6m
ruby-hello-world-1-build   0/1       ExitCode:0   0          31m

$ oc rsh helloworld-v3-1-bmwo6
bash-4.2$ ps -elZ
LABEL                           F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
system_u:system_r:svirt_lxc_net_t:s0:c4,c7 4 S 1001 1 0  0  80   0 - 118986 poll_s ?       00:00:00 httpd
system_u:system_r:svirt_lxc_net_t:s0:c4,c7 1 S 1001 14 1  0 80   0 - 118986 inet_c ?       00:00:00 httpd
...(略)...

$ oc rsh ruby-hello-world-1-45sru
bash-4.2$ ps -elZ
LABEL                           F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
system_u:system_r:svirt_lxc_net_t:s0:c4,c7 4 S 1001 1 0  0  80   0 -  1076 wait   ?        00:00:00 scl
system_u:system_r:svirt_lxc_net_t:s0:c4,c7 0 S 1001 28 1  0 80   0 -  2902 wait   ?        00:00:00 bash
...(略)...
~~~

2つのアプリケーションに同じラベル、`system_u:system_r:svirt_lxc_net_t:s0:c4,c7` が付与されていることが分かります。今度は、別プロジェクトにログインして、同様のアプリケーションを作成してみます。

~~~
$ oc project project-b
Now using project "project-b" on server "https://ose3-master.example.com:8443".

$ oc new-app https://github.com/nak3/helloworld-v3.git -l app=php
$ oc new-app https://github.com/openshift/ruby-hello-world.git -l app=ruby

$ oc get pod
NAME                       READY     STATUS       RESTARTS   AGE
helloworld-v3-1-5oa7y      1/1       Running      0          13m
helloworld-v3-1-build      0/1       ExitCode:0   0          26m
ruby-hello-world-1-build   0/1       ExitCode:0   0          26m
ruby-hello-world-1-ztygq   1/1       Running      0          10m

$ oc rsh helloworld-v3-1-5oa7y
bash-4.2$ ps -elZ
LABEL                           F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
system_u:system_r:svirt_lxc_net_t:s0:c2,c8 4 S 1001 1 0  0  80   0 - 118986 poll_s ?       00:00:00 httpd
system_u:system_r:svirt_lxc_net_t:s0:c2,c8 1 S 1001 14 1  0 80   0 - 118986 inet_c ?       00:00:00 httpd

$ oc rsh ruby-hello-world-1-ztygq
bash-4.2$ ps -elZ
LABEL                           F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
system_u:system_r:svirt_lxc_net_t:s0:c2,c8 4 S 1001 1 0  0  80   0 -  1076 wait   ?        00:00:00 scl
system_u:system_r:svirt_lxc_net_t:s0:c2,c8 0 S 1001 28 1  0 80   0 -  2902 wait   ?        00:00:00 bash
~~~

今度は、`system_u:system_r:svirt_lxc_net_t:s0:c2,c8` が付与されていて、先に見た２つのアプリケーションとは、異なるMCSラベル(末尾の c2,c8) が付与されていることが分かります。つまり万が一、悪意を持ったDockerプロセスを起動するユーザーがいて、別プロジェクトで稼働中のDockerリソースに不正アクセスを働こうとしても、アクセス制御が可能になっているのです。

(注: 現時点のOpenShift v3.0.x.0では、NFSを使った場合のSELinuxラベルの対応がまだできていません。NFSをストレージに利用した場合、SELinuxは、NFSコンテキストのラベルが付与されます。)

###おわり

もちろん、SELinuxのアクセス制御は、そもそも不正を働こうとしたユーザーがいた場合でも、アクセスができないようにするもので、デフォルトの状態でDockerを利用しても、ある程度のアクセス制限は担保されています。ただ、現状rootユーザーで実行するDockerfileが大量に出回っている状況で、それを稼働させてしまう環境では、SELinuxレベルの制御が必須でしょう。(**注**:素晴らしいことに、OpenShift v3では、ユーザー未定義のDockerコンテナをデプロイした場合には、rootでなく任意のUIDを割り振って非rootユーザーで稼働させてくれます。もちろん、root権限が必要なDockerコンテナは起動できませんが、セキュリティリスクを承知の上なら設定を変更して稼働することもできるのです。 参考: https://docs.openshift.org/latest/creating_images/guidelines.html#openshift-specific-guidelines)

また、過去に言われていた、Dockerのセキュリティ面の懸念は、今回紹介したSELinux以外にも多くの機能を使ってセキュリティ制約を実現し、徐々に改善されつつあるようです。Dockerを利用しているOpenShift v3では、それらの機能を最大限活用し、Dockerで実現不可能な部分(特にマルチテナント対応)を独自に補っています。(今回のプロジェクト内のMCSラベルの共有は、それをお見せするために、取り上げてみました・・^^;)