ユーザ名前空間でコンテナを分離

Linux 名前空間(namespace)は実行中のプロセスに対する隔離(isolate)を提供し、システムリソースに対するアクセスを制限しますが、実行中のプロセスは制限されていることが分かりません。Linux 名前空間に関する情報は、 Linux namespaces をご覧ください。

コンテナ内部からの権限昇格による攻撃を防ぐ最大の方法は、コンテナのアプリケーションを非特権ユーザで実行することです。 コンテナ内において、プロセスを root ユーザで実行しなければならない場合は、この root ユーザを、Docker ホスト上のより権限の少ないユーザに再割り当て(re-map)します。 名前空間内では通常 0 から 65536 という範囲の UID が正しく機能しますが、割り当て対象のユーザには、この範囲内で UID を定めます。 ただしこの UID はホストマシン上では何の権限もないものです。

ユーザ ID、グループ ID の再割り当てとサブ ID

再割り当て(remapping)そのものは2つのファイル、 /etc/subuid/etc/subgid で扱います。各ファイルは同じように機能しますが、一方はユーザ ID の範囲を扱い、もう一方はグループ ID の範囲を扱います。以下のような /etc/subuid のエントリを考えましょう。

testuser:231072:65536

上の意味は testuser のサブ ID を 231072 から 65536 個分の連続した整数範囲で割り当てるものです。 UID 231072 は、名前空間内(ここではコンテナ内)においては UID 0root)に割り当てられています。 同じく UID 231073 は UID 1 へ割り当てられています。 以下同様です。 名前空間の外部から権限昇格を試みるようなプロセスがあったとします。 ホスト上では権限を持たない大きな数値の 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 ホスト上のリソースにコンテナがアクセスする必要がある場合です。 具体的にバインド・マウントでは、システム・ユーザが書き込み不能なファイルシステムの領域にマウントを行います。 セキュリティの観点からは、こういった状況は避けることが一番です。

事前準備

  1. サブ UID とサブ GID の設定範囲は、既存ユーザに対して関連づいていなければなりません。 ただし関連づけは、実装上の都合によるものです。 ユーザは /var/lib/docker/ 配下に、名前空間により分けられた保存ディレクトリを所有します。 既存ユーザを利用したくない場合は、Docker がかわりにユーザを生成して利用してくれます。 逆に既存ユーザの名前または ID を利用したい場合は、あらかじめ存在していなければなりません。 通常は /etc/passwd/etc/group 内に、対応するエントリが存在していなければなりませんが、別の認証システムをバックエンドに利用している場合は、そのファイルのエントリは、別の形で取り扱われることになります。

これを確認するには、 id コマンドを使います。

$ id testuser

uid=1001(testuser) gid=1001(testuser) groups=1001(testuser)
  1. 名前空間の再割り当てがホスト上において処理される際には、2 つのファイルが利用されます。 /etc/subuid/etc/subgid です。 このファイルは通常は、ユーザやグループの追加、削除の際に、自動的に生成管理されます。 ただし RHEL や CentOS 7.3 のような一部のディストリビューションでは、このファイルの手動での管理を必要とするものがあります。

この 2 つのファイルでは 3 つの項目が記述されます。 ユーザ名あるいはユーザ ID、続いて UID または GID の開始値(名前空間内では UID または GID がゼロとして扱われるもの)、最後にそのユーザにおいて利用可能な UID または GID の最大数です。 たとえば以下のようなエントリがあったとします。

testuser:231072:65536

上が意味することは以下のとおりです。 testuser によって起動されたユーザ名前空間のプロセスは、ホスト上の 231072 (名前空間内では UID 0 として見えるもの)から 296607 (231072 + 65536 - 1) までの間の UID によって所有されます。 この範囲は他と重複してはなりません。 これを確実に行うことで、名前空間内のプロセスが別の名前空間へアクセスできないようにします。

ユーザを追加したら /etc/subuid/etc/subgid のそれぞれにおいて、追加したユーザを表わすエントリが含まれていることを確認してください。 もしエントリが存在しなければ、追加してください。 ID の重複には十分に注意してください。

