LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第16回 Linuxカーネルのコンテナ機能 [6] ─ユーザ名前空間

この記事を読むのに必要な時間:およそ 7 分

年末を迎えて今年も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_mapgid_mapというファイルに書き込みます。つまり名前空間内のプロセスに対してマッピングを定義するわけですね。

この定義は同じユーザ名前空間内のプロセスにも継承されますので,コンテナの最初のプロセスにマッピングを書けば,コンテナ内のプロセスには全て同じマッピングが定義されます。

まずはホストOS上の通常のプロセスのこの2つのファイルの中身を見てみましょう。

# cat /proc/1/uid_map
         0          0 4294967295
# cat /proc/1/gid_map
         0          0 4294967295

3つの数字が並んでいますね。このようにuid_mapgid_mapの書式は同じで,以下のような意味となります。

(名前空間内の最初のID) (名前空間外の最初のID) (範囲)

名前空間内で使用する最初の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までにひもづけられます。

前述の例をマッピングファイルに書くと以下のようになります。

0 100000 65536

名前空間を作成した直後など,マッピングファイルへのマッピングがされていない状態では名前空間内部の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以降です。

まずは現在のユーザの確認です。

$ whoami ; id -u ; id -g
ubuntu
1000
1000

ご覧のようにUID/GID=1000/1000のubuntuというユーザです。

先に説明したマッピングがされない状態でのマッピング先も確認しておきます。

$ cat /proc/sys/kernel/overflowuid /proc/sys/kernel/overflowgid
65534
65534

次にunshareコマンドを実行して新しいユーザ名前空間を作成してみましょう。ユーザ名前空間を作成するには--userオプションを指定します。特にコマンドを指定しなければ作成した名前空間でシェルが起動します。

$ unshare --user  (ユーザ名前空間を作成してシェルを起動)
$ echo $$         (作成した名前空間内で実行されている)
1551

新しいユーザ名前空間で起動したシェルで,自身のUID/GIDとPIDを確認します。

$ grep "[U|G]id" /proc/1551/status
Uid:    65534   65534   65534   65534
Gid:    65534   65534   65534   65534
$ ls -ld /proc/1551
dr-xr-xr-x 9 nobody nogroup 0 Dec  1 16:53 /proc/1551
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

まだマッピングのための操作を行っていませんので,先に説明した通りデフォルトのUID/GIDへマッピングされているのがわかります。/proc/1551/statusのUid/Gidの行で,いずれの値も65534となっていますね。statusファイルのUidGidの行の意味はman 5 procで調べてくださいね。

さて,ここで別のターミナルを開いて,ホストOS上の親の名前空間上でどうなっているかを同じコマンドを実行して見てみましょう。

$ grep "[U|G]id" /proc/1551/status
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
$ ls -ld /proc/1551/status
-r--r--r-- 1 ubuntu ubuntu 0 Dec  1 16:54 /proc/1551/status

ご覧のように親の名前空間上では,いずれのIDも1000となっています。つまり

  • ホストOSの名前空間のUID/GID=1000/1000 → 作成した名前空間内のUID/GID=65534/65534

というマッピングがされており,それぞれの名前空間ではそれぞれの名前空間でのIDで処理が行われているのがわかります。

ではマッピングを作成してみましょう。このマッピング操作は作成したユーザ名前空間の親の名前空間から実行する必要があります

新しく作成したユーザ名前空間内のUID/GID=0/0のユーザ,つまりrootを,ホストOSの名前空間上のUID/GID=1000/1000のユーザに割り当ててみます。

$ echo '0 1000 1' > /proc/1551/uid_map
$ echo '0 1000 1' > /proc/1551/gid_map
$ grep "[U|G]id" /proc/1551/status
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
※)
その後、カーネルの修正によりgid_mapへの書き込みは一般ユーザ権限ではできなくなっています(正確には`CAP_SETGID`のケーパビリティが必要)。上記のecho '0 1000 1' > /proc/1551/gid_mapという操作はエラーになりますので,echo '0 1000 1' | sudo tee /proc/1551/gid_mapのようにする必要があります。

ホストOS上ではプロセスのUID/GIDは変化していません。ずっと同じユーザで実行しているのでこれは当たり前ですね。

ここで作成したユーザ名前空間のシェルに戻ってみましょう。

$ grep "[U|G]id" /proc/1551/status
Uid:    0   0   0   0
Gid:    0   0   0   0
$ id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
$ ls -ld /proc/1551
dr-xr-xr-x 9 root root 0 Dec  1 16:53 /proc/1551

