ユーザー名前空間を使ったコンテナーの分離

読む時間の目安: 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 0root)に割り当てられています。 同じく 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 ホスト上のリソースにコンテナーがアクセスする必要がある場合です。 具体的にバインドマウントでは、システムユーザーが書き込み不能なファイルシステムの領域にマウントを行います。 セキュリティの観点からは、こういった状況は避けることが一番です。

前提条件

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

    上のことを確認するためにidコマンドを実行します。

    $ id testuser
    
    uid=1001(testuser) gid=1001(testuser) groups=1001(testuser)
    
  2. 名前空間の再割り当てがホスト上において処理される際には、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 を再起動した 後に 行ってください。

  3. Docker ホスト上に、非特権ユーザーが書き込みを必要とするディレクトリがあるとします。 その場合はそのディレクトリのパーミッションを適切に調整してください。 これは Docker によって自動生成されたdockremapユーザーを利用する場合も同様ですが、このときにはパーミッション変更後に Docker を再起動しない限り、その設定変更は反映されません。

  4. userns-remapを有効にすることで、既存イメージやコンテナーのレイヤーは効果的に保護されます。 これは/var/lib/docker/内にある Docker オブジェクトすべてについて言えることです。 そもそも Docker ではそういったリソース類の所有者を調整する必要があり、そうして/var/lib/docker/内のサブディレクトリに情報を保存するからです。 新たな Docker インストールの際に、この機能を有効にして利用していくことがベストです。

    同じような話として、userns-remapを無効化すると、有効化していたときに生成したリソースへは、いっさいアクセスできなくなります。

  5. ユーザー名前空間に関する 制約 を確認し、利用することが可能かどうかを判断してください。

デーモン上での 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 を再起動します。

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

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

  4. hello-worldイメージを使ってコンテナーを起動します。

    $ docker run hello-world
    
  5. /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の各コマンド実行の際に--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ユーザーによって実行されているコンテナー内においては、デバイスの生成は拒否されます。

security, namespaces