Dockerコンテナで利用できるリソースや権限を制限する(Dockerの最新機能を使ってみよう:第3回)

新たなサーバー環境構築ツールとして普及が始まっているDockerは、その開発も積極的に行われている。そこで本連載記事では、4回に渡って最近Dockerに実装された新機能について紹介していく。今回は、コンテナに割り当てるリソースの制限やDockerが持つ権限分離機構について紹介する。

Dockerが備えるリソース制限機構

仮想化技術を利用するメリットの1つに、柔軟にリソースを管理できる点がある。Dockerでは古くからコンテナに割り当てるCPUリソースやメモリ容量を指定する機能があったが、Docker 1.6以降では割り当てるCPUリソースやメモリをより詳細に指定できるようになった。また、Docker 1.10ではブロックI/Oに関する制限も設定できるようになっている。以下では、これらコンテナに向けたリソース制限機能について紹介する。

稼働中コンテナのリソース使用状況を確認する

リソースの制限について説明する前に、まずは各コンテナがどの程度のリソースを使用しているのかを確認する方法を紹介しておこう。こういった情報はコンテナ内で各種計測コマンドを実行しても確認できるのだが、Dockerにはコンテナのリソース使用状況を確認するための「docker stats」コマンドが用意されており、これを利用すると容易に各コンテナの状況を確認できる。

# docker stats <コンテナ名もしくはコンテナID>

対象とするコンテナ名もしくはコンテナIDは複数を指定可能だ。また、Docker 1.10以降ではコンテナ名もしくはコンテナID指定を省略した場合、ホスト上のすべてのコンテナに関する状況が表示される。

↓コンテナを実行
# docker run -td busybox
ac9206ea6439f2043b76d00745eeed73f7937f29c449c256fd14b73ecb09ea2f
↓コンテナのリソース使用状況を確認
# docker stats
CONTAINER           CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O
ac9206ea6439        0.00%               57.34 kB / 1.041 GB   0.01%               648 B / 648 B       0 B / 0 B

なお、「docker stats」コマンドで確認できるのはCPUおよびメモリ、ネットワークI/O、ブロックI/O(Docker 1.9以降)だ。

CPU使用量の制限

Dockerではコンテナを起動する「docker run」コマンドに特定のオプションを与えることで、起動するコンテナが利用できるリソースを制限できる。CPU関連のリソースを制限するオプションとしては、表1のものが用意されている。

表1 CPU関連のリソース制限オプション
オプション 説明 備考
--cpuset-cpus 使用するCPUのID Docker 1.5以前は「--cpuset」
--cpuset-mems 使用するメモリノードのID NUMA環境向け。Docker 1.7以降
--cpu-shares CPU配分の際の重み デフォルトは1024
--cpu-period スケジューリング期間 マイクロ秒で指定。Docker 1.7以降
--cpu-quota スケジューリング期間中のうちコンテナに割り当てる期間 マイクロ秒で指定。Docker 1.7以降

このうち、Dockerで古くからサポートされているのが「--cpu-shares」オプションだ。これはそのホスト上で稼動しているコンテナに割り当てるCPUリソースを相対値で指定するもので、デフォルト値は1024となっている。コンテナに割り振られるCPUリソースは、次のように計算できる。

(<対象のコンテナに対し指定された--cpu-sharesの値>÷<各コンテナに対し指定された--cpu-sharesの値の総計>)×<利用できるCPUリソースの総量>

ホスト上のすべてのコンテナが同じ値(たとえばデフォルト値の1024)の場合、各コンテナには同じだけのCPUリソースが割り当てられる(図1)。

図1 「--cpu-shares」オプション指定によるCPUリソース割り当て

 また、たとえば4つのコンテナがあり、その1つに「2048」、1つに「512」、残り2つがデフォルトの「1024」だった場合、利用できるCPUリソースはそれぞれ4/9、1/9、2/9、2/9となる。

なお、「--cpu-shares」での指定は、あくまですべてのコンテナがフルにCPUリソースを消費しようとした際の割り当てを指定するものであり、あるコンテナがアイドル状態である場合、その分のCPUリソースは他のコンテナに配分されるようになっている。

