前のシリーズが終わる前に新シリーズを書いていく。
皆さんのIDはマッピングされていますか?
User Namespace とは?
まず、TenForwardさんによる日本語でのとてもわかりやすい解説がすでに存在するので、そちらを参照してから...。
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での対応状況とかを整理したい。