今更ながら「コンテナ技術入門」をやってみた (その2)

はじめに

前回はとりあえずコンテナを作成してみてプロセスが隔離されている様子やリソース制限を確認したが、今回はコンテナを構成する要素技術の Namespace、cgroup、Capability を深ぼっていく

kazuki217.hatenablog.com

Namespace

Namespace はプロセスを隔離するために利用され、他の Namespace とは異なるリソースを参照することができる
Namespace はすべてのプロセスに関連付けされていて、指定がなければ親プロセスと同じ Namespace を参照するため、プロセス間で共通のリソースを扱うことになる

元の記事では 7 種類となっていたが、本記事執筆時点では 8 種類のリソースを隔離することが可能

Namespace Flag Isolates
cgroup CLONE_NEWCGROUP cgroup のルートディレクト
IPC CLONE_NEWIPC System V IPC、POSIX メッセージキュー
Network CLONE_NEWNET ネットワークデバイス、スタック、ポートなど
Mount CLONE_NEWNS マウントポイント
PID CLONE_NEWPID プロセス ID
Time CLONE_NEWTIME 起動時間、単調増加時計
User CLONE_NEWUSER ユーザおよびグループ ID
UTS CLONE_NEWUTS ホスト名および NIS ドメイン

namespaces(7) - Linux manual page

プロセスの Namespace は /proc/<PID>/ns で確認でき、括弧の中が inode 番号となっている
inode 番号が同じプロセスは同じ Namespace を参照しているということ

$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 net -> 'net:[4026531840]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 time -> 'time:[4026531834]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 user -> 'user:[4026531837]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:16 uts -> 'uts:[4026531838]'

試しに unshare でプロセスを隔離してみると、inode 番号が先ほどと異なっていることを確認できる

$ unshare -muipr --fork /bin/sh &
[1] 2393

$ ls -l /proc/2393/ns
total 0
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 ipc -> 'ipc:[4026532451]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 mnt -> 'mnt:[4026532449]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 net -> 'net:[4026531840]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 pid_for_children -> 'pid:[4026532452]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 time -> 'time:[4026531834]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 user -> 'user:[4026532448]'
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 17 08:19 uts -> 'uts:[4026532450]'

UTS を隔離したプロセスでホスト名を変更してみると、親プロセスには影響せずにホスト名を変更できていることが確認できる
nsenter は実行中のプロセスの Namespace に接続して指定したコマンドを実行できるというもので docker exec のようなもの

$ unshare -muipr --fork /bin/sh -c 'hostname foobar; sleep 15' &
[1] 2786

$ hostname
colima

$ sudo nsenter -u -t 2739  hostname
foobar

Namespace はプロセスが終了すると消えてしまうが、/proc/<PID>/ns 以下のファイルをマウントしておくことで維持ができる
nsentar でこのファイルを指定すると、Namespace にプロセスを関連づけることができる

$ touch ns_uts
$ sudo unshare --uts=ns_uts /bin/sh -c 'hostname foobar'
$ mount | grep ns_uts
nsfs on /path/to/ns_uts type nsfs (rw)

$ sudo nsenter --uts=ns_uts hostname
foobar

cgroup

cgroup はプロセスをグループ化して、リソース (CPU、Memory など) の使用量を制御したりリソースを監視することが可能な仕組み
サブシステムと呼ばれるものでリソース毎の管理が可能で、各サブシステムの操作はコントローラを介して行う

cgroup は cgroupfs というインターフェースを介して操作が可能で、/sys/fs/cgroup/cgroup にマウントされている

$ mount -t cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

/sys/fs/cgroup 配下を確認すると、サブシステムが見れる
cgroup v1 ではサブシステム毎に階層構造を持つことができたが、cgroup v2 では階層は 1 つになっている