コンテナが使用するCPUコアを指定することも可能だ。これは、「--cpuset-cpus」オプションで指定できる。なお、バージョン1.5以前はこのオプションは「--cpuset」という名称だった。このオプションではそのコンテナが使用するCPUコアのIDを数値で指定する。たとえばIDが0のコアのみを使用するよう指定するには、次のようにオプションを指定すれば良い。

--cpuset-cpus=0

カンマや「-」で複数のコアを指定することも可能だ。たとえばIDが0から2までのコアを指定するには次のようにする。

--cpuset-cpus=0-2

また、NUMAアーキテクチャを持つマシンでは同様の形式で使用するメモリノードを指定できる「--cpuset-mems」オプションも用意されている。

「--cpu-shares」オプションはコンテナが使用するCPUリソースを相対的に配分するためのものだが、Docker 1.7以降ではスケジューラ(Completely Fair Scheduler、CFS)単位でのCPUリソース制限を行う「--cpu-period」および「--cpu-quota」オプションが追加された。こちらでは「--cpu-period」で指定した時間のうち「--cpu-quota」で指定した時間までコンテナにCPUを割り当てる、という設定が行える。

たとえば、1秒(1000000マイクロ秒)当たり最大0.2秒(200000マイクロ秒)間1つのCPUを利用できるようにするには、次のように指定する。

--cpu-period=1000000 --cpu-quota=200000

メモリの制限

続いて、メモリ関連のオプションを紹介しよう(表2)。

表2 メモリ関連のリソース制限オプション
オプション 説明 備考
-m、--memory メモリ容量を制限する
--memory-reservation メモリ容量を緩く制限する Docker 1.9以降
--kernel-memory カーネルが利用できるメモリ量を制限する Docker 1.9以降
--memory-swap メモリ+SWAPの総量を制限する Docker 1.5以降
--memory-swappiness= コンテナ内でのスワップメモリ利用頻度を調整する Docker 1.8以降
--oom-kill-disable OOM Killerを無効化する Docker 1.7以降
--oom-score-adj コンテナ自体のOOM Killer優先度を調整する Docker 1.10以降
--shm-size /dev/shmに割り当てる容量を指定する Docker 1.10以降

「--memory」(もしくは「-m」)オプションでは、そのコンテナが利用できる最大メモリ量を指定できる。指定の際は「k」や「m」、「g」といった単位および「b」(バイト)も利用可能だ。たとえばコンテナが利用できるメモリ量を最大256MBに制限するには、以下のオプションを指定してコンテナを起動すれば良い。

--memory=256mb

なお、実際の制限値はOSのページサイズの倍数に丸められるため、指定した値がそのまま制限値になるわけでは無い。たとえば上記のように「--memory=256mb」を指定したコンテナについて「docker stats」コマンドでリソース使用状況を確認すると、メモリのリミット(MEM LIMIT)が「268.4MB」となっていることが分かる。

# docker stats
CONTAINER           CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O
1f5816cebef4        0.00%               61.44 kB / 268.4 MB   0.02%               508 B / 508 B       0 B / 0 B

Docker 1.9ではこれに加え、空きメモリに余裕がない場合にのみ制限が適用される「--memory-reservation」オプションや、コンテナにおいてカーネルが使用するメモリ量を制限する「--kernel-memory」オプションも導入されている。これらオプションも「--memory」オプションと同様の形式でサイズを指定できる。

コンテナが利用するスワップメモリの量も制限可能だ。これは「--memory-swap」オプションで指定できる。なお、このオプションで指定する値はメモリ量とスワップメモリ量の合計になる点に注意したい。また、このオプションを利用する場合は必ず「--memory」オプションも同時に指定しておく必要があり、かつ「--memory」オプションよりも大きい値を指定する必要がある。なお、デフォルトではメモリ量と同量がスワップメモリとして利用可能となる。

Linuxカーネルにはメモリのスワップ頻度を調整するパラメータであるswappiness(/proc/sys/vm/swappiness)があるが、Docker 1.8以降ではコンテナごとにこのパラメータを変更できるようになった。これは「--memory-swappiness」オプションで指定できる。Linuxカーネルでの指定の場合と同様、指定できる値は0から100までで、デフォルトは60だ。数値が大きいほど、頻繁にスワップを行うようになる。

