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

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

コンテナ内からの権限昇格攻撃(privilege-escalation attack:一般的に、一般ユーザ権限で root に準じる権限を得られるようにする攻撃のこと)を防ぐベストな方法は、特権のないユーザ(unprivileged user)としてコンテナのアプリケーションを実行するよう設定することです。コンテナを所有するプロセスは root ユーザとして実行する必要がありますが、コンテナ内では、このユーザを Docker ホスト上で特権を持たないユーザに再割り当て(re-map)できます。割り当てるユーザには通常の範囲内で UID が割り当てられ、名前空間内では通常の UID 0 から 65536 の範囲で機能しますが、ホストマシン上自身では何ら特権を持ちません(訳者注:コンテナ内では特定の UID や GID を持っているように見えますが、ホスト上では別の UID や GID がコンテナごとに割り当てられる機能です)。

ユーザとグループ ID の再割り当てとサブオーディネイト

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

testuser:231072:65536

この意味は、 testuser に対して割り当てられるサブオーディネイトユーザ ID(subordinate)範囲とは、 231072 から 65536 まで達するまでの連続した整数値です。UID 231072 は名前空間内(この例では、コンテナ内のことです)では UID が 0root )として割り当てられます。 231073 は UID 1 として割り当てられ、以降も同様です。もしもプロセスが名前空間の外に権限を昇格させようとしても、ホスト上におけるこのプロセスは、権限を持たない遙かに大きな UID として実行しています。つまり、ホスト上の実際のユーザとしては動作していないのです。つまり、このプロセスはホスト・システム上で全く権限を持ちません。

注釈

複数の範囲

可能であれば、ユーザまたはグループに対するサブオーディネイト範囲の割当は、同じユーザやグループと重複しない複数範囲を /etc/subuid/etc/subgid ファイルで追加します。今回の例では、 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 ディストリビューションでは、システム・ユーティリティがユーザの追加・削除時にこの範囲を管理します。

この再割り当てはコンテナに対して透過的です。しかし、コンテナが Docker ホスト上のリソースに対してアクセスを必要とするような場合は、状況によっては導入がいささか複雑になります。たとえばホスト上のファイルシステムの領域にバインド・マウントする方法では、システム・ユーザは書き込みができません。セキュリティの観点からは、これらの状況を避けるのがベストでしょう。

事前準備

  1. サブオーディネイト UID と GID の範囲は、既存のユーザと関連付ける必要がありますが、これは実装上の詳細に関連しています。ユーザ自身は /var/lib/docker/ 以下のディレクトリに、名前空間化したストレージ(namespeced storage)を所有します。もしも既存のユーザを利用する必要がなければ、Docker がこの用途のためにストレージを1つ作成します。既存のユーザ名やユーザ ID を使いたければ、既に存在している必要があります。通常、これは /etc/passwd/etc/group に関連するエントリが必要なのを意味しますが、もしも異なる認証バックエンドを用いている場合は、この準備の手順を変える必要があるかもしれません。

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

    $ id testuser
    
    uid=1001(testuser) gid=1001(testuser) groups=1001(testuser)
    
  1. 名前空間の再割り当てをするには、ホスト上の /etc/subuid/subgid の2つのファイルを扱います。各ファイルはユーザやグループの作成または追加時、通常は自動的に管理されます。しかし、 RHEL や CentOS 7.3 のようないくつかのディストリビューションでは、各ファイルを手動で管理する必要があります。

各ファイルには3つのフィールドを含みます:ユーザのユーザ名か ID 、続いて開始する UID か GID (これが名前空間内で UID または GID が 0 として扱われます)、そしてユーザが利用可能な最大の UID または GID です。たとえば、以下のようなエントリを与えたとします。

testuser:231072:65536

この意味は、ユーザ名前空間化したプロセスは、 testuser によって開始され、これはホスト UID 231072 (名前空間内では UID 0 として見える)から 296607 まで(231072 + 65536 - 1)によって所有されます。これらの範囲は重複すべきではありません。なぜなら、名前空間化したプロセスは、お互いの名前空間をアクセスできないからです。