Docker によって自動的に生成される dockremap ユーザーを利用したい場合は、dockremap のエントリーがそのファイル内にあるかどうかを確認しますが、それは設定を行って Docker を再起動した 後に 行ってください。

  1. Docker ホスト上に、非特権ユーザが書き込みを必要とするディレクトリがあるとします。 その場合はそのディレクトリのパーミッションを適切に調整してください。 これは Docker によって自動生成された dockremap ユーザを利用する場合も同様ですが、このときにはパーミッション変更後に Docker を再起動しない限り、その設定変更は反映されません。
  1. userns-remap を有効にすることで、既存イメージやコンテナのレイヤは効果的に保護されます。 これは /var/lib/docker/ 内にある Docker オブジェクトすべてについて言えることです。 そもそも Docker ではそういったリソース類の所有者を調整する必要があり、そうして /var/lib/docker/ 内のサブディレクトリに情報を保存するからです。 新たな Docker インストールの際に、この機能を有効にして利用していくことがベストです。
同じような話として、userns-remap を無効化すると、有効化していたときに生成したリソースへは、いっさいアクセスできなくなります。
  1. ユーザ名前空間に関する 制約 を確認し、利用することが可能かどうかを判断してください。

デーモン上で userns-remap の有効化

dockerd の実行時には --userns-remap フラグを利用することができます。 または以降の手順に示すように、設定ファイル daemon.json を使ってデーモンを設定することができます。 daemon.json ファイルを用いる方法が推奨されます。 フラグを利用する方法をとる場合、コマンドのひな形は以下のようになります。

$ dockerd --userns-remap="testuser:testuser"
  1. /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 を再起動します。

  1. 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 の重複がないようにします。

  1. docker image ls コマンドを実行し、以前利用していたイメージがないことを確認します。 出力には何も表示されないはずです。
  1. hello-world イメージからコンテナを起動します。

    $ docker run hello-world
    
  1. /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 createdocker container rundocker container exec コマンドで --userne=host を使います。

フラグを使うと思わぬ副作用が発生する場合があります。つまり、ユーザの再割り当てはコンテナに対しては有効化されないものの、読み込み専用の(イメージ)レイヤはコンテナ間でも共有されているため、コンテナのファイルシステムの所有者は再割り当てされたままです。

これはどういう事か説明しますと、コンテナのファイルシステム全体は、 --userns-remap デーモン設定(先ほどの例では 231072 )で指定したユーザが所有します。これにより、コンテナ内のプログラムが予期しない挙動を引き起こす場合があります。たとえば、 sudo (これはバイナリがユーザ 0 に所属しているかどうかを調べるため)やバイナリに setuid フラグが付いている場合です。

ユーザ名前空間における既知の制限

ユーザ名前空間を有効化する Docker デーモンの実行は、以下の標準的 Docker 機能と互換性がありません。

  • ホストとの PID あるいは NET 名前空間の共有( --pid=host--network=host
  • 外部(ボリュームやストレージ)ドライバは、デーモンによるユーザ割り当てについて、考慮されていないか互換性がありません。
  • docker run--privileged モードのフラグを使うとき、 --userns=host も指定

ユーザ名前空間は高度な機能であり、他のケーパビリティとの調整も必要になります。たとえば、ボリュームをホストからマウントする場合、ファイルの所有権はボリュームとして使うコンテナから読み込みまたは書き込み可能なように、あらかじめ調整が必要です。

ユーザ名前空間を利用したコンテナのプロセス内において root ユーザは、コンテナ内のスーパーユーザとして期待される数多くの権限を持ちます。 しかし Linux カーネルは、そこがユーザ名前空間内のプロセスであることを知っていて、それに基づいた機能制約を課します。 明らかな制約の例が、mknod コマンドを使えなくすることです。 root ユーザによって実行されているコンテナ内においては、デバイスの生成は拒否されます。

参考

Isolate containers with a user namespace
https://docs.docker.com/engine/security/userns-remap/