Linuxでは利用できるメモリが少なくなるとその中身をスワップメモリに追い出して利用できるメモリを確保しようとするが、たとえばスワップメモリを使い果たすといった状況となってそれでもメモリが不足した場合、OOM Killer(Out of Memory Killer)というシステムによって稼働中のプロセスが強制終了され、カーネルの稼動に必要なメモリを確保しようとする。Docker 1.7では、このOOM Killerを無効化する「--oom-kill-disable」オプションも追加された。この場合、空きメモリ以上のメモリを要求した処理はメモリが確保されるまで一時停止するようになる。

OOM Killerでは、強制終了させるプロセスを「oom_score_adj」という値を使って管理している。Docker 1.10では、コンテナ内プロセスのoom_score_adjの値を指定する「--oom-score-adj」オプションも追加された。指定できる値は-1000から1000の間で、値が小さいほど強制終了の対象になりやすい。また、-1000を設定すると強制終了の対象外となる。これを利用して、ホストの空きメモリ容量が少なくなった場合でもコンテナを強制終了されにくくすることが可能になる。

sのほか、同じくDocker 1.10ではコンテナ内で/dev/shmに割り当てる容量を指定する「--shm-size」オプションも追加された。/dev/shmはLinuxカーネルによって提供されるRAMディスクのマウント先で、デフォルト値は64MBだ。

I/O関連の制限

DockerではCPUやメモリ以外のリソースに対する制限は当初実装されていなかったが、最近になってブロックI/Oに関する制限やデバイス単位での制限を行える機能が追加されている(表3)。

表3 I/O関連のリソース制限オプション
オプション 説明 備考
--blkio-weight ブロックI/Oデバイスに対する優先度。10~1000の間で指定 Docker 1.7以降
--blkio-weight-device=[] 指定したブロックI/Oデバイスに対する優先度。10~1000の間で指定 Docker 1.10以降
--device-read-bps 指定したブロックI/Oデバイスに対する読み込みアクセス上限をバイト/秒で設定 Docker 1.10以降
--device-read-iops 指定したブロックI/Oデバイスに対する読み込みアクセス上限をIO数/秒で設定 Docker 1.10以降
--device-write-bps 指定したブロックI/Oデバイスに対する書き込みアクセス上限をバイト/秒で設定 Docker 1.10以降
--device-write-iops 指定したブロックI/Oデバイスに対する書き込みアクセス上限をIO数/秒で設定 Docker 1.10以降

まずDocker 1.7で追加されたのが、ブロックデバイスに対するI/Oを調整する「--blkio-weight」オプションだ。このオプションでは、10から1000の範囲でそのコンテナに対するI/Oの優先度を指定できる。複数のコンテナが同時にI/Oを行っている際、優先度が大きいコンテナほどより多くのI/Oを行える。Docker 1.10ではデバイスごとにコンテナのI/O優先度を指定できる「--blkio-weight-device」オプションも追加されている。

同じくDocker 1.10では、指定したデバイスに対し毎秒当たりのバイト数という単位でI/Oを制限できる「--device-read-bps」および「--device-write-bps」オプションと、毎秒当たりのI/O数という単位でI/Oを制限できる「--device-read-iops」および「--device-write-iops」オプションが追加された。これらオプションは、「<デバイス名>:<上限>」といった形式で対象とするデバイスおよび上限を指定できる。

たとえば、fedora 23+Docker 1.10.3のデフォルト設定ではコンテナのファイルシステムが/dev/mapper/fedora-rootというデバイス上に作られる。このデバイスに対し書き込みを毎秒1MBまでに制限するには、以下のようにしてコンテナを起動すれば良い。

# docker run -ti --device-write-bps=/dev/mapper/fedora-root:1mb busybox

このようにして起動したコンテナ内でddコマンドでファイルシステムへの書き込み速度を測定したところ、毎秒928.5KBという結果となった。