ユーザを追加したら、 /etc/subuid/etc/subgid を確認し、それぞれのファイルにユーザのエントリが追加されているかどうかを見ます。もしもなければ、追加する必要がありますが、重複しないように気を付ける必要があります。

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

  1. Docker ホスト上のどこかに対し、権限のないユーザが書き込む必要がある場合は、適切な場所に対する権限(パーミッション)を調整する必要があります。これは Docker によって自動的に作成される dockremap を使う場合でも同様ですが、設定を変更し、 Docker の再起動をした後でないと権限を変更できません。
  1. userns-remap の有効化は、既存のイメージやコンテナのレイヤを効果的にマスクするだけでなく、 /var/lib/docker 内にある他の Docker オブジェクトも対象です。これは Docker が必要とする各リソースの調整が必要になるためで、Docker オブジェクトが /var/lib/docker 内のサブディレクトリに保管されているからです。この機能を有効化するベストな方法は、既存の Docker を使うよりは、むしろ新しい Docker のインストールでしょう。

    これらの手順に従い、 userns-remap を無効化したら、有効化後に作成したリソースには一切できなくなります。(訳者注:userne-remap を有効化時、無効化時、 /var/lib/docker/ 以下の異なるディレクトリに Docker オブジェクトを保存します。そのため、有効化する前にあったコンテナやイメージはは有効化によって見えなくなりますし、無効化によっても有効化時のコンテナやイメージが見えなくなります)

  1. ユースケースが可能であれば、ユーザ名前空間上の 制限 も確認ください。

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

dockerd の開始時に --userns-remap フラグを有効化するか、以下の手順にある、デーモンが使う設定ファイル daemon.json の設定を変更できます。 daemon.json を使う方法を推奨しています。フラグを使いたい場合は、次のコマンドを使います。

$ dockerd --userns-remap="testuser:testuser"
  1. /etc/docker/daemon.json を編集します。以下の手順における想定は、ファイルが空っぽでは、 userns-remap を有効化するために使うユーザとグループは testuser とします。ユーザとグループは ID あるいは名前で割り当て可能です。グループ名や ID を指定する必要があるのは、ユーザ名または ID と異なる場合のみです。もしも、ユーザとグループ両方の名前または ID を指定する時は、これらをコロン文字( : )で区切ります。以下は全て値として認識できる形式であり、testuser の UID と GID は 1001 と仮定します。

    • testuser
    • testuser:testuser
    • 1001
    • 1001:1001
    • testuser:1001
    • 1001:testuser
    {
      "userns-remap": "testuser"
    }
    

    dockremap ユーザを使うと、 Docker が自動的に作成しますが、その場合 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 を割り当てます。UID と GID は最も高く割り当てられたものより 1 つ加えたオフセット(この例では、 65536 )にします。この範囲は他と重複しないように、気を付けてください。

  1. docker image ls コマンドを使って、以前のイメージが利用できないことを核にします。出力結果は空っぽになります。
  1. hello-world イメージからコンテナを起動します。

    $ docker run hello-world
    
  1. /var/lib/docker 内に名前空間化ディレクトリ(namespaced directory)があるのを確認します。ここは、名前空間化ユーザとして 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/ です )。以前のディレクトリは userns-remap を有効化しない限り、 Docker からは使われません。

コンテナに対する名前空間の再割り当てを無効化

デーモン上でユーザ名前空間を有効化すると、デフォルトで全てのコンテナがユーザ名前空間を有効化して起動します。同様に、特権コンテナ(privileged container)の実行時は、特定のコンテナに対するユーザ名前空間を無効化する必要があるでしょう。これらの制限に関しては ユーザ名前空間における既知の制限 をご覧ください。

特定のコンテナに対してユーザ名前空間を無効化するには、 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 カーネルは内部のナレッジに基づいた制限を課します。つまり、これがユーザ名前空間化したプロセスです。有名な制限の1つは、 mknod コマンドの使用を不可能にします。 root ユーザとして実行する時は、コンテナ内でデバイスの作成権限は拒否されます。

参考

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