DockerのRootlessモードを調べている中で、unshare
というコマンドを見つけました。
Linuxカーネルの機能である名前空間の分離が出来るコマンドです。
今回は unshare
コマンドを使ってユーザー名前空間の分離を試してみました。
はじめに
以前、DockerのRootlessモードを調べている中で、unshare
というコマンドを見つけました。
unshare
を使ってユーザー名前空間の分離を試してみましたが、
なかなか苦戦したので、メモがてら残しておこうと思います。
検証環境
本文章執筆時の検証環境の情報は以下のとおりです。
-
OS: AlmaLinux 9.5
-
Kernel:
5.14.0-503.38.1.el9_5.x86_64
-
unshare:
unshare --version unshare from util-linux 2.37.4
unshare
コマンドとは
unshare
コマンドは、Linuxカーネルの機能である名前空間を作成し、新しい名前空間で任意のコマンドを実行するためのコマンドです。
引数が指定されない場合はデフォルトで /bin/sh
が実行されます。
また、 $SHELL
の環境変数が定義されている場合は、 $SHELL
の値が実行されます。
オプションを指定することで様々な名前空間を分離することが出来ます。
$ unshare --help
Usage:
unshare [options] [<program> [<argument>...]]
Run a program with some namespaces unshared from the parent.
Options:
-m, --mount[=<file>] unshare mounts namespace
-u, --uts[=<file>] unshare UTS namespace (hostname etc)
-i, --ipc[=<file>] unshare System V IPC namespace
-n, --net[=<file>] unshare network namespace
-p, --pid[=<file>] unshare pid namespace
-U, --user[=<file>] unshare user namespace
-C, --cgroup[=<file>] unshare cgroup namespace
-T, --time[=<file>] unshare time namespace
今回はユーザー名前空間の分離を試してみました。 その他の名前空間の分離に関しては本記事では触れていません。
ユーザー名前空間の分離
新しいユーザー名前空間でコマンドを実行するには --user
オプションを指定します。
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 1001 1001 1001 1001
Gid: 1001 1001 1001 1001
Groups: 989 1001
$ unshare --user /bin/bash
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 65534 65534 65534 65534
Gid: 65534 65534 65534 65534
Groups: 65534 65534
UID/GIDはそのままだと 65534(nobody)
になってしまいます。
これはマッピングがないときのUID/GIDが kernel.overflowgid
kernel.overflowuid
で定義されているためです。
$ sysctl kernel.overflowuid
kernel.overflowgid = 65534
$ sysctl kernel.overflowgid
kernel.overflowgid = 65534
ここから newuidmap
newgidmap
コマンドを使って、UID/GIDのマッピングを行います。
マッピング設定は unshare
で作成した名前空間内では行えないので、
別の端末を開いて実行します。
ユーザー名前空間の設定
UID/GIDマッピングは /etc/subuid
/etc/subgid
に記載されている範囲でのみ設定可能です。
現在の設定は getsubids
コマンドで確認出来ます。
詳しくは 以前の記事を参照してください。
今回はUID1001番のユーザーに 300000-365535
の範囲を、ユーザー名前空間で利用なUID/GIDとして割り当てます。
$ sudo usermod --add-subuids 300000-365535 --add-subgids 300000-365535 $USER
$ getsubids $USER; getsubids -g $USER
0: shun 300000 65536
0: shun 300000 65536
再度 unshare
コマンドで新しい名前空間を作成します。
$ unshare --user
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 65534 65534 65534 65534
Gid: 65534 65534 65534 65534
Groups: 65534 65534
プロセスごとに設定されているUID/GIDのマッピングは
/proc/<pid>/uid_map
/proc/<pid>/gid_map
で確認出来ます。
unshare --user
で作成した名前空間はまだ何も設定がいないので、
/proc/self/uid_map
/proc/self/gid_map
は空です。
$ cat /proc/self/uid_map
$ cat /proc/self/gid_map
次に newuidmap
newgidmap
コマンドを使って、UID/GIDのマッピングを行います。
newuidmap <PID> <名前空間内でのUID> <ホスト側でのUID> <マッピング数>
の形式で指定します。
unshare
で実行しているプロセスのPIDは $$
という変数で確認出来ます。
$ echo $$
37180
試しに名前空間内のrootユーザー(UID=0,GID=0)をホスト側の一般ユーザー(UID=1001,GID=1001)にマッピングしてみます。
unshare
を実行している端末とは別の端末を開いて実行してください。
$ newuidmap 37180 0 1001 1
$ newgidmap 37180 0 1001 1
unshare
で実行している端末に戻ると、実行ユーザーがrootユーザー(UID=0,GID=0)に変わっていることが確認出来ます。
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 0 0 0 0
Gid: 0 0 0 0
Groups: 65534 0
/proc/self/uid_map
/proc/self/gid_map
を確認すると、マッピングが設定されていることが確認出来ます。
$ cat /proc/self/{u,g}id_map
0 1001 1
0 1001 1
ホスト側での一般ユーザー(UID=1001,GID=1001)で作成したファイルは、
unshare
で作成した名前空間内でrootユーザー(UID=0,GID=0)がオーナーのファイルとして確認出来ます。
# ホスト側
$ date > test1.txt
$ ls -aln
total 4
drwxr-xr-x 2 1001 1001 23 May 13 16:19 .
drwxr-xr-x 3 1001 1001 26 May 13 11:36 ..
-rw-r--r-- 1 1001 1001 32 May 13 16:19 test1.txt
# unshareで作成した名前空間内
$ ls -aln
total 4
drwxr-xr-x 2 0 0 23 May 13 16:19 .
drwxr-xr-x 3 0 0 26 May 13 11:36 ..
-rw-r--r-- 1 0 0 32 May 13 16:19 test1.txt
以前の記事で紹介した RootlessなDocker環境でコンテナ内プロセスをコンテナ内rootユーザーで動かす場合 と似たようなマッピングになりましたね。
ただこのままでは名前空間内のrootユーザー(UID=0)がホスト側の一般ユーザー(UID=1001)にマッピングしただけで、
先の記事のようにコンテナ内の一般ユーザー(UID=1000)がホスト側の一般ユーザー(UID=300999)にマッピングされることはありません。
そのためには newuidmap
newgidmap
コマンドでマッピングを設定する際に、追加の設定を行う必要があります。
ユーザー名前空間の複数マッピング
RootlessモードのDocker環境にならって、名前空間内のrootユーザー(UID=0)をホスト側の一般ユーザー(UID=1001)にマッピングし、 名前空間内の一般ユーザー(UID=1)以降のユーザーをホスト側の一般ユーザー(UID=300000)以降にマッピングしてみます。
unshare
コマンドで新しく名前空間を作成します。
$ unshare --user
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 65534 65534 65534 65534
Gid: 65534 65534 65534 65534
Groups: 65534 65534
$ echo $$
37820
別端末からUID/GIDマッピングを設定します。
$ newuidmap 37820 0 1001 1 1 300000 5000
$ newgidmap 37820 0 1001 1 1 300000 5000
名前空間内に戻ってみます。
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 0 0 0 0
Gid: 0 0 0 0
Groups: 65534 0
$ ls -aln
total 4
drwxr-xr-x 2 0 0 23 May 13 16:19 .
drwxr-xr-x 3 0 0 26 May 13 11:36 ..
-rw-r--r-- 1 0 0 32 May 13 16:19 test1.txt
先程と同様名前空間内のrootユーザー(UID=0)がホスト側の一般ユーザー(UID=1001)にマッピングされていることが確認出来ます。
ここで名前空間内の一般ユーザーがオーナーのファイルをいくつか作成してみます。
$ date > tmp
$ for f in $(seq 3); do echo "[UID=$f]" && install -v -o $f -g $f -m 644 tmp namespace-$f.txt; done
[UID=1]
removed 'namespace-1.txt'
'tmp' -> 'namespace-1.txt'
[UID=2]
removed 'namespace-2.txt'
'tmp' -> 'namespace-2.txt'
[UID=3]
removed 'namespace-3.txt'
'tmp' -> 'namespace-3.txt'
$ for f in $(seq 1000 1003); do echo "[UID=$f]" && install -v -o $f -g $f -m 644 tmp namespace-$f.txt; done
[UID=1000]
'tmp' -> 'namespace-1000.txt'
[UID=1001]
'tmp' -> 'namespace-1001.txt'
[UID=1002]
'tmp' -> 'namespace-1002.txt'
[UID=1003]
'tmp' -> 'namespace-1003.txt'
$ mv -v tmp namespace-0.txt
renamed 'tmp' -> 'namespace-0.txt'
$ ls -aln
total 40
drwxr-xr-x 2 0 0 4096 May 14 10:57 .
drwxr-xr-x 3 0 0 26 May 13 11:36 ..
-rw-r--r-- 1 0 0 32 May 14 10:52 namespace-0.txt
-rw-r--r-- 1 1000 1000 32 May 14 10:56 namespace-1000.txt
-rw-r--r-- 1 1001 1001 32 May 14 10:56 namespace-1001.txt
-rw-r--r-- 1 1002 1002 32 May 14 10:56 namespace-1002.txt
-rw-r--r-- 1 1003 1003 32 May 14 10:56 namespace-1003.txt
-rw-r--r-- 1 1 1 32 May 14 10:56 namespace-1.txt
-rw-r--r-- 1 2 2 32 May 14 10:56 namespace-2.txt
-rw-r--r-- 1 3 3 32 May 14 10:56 namespace-3.txt
-rw-r--r-- 1 0 0 32 May 13 16:19 test1.txt
ではホスト側から確認してみましょう。
$ ls -aln
total 40
drwxr-xr-x 2 1001 1001 4096 May 14 10:57 .
drwxr-xr-x 3 1001 1001 26 May 13 11:36 ..
-rw-r--r-- 1 1001 1001 32 May 14 10:52 namespace-0.txt
-rw-r--r-- 1 300999 300999 32 May 14 10:56 namespace-1000.txt
-rw-r--r-- 1 301000 301000 32 May 14 10:56 namespace-1001.txt
-rw-r--r-- 1 301001 301001 32 May 14 10:56 namespace-1002.txt
-rw-r--r-- 1 301002 301002 32 May 14 10:56 namespace-1003.txt
-rw-r--r-- 1 300000 300000 32 May 14 10:56 namespace-1.txt
-rw-r--r-- 1 300001 300001 32 May 14 10:56 namespace-2.txt
-rw-r--r-- 1 300002 300002 32 May 14 10:56 namespace-3.txt
-rw-r--r-- 1 1001 1001 32 May 13 16:19 test1.txt
いい感じにマッピング出来ていますね。 名前空間内のrootユーザー(UID=0)のみホスト側の一般ユーザー(UID=1001)にマッピングされていて、 名前空間内の一般ユーザー(UID=1)以降はホスト側の一般ユーザー(UID=300000)以降にマッピングされています。
RootlessなDocker環境でのホストOSとコンテナ間のUID/GIDマッピングと同じような形になりました。
/etc/subuid
/etc/subgid
の設定範囲内であれば結構自由にマッピング設定が出来そうですね。
unshare
コマンドのオプションで設定
一部のマッピング設定は unshare
のオプションを指定することでも。
--map-user
--map-group
オプションを指定すると、名前空間内の実行ユーザーのUID/GID指定でき、指定されたユーザーはホスト側の実行ユーザー(UID=1001,GID=1001)にマッピングされます。
$ unshare --user --map-user=1000 --map-group=1000
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
Groups: 65534 1000
$ cat /proc/self/{u,g}id_map
1000 1001 1
1000 1001 1
名前空間内のUID=1000,GID=1000のユーザーがホスト側のUID=1001,GID=1001のユーザーにマッピングされていることが確認出来ます。
--map-root
オプションを指定すると、名前空間内の実行ユーザーがroot(UID=0)のユーザーになり、名前空間内のrootユーザー(UID=0)をホスト側の一般ユーザー(UID=1001)にマッピングしてくれます。
--map-user=0
--map-group=0
と同じような動作ですね。
$ unshare --user --map-root
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 0 0 0 0
Gid: 0 0 0 0
Groups: 65534 0
$ cat /proc/self/{u,g}id_map
0 1001 1
0 1001 1
また、 --map-current-user
オプションを指定すると、名前空間内の実行ユーザーが現在のユーザー(UID=1001)になり、名前空間内の一般ユーザー(UID=1001)をホスト側の一般ユーザー(UID=1001)にマッピングしてくれます。
こちらは --map-user=1001
--map-group=1001
と同じような動作になります。
$ unshare --user --map-current-user
$ cat /proc/self/status | grep -E "Uid|Gid|Groups"
Uid: 1001 1001 1001 1001
Gid: 1001 1001 1001 1001
Groups: 65534 1001
$ cat /proc/self/{u,g}id_map
1001 1001 1
1001 1001 1
他にも util-linux
のバージョンが 2.39
以降くらいから、
--map-users
--map-groups
というオプションが追加されているようで、
さらに複雑な設定が unshare
のオプションで出来るようになっているようです。
詳しくは unshare(1)
を参照してください。
まとめ
今回は unshare
コマンドを使ってユーザー名前空間の分離を試してみました。
newuidmap
newgidmap
コマンドの使い方や、
名前空間内でのUID/GIDマッピングについての理解が深まった気がします。
お手軽にユーザー名前空間のマッピング設定が試せたのでだいぶ面白いです。 他の名前空間の分離も試してみたいですね。