/ # dd bs=1M count=10 if=/dev/zero of=/test conv=fsync
10+0 records in
10+0 records out
10485760 bytes (10.0MB) copied, 11.029001 seconds, 928.5KB/s

同じ環境で「--device-write-bps=」オプションを指定せずに起動したコンテナで同様の操作を行った場合は毎秒142.4MBという結果になっており、「--device-write-bps=」オプションによって書き込みが毎秒1MB未満に制限されていることが分かる。

/ # dd bs=1M count=10 if=/dev/zero of=/test conv=fsync
10+0 records in
10+0 records out
10485760 bytes (10.0MB) copied, 0.070217 seconds, 142.4MB/s

Docker 1.6ではコンテナ内でのulimit設定を行う「--ulimit」オプションも追加されている。このオプションでは、LinuxのPAMが利用するlimits.confファイルに似た書式でコンテナが利用するリソースを制限できる。ここで指定できる値についてはlimits.confのmanページ(「man limits.conf」コマンドで確認できる)で確認できるのでそちらを参照してほしい。たとえばファイルディスクリプタの最大数を2048に設定するには、以下のように指定する。

--ulimit="nofile=2048"

稼働中コンテナに対するリソース制限の追加/変更

Docker 1.10では新たに「docker update」コマンドが追加され、コンテナの起動時でリソース制限を設定するだけでなく、稼働中のコンテナに対してもリソース制限を追加したり、その値を変更することが可能となった。ただし、すべての制限を追加/変更できるわけではなく、設定できるのは表4のものとなる。

表4 「docker update」コマンドで設定できるリソース制限
オプション 説明
--blkio-weight ブロックI/Oデバイスに対する優先度。10~1000の間で指定
--cpu-shares CPU配分の際の重み。デフォルトは1024
--cpu-period スケジューリング期間。マイクロ秒で指定
--cpu-quota スケジューリング期間中のうちコンテナに割り当てる期間。マイクロ秒で指定
--cpuset-cpus 使用するCPUのID
--cpuset-mems 使用するメモリノードのID
--kernel-memory カーネルが利用できる割り当てるメモリ量を制限する
-m、--memory メモリ容量を制限する
--memory-reservation メモリ容量を緩やかに制限する
--memory-swap メモリ+SWAPの総量を制限する

ホスト−コンテナ間でのユーザー/グループの分離

続いては、Dockerが持つ権限分離機構について紹介していこう。Dockerのデフォルト設定ではコンテナ内のプロセスはroot権限で実行される。Dockerではgroupやnamespaceといった機構を使ってコンテナ内のプロセスをほかのプロセスからアクセスできないよう分離しているが、コンテナ外から見るとコンテナ内のプロセスは特権を持っているように見えてしまうのだ。コンテナ内のプロセスがコンテナ内のファイルシステムにのみアクセスするのであれば問題は無いが、ボリュームなどの機構を使ってホストのディレクトリをマウントする場合、コンテナ内のプロセスはroot権限でマウントしたディレクトリにアクセスできてしまう。

たとえば、以下のように-vオプションを利用してホストのルートディレクトリをコンテナの/hostrootディレクトリにマウントした場合、コンテナ内から/hostrootディレクトリ経由でDockerホストのすべてのディレクトリにアクセスできてしまう。

# docker run -ti -v /:/hostroot busybox

通常このような設定を行うことはないだろうが、設定ミス等で意図しないディレクトリをコンテナ内にマウントしてしまうことは考えられる。そこでDocker 1.10では、コンテナ内におけるユーザー/グループをコンテナ外のユーザー/グループと分離する「User Namespaces」という機構が導入された。これによってコンテナ内のrootユーザーをDockerホスト上では別のユーザーとして扱うことが可能になり、Dockerホストへの無制限のアクセスを防ぐことができるようになる。

User Namespacesを有効にするには、Dockerデーモンの起動時に「--userns-remap」オプションを指定すれば良い。このオプションでは、以下の書式でコンテナ内のrootユーザー/rootグループに対応させるユーザー/グループを指定できる。

--userns-remap=<ユーザーID>
--userns-remap=<ユーザーID>:<グループID>
--userns-remap=<ユーザー名>
--userns-remap=<ユーザー名>:<グループ名>
--userns-remap=default

