ユーザー名前空間を使ったコンテナーの分離
読む時間の目安: 3 分
Linux の名前空間は、プロセスによるシステムリソースへのアクセスを制限しながら、プロセスを分離して実行します。 実行されたプロセスにとっては、アクセスが制限されていることはわかりません。 Linux の名前空間に関する詳細は Linux 名前空間 を参照してください。
コンテナー内部からの権限昇格による攻撃を防ぐ最大の方法は、コンテナーのアプリケーションを非特権ユーザーで実行することです。
コンテナー内において、プロセスをroot
ユーザーで実行しなければならない場合は、このroot
ユーザーを、Docker ホスト上のより権限の少ないユーザーに再割り当て(re-map)します。
名前空間内では通常 0 から 65536 という範囲の UID が正しく機能しますが、割り当て対象のユーザーには、この範囲内で UID を定めます。
ただしこの UID はホストマシン上では何の権限もないものです。
ユーザー ID、グループ ID の再割り当てとサブ ID
再割り当て自体は 2 つのファイル、/etc/subuid
と/etc/subgid
によって扱われます。
2 つのファイルとも同様の動作をしますが、一方はユーザー ID 範囲に関して、他方はグループ ID 範囲に関して取り扱うものです。
/etc/subuid
内に以下のエントリーがあるとします。
testuser:231072:65536
上の意味は、testuser
のサブ ID を 231072
から 65536 個分の連続した整数範囲で割り当てるものです。
UID231072
は、名前空間内(ここではコンテナー内)においては UID 0
(root
)に割り当てられています。
同じく UID231073
は UID1
へ割り当てられています。
以下同様です。
名前空間の外部から権限昇格を試みるようなプロセスがあったとします。
ホスト上では権限を持たない大きな数値の UID によってプロセスが起動しており、その UID は現実のユーザーには割り当てられていません。
つまりそのプロセスは、ホストシステム上での権限をまったく持たないということです。
複数の範囲指定
1 つのユーザーまたはグループに対して、サブ ID の範囲を複数割り当てることができます。 これを行うには
/etc/subuid
または/etc/subgid
において 1 つのユーザーあるいはグループに対して、互いに重複しない範囲指定を複数行います。 これを行った場合、Docker は複数の範囲指定の中から、はじめの 5 つ分のみを利用します。 カーネルが/proc/self/uid_map
や/proc/self/gid_map
において、5 つ分のエントリーしか取り扱わないという制約に従ったものです。
Docker においてuserns-remap
機能を利用する際には、必要に応じて既存のユーザーやグループを指定することができます。
あるいはdefault
を指定することもできます。
default
を指定した場合、dockremap
というユーザーおよびグループが生成され、この機能のために利用されます。
警告
RHEL や CentOS 7.3 などのディストリビューションにおいて、
/etc/subuid
と/etc/subgid
に対して新たなグループの追加を自動では行わないものがあります。 その場合はこれらのファイルを編集する必要があり、他とは重複しないような範囲指定を行う必要があります。 このことは 前提条件 において触れています。
範囲指定は重複していないことがとても重要です。 そうなっていないと、プロセスが別の名前空間内でのアクセスを実現できません。 Linux ディストリビューションの多くでは、ユーザーの追加、削除を行う際の ID 範囲指定を制御するシステムユーティリティーを提供しています。
この再割り当ての機能は、コンテナーにおいてはわかりやすいものです。 ただし設定を行う上では複雑な状況がありえます。 たとえば Docker ホスト上のリソースにコンテナーがアクセスする必要がある場合です。 具体的にバインドマウントでは、システムユーザーが書き込み不能なファイルシステムの領域にマウントを行います。 セキュリティの観点からは、こういった状況は避けることが一番です。
前提条件
-
サブ UID とサブ GID の設定範囲は、既存ユーザーに対して関連づいていなければなりません。 ただし関連づけは、実装上の都合によるものです。 ユーザーは
/var/lib/docker/
配下に、名前空間により分けられた保存ディレクトリを所有します。 既存ユーザーを利用したくない場合は、Docker がかわりにユーザーを生成して利用してくれます。 逆に既存ユーザーの名前または ID を利用したい場合は、あらかじめ存在していなければなりません。 通常は/etc/passwd
や/etc/group
内に、対応するエントリーが存在していなければなりませんが、別の認証システムをバックエンドに利用している場合は、そのファイルのエントリーは、別の形で取り扱われることになります。上のことを確認するために
id
コマンドを実行します。$ id testuser uid=1001(testuser) gid=1001(testuser) groups=1001(testuser)
-
名前空間の再割り当てがホスト上において処理される際には、2 つのファイルが利用されます。
/etc/subuid
と/etc/subgid
です。 このファイルは通常は、ユーザーやグループの追加、削除の際に、自動的に生成管理されます。 ただし RHEL や CentOS 7.3 のような一部のディストリビューションでは、このファイルの手動での管理を必要とするものがあります。この 2 つのファイルでは 3 つの項目が記述されます。 ユーザー名あるいはユーザー ID、続いて UID または GID の開始値(名前空間内では UID または GID がゼロとして扱われるもの)、最後にそのユーザーにおいて利用可能な UID または GID の最大数です。 たとえば以下のようなエントリーがあったとします。
testuser:231072:65536
上が意味することは以下のとおりです。
testuser
によって起動されたユーザー名前空間のプロセスは、ホスト上の231072
(名前空間内では UID0
として見えるもの)から296607
(231072 + 65536 - 1) までの間の UID によって所有されます。 この範囲は他と重複してはなりません。 これを確実に行うことで、名前空間内のプロセスが別の名前空間へアクセスできないようにします。ユーザーを追加したら
/etc/subuid
と/etc/subgid
のそれぞれにおいて、追加したユーザーを表わすエントリーが含まれていることを確認してください。 もしエントリーが存在しなければ、追加してください。 ID の重複には十分に注意してください。Docker によって自動的に生成される
dockremap
ユーザーを利用したい場合は、dockremap
のエントリーがそのファイル内にあるかどうかを確認しますが、それは設定を行って Docker を再起動した 後に 行ってください。 -
Docker ホスト上に、非特権ユーザーが書き込みを必要とするディレクトリがあるとします。 その場合はそのディレクトリのパーミッションを適切に調整してください。 これは Docker によって自動生成された
dockremap
ユーザーを利用する場合も同様ですが、このときにはパーミッション変更後に Docker を再起動しない限り、その設定変更は反映されません。 -
userns-remap
を有効にすることで、既存イメージやコンテナーのレイヤーは効果的に保護されます。 これは/var/lib/docker/
内にある Docker オブジェクトすべてについて言えることです。 そもそも Docker ではそういったリソース類の所有者を調整する必要があり、そうして/var/lib/docker/
内のサブディレクトリに情報を保存するからです。 新たな Docker インストールの際に、この機能を有効にして利用していくことがベストです。同じような話として、
userns-remap
を無効化すると、有効化していたときに生成したリソースへは、いっさいアクセスできなくなります。 -
ユーザー名前空間に関する 制約 を確認し、利用することが可能かどうかを判断してください。
デーモン上での userns-remap の有効化
dockerd
の実行時には--userns-remap
フラグを利用することができます。
または以降の手順に示すように、設定ファイルdaemon.json
を使ってデーモンを設定することができます。
daemon.json
ファイルを用いる方法が推奨されます。
フラグを利用する方法をとる場合、コマンドのひな形は以下のようになります。
$ dockerd --userns-remap="testuser:testuser"
-
/etc/docker/daemon.json
を編集します。 ファイルはまったくの空であったとします。 以下に示す項目は、testuser
というユーザーおよびグループを使ってuserns-remap
を有効にするものです。 ユーザーやグループは、ID と名前のいずれでも指定が可能です。 グループ名やグループ ID は、それがユーザー名またはユーザー ID とは異なる場合のみ、指定することが必要です。 ユーザーとグループの名前あるいは ID をともに指定する場合は、両者をコロン(:
)で区切ります。 以下の書式は、すべて有効な指定です。 ここでtestuser
の UID および GID は1001
であるものとします。testuser
testuser:testuser
1001
1001:1001
testuser:1001
1001:testuser
{ "userns-remap": "testuser" }
メモ
dockremap
ユーザーは Docker が生成します。dockremap
ユーザーを利用する場合は、設定値にtestuser
ではなくdefault
を指定してください。ファイルを保存して Docker を再起動します。
-
dockremap
ユーザーを利用する場合は、id
コマンドを実行して Docker がそのユーザーを生成していることを確認します。$ id dockremap uid=112(dockremap) gid=116(dockremap) groups=116(dockremap)
/etc/subuid
と/etc/subgid
に対してエントリーが追加されていることを確認します。$ grep dockremap /etc/subuid dockremap:231072:65536 $ grep dockremap /etc/subgid dockremap:231072:65536
上のようなエントリーが存在しない場合は、
root
ユーザーになってこのファイルを編集します。 そして UID または GID の開始値として、すでに割り当てられている最大値を割り当て、これに加えてオフセット値(ここでは65536
)を指定します。 複数の範囲指定のそれぞれにて ID の重複がないようにします。 -
docker image ls
コマンドを実行し、以前利用していたイメージがないことを確認します。 出力には何も表示されないはずです。 -
hello-world
イメージを使ってコンテナーを起動します。$ docker run hello-world
-
/var/lib/docker/
配下に名前空間によるディレクトリがあることを確認します。 ディレクトリ名には、名前空間におけるユーザーの UID と GID が用いられています。 その所有は UID および GID であり、グループやワールドは読み込み権限がありません。 サブディレクトリの中にはroot
が所有しているものがあり、パーミッションも別のものになっています。$ sudo ls -ld /var/lib/docker/231072.231072/ drwx------ 11 231072 231072 11 Jun 21 21:19 /var/lib/docker/231072.231072/ $ sudo ls -l /var/lib/docker/231072.231072/ total 14 drwx------ 5 231072 231072 5 Jun 21 21:19 aufs drwx------ 3 231072 231072 3 Jun 21 21:21 containers drwx------ 3 root root 3 Jun 21 21:19 image drwxr-x--- 3 root root 3 Jun 21 21:19 network drwx------ 4 root root 4 Jun 21 21:19 plugins drwx------ 2 root root 2 Jun 21 21:19 swarm drwx------ 2 231072 231072 2 Jun 21 21:21 tmp drwx------ 2 root root 2 Jun 21 21:19 trust drwx------ 2 231072 231072 3 Jun 21 21:19 volumes
特にコンテナーのストレージドライバーとして
aufs
以外のものを利用している場合に、ディレクトリの一覧は、上とは異なる場合があります。再割り当てによるユーザーが所有するディレクトリは、
/var/lib/docker/
直下にある同名ディレクトリとは切り離されて利用されます。 同名ディレクトリの使用しなくなった方(この例においては/var/lib/docker/tmp/
など)は削除してかまいません。 Docker はuserns-remap
が有効になっている間は、それを利用しません。
コンテナーの名前空間再割り当ての無効化
デーモンにおいてユーザー名前空間を有効にした場合に、コンテナーを起動すると、どのコンテナーにおいてもデフォルトでユーザー名前空間が有効になります。 特定の権限により実行されているコンテナーのような場合には、そのコンテナーに対してユーザー名前空間を明示的に無効にすることが必要になります。 そういった制約に関しては ユーザー名前空間における既知の制約 を参照してください。
特定のコンテナーに対してユーザー名前空間を無効とするには、docker container create
、docker container run
、docker container exec
の各コマンド実行の際に--userns=host
フラグを追加します。
このフラグを利用した場合には副作用があります。 ユーザーの再割り当てはそのコンテナーにおいて有効になりませんが、読み込み専用の(イメージ)レイヤーはコンテナー間で共有されるため、コンテナーのファイルシステムの所有者は、再割り当てされたままです。
これは以下を意味します。
コンテナーのファイルシステムはすべて、デーモンフラグ--userns-remap
において指定されたユーザー(上の例では 231072
)に属します。
このことから、コンテナー内のプログラムが予期しない動作となることがあります。
たとえば sudo
(実行するバイナリがユーザー0
に属するかどうかを確認する)やsetuid
フラグがついている実行バイナリの場合です。
ユーザー名前空間における既知の制約
以下に示す標準的な Docker の機能は、ユーザー名前空間を有効にして Docker デーモンを起動した際には、動作が変わります。
- ホストの指定(
--pid=host
または--network=host
)を行った PID 名前空間や NET 名前空間の共有。 - デーモンのユーザー名前空間利用について、その利用がわからない、あるいは処理できない外部の(ボリュームまたはストレージ)ドライバー。
docker run
の実行において--userns=host
がなく--privileged
モードフラグを指定した場合。
ユーザー名前空間は応用的な機能であって、他のケーパビリティーと連携が必要になります。 たとえばホストからボリュームをマウントするときには、あらかじめファイルの所有権を整備しておく必要があり、ボリューム内への読み書きの権限を与えておく必要があります。
ユーザー名前空間を利用したコンテナーのプロセス内において root ユーザーは、コンテナー内のスーパーユーザーとして期待される数多くの権限を持ちます。
しかし Linux カーネルは、そこがユーザー名前空間内のプロセスであることを知っていて、それに基づいた機能制約を課します。
明らかな制約の例が、mknod
コマンドを使えなくすることです。
root
ユーザーによって実行されているコンテナー内においては、デバイスの生成は拒否されます。