linux:kernel:namespace:user_namespace

名前空間の操作,その 6: ユーザ名前空間

進行中の名前空間のシリーズを続けよう.この記事ではユーザ名前空間について細かく見ていく.この機能は大枠は Linux 3.8 で実装が完了した機能である (XFS他の多数のファイルシステムに対する残作業は続いている.後者は 3.9 でマージされた).ユーザ名前空間はユーザとグループ ID の名前空間ごとのマッピングが可能になる.これはユーザ名前空間内と名前空間外で異なるプロセスとグループの ID を持つ事が可能になるということである.中でも注目すべきは,プロセスは名前空間外では 0 でない ID を持ち,同時に名前空間内では 0 のユーザ ID を持つ事ができることである.言い換えると,プロセスはユーザ名前空間外では特権を持たない操作だが,名前空間内では root 特権を持っているという事である.

ユーザ名前空間は clone() や unshare() を呼ぶ時に CLONE_NEWUSER フラグを指定することで作成する.(他のタイプの名前空間を作る時に使うフラグと違って) Linux 3.8 から使え,ユーザ名前空間を作成するのに特権は不要である.以下で挙げる例では,ユーザ名前空間の全ては ID 1000 の非特権ユーザを使って作られている.

ユーザ名前空間の調査を始めるために,小さなプログラム demo_userns.cを使う.これは新しいユーザ名前空間内で子プロセスを作成する.子プロセスは単純に実効ユーザとグループ ID をケーパビリティと同時に表示するだけのものである.非特権ユーザでこのプログラムを実効すると以下のような結果が得られる.

    $ id -u          # Display effective user ID of shell process
    1000
    $ id -g          # Effective group ID of shell
    1000
    $ ./demo_userns 
    eUID = 65534;  eGID = 65534;  capabilities: =ep

このプログラムからの出力には面白いことがいくつかある.その一つは,子プロセスに割り当てられるケーパビリティである.“=ep” という文字列 (cap_to_text() 関数がケーパビリティをテキスト表示で表示) は,子プロセスで有効で許可されたフルセットのケーパビリティを持つことを示している.このプログラムが非特権アカウントで実効したにも関わらずである.ユーザ名前空間が作られると,名前空間の最初のプロセスは名前空間内でのフルセットのケーパビリティを与えられる.これによりプロセスは,名前空間内で他のプロセスが作成される前に,名前空間内での必要な初期化が可能になる.

2 つ目は,子プロセスのユーザとグループの ID である.先に述べたように,プロセスのユーザとグループの ID はユーザ名前空間の内外で異なる可能性がある.しかし,名前空間内のユーザ ID とそれに一致する名前空間外のユーザ ID のマッピングが必要である.これはグループ ID も同様である.これにより,システムはユーザ名前空間内のプロセスがシステム全体に影響を及ぼすような操作を行う時に,適切なパーミッションのチェックをすることができる.(例えば,名前空間外のプロセスへシグナルを送るとかファイルにアクセスするとか)

getuid(), getgid() のようなプロセスのユーザ,グループ ID を返すシステムコールは常に,呼び出したプロセスが属するユーザ名前空間内で見える通りのクレデンシャルを返す.もしユーザ ID が名前空間内でのマッピングを持たない場合,ユーザ ID を返すシステムコールは /proc/sys/kernel/overflowuid で定義された値を返す.これは通常のデフォルトは 65534 である.はじめは,ユーザ名前空間はマッピングを持たない.なので,名前空間内の全てのユーザ ID はこの値にマップされる.同様に新しい名前空間はグループ ID のマッピングも持たない.全てのマッピングされないグループ ID は/proc/sys/kernel/overflowgid にマップされる (デフォルト値は overflowuid と同じ)

上記の出力から得られない他の注目に値する重要な点がひとつある.新しいプロセスは新しいユーザ名前空間内でフルセットのケーパビリティを持つが,親の名前空間では特権を持たないことである.これは clone() を呼んだプロセスのクレデンシャルとケーパビリティに関わらずである.特に,root が clone(CLONE_NEWUSER) を使った時でさえ,結果として出来る子プロセスは親の名前空間ではケーパビリティがない.

ユーザ名前空間の作成に関する最後の興味深い点は,名前空間はネスト可能であるということである.これは,初期のユーザ名前空間以外のユーザ名前空間のそれぞれは,親のユーザな前空間を持つという事であり,子のユーザ名前空間と持たないか,1 つ以上の子ユーザ名前空間を持つという事である.CLONE_NEWUSER フラグを使って clone() または unshare() でユーザ名前空間を作成するプロセスが属するユーザ名前空間がユーザ名前空間の親となる.ユーザ名前空間の親子関係の重要性は,この記事の後の部分で明らかになる.