ユーザーIDもしくはユーザー名のみが指定された場合、コンテナ内のrootユーザーはコンテナ外では指定したユーザーとして認識される。また、グループIDやグループ名が指定された場合は、コンテナ内のrootグループが指定されたグループとして認識される。なお、「default」が指定された場合は「dockermap」というユーザー/グループが指定されたものとして認識される。

たとえばfedoraの場合、Dockerデーモン実行時に与えるオプションは/etc/sysconfig/docker内の「OPTIONS=」行で指定できるので、ここに以下のように「--userns-remap=」オプションを追加すれば良い。

OPTIONS='--userns-remap=default --selinux-enabled'

また、コンテナ内とコンテナ外でのユーザーID/グループIDの対応付けを設定するため、/etc/subuidおよび/etc/subgidファイルの設定も必要となる。ここでは、以下のような形でIDの対応付けを記述する。

<ユーザー名/グループ名>:<コンテナ内でrootユーザーとして使用するUID/GID):65536

たとえば/etc/subuidファイルに以下のように記述すると、コンテナ内のroot(UID:0)はコンテナ外ではUIDが300000のユーザーとして扱われる。

dockremap:300000:65536

同様に/etc/subgidファイルにも同じ内容を記述し、Dockerデーモンを再起動すると設定が有効になる。

それでは、この設定が実際に有効になっているかを確認してみよう。まず、通常通りbusyboxコンテナを起動し、コンテナ外からpsコマンドでプロセスを確認してみると、コンテナ内のプロセスは先ほど設定した「300000」というユーザーIDで実行されていることが分かる。

↓コンテナを起動する
# docker run -ti --rm busybox
↓別のシェル上でpsコマンドを実行する
# ps aux
  
root      3311  0.2  2.9 125100 29736 pts/1    Sl+  22:06   0:00 docker run -ti --rm -v /var/testdata:/testdata busybox
  
↓コンテナ内のプロセスが先ほど設定した「300000」というユーザーで実行されている
300000    3327  0.4  0.0   1208     4 pts/0    Ss+  22:06   0:00 sh
  

また、Dockerホストのroot権限で/var/testdataというディレクトリを作成し、それをコンテナにボリュームとしてマウントしてアクセスしてみると、パーミッションがないとして書き込みなどの操作が行えないことが分かる。

↓ディレクトリを作成する
# mkdir /var/testdata
# ls -ld /var/testdata
drwxrwxr-x. 2 root root 17  5月 12 21:33 /var/testdata

↓このディレクトリをボリュームとして/testdataにマウントしたコンテナを起動する
# docker run -ti --rm -v /var/testdata:/testdata busybox

↓ディレクトリのオーナー/グループがrootではなく「65534」に設定されている
/ # ls -ld /testdata
drwxrwxr-x    2 65534    65534           17 May 12 12:33 /testdata

↓ディレクトリへの書き込みや所有者変更といった操作は拒否される
/ # touch /testdata/foo
touch: /testdata/foo: Permission denied
/ # chown root:root /testdata
chown: /testdata: Operation not permitted

いっぽう、ホスト上でこのディレクトリの所有者を/etc/subuidファイルで指定した「300000」に指定すると、コンテナ内でこのディレクトリへのアクセスが可能になる。

↓ホスト上で/var/testdataディレクトリの所有者を変更する
# chown 300000 /var/testdata
# ls -ld /var/testdata
drwxrwxr-x. 2 300000 root 17  5月 12 21:33 /var/testdata

↓このディレクトリをボリュームとして/testdataにマウントしたコンテナを起動する
# docker run -ti --rm -v /var/testdata:/testdata busybox

↓/testdataディレクトリの所有者がrootになっている
/ # ls -ld /testdata
drwxrwxr-x    2 root     65534           17 May 12 12:33 /testdata

↓ディレクトリへの書き込みも行える
/ # touch /testdata/foo

コンテナに対しSELinuxやAppArmorを使った制限を適用する