$ ls -l /sys/fs/cgroup
total 0
-r--r--r--  1 root root 0 Mar 17 08:16 cgroup.controllers
-rw-r--r--  1 root root 0 Mar 17 08:41 cgroup.max.depth
-rw-r--r--  1 root root 0 Mar 17 08:41 cgroup.max.descendants
-rw-r--r--  1 root root 0 Mar 17 08:41 cgroup.pressure
-rw-r--r--  1 root root 0 Mar 17 08:16 cgroup.procs
-r--r--r--  1 root root 0 Mar 17 08:41 cgroup.stat
-rw-r--r--  1 root root 0 Mar 17 08:16 cgroup.subtree_control
-rw-r--r--  1 root root 0 Mar 17 08:41 cgroup.threads
-rw-r--r--  1 root root 0 Mar 17 08:41 cpu.pressure
-r--r--r--  1 root root 0 Mar 17 08:41 cpu.stat
-r--r--r--  1 root root 0 Mar 17 08:16 cpuset.cpus.effective
-r--r--r--  1 root root 0 Mar 17 08:16 cpuset.mems.effective
drwxr-xr-x  2 root root 0 Mar 17 08:16 dev-hugepages.mount
drwxr-xr-x  2 root root 0 Mar 17 08:16 dev-mqueue.mount
drwxr-xr-x  2 root root 0 Mar 17 08:16 init.scope
-rw-r--r--  1 root root 0 Mar 17 08:41 io.cost.model
-rw-r--r--  1 root root 0 Mar 17 08:41 io.cost.qos
-rw-r--r--  1 root root 0 Mar 17 08:41 io.pressure
-rw-r--r--  1 root root 0 Mar 17 08:41 io.prio.class
-r--r--r--  1 root root 0 Mar 17 08:41 io.stat
-r--r--r--  1 root root 0 Mar 17 08:41 memory.numa_stat
-rw-r--r--  1 root root 0 Mar 17 08:41 memory.pressure
--w-------  1 root root 0 Mar 17 08:41 memory.reclaim
-r--r--r--  1 root root 0 Mar 17 08:41 memory.stat
-r--r--r--  1 root root 0 Mar 17 08:41 misc.capacity
-r--r--r--  1 root root 0 Mar 17 08:41 misc.current
drwxr-xr-x  2 root root 0 Mar 17 08:16 proc-sys-fs-binfmt_misc.mount
drwxr-xr-x  2 root root 0 Mar 17 08:16 sys-fs-fuse-connections.mount
drwxr-xr-x  2 root root 0 Mar 17 08:16 sys-kernel-config.mount
drwxr-xr-x  2 root root 0 Mar 17 08:16 sys-kernel-debug.mount
drwxr-xr-x  2 root root 0 Mar 17 08:16 sys-kernel-tracing.mount
drwxr-xr-x 22 root root 0 Mar 17 08:31 system.slice
drwxr-xr-x  3 root root 0 Mar 17 08:16 user.slice

cgcreate を利用して実際に cpu,memory サブシステムに関連づけたサブグループを作成してみると、サブグループ内にもサブシステムが作成されている

$ UUID=$(./uuidgen)
$ sudo cgcreate -g cpu,memory:$UUID
$ tree /sys/fs/cgroup/$UUID
/sys/fs/cgroup/2632c499-93a1-4aed-9ab1-3f9fb677f868
├── cgroup.controllers
├── cgroup.events
├── cgroup.freeze
├── cgroup.kill
├── cgroup.max.depth
├── cgroup.max.descendants
├── cgroup.pressure
├── cgroup.procs
├── cgroup.stat
├── cgroup.subtree_control
├── cgroup.threads
├── cgroup.type
├── cpu.idle
├── cpu.max
├── cpu.max.burst
├── cpu.pressure
├── cpu.stat
├── cpu.uclamp.max
├── cpu.uclamp.min
├── cpu.weight
├── cpu.weight.nice
├── cpuset.cpus
├── cpuset.cpus.effective
├── cpuset.cpus.partition
├── cpuset.mems
├── cpuset.mems.effective
├── io.max
├── io.pressure
├── io.prio.class
├── io.stat
├── io.weight
├── memory.current
├── memory.events
├── memory.events.local
├── memory.high
├── memory.low
├── memory.max
├── memory.min
├── memory.numa_stat
├── memory.oom.group
├── memory.peak
├── memory.pressure
├── memory.reclaim
├── memory.stat
├── memory.swap.current
├── memory.swap.events
├── memory.swap.high
├── memory.swap.max
├── memory.swap.peak
├── memory.zswap.current
├── memory.zswap.max
├── pids.current
├── pids.events
├── pids.max
└── pids.peak

1 directory, 55 files

サブグループに所属するプロセスは /sys/fs/cgroup/<SUBGROUP>/cgroup.procs で確認できる