通常,新しいユーザ名前空間を作った後の最初の段階の作業の一つに,名前空間内で作られるプロセスのユーザ,グループ ID が使うマッピングの定義がある.この作業は,ユーザ名前空間内のプロセスの 1 つに対する /proc/PID/uid_map, /proc/PID/gid_map ファイルにマッピング情報を書き込む事である (初期状態はこの 2 つのファイルは空である).この情報は 1 行以上であり,スペース区切りの 3 つの値が含まれる.

    ID-inside-ns   ID-outside-ns   length

ID-inside-ns と length が名前空間内の ID の範囲を定義する.この範囲が名前空間外の同じ長さの範囲の ID にマップされる.ID-outside-ns には外側での範囲の開始番号を指定する./proc/PID/uid_map (または /proc/PID/gid_map) ファイルを open するあるプロセスが,PID を持つ別のプロセスと同じ名前空間内にいるかどうかによって,どのように変換されるか.

  • もし 2 つのプロセスが同じ名前空間に存在する場合,ID-outside-ns は PID を持つプロセスの親の名前空間内のユーザ ID (グループ ID) に変換される.ここで共通の事は,プロセスは自身の持つマッピングファイルに書き込むということである.
  • もし 2 つのプロセスが異なる名前空間に存在する場合,ID-outside-ns は /proc/PID/uid_map (/proc/PID/gid_map) を開いたプロセスのユーザ名前空間内のユーザ ID (グループID) に変換される.(マッピングを) 書き込むプロセスは,その時に自身のユーザ名前空間に対するマッピングを定義することになる.

1 つ以上の demo_userns プログラムを起動したと仮定しよう.しかし,この時,単一のコマンドライン行引数 (任意の文字列) を持つと仮定しよう.この時プログラムは,数秒毎にクレデンシャルとケーパビリティを表示し続けながらループする.

    $ ./demo_userns x
    eUID = 65534;  eGID = 65534;  capabilities: =ep
    eUID = 65534;  eGID = 65534;  capabilities: =ep

ここで,別のターミナルウィンドウにスイッチしてみよう.別の名前空間でシェルプロセスを実行するためである (つまり,demo_userns を実行しているプロセスの親の名前空間ということである).そして demo_userns が作った新しい名前空間内の子プロセスに対するユーザ ID のマッピングを作成する.

    $ ps -C demo_userns -o 'pid uid comm'      # Determine PID of clone child
      PID   UID COMMAND 
     4712  1000 demo_userns                    # This is the parent
     4713  1000 demo_userns                    # Child in a new user namespace
    $ echo '0 1000 1' > /proc/4713/uid_map

もし,demo_userns が実行されているウィンドウの表示があった場合,こんな出力になる.

    eUID = 0;  eGID = 65534;  capabilities: =ep

言い換えると,親のユーザ名前空間でユーザ ID 1000 (元は 65534 にマップされていた) は,demo_userns が作ったユーザ名前空間ではユーザ ID 0 にマップされている.この点から,このユーザ ID を使う新しい名前空間内での全ての操作は番号 0 が見えることになるだろう.一方で親のユーザ名前空間内でのこれに一致する操作はユーザ ID 1000 を持つプロセスと同じように見えるだろう.

同様にして新しいユーザ名前空間内でのグループ ID のマッピングを作る事も可能である.他のターミナルウィンドウに移動して,親の名前空間内でグループ ID 1000 に対するユーザ名前空間内でのグループ ID 0 の単一のマッピングを作成する.

    $ echo '0 1000 1' > /proc/4713/gid_map

demo_userns を実行しているウィンドウに戻ってみると,有効なグループ ID に表示が反映されているのが見える.

    eUID = 0;  eGID = 0;  capabilities: =ep

たくさんの uid_maps を書いて管理を行うためのルールがある.似ているルールが gid_map ファイルを書く時ものに対しても追加される.最も重要なのは以下の物である.

マッピングの定義は名前空間毎に 1 度である.名前空間内のプロセスの 1 つの uid_map ファイルに 1 度だけ書き込む事ができる (改行で区切られる複数レコードも含む).さらに,ファイルに書かれる行数は現時点では 5 行に制限されている (将来的には任意の制限に増えるかもしれない).