Linuxでは、「SELinux」や「AppArmor」といったセキュリティ機構が利用できる。これらはアプリケーションが利用する各種リソースに対し、UNIX/Linuxの伝統的なアクセス制限機構であるパーミッションを超えた強制的なアクセス制限を提供するものだ。Red Hat Enterprise LinuxやCentOSなどのその互換ディストリビューションおよびFedoraではSELinuxが、UbuntuではAppArmorが採用されている。

SELinuxでは「ラベル」という属性を各種リソースに対して指定することで、各プロセスが利用できるリソースを制限できる。また、AppArmorではプロセスに対し「ドメイン」という属性を設定し、それに対応するプロファイルと呼ばれる設定ファイル中で各種リソースに対するアクセスの可否を記述することで、各プロセスが利用できるリソースを制限できる。

Docker 1.3.0では、このSELinuxやAppArmorをDockerで利用するためのオプションとして、「--security-opt」オプションが追加された。このオプションはdockerコンテナ内で実行されるプロセスに対し、SELinuxのラベルやAppArmorのドメインを割り当てるものだ。

たとえば、AppArmorが有効化されているホスト上でコンテナに対し「docker_httpd」というドメインを指定するには、次のようなオプション付きで「docker run」コマンドを実行すれば良い。

--security-opt=apparmor:docker_httpd

また、SELinuxが有効化されているホスト上では、表5の書式でコンテナに割り当てるラベルを指定できる。

表5 SELinux向けのラベル指定書式
書式 説明
label:user:<ユーザーID> コンテナに指定したSELinux user IDを割り当てる
label:role:<ロール> コンテナに指定したroleを割り当てる
label:type:<タイプ> コンテナに指定したtypeを割り当てる
label:level:<レベル> コンテナに指定したlevelを割り当てる
label:disable コンテナにラベルを割り当てない

なお、SELinuxが有効になっている環境でラベルを指定しなかった場合、コンテナには自動的に「svirt_lxc_net_t」というタイプが割り当てられる。各プロセスに割り当てられているラベルは、以下のように「-Z」オプション付きでpsコマンドを実行することで確認できる。

# docker run -ti --rm busybox

↓別のシェルで実行する
# ps uxZ
  
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 root 3570 0.0  2.9 190636 29748 pts/0 Sl+ 15:56   0:00 docker run -ti --rm busybox
  
system_u:system_r:svirt_lxc_net_t:s0:c144,c189 root 3644 0.1  0.0 1208 4 pts/1 Ss+  15:56   0:00 sh
  

これにより、SELinuxが有効になっている環境ではデフォルトでDockerコンテナがアクセスできるリソースが制限されるようになっている。たとえば、SELinuxが有効になっている場合はコンテナから/rootディレクトリへのアクセスは行えない。次の例は、/rootディレクトリをコンテナ内の/hostrootディレクトリににマウントして操作しようとするものだが、このディレクトリに対してlsコマンドやtouchコマンドを実行すると、「Permission denied」としてアクセスが拒否されていることが分かる。

↓SELinuxのモードを確認する。「Enforcing」なら有効化されている
# getenforce
Enforcing

↓/rootディレクトリをコンテナ内の/hostrootディレクトリにマウントしてbusyboxコンテナを起動
# docker run -ti -v /root:/hostroot busybox

↓コンテナ内から/rootへのアクセスを試みる
/ # cd /hostroot/
/hostroot # ls
ls: can't open '.': Permission denied
/hostroot # touch /hostroot/foo
touch: /hostroot/foo: Permission denied

なお、次のようにしてSELinuxを「Permissive」(権限のチェックのみ実行し、実際のアクセス制限は行わない)に設定すると、同様の操作はエラーとならずに実行できていることが分かる。

↓SELinuxのモードを「Permissive」に設定しDockerデーモンを再起動する
# setenforce 0
# getenforce
Permissive
# systemctl restart docker 

↓/rootディレクトリをコンテナ内の/hostrootディレクトリにマウントしてbusyboxコンテナを起動
# docker run -ti -v /root:/hostroot busybox

↓touchコマンドもlsコマンドも成功する
/ # touch /hostroot/foo
/ # ls /hostroot/
anaconda-ks.cfg  foo              rpmbuild

seccompによるシステムコールの呼び出し制限

