ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

ID Mapping/User Namespace再入門 その(1)

前のシリーズが終わる前に新シリーズを書いていく。

皆さんのIDはマッピングされていますか?

User Namespace とは?

まず、TenForwardさんによる日本語でのとてもわかりやすい解説がすでに存在するので、そちらを参照してから...。

gihyo.jp

User Namespace自体は、Linux名前空間機能の一つであり、ホストのユーザID(グループIDも含むが、簡便のためこの記事では「ユーザID」とだけ表現する)の振り出し方とコンテナ(名前空間)内部のIDの振り出し方を分離する機能である。

実は、User Namespaceを(unshare(2)などで)分離しただけでは、ホストのユーザIDが見えなくなるだけで、コンテナ内部では全てのIDがnobodyになってしまい、現実のプログラムを動かせない。Network Namespaceにおいて、分離後vethなどを引き出さないと結局現実の環境で使えないのと同じイメージである。

ということでこの際にホストのユーザIDとコンテナ内部IDとの対応関係を指定する必要がある。例えば、ホストの ID=100000 ~ ID=165534 を、コンテナの ID=0 ~ ID=65534 に対応させる、ということができる。この機能がID Mappingである。

そして、これもまた重要なのであるが、この場合にコンテナ内部で ID=0 に割り振られたユーザには、そのプロセスでの特権が与えられる。とはいえ、(時刻など)ホスト全体であったり、ホストの名前空間の操作に関わることはできない。一方で、そのプロセスのために別の名前空間を分離している場合は、その名前空間での操作が可能になる。たとえば、Network Namespaceを一緒に分離していればネットワークデバイスの設定ができるし、UTS Namespaceを一緒に分離していればホスト名の変更ができる。

また、User Namespaceはホストの一般ユーザが作成できる。非特権コンテナにおいては、User Namespaceのこういった特徴を利用して一般ユーザがなるべく安全にコンテナを作成できるようにしている。

User Namespace が解決してくれる問題

主に以下の二つの課題が解決できる。

  • もし特権を付与したコンテナをunjailされた場合等に、ホストのファイルシステムを操作されるなどの被害を最低限にしたい
  • 一般ユーザで、なるべく安全にコンテナを作成したい

User Namespace をユーザランドの実装的にはどう実現しているのか

unshare(1)システムコールを追いかけると何となくイメージできるかもしれない。

unshareコマンドを unshare --user --map-root-user のオプションで起動すると、User Namespaceを作り、新しいNamespaceのrootを起動した時のユーザIDと対応させてくれる。

vagrant@ubuntu-bionic:~$ ls -l
total 200176
drwxr-xr-x 16 vagrant vagrant      4096 Sep  4 01:41 criu-test
drwxrwxr-x  2 vagrant vagrant      4096 Jan  9 19:43 docker
-rw-rw-r--  1 vagrant vagrant  29704322 Jun  8  2018 docker-17.06.2-ce.tgz
-rw-rw-r--  1 vagrant vagrant  48014383 Jan  9 21:12 docker-18.09.1.tgz
...
vagrant@ubuntu-bionic:~$ unshare --user --map-root-user
root@ubuntu-bionic:~# ls -l
total 200176
drwxr-xr-x 16 root root      4096 Sep  4 01:41 criu-test
drwxrwxr-x  2 root root      4096 Jan  9 19:43 docker
-rw-rw-r--  1 root root  29704322 Jun  8  2018 docker-17.06.2-ce.tgz
-rw-rw-r--  1 root root  48014383 Jan  9 21:12 docker-18.09.1.tgz
...

straceするとどういうシステムコールが呼ばれ、どういうファイルが編集されるかわかる。全ての結果は長いので Gist に書き出す 。要点としては:

92 unshare(CLONE_NEWUSER) = 0

CLONE_NEWUSER を指定して名前空間を分離・新規作成している。

 96 openat(AT_FDCWD, "/proc/self/uid_map", O_WRONLY) = 3
 97 write(3, "0 1000 1", 8)                 = 8
 98 close(3)                                = 0
 99 openat(AT_FDCWD, "/proc/self/gid_map", O_WRONLY) = 3
100 write(3, "0 1000 1", 8)                 = 8
101 close(3) = 0

自分の /proc/*/uid_map/proc/*/gid_map に、「コンテナのID=0を」「ホストのID=1000に対応させ」「1000のみ(1つだけ)割り振る」ということをやっている。

ちなみにこういう指定をするとこのNamespaceにはrootしか存在しないことになる。新しいNamespaceのpasswdにはvagrantというユーザが当然見えるのだけれど、suすることはできない。

root@ubuntu-bionic:~# getent passwd | grep vagrant
vagrant:x:1000:1000:,,,:/home/vagrant:/bin/bash
root@ubuntu-bionic:~# su - vagrant
su: Authentication failure
(Ignored)
setgid: Invalid argument

で、戻って、execveをして新世界のrootとしてidコマンドを実行している、という感じ。

105 execve("/usr/bin/id", ["id"], 0x7fff9c568b60 /* 22 vars */) = 0

各種コンテナやHaconiwaも、基本はこういう感じで実現している。


サクッと書いた。次回はHaconiwaでの対応状況とかを整理したい。