/proc/PID/uid_map ファイルは名前空間を作成したユーザ ID が所有する.そしてそのユーザか (特権ユーザ) のみが書き込み可能である.加えて,以下の全ての必要条件を満たさなければならない.

  • 書き込むプロセスは PID のプロセスの属する名前空間内で CAP_SETUID (gid_map の場合は CAP_SETGID) のケーパビリティを持たなければならない
  • ケーパビリティに関わらず,書き込むプロセスは PID のプロセスのユーザ名前空間か,PID のプロセスの親の名前空間 (直接の親) の中に存在しなければならない
  • 以下の 1 つが真でなければならない
    • uid_map (gid_map) に書くデータは,書き込むプロセスの親の名前空間での実効ユーザ ID (グループ ID) だけをユーザ名前空間のユーザ ID(グループ ID) にマッピングする単一行により構成される.このルールにより,ユーザ名前空間内の最初のプロセス (つまり clone() で作られた子供) が自身のユーザ ID (グループ ID) に対するマッピングを書けるようになる.
    • プロセスは CAP_SETUID (gid_map は CAP_SETGID) ケーパビリティを親のユーザ名前空間に対して持つ.このようなプロセスは,親のユーザ名前空間内で任意のユーザ ID (グループ ID) のマッピングを定義できる.先に述べたように,新しいユーザ名前空間の最初のプロセスは親の名前空間に対してはケーパビリティを持たない.従って,親の名前空間内のプロセスだけが,親のユーザ名前空間内の任意の ID をマップするマッピングを書く事ができる.

このシリーズの先の記事で,ns_child_exec というプログラムを開発した.このプログラムは clone() を使い,コマンドラインオプションで指定した新しい名前空間で子プロセスを作成する.そして子プロセス内でシェルプロセスを実行する.

このプログラムを,新しいユーザ名前空間でシェルを実行する事に使用する.そしてそれから,そのシェル内でその新しいユーザ名前空間用のユーザ ID のマッピングを定義する.そうして,問題へ突入しよう.

    $ ./ns_child_exec -U  bash
    $ echo '0 1000 1' > /proc/$$/uid_map       # $$ is the PID of the shell
    bash: echo: write error: Operation not permitted

このエラーは,以下のコマンドで見るように,シェルが新しいユーザ名前空間内でケーパビリティを持たないから起こっている.

    $ id -u         # Verify that user ID and group ID are not mapped
    65534
    $ id -g
    65534
    $ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
    CapInh: 0000000000000000
    CapPrm: 0000000000000000
    CapEff: 0000000000000000

問題は bash シェルを実行する execve() の呼び出しで行っている.0 でないユーザ ID のプロセスが execve() を実行したとき,プロセスのケーパビリティセットがクリアされる (capabilities(7) man ページが execve() 呼び出しのケーパビリティの扱いについて詳述している).

この問題を避けるため,execve() を実行する前にユーザ名前空間内でユーザ ID のマッピングを作成する必要がある.これは ns_child_exec プログラムでは不可能である.これを可能にする少し機能強化されたバージョンが必要である.

userns_child_exec.c プログラムが ns_child_exec と同じコマンドラインインターフェースを持ち,同じ働きを行う.ただし,追加のコマンドラインオプションの -M と -G だけは違う.このオプションは文字列引数を指定でき,この文字列引数によりユーザとグループ ID のマッピングを新しいユーザ名前空間内で定義する.

    $ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash

今回は,マッピングファイルの更新が成功し,シェルが期待するユーザ ID,グループ ID,ケーパビリティを持っているのがわかる.

    $ id -u
    0
    $ id -g
    0
    $ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
    CapInh: 0000000000000000
    CapPrm: 0000001fffffffff
    CapEff: 0000001fffffffff

userns_child_exec プログラムの実装には微妙な点が少し存在する.まず,親プロセス (すなわち clone() を呼び出した側) も新しい子プロセスも,新しいユーザ名前空間の uid/gid のマッピングを更新可能であることである.しかし,先に述べたルールにより,子プロセスは自身の実効ユーザ ID だけをマップするマッピングを定義できる.もし,任意の子供内でのユーザ ID,グループ ID のマッピングを定義したい場合,定義は親プロセス側で行わなければならない.その上,親プロセスは適切なケーパビリティを持っていなければならない.具体的には CAP_SETUID, CAP_SETGID, (そして親プロセスはマッピングファイルを開くのに必要なパーミッションを持っていることを保証するために) CAP_DAC_OVERRIDE である.