ご覧のように,先ほどは65534だったUIDとGIDが0になっています。つまりマッピングを定義した時点でそのマッピングが有効になり,定義したユーザでの処理になります。

ユーザ名前空間内でのいろいろな操作

このユーザ名前空間内でファイルを作成してみましょう。

$ pwd
/home/ubuntu
$ touch testfile
$ ls -l testfile 
-rw-rw-r-- 1 root root 0 Dec  1 17:22 testfile

rootの所有でファイルが作成されましたね。親の名前空間でこのファイルを見ると,

$ ls -l /home/ubuntu/testfile 
-rw-rw-r-- 1 ubuntu ubuntu 0 Dec  1 17:22 /home/ubuntu/testfile

親の名前空間上のユーザの所有のファイルとして見えていますね。

次に,ユーザ名前空間内ではrootユーザであっても,名前空間の外では特権を持たないことがわかる例を見てみましょう。

ここまでの例では,新たなユーザ名前空間のみを作成しました。しかし他の名前空間は作成していませんので,親であるホストOSの名前空間と同じ名前空間にいることになります。ネットワークインターフェースを見てみると,親の名前空間と同じインターフェースが見えます。

$ ip link show  (親の名前空間でも作成した名前空間でも同じ結果が得られる)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:53:a8:9c brd ff:ff:ff:ff:ff:ff

通常はrootであればネットワークインターフェースをdownさせたり,新しくインターフェースを作成することができます。しかしユーザ名前空間内のrootは親の名前空間では一般ユーザであり特権を持ちませんのでそのような操作はできません。

$ whoami
root
$ ip link set eth0 down
RTNETLINK answers: Operation not permitted
$ ip link add name veth0-host type veth peer name veth0-ct
RTNETLINK answers: Operation not permitted

rootであっても,このようにいずれの操作もエラーで返ってきているのがわかります。

せっかくrootユーザになったのに,これでは何もできないのではないか? と思われるかも知れません。しかしそんなことはありません。ユーザ名前空間内の特権ユーザは新たに他の名前空間を作ることができます。通常は一般ユーザではユーザ名前空間以外の名前空間は新たに作れません。

試しにホストOS上の一般ユーザでネットワーク名前空間を作るコマンドを実行してみましょう。

$ whoami
ubuntu
$ unshare --net  (親の名前空間では一般ユーザはユーザ以外の名前空間は作れない)
unshare: unshare failed: Operation not permitted

以上のように親の名前空間で一般ユーザで実行するとエラーになりますね。ところが,ユーザ名前空間内のrootユーザのシェルで同じコマンドを実行すると,この操作が実行できます。

$ whoami
root
$ unshare --net    (ユーザ名前空間内では新たにネットワーク名前空間を作れる)
# ip link show     (新しいネットワーク名前空間なのでループバックインターフェースのみ存在している)
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# ip link add name veth0-host type veth peer name veth0-ct (vethインターフェースの作成)
# ip link show     (作成できた)
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: veth0-ct: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 76:29:7f:1f:8f:60 brd ff:ff:ff:ff:ff:ff
3: veth0-host: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether ce:89:db:2d:33:2b brd ff:ff:ff:ff:ff:ff

以上のように作成したユーザ名前空間内の特権ユーザであれば,新たにネットワーク名前空間が作れます。この新たに作ったネットワーク名前空間では特権がありますので,自由にインターフェースを定義できます。

もちろん,外部と通信を行う場合は親環境であるホストOS上での特権が必要になるので,ユーザ名前空間を作成したからと言って,自由に外部と通信ができるわけではありません。しかし,名前空間内部では通常のrootのように自由に操作できますので,通常のrootと同じようにコンテナが管理できるわけです。

著者プロフィール

加藤泰文(かとうやすふみ)

2009年頃にLinuxカーネルのcgroup機能に興味を持って以来,Linuxのコンテナ関連の最新情報を追っかけたり,コンテナの勉強会を開いたりして勉強しています。英語力のない自分用にLXCのmanページを日本語訳していたところ,あっさり本家にマージされてしまい,それ以来日本語訳のパッチを送り続けています。

Plamo Linuxメンテナ。ファーストサーバ株式会社所属。

Twitter:@ten_forward
技術系のブログ:http://d.hatena.ne.jp/defiant/

コメント

コメントの記入