$ sudo cgexec -g cpu:$UUID sleep 10 &
[1] 7662
$ cat /sys/fs/cgroup/$UUID/cgroup.procs
7662

リソース制限については前回確認しているためスキップする

Capability

特権 (root) ユーザで動作するプロセスはすべての権限を持つため、実行しているプログラムに脆弱性があった場合の影響範囲が広い Capability を操作して権限を細分化し、プロセスに必要な権限だけを許可することにより、その影響範囲を狭めることができる

一般的に非特権ユーザは RAW ソケットを扱うことができないため ping は実行できないが、SUID (Set User ID) により特権ユーザで動作するので、非特権ユーザでも RAW ソケットを扱うことができるらしい

試してみようとしたところ、ping がインストールされていなかったので、まずはインストールから

$ sudo apt install -y iputils-ping
$ which ping
/usr/bin/ping

インストールした ping に SUID を設定する

$ ls -l /usr/bin/ping
-rwxr-xr-x 1 root root 81448 Nov 27  2022 /usr/bin/ping

$ sudo chmod 4755 /usr/bin/ping
$ ls -l /usr/bin/ping
-rwsr-xr-x 1 root root 81448 Nov 27  2022 /usr/bin/ping

問題なく実行できる

$ ping -c1 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.136 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.136/0.136/0.136/0.000 ms

ping をコピーして SUID が設定されてない状態だと本来は動作しないはずが、なぜか動作してしまった

$ cp /usr/bin/ping .
$ ls -l ping
-rwxr-xr-x 1 xxxxx xxxxx 81448 Mar 17 09:05 ping

$ ./ping -c1 -q 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.113/0.113/0.113/0.000 ms

もちろん Capability は持ってない

$ getcap /usr/bin/ping
/usr/bin/ping cap_net_raw=ep

$ getcap ./ping

何やら net.ipv4.ping_group_range というカーネルパラメータがあり、RAW ソケットを作成できるグループの範囲を設定できるらしい

ping を実行するのに CAP_NET_RAW は必要なくなっていた

実際に確認してみると gid のレンジが 0 ~ 2147483647 ととても広い

$ sysctl net.ipv4.ping_group_range
net.ipv4.ping_group_range = 0   2147483647

現在のプロセスを実行している gid は 1000 なので該当する

$ id
uid=501(xxxxx) gid=1000(xxxxx) groups=1000(xxxxx),995(docker)

gid のレンジを変更してみる

$ sudo sysctl -w net.ipv4.ping_group_range="1 0"
net.ipv4.ping_group_range = 1 0

この状態で試してみると、RAW ソケットの権限がなく実行できない

$ ./ping -c1 -q 127.0.0.1
./ping: socktype: SOCK_RAW
./ping: socket: Operation not permitted
./ping: => missing cap_net_raw+p capability or setuid?

RAW ソケットを扱う権限を付与すると、再度実行できるようになった

$ sudo setcap CAP_NET_RAW+ep ./ping
$ ./ping -c1 -q 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.314/0.314/0.314/0.000 ms

Capability は以下の Capability Set というものを利用して表現する

  • Permitted
  • Inheritable
  • Effective
  • Ambient
  • Bounding Set

プロセスはすべての Capability Set を扱えるが、ファイルの場合は PermittedInheritableEffective の 3 つのみを利用できるとのこと
実際にカーネルがチェックする Capability Set は Effective とのことだが、Capability Set のアルゴリズムを理解できていないため、ここは引き続き学習していこうと思う

第42回 Linuxカーネルのケーパビリティ[1] | gihyo.jp

ちなみに、Docker コンテナのデフォルトの Capability は以下の通り

$ capsh --decode=$(cat /proc/$(sudo docker container inspect $(sudo docker run --rm -d busybox sleep 10) -f '{{.State.Pid}}')/status | awk '/^CapEff:/{print $2}')
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap

--privileged オプションを付与したコンテナの場合は以下の通りで、現時点で 41 種類あるすべての Capability がセットされている

$ capsh --decode=$(cat /proc/$(sudo docker container inspect $(sudo docker run --privileged --rm -d busybox sleep 10) -f '{{.State.Pid}}')/status | awk '/^CapEff:/{print $2}')
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore

さいごに

コンテナ要素技術の Namespace、cgroup、Capability について深ぼってみて、「コンテナ」への理解が進んだと思う
次回はファイルシステムとネットワークについて学ぶ予定