年末を迎えて今年もAdvent Calendarが多数作られていますね。この連載の今回の記事はLinuxカーネルの機能を紹介するので、Linux Advent Calendar 2014の16日目の記事としても書きました。興味深い記事が並んでいて勉強になりますね。
さて、第13回から3回、田向さんにPlamo LinuxでのLXCの利用に焦点を当てて記事を書いていただきました。テンプレート内部の詳しい解説から、Plamo Linuxでのコンテナの作成、ネットワーク構成の応用的な解説、コンテナでサウンドを扱う話まで、面白い記事が続きましたね。
ネットワークの話やサウンドの話はPlamo Linux以外でも十分に応用ができる話でしたし、サウンドの記事に関してはサウンド以外のデバイスをコンテナで使う場合にも非常に参考になる話だったと思います。
田向さん担当の記事のうち、第14回と第15回では一般ユーザによるコンテナの利用の話が出てきました。
今回はこの一般ユーザがコンテナを起動する際に利用するLinuxカーネルのユーザ名前空間(User Namespace)について説明しましょう。
ユーザ名前空間
ユーザ名前空間については第2回で少し説明しました。しかし、だいぶ回が開いてしまいましたので、改めて基本的なところから説明していきたいと思います。
ユーザ名前空間は3.8カーネルで実装された、現時点では一番新しい名前空間です。この機能により名前空間内で独立したUID、GIDを持てるようになります。名前空間内のUID、GIDとホストOS上のUID、GIDの間はマッピングによるひもづけが行われます。
つまり名前空間内のUID、GIDはホスト上では別のUID、GIDを持つことになります。たとえば、名前空間内ではUID、GIDが共に0のrootユーザを、ホストOS上ではUID、GID共に100000である一般ユーザとして扱えます。
この機能により、コンテナ内では特権を持ちつつ、ホストOS上では特権を持たないユーザが作成でき、ホストOSとコンテナの間で権限の分離ができるようになり、セキュアにコンテナ環境を提供できるようになります。
このような機能であるため、他の名前空間と違って ユーザ名前空間は一般ユーザが作成できます。
もちろん名前空間内の特権ユーザは本来の特権ユーザとは異なりますので、名前空間内であってもホストOS上の特権ユーザができる操作の全てができるわけではありません。
名前空間内の特権ユーザに許可する操作についてはまだ議論がされていたりしますし、未知のセキュリティ問題が出てくるかも知れません。今後も細かい動きは変化していくかもしれません。
それではユーザ名前空間について細かく見ていきましょう。
UID、GIDのマッピング
前述のように、ユーザ名前空間を作成すると、名前空間内と名前空間外で二重にIDを持つことになります。名前空間を作成した後にこの二重のIDの間をひもづけるためのマッピングを作成します。
このマッピングは/proc
以下の各PID名のディレクトリ以下のuid_map
とgid_map
というファイルに書き込みます。つまり名前空間内のプロセスに対してマッピングを定義するわけですね。
この定義は同じユーザ名前空間内のプロセスにも継承されますので、コンテナの最初のプロセスにマッピングを書けば、コンテナ内のプロセスには全て同じマッピングが定義されます。
まずはホストOS上の通常のプロセスのこの2つのファイルの中身を見てみましょう。
3つの数字が並んでいますね。このようにuid_map
とgid_map
の書式は同じで、以下のような意味となります。
名前空間内で使用する最初のUID/GIDを「(名前空間内の最初のID)」で指定します。"0"を指定すると、名前空間内で通常のOS環境のようにrootユーザから使い始められます。アプリケーションコンテナを使う場合でrootユーザは不要であれば必ずしも"0"から始める必要はありません。
そして、「(名前空間内の最初のID)」に対応する名前空間外のIDを「(名前空間外の最初のID)」で指定します。「名前空間内の最初のUID」に"0"を、「名前空間外の最初のUID」に"100000"と指定すると、名前空間内のrootは名前空間外のID=100000にひもづけられます。
最後に、名前空間内で使用するIDの個数を「(範囲)」で指定します。たとえば"65536"と指定すると名前空間内で0~65535までのIDが使用できるということになります。そして前述の例だと名前空間外では100000~165535までにひもづけられます。
前述の例をマッピングファイルに書くと以下のようになります。
名前空間を作成した直後など、マッピングファイルへのマッピングがされていない状態では名前空間内部のIDは
- UIDとして
/proc/sys/kernel/overflowuid
の値
- GIDとして
/proc/sys/kernel/overflowgid
の値
が使用されます。
ユーザ名前空間でのUID、GIDのマッピングの様子
では、実際にユーザ名前空間を作成して、マッピングの様子を見てみましょう。
ここではUbuntu 14.10の環境を使用しています。14.04 LTSではutil-linuxのバージョンが低いために、これから使用するunshare
コマンドにユーザ名前空間を作成する機能がありません。unshare
コマンドがユーザ名前空間をサポートするのはutil-linux 2.23以降です。
まずは現在のユーザの確認です。
ご覧のようにUID/GID=1000/1000のubuntu
というユーザです。
先に説明したマッピングがされない状態でのマッピング先も確認しておきます。
次にunshare
コマンドを実行して新しいユーザ名前空間を作成してみましょう。ユーザ名前空間を作成するには--user
オプションを指定します。特にコマンドを指定しなければ作成した名前空間でシェルが起動します。
新しいユーザ名前空間で起動したシェルで、自身のUID/GIDとPIDを確認します。
まだマッピングのための操作を行っていませんので、先に説明した通りデフォルトのUID/GIDへマッピングされているのがわかります。/proc/1551/status
のUid/Gidの行で、いずれの値も65534となっていますね。status
ファイルのUid
とGid
の行の意味はman 5 proc
で調べてくださいね。
さて、ここで別のターミナルを開いて、ホストOS上の親の名前空間上でどうなっているかを同じコマンドを実行して見てみましょう。
ご覧のように親の名前空間上では、いずれのIDも1000となっています。つまり
- ホストOSの名前空間のUID/GID=1000/1000 → 作成した名前空間内のUID/GID=65534/65534
というマッピングがされており、それぞれの名前空間ではそれぞれの名前空間でのIDで処理が行われているのがわかります。
ではマッピングを作成してみましょう。このマッピング操作は作成したユーザ名前空間の親の名前空間から実行する必要があります。
新しく作成したユーザ名前空間内のUID/GID=0/0のユーザ、つまりrootを、ホストOSの名前空間上のUID/GID=1000/1000のユーザに割り当ててみます。
ホストOS上ではプロセスのUID/GIDは変化していません。ずっと同じユーザで実行しているのでこれは当たり前ですね。
ここで作成したユーザ名前空間のシェルに戻ってみましょう。
ご覧のように、先ほどは65534だったUIDとGIDが0になっています。つまりマッピングを定義した時点でそのマッピングが有効になり、定義したユーザでの処理になります。
ユーザ名前空間内でのいろいろな操作
このユーザ名前空間内でファイルを作成してみましょう。
rootの所有でファイルが作成されましたね。親の名前空間でこのファイルを見ると、
親の名前空間上のユーザの所有のファイルとして見えていますね。
次に、ユーザ名前空間内ではrootユーザであっても、名前空間の外では特権を持たないことがわかる例を見てみましょう。
ここまでの例では、新たなユーザ名前空間のみを作成しました。しかし他の名前空間は作成していませんので、親であるホストOSの名前空間と同じ名前空間にいることになります。ネットワークインターフェースを見てみると、親の名前空間と同じインターフェースが見えます。
通常はrootであればネットワークインターフェースをdownさせたり、新しくインターフェースを作成することができます。しかしユーザ名前空間内のrootは親の名前空間では一般ユーザであり特権を持ちませんのでそのような操作はできません。
rootであっても、このようにいずれの操作もエラーで返ってきているのがわかります。
せっかくrootユーザになったのに、これでは何もできないのではないか? と思われるかも知れません。しかしそんなことはありません。ユーザ名前空間内の特権ユーザは新たに他の名前空間を作ることができます。通常は一般ユーザではユーザ名前空間以外の名前空間は新たに作れません。
試しにホストOS上の一般ユーザでネットワーク名前空間を作るコマンドを実行してみましょう。
以上のように親の名前空間で一般ユーザで実行するとエラーになりますね。ところが、ユーザ名前空間内のrootユーザのシェルで同じコマンドを実行すると、この操作が実行できます。
以上のように作成したユーザ名前空間内の特権ユーザであれば、新たにネットワーク名前空間が作れます。この新たに作ったネットワーク名前空間では特権がありますので、自由にインターフェースを定義できます。
もちろん、外部と通信を行う場合は親環境であるホストOS上での特権が必要になるので、ユーザ名前空間を作成したからと言って、自由に外部と通信ができるわけではありません。しかし、名前空間内部では通常のrootのように自由に操作できますので、通常のrootと同じようにコンテナが管理できるわけです。
複数のIDのマッピング
ここまでの例では、UID/GID=1000/1000の一般ユーザであるubuntu
ユーザのIDのみを、新たに作成したユーザ名前空間にマッピングしていましたので、マッピングファイルに書き込む3つの数値のうち、「範囲」を表す値を1
にしていました。
この場合、一般ユーザであるubuntu
ユーザでマッピングファイルへの書き込みができました。自身で起動したプロセスに対する操作ですので権限があるためです。
マッピングファイルに書き込む値の「範囲」を表す数値を2以上にすれば、複数のIDに対してマッピングできます。しかしその場合は親の名前空間でroot権限が必要になります。なぜなら自身のUID/GID以外のIDと名前空間内のIDのマッピングを行うことになるからです。自身のID以外のマッピングが自由にできるのは問題がありますよね。
コンテナを使う場合、通常はコンテナ内で複数のユーザを使う必要があることが多いでしょう。一般ユーザでコンテナを起動する際にもroot権限でマッピングを書き込む必要があるということになると、利便性が大きく損なわれます。
このような一般ユーザでのコンテナの利用の際のマッピングの問題を解決するために、DebianとUbuntuではshadowを拡張し、あるユーザが自由に使える「サブID」を定義できるようになりました。Plamoでもこの拡張されたshadowを採用しています。
この「サブID」はUID、GIDそれぞれに定義でき、管理者があらかじめ定義しておきます。丁度、ユーザ作成時にそのユーザがどのグループに属するかを定義するのと同じようなものですね。
この「サブID」を定義するにはusermod
コマンドの-v
オプション(サブUID)と-w
オプション(サブGID)を使います。
たとえば、test
ユーザにUID/GID共に200000から65536個の使用を許可する場合、以下のように実行します。
この定義は/etc/subuid
と/etc/subgid
に保存されます。
ちなみにUbuntuではインストール時に作成したubuntu
ユーザに対して、デフォルトで100000から65536個のサブUID/GIDが割り当てられていました。
この状態で新たにユーザを追加すると、以下のようにその後の使われていない範囲から65536個割り当たるようになっているようです。※
このサブUIDとサブGIDを使ってマッピングを作成するには、uidmapパッケージが必要です。
インストールすると、以上のようにsetuidされたnewuidmap
コマンドとnewgidmap
コマンドがインストールされます。
では、newuidmap
コマンドを使って複数のIDをユーザ名前空間に対してマッピングしてみましょう。
ここでは実験のためにデフォルトで定義されているサブUID以外にさらに定義を追加します。
1000と1001の2つがubuntu
ユーザで使えるようになりました。
新しいユーザ名前空間で起動しているPID=1777のシェルに対してマッピングを定義します。名前空間内のUID=0と1を名前空間外のUID=1000と1001にそれぞれマッピングします。
newuidmap
コマンドには、以上のように「(PID) (名前空間内の最初のUID) (名前空間外の最初のUID) (範囲)」の順で引数を与えて実行します。uid_map
、gid_map
ファイルと似ていますね。
それではユーザ名前空間内で実効UIDを1に変えてみましょう。
以上のようにPythonを起動し、seteuid
してみました。Pythonのpid=1800ですので、親の名前空間であるホストOS上からこのプロセスの状態を確認してみましょう。
以上のように実効UID=1001で実行されているのがわかります。
LXCでは、一般ユーザでコンテナを作成したり起動したりする際に、ユーザ名前空間を作成し、このnewuidmap
とnewgidmap
コマンドを使って、マッピングの範囲を指定し、コンテナの操作を行っています。
まとめ
今回は一般ユーザでコンテナを操作する際に使うLinuxカーネルの機能であるユーザ名前空間について説明しました。
次回はUbuntu上での一般ユーザでのコンテナの操作について説明したいと思います。
最近のLXC関連の動き
この記事の原稿を書いている間にLXC 1.0.7がリリースされました(12月5日)。1.1.0リリースの話もメーリングリスト上で話題にのぼるようになってきました。原稿執筆時点で1.1.0.alpha3というバージョンになっています。2015年早々に1.1.0をリリースしたいようですが、最新のsystemdへの対応など、結構大きなテーマが残っているのでスケジュール通りに進むかどうかはわかりません。
これまでLXC関連のプロジェクトはLXCとcgroupを管理するためのCGManagerの2つでしたが、11月の初めにはLXDというプロジェクトがアナウンスされました。
当初は文書だけでコードはない状態でしたが、その後急速に開発が進み、とりあえず動作する状態になっているようです。LXDはliblxcのGoバインディングを使って開発されており、最近の流行に乗っているような感じがしますね。今後が楽しみです。
また、プロジェクトが増えたのに合わせて、LXCのメンテナであるStéphane Graber氏が、ウェブサイトのリニューアルに着手しました。サイトのコンテンツはGithubで管理されており、英語以外の言語のコンテンツも簡単に追加できることを考えて作られています。
この新しいサイトも12月の初めには正式に公開されました。URLは変わっていません。
英語のコンテンツはまだとりあえず揃えただけという感が強いですが一通りのコンテンツが揃いました。日本語の翻訳もとりあえず私が一通り行い、公開が済んでいます。
言語独自のコンテンツを追加することも特に問題なさそうですので、この連載へのリンクを早速公式ページの日本語コンテンツに追加しておきました。日本独自の役に立つのコンテンツを充実させていくこともできると思います。
誰でもGitHub上でforkして変更や追加を行ってプルリクエストを送れますので、興味のある方は是非ご参加いただければと思います。日本語訳はまだまだ改良の余地があると思っていますので、品質の向上に協力いただける方もお待ちいたしております。