その上に,親は子供が execve() を呼ぶ前にマッピングファイルを確実に更新する必要がある (でなければ,子供が execve() の間にケーパビリティを失うという,まさに前述のような問題が生じる).確実に実行するために,2 つのプロセスは必要な同期を取るためパイプを採用している.プログラムのソースコードのコメントに詳細な説明がある.

今までの所の例で /proc/PID/uid_map と /proc/PID/gid ファイルをマッピングを定義するために使う事を示した.これらのファイルはプロセスが使用しているマッピングを見るのに使う事もできる.これらのファイルに書き込む時と同様に,2 つ目の値 (ID-outside-ns) は親のユーザ名前空間に関する定義がなされている.もしファイルを開いているプロセスが異なるユーザ名前空間にいる場合,ID-outside-ns がファイルを開いているユーザ名前空間に関する定義となる.

シェルを実行する 2 つのユーザ名前空間を作成し,名前空間内のプロセスの uid_map ファイルを調べる事で,これを示す事ができる.シェルを実行する新しいユーザ名前空間を作成して,始めよう.

    $ id -u            # Display effective user ID
    1000
    $ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash
    $ echo $$          # Show shell's PID for later reference
    2465
    $ cat /proc/2465/uid_map
             0       1000          1
    $ id -u            # Mapping gives this process an effective user ID of 0
    0

ここで別のターミナルウィンドウに移り,異なるユーザとグループのマッピングを持つ兄弟となるユーザ名前空間を作る.

    $ ./userns_child_exec -U -M '200 1000 1' -G '200 1000 1' bash
    $ cat /proc/self/uid_map
           200       1000          1
    $ id -u            # Mapping gives this process an effective user ID of 200
    200
    $ echo $$          # Show shell's PID for later reference
    2535

2 つ目のユーザ名前空間のシェルを実行している 2 つ目のターミナルウィンドウで,他のユーザ名前空間内のプロセスのユーザ ID のマッピングを見よう.

    $ cat /proc/2465/uid_map
             0        200          1

このコマンドの出力は,他のユーザ名前空間内でのユーザ ID 0 が,自分の名前空間内ではユーザ ID 200 にマッピングされていることを示している.同じコマンドが他のユーザ名前空間内で実行されると,異なる出力を生成していることに注意すること.これは,ファイルから読み込みを行っているプロセスの属するユーザ名前空間に従って,カーネルが ID-outside-ns の値を生成しているからである.

もし,最初のターミナルウィンドウに戻って,2 つ目のユーザ名前空間内のプロセスに対するマッピングファイルを表示したとすると,以下のような出力が見られるだろう.

    $ cat /proc/2535/uid_map
           200          0          1

再び,ここでの出力は 2 つ目のユーザ名前空間で実行した時の同じコマンドと異なる.これは ID-outside-ns の値はファイルから読み込みを行っているプロセスのユーザ名前空間に従って生成されているからである.もちろん,初期の名前空間では,最初のユーザ名前空間のユーザ ID 0 と,2 つ目の名前空間のユーザ ID 200 は,両方ともユーザ ID 1000 にマッピングされている.これを,初期の名前空間で実行する 3 つ目のシェルウィンドウで以下のコマンドを実行して調べてみよう.

    $ cat /proc/2465/uid_map
             0       1000          1
    $ cat /proc/2535/uid_map
           200       1000          1

この記事では,ユーザ名前空間の基礎を見てきた.ユーザ名前空間の作成,ユーザとグループ ID のマッピングファイルの使用,ユーザ名前空間とケーパビリティの関係である.

先の記事で述べたように,ユーザ名前空間実装の動機の一つは,元は root ユーザに限られていた機能へのアクセスを非 root アプリケーションに与える事である.伝統的な UNIX システムでは,様々な機能は,非特権ユーザが特権プログラムの実行環境の操作するのを防ぐために root ユーザに制限されていた.期待しない,望まない方法でこれらのプログラムの動きが影響を受ける可能性があったためである.

ユーザ名前空間は,(名前空間外では特権を持たない) プロセスが,その名前空間に特権の範囲を制限すると同時に root 特権を持てるようにする.その結果,プロセスはシステム全体の特権プログラムの実行環境の操作はできない.これらの root 特権を有意義に使うために,他のタイプの名前空間と同時にユーザ名前空間を同時に使う必要がある.このトピックはこのシリーズの次の記事の主題を形作る.

  • linux/kernel/namespace/user_namespace.txt
  • 最終更新: 2013/09/03 16:46
  • by tenforward