差分
このページの2つのバージョン間の差分を表示します。
両方とも前のリビジョン 前のリビジョン 次のリビジョン | 前のリビジョン | ||
linux:kernel:namespace:user_namespace [2013/08/29 11:43] – [ユーザ名前空間の作成] tenforward | linux:kernel:namespace:user_namespace [2013/09/03 16:46] (現在) – [ユーザとグループの ID のマッピングを眺める] tenforward | ||
---|---|---|---|
行 19: | 行 19: | ||
このプログラムからの出力には面白いことがいくつかある.その一つは,子プロセスに割り当てられるケーパビリティである." | このプログラムからの出力には面白いことがいくつかある.その一つは,子プロセスに割り当てられるケーパビリティである." | ||
+ | |||
+ | 2 つ目は,子プロセスのユーザとグループの ID である.先に述べたように,プロセスのユーザとグループの ID はユーザ名前空間の内外で異なる可能性がある.しかし,名前空間内のユーザ ID とそれに一致する名前空間外のユーザ ID のマッピングが必要である.これはグループ ID も同様である.これにより,システムはユーザ名前空間内のプロセスがシステム全体に影響を及ぼすような操作を行う時に,適切なパーミッションのチェックをすることができる.(例えば,名前空間外のプロセスへシグナルを送るとかファイルにアクセスするとか) | ||
+ | |||
+ | getuid(), getgid() のようなプロセスのユーザ,グループ ID を返すシステムコールは常に,呼び出したプロセスが属するユーザ名前空間内で見える通りのクレデンシャルを返す.もしユーザ ID が名前空間内でのマッピングを持たない場合,ユーザ ID を返すシステムコールは / | ||
+ | |||
+ | 上記の出力から得られない他の注目に値する重要な点がひとつある.新しいプロセスは新しいユーザ名前空間内でフルセットのケーパビリティを持つが,親の名前空間では特権を持たないことである.これは clone() を呼んだプロセスのクレデンシャルとケーパビリティに関わらずである.特に,root が clone(CLONE_NEWUSER) を使った時でさえ,結果として出来る子プロセスは親の名前空間ではケーパビリティがない. | ||
+ | |||
+ | ユーザ名前空間の作成に関する最後の興味深い点は,名前空間はネスト可能であるということである.これは,初期のユーザ名前空間以外のユーザ名前空間のそれぞれは,親のユーザな前空間を持つという事であり,子のユーザ名前空間と持たないか,1 つ以上の子ユーザ名前空間を持つという事である.CLONE_NEWUSER フラグを使って clone() または unshare() でユーザ名前空間を作成するプロセスが属するユーザ名前空間がユーザ名前空間の親となる.ユーザ名前空間の親子関係の重要性は,この記事の後の部分で明らかになる. | ||
+ | |||
+ | ===== ユーザとグループ ID のマッピング ===== | ||
+ | |||
+ | 通常,新しいユーザ名前空間を作った後の最初の段階の作業の一つに,名前空間内で作られるプロセスのユーザ,グループ ID が使うマッピングの定義がある.この作業は,ユーザ名前空間内のプロセスの 1 つに対する / | ||
+ | |||
+ | < | ||
+ | ID-inside-ns | ||
+ | </ | ||
+ | |||
+ | ID-inside-ns と length が名前空間内の ID の範囲を定義する.この範囲が名前空間外の同じ長さの範囲の ID にマップされる.ID-outside-ns には外側での範囲の開始番号を指定する./ | ||
+ | |||
+ | * もし 2 つのプロセスが同じ名前空間に存在する場合,ID-outside-ns は //PID// を持つプロセスの親の名前空間内のユーザ ID (グループ ID) に変換される.ここで共通の事は,プロセスは自身の持つマッピングファイルに書き込むということである. | ||
+ | * もし 2 つのプロセスが異なる名前空間に存在する場合,ID-outside-ns は / | ||
+ | |||
+ | 1 つ以上の demo_userns プログラムを起動したと仮定しよう.しかし,この時,単一のコマンドライン行引数 (任意の文字列) を持つと仮定しよう.この時プログラムは,数秒毎にクレデンシャルとケーパビリティを表示し続けながらループする. | ||
+ | |||
+ | < | ||
+ | $ ./ | ||
+ | eUID = 65534; | ||
+ | eUID = 65534; | ||
+ | </ | ||
+ | |||
+ | ここで,別のターミナルウィンドウにスイッチしてみよう.別の名前空間でシェルプロセスを実行するためである (つまり,demo_userns を実行しているプロセスの親の名前空間ということである).そして demo_userns が作った新しい名前空間内の子プロセスに対するユーザ ID のマッピングを作成する. | ||
+ | |||
+ | < | ||
+ | $ ps -C demo_userns -o 'pid uid comm' | ||
+ | PID UID COMMAND | ||
+ | | ||
+ | | ||
+ | $ echo '0 1000 1' > / | ||
+ | </ | ||
+ | |||
+ | もし,demo_userns が実行されているウィンドウの表示があった場合,こんな出力になる. | ||
+ | |||
+ | < | ||
+ | eUID = 0; eGID = 65534; | ||
+ | </ | ||
+ | |||
+ | 言い換えると,親のユーザ名前空間でユーザ ID 1000 (元は 65534 にマップされていた) は,demo_userns が作ったユーザ名前空間ではユーザ ID 0 にマップされている.この点から,このユーザ ID を使う新しい名前空間内での全ての操作は番号 0 が見えることになるだろう.一方で親のユーザ名前空間内でのこれに一致する操作はユーザ ID 1000 を持つプロセスと同じように見えるだろう. | ||
+ | |||
+ | 同様にして新しいユーザ名前空間内でのグループ ID のマッピングを作る事も可能である.他のターミナルウィンドウに移動して,親の名前空間内でグループ ID 1000 に対するユーザ名前空間内でのグループ ID 0 の単一のマッピングを作成する. | ||
+ | |||
+ | < | ||
+ | $ echo '0 1000 1' > / | ||
+ | </ | ||
+ | |||
+ | demo_userns を実行しているウィンドウに戻ってみると,有効なグループ ID に表示が反映されているのが見える. | ||
+ | |||
+ | < | ||
+ | eUID = 0; eGID = 0; capabilities: | ||
+ | </ | ||
+ | |||
+ | ===== マッピングファイルを書くルール ===== | ||
+ | |||
+ | たくさんの uid_maps を書いて管理を行うためのルールがある.似ているルールが gid_map ファイルを書く時ものに対しても追加される.最も重要なのは以下の物である. | ||
+ | |||
+ | マッピングの定義は名前空間毎に 1 度である.名前空間内のプロセスの 1 つの uid_map ファイルに 1 度だけ書き込む事ができる (改行で区切られる複数レコードも含む).さらに,ファイルに書かれる行数は現時点では 5 行に制限されている (将来的には任意の制限に増えるかもしれない). | ||
+ | |||
+ | / | ||
+ | |||
+ | * 書き込むプロセスは //PID// のプロセスの属する名前空間内で CAP_SETUID (gid_map の場合は CAP_SETGID) のケーパビリティを持たなければならない | ||
+ | * ケーパビリティに関わらず,書き込むプロセスは //PID// のプロセスのユーザ名前空間か,// | ||
+ | * 以下の 1 つが真でなければならない | ||
+ | * uid_map (gid_map) に書くデータは,書き込むプロセスの親の名前空間での実効ユーザ ID (グループ ID) だけをユーザ名前空間のユーザ ID(グループ ID) にマッピングする単一行により構成される.このルールにより,ユーザ名前空間内の最初のプロセス (つまり clone() で作られた子供) が自身のユーザ ID (グループ ID) に対するマッピングを書けるようになる. | ||
+ | * プロセスは CAP_SETUID (gid_map は CAP_SETGID) ケーパビリティを親のユーザ名前空間に対して持つ.このようなプロセスは,親のユーザ名前空間内で任意のユーザ ID (グループ ID) のマッピングを定義できる.先に述べたように,新しいユーザ名前空間の最初のプロセスは親の名前空間に対してはケーパビリティを持たない.従って,親の名前空間内のプロセスだけが,親のユーザ名前空間内の任意の ID をマップするマッピングを書く事ができる. | ||
+ | |||
+ | ===== ケーパビリティ,execve(),user ID 0 ===== | ||
+ | |||
+ | このシリーズの先の記事で,ns_child_exec というプログラムを開発した.このプログラムは clone() を使い,コマンドラインオプションで指定した新しい名前空間で子プロセスを作成する.そして子プロセス内でシェルプロセスを実行する. | ||
+ | |||
+ | このプログラムを,新しいユーザ名前空間でシェルを実行する事に使用する.そしてそれから,そのシェル内でその新しいユーザ名前空間用のユーザ ID のマッピングを定義する.そうして,問題へ突入しよう. | ||
+ | |||
+ | < | ||
+ | $ ./ | ||
+ | $ echo '0 1000 1' > / | ||
+ | bash: echo: write error: Operation not permitted | ||
+ | </ | ||
+ | |||
+ | このエラーは,以下のコマンドで見るように,シェルが新しいユーザ名前空間内でケーパビリティを持たないから起こっている. | ||
+ | |||
+ | < | ||
+ | $ id -u # Verify that user ID and group ID are not mapped | ||
+ | 65534 | ||
+ | $ id -g | ||
+ | 65534 | ||
+ | $ cat / | ||
+ | CapInh: 0000000000000000 | ||
+ | CapPrm: 0000000000000000 | ||
+ | CapEff: 0000000000000000 | ||
+ | </ | ||
+ | |||
+ | 問題は bash シェルを実行する execve() の呼び出しで行っている.0 でないユーザ ID のプロセスが execve() を実行したとき,プロセスのケーパビリティセットがクリアされる (capabilities(7) man ページが execve() 呼び出しのケーパビリティの扱いについて詳述している). | ||
+ | |||
+ | この問題を避けるため,execve() を実行する前にユーザ名前空間内でユーザ ID のマッピングを作成する必要がある.これは ns_child_exec プログラムでは不可能である.これを可能にする少し機能強化されたバージョンが必要である. | ||
+ | |||
+ | [[http:// | ||
+ | |||
+ | < | ||
+ | $ ./ | ||
+ | </ | ||
+ | |||
+ | 今回は,マッピングファイルの更新が成功し,シェルが期待するユーザ ID,グループ ID,ケーパビリティを持っているのがわかる. | ||
+ | |||
+ | < | ||
+ | $ id -u | ||
+ | 0 | ||
+ | $ id -g | ||
+ | 0 | ||
+ | $ cat / | ||
+ | CapInh: 0000000000000000 | ||
+ | CapPrm: 0000001fffffffff | ||
+ | CapEff: 0000001fffffffff | ||
+ | </ | ||
+ | |||
+ | userns_child_exec プログラムの実装には微妙な点が少し存在する.まず,親プロセス (すなわち clone() を呼び出した側) も新しい子プロセスも,新しいユーザ名前空間の uid/gid のマッピングを更新可能であることである.しかし,先に述べたルールにより,子プロセスは自身の実効ユーザ ID だけをマップするマッピングを定義できる.もし,任意の子供内でのユーザ ID,グループ ID のマッピングを定義したい場合,定義は親プロセス側で行わなければならない.その上,親プロセスは適切なケーパビリティを持っていなければならない.具体的には CAP_SETUID, CAP_SETGID, | ||
+ | |||
+ | その上に,親は子供が execve() を呼ぶ前にマッピングファイルを確実に更新する必要がある (でなければ,子供が execve() の間にケーパビリティを失うという,まさに前述のような問題が生じる).確実に実行するために,2 つのプロセスは必要な同期を取るためパイプを採用している.プログラムのソースコードのコメントに詳細な説明がある. | ||
+ | |||
+ | ===== ユーザとグループの ID のマッピングを眺める ===== | ||
+ | |||
+ | 今までの所の例で / | ||
+ | |||
+ | シェルを実行する 2 つのユーザ名前空間を作成し,名前空間内のプロセスの uid_map ファイルを調べる事で,これを示す事ができる.シェルを実行する新しいユーザ名前空間を作成して,始めよう. | ||
+ | |||
+ | < | ||
+ | $ id -u # Display effective user ID | ||
+ | 1000 | ||
+ | $ ./ | ||
+ | $ echo $$ # Show shell' | ||
+ | 2465 | ||
+ | $ cat / | ||
+ | | ||
+ | $ id -u # Mapping gives this process an effective user ID of 0 | ||
+ | 0 | ||
+ | </ | ||
+ | |||
+ | ここで別のターミナルウィンドウに移り,異なるユーザとグループのマッピングを持つ兄弟となるユーザ名前空間を作る. | ||
+ | |||
+ | < | ||
+ | $ ./ | ||
+ | $ cat / | ||
+ | | ||
+ | $ id -u # Mapping gives this process an effective user ID of 200 | ||
+ | 200 | ||
+ | $ echo $$ # Show shell' | ||
+ | 2535 | ||
+ | </ | ||
+ | |||
+ | 2 つ目のユーザ名前空間のシェルを実行している 2 つ目のターミナルウィンドウで,他のユーザ名前空間内のプロセスのユーザ ID のマッピングを見よう. | ||
+ | |||
+ | < | ||
+ | $ cat / | ||
+ | | ||
+ | </ | ||
+ | |||
+ | このコマンドの出力は,他のユーザ名前空間内でのユーザ ID 0 が,自分の名前空間内ではユーザ ID 200 にマッピングされていることを示している.同じコマンドが他のユーザ名前空間内で実行されると,異なる出力を生成していることに注意すること.これは,ファイルから読み込みを行っているプロセスの属するユーザ名前空間に従って,カーネルが ID-outside-ns の値を生成しているからである. | ||
+ | |||
+ | もし,最初のターミナルウィンドウに戻って,2 つ目のユーザ名前空間内のプロセスに対するマッピングファイルを表示したとすると,以下のような出力が見られるだろう. | ||
+ | |||
+ | < | ||
+ | $ cat / | ||
+ | | ||
+ | </ | ||
+ | |||
+ | 再び,ここでの出力は 2 つ目のユーザ名前空間で実行した時の同じコマンドと異なる.これは ID-outside-ns の値はファイルから読み込みを行っているプロセスのユーザ名前空間に従って生成されているからである.もちろん,初期の名前空間では,最初のユーザ名前空間のユーザ ID 0 と,2 つ目の名前空間のユーザ ID 200 は,両方ともユーザ ID 1000 にマッピングされている.これを,初期の名前空間で実行する 3 つ目のシェルウィンドウで以下のコマンドを実行して調べてみよう. | ||
+ | |||
+ | < | ||
+ | $ cat / | ||
+ | | ||
+ | $ cat / | ||
+ | | ||
+ | </ | ||
+ | |||
+ | ===== 最後に ===== | ||
+ | |||
+ | この記事では,ユーザ名前空間の基礎を見てきた.ユーザ名前空間の作成,ユーザとグループ ID のマッピングファイルの使用,ユーザ名前空間とケーパビリティの関係である. | ||
+ | |||
+ | [[http:// | ||
+ | |||
+ | ユーザ名前空間は,(名前空間外では特権を持たない) プロセスが,その名前空間に特権の範囲を制限すると同時に root 特権を持てるようにする.その結果,プロセスはシステム全体の特権プログラムの実行環境の操作はできない.これらの root 特権を有意義に使うために,他のタイプの名前空間と同時にユーザ名前空間を同時に使う必要がある.このトピックはこのシリーズの次の記事の主題を形作る. | ||
+ |