Docker 1.10では上記のSELinuxやAppArmorを使ったアクセス制御に加えて、「seccomp(Secure Computing Mode)」と呼ばれる機構を利用したセキュリティ機構が追加されている。seccompはLinuxカーネル2.6.12以降で実装されたシステムコール呼び出しを制限するための機構で、これを利用することでコンテナ内のプロセスが呼び出せるシステムコールを制限できる。

使用中のLinuxカーネルがseccompに対応しているかどうかは、次のようにして確認できる。

cat /boot/config-`uname -r` | grep CONFIG_SECCOMP=

ここで、「CONFIG_SECCOMP=y」と出力されればseccompが利用可能だ。

Dockerでは、以下のように「--security-opt」オプションに「seccomp:」とともにJSON形式で利用可能なシステムコールを記述したファイルを指定できる。

--security-opt seccomp:<ファイル名>

なお、Dockerがデフォルトで許可するシステムコールを記述した設定ファイルがGitHubで公開されているので、利用の際はこれをベースにカスタマイズを行うと良いだろう。この設定ファイル内では許可されるシステムコールが列挙されているので、禁止したいシステムコールの部分をファイル内から削除すれば良い。

たとえば、ディレクトリを作成する「mkdir」システムコールを禁止するには、下記の部分を削除すれば良い。

{
  "name": "mkdir",
  "action": "SCMP_ACT_ALLOW",
  "args": []
},

このように変更した設定ファイルを「--security-opt」オプションで指定して実行したコンテナ内でmkdirコマンドを実行すると、次のように「Operation not permitted」とのエラーが発生し、システムコールが実行できていないことが分かる。

# docker run -ti --security-opt seccomp:default.json busybox 
↓コンテナ内でmkdirコマンドを実行する
/ # mkdir foo
mkdir: can't create directory 'foo': Operation not permitted

なお、Dockerがデフォルトで許可する/禁止するシステムコールについてはSeccomp security profiles for Dockerドキュメントで説明されている。デフォルトで禁止されているシステムコールはシステム管理に使われるもので、通常コンテナ内で必要とするケースは少ないが、もしこれらシステムコール制限を無効化したい場合、次のように「--security-opt」オプションを「seccomp=unconfined」という引数を付けて指定すれば良い。

--security-opt seccomp=unconfined

Dockerコンテナのセキュリティ

Dockerのセキュリティ的な問題として、何らかの方法でコンテナ内からコンテナ外へのアクセスが可能になってしまった場合、そのアクセスがroot権限で行われてしまうという点が以前から指摘されていた。

たとえば、root権限以外でdockerコマンドを実行させるために、Dockerへのアクセスに使用される/var/run/docker.sockファイルのグループを「docker」といったroot以外のグループに設定し、かつそのグループに読み書きアクセスを与える、というケースがある。

# chgrp docker /var/run/docker.sock
# chmod g+rw /var/run/docker.sock
# ls -l /var/run/docker.sock 
srw-rw---- 1 root docker 0 Feb  2 17:16 /var/run/docker.sock

このような設定をおこなうと、dockerグループに所属しているユーザーがdockerコマンドを使ってコンテナを起動できるようになる。

$ id
uid=1001(hylom) gid=1001(hylom) groups=1001(hylom),113(docker)
$ docker run -ti busybox
/ # 

しかしこの設定は、dockerグループに所有するユーザーにroot権限を与えたのと同じ事を意味する。以下のようにルートディレクトリをコンテナ内にボリュームとしてマウントすることで、Dockerホストのすべてのファイル/ディレクトリへのアクセスが可能になるからだ。

$ docker run -ti -v /:/hostroot busybox

このようにホストのルートディレクトリをコンテナ内にマウントする行為はセキュリティ的に問題であり、意図的に行うことはないだろうが、うっかりミスでホスト上で重要なデータが格納されているディレクトリをコンテナ内にマウントしてしまう、という可能性は十分あり得る。そういった場合、SELinuxやAppArmorの設定や、User Namespacesによる設定を適切におこなっておけば、問題が発生した際の被害を最小限に抑えることが可能となる。うまく利用して、セキュリティの向上に役立てて欲しい。