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

はじめに

前回はコンテナを構成する要素技術の Namespace、cgroup、Capability について学んだが、今回はファイルシステム、ネットワークについて深ぼっていく

kazuki217.hatenablog.com

ファイルシステムの分離

chroot と pivot_root はプロセスのルートファイルシステムを隔離する目的で使用する
ファイルシステムを隔離すると、ホスト OS ファイルシステムへのアクセス制限を設けることができ、よりセキュアな状態となる

chroot と pivot_root は操作対象が異なる

まずは現在のプロセスのルートディレクトリを確認してみると / であることがわかる

$ ls -l /proc/$$/root
lrwxrwxrwx 1 xxxxx xxxxx 0 Mar 18 07:11 /proc/2111/root -> /

ルートファイルシステム/dev/root とわかる

$ awk '{ if ($5 == "/") print $0 }' /proc/$$/mountinfo
26 1 253:1 / / rw,relatime shared:1 - ext4 /dev/root rw,discard,errors=remount-ro,commit=30

chroot でルートディレクトリを変更しファイルシステムを隔離してみる

変更先のディレクトリにバイナリとライブラリをコピーしておく

$ ROOTFS=$(mktemp -d)
$ cp -a /bin /lib $ROOTFS
$ ls -l /bin /lib
lrwxrwxrwx 1 root root 7 Jan 26 00:42 /bin -> usr/bin
lrwxrwxrwx 1 root root 7 Jan 26 00:42 /lib -> usr/lib

/bin/libusr/binusr/lib を参照しているので /usr もコピーしておく

$ cp -a /usr $ROOTFS
$ ls -l $ROOTFS
lrwxrwxrwx  1 xxxxx xxxxx    7 Jan 26 00:42 bin -> usr/bin
lrwxrwxrwx  1 xxxxx xxxxx    7 Jan 26 00:42 lib -> usr/lib
drwxr-xr-x 11 xxxxx xxxxx 4096 Jan 26 00:42 usr

chroot でルートディレクトリを変更すると、/etc/passwd が参照できなくなっていることを確認できる

$ sudo chroot $ROOTFS /bin/sh

$ cat /etc/passwd
cat: /etc/passwd: No such file or directory

有名な話だが、chroot でルートディレクトリを変更しファイルシステムを隔離しても Capability CAP_SYS_CHROOT が有効なプロセスであれば以下のプログラムを実行することで抜け出せてしまう
筆者は C 言語が読み書きできないが、おそらく以下のようなことを実施していると推測する
コマンド chroot はカレントディレクトリの移動も一緒に行われるが、システムコール chroot はカレントディレクトリは変わらないらしい
新たに作成した /$ROOTFS/.dummy ディレクトリにシステムコール chroot を行うと、コマンド chroot で切り替わったルートディレクトリは無効となり、かつカレントディレクトリは移動せず /$ROOTFS のままなので、ルートディレクト/$ROOTFS/.dummy でありながらカレントディレクトリは /$ROOTFS という状態となる
この状態で chdir を繰り返すと / まで移動ができ cat /etc/passwd が実行できてしまう

http://www.gcd.org/blog/2007/09/132/

$ cat <<EOF > escape-chroot.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[]) {
  int i;
  mkdir(".dummy", 0755);
  chroot(".dummy");
  for (i = 0; i < 256; i++) {
    chdir("..");
  }
  if (chroot(".") < 0) {
    fprintf(stderr, "chroot failed: %s\n", strerror(errno));
    return 1;
  }
  argv++;
  execvp(argv[0], argv);
  fprintf(stderr, "%s failed: %s\n", argv[0], strerror(errno));
  return 0;
}
EOF

$ gcc -Wall -o escape-chroot escape-chroot.c

実際に試してみると cat /etc/passwd が実行できてしまった

$ ROOTFS=$(mktemp -d)
$ cp -a /bin /lib /usr escape-chroot $ROOTFS
$ sudo chroot $ROOTFS /bin/sh

# cat /etc/passwd
cat: /etc/passwd: No such file or directory

# ./escape-chroot cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
[...]

この抜け出しには Capability CAP_SYS_CHROOT を無効にすることで対処できる

$ sudo capsh --chroot=$ROOTFS --drop=cap_sys_chroot --

# ./escape-chroot cat /etc/passwd
chroot failed: Operation not permitted

続いて pivot_root でプロセスのルートファイルシステムを入れ替えてみる

pivot_root は現在のルートファイルシステムと新しいルートファイルシステムを別の場所にマウントし、ファイルシステムの隔離を行うもので chroot では行えていた抜け出しに対処できる

なお、pivot_root を利用するには以下の条件を満たす必要がある

  1. 新しいファイルシステム (new_root) と元のファイルシステムの移動先 (put_old) はディレクトリではなくてはならない
  2. new_root と put_old は現在のルートディレクトリと同じマウントポイントにあってはならない
  3. put_old は new_root 配下になければならない
  4. new_root はマウントポイントへのパスでなければならないが、ルートは利用できない
  5. new_root の親マウントおよび現在のルートディレクトリの親マウントの伝搬タイプは MS_SHARED であってはならない
  6. 現在のルートディレクトリはマウントポイントでなくてはならない

pivot_root(2) - Linux manual page

条件の 1,2,3 に対応するため、先ほど作成したファイルシステム配下に .put_old を作成する
またコンテナから /proc をマウントするために proc ディレクトリを作成する

$ NEW_ROOT=$ROOTFS
$ mkdir $NEW_ROOT/{.put_old,proc}

以下のコマンドを実行することでルートファイルシステム$ROOTFS に入れ替えることが可能

$ unshare -mpfr /bin/sh -c "
  mount --bind $NEW_ROOT $NEW_ROOT && \
  mount -t proc /proc $NEW_ROOT/proc && \
  pivot_root $NEW_ROOT $NEW_ROOT/.put_old && \
  umount -l /.put_old && \
  cd / && \
  exec /bin/sh
"

ルートファイルシステムが入れ替わったため、/etc/passwd が存在せずエラーとなっている
もちろん脱出プログラムを実行しても参照できない

# ls -l /
total 20
lrwxrwxrwx   1     0     0     7 Jan 25 15:42 bin -> usr/bin
-rwxrwxr-x   1     0     0 70624 Mar 17 22:17 escape-chroot
lrwxrwxrwx   1     0     0     7 Jan 25 15:42 lib -> usr/lib
dr-xr-xr-x 151 65534 65534     0 Mar 17 22:19 proc
drwxr-xr-x  11     0     0  4096 Jan 25 15:42 usr

# cat /etc/passwd
cat: /etc/passwd: No such file or directory

# ./escape-chroot cat /etc/passwd
cat: /etc/passwd: No such file or directory

何をしているかを細かく見ていくと、まずは Namespace を隔離して /bin/sh を実行している

$ unshare -mpfr /bin/sh -c

条件の 4 つ目である new_root のパスをマウントポイントとするため、$NEWROOT をバインドマウントする

  mount --bind $NEW_ROOT $NEW_ROOT

/proc をマウントする
これをマウントしておかないとプロセス情報はもちろん /proc/mounts などにアクセスできなくなってしまう

  mount -t proc /proc $NEW_ROOT/proc

ルートファイルシステム$NEW_ROOT に移して、古いルートファイルシステム.put_old に変更している

  pivot_root $NEW_ROOT $NEW_ROOT/.put_old

古いルートファイルシステムは不要なので、アンマウントしている

  umount -l /.put_old

新しいルートファイルシステム/ に移動する

  cd /

プロセスを /bin/sh に入れ替えている

  exec /bin/sh

古いルートシステムをアンマウントしないと、現在のルートディレクトリを確認することもできる

$ unshare -mpfr /bin/sh -c "
  mount --bind $NEW_ROOT $NEW_ROOT && \
  mount -t proc /proc $NEW_ROOT/proc && \
  pivot_root $NEW_ROOT $NEW_ROOT/.put_old && \
  cd / && \
  exec /bin/sh
"

# ls -l .put_old
total 64
drwxr-xr-x   3 65534 65534  4096 Mar 17 22:06 Users
lrwxrwxrwx   1 65534 65534     7 Jan 25 15:42 bin -> usr/bin
drwxr-xr-x   5 65534 65534  4096 Jan 25 15:48 boot
drwxr-xr-x  19 65534 65534  4060 Mar 17 22:06 dev
drwxr-xr-x  66 65534 65534  4096 Mar 17 22:16 etc
drwxr-xr-x   3 65534 65534  4096 Mar 17 22:06 home
lrwxrwxrwx   1 65534 65534     7 Jan 25 15:42 lib -> usr/lib
drwx------   2 65534 65534 16384 Jan 25 15:47 lost+found
drwxr-xr-x   2 65534 65534  4096 Jan 25 15:42 media
drwxr-xr-x   3 65534 65534  4096 Mar 17 22:06 mnt
drwxr-xr-x   4 65534 65534  4096 Mar 17 22:06 opt
dr-xr-xr-x 151 65534 65534     0 Mar 17 22:06 proc
drwx------   3 65534 65534  4096 Feb  2 08:18 root
drwxr-xr-x  19 65534 65534   620 Mar 17 22:06 run
lrwxrwxrwx   1 65534 65534     8 Jan 25 15:42 sbin -> usr/sbin
drwxr-xr-x   2 65534 65534  4096 Jan 25 15:42 srv
dr-xr-xr-x  13 65534 65534     0 Mar 17 22:06 sys
drwxrwxrwt  11 65534 65534  4096 Mar 17 22:17 tmp
drwxr-xr-x  11 65534 65534  4096 Jan 25 15:42 usr
drwxr-xr-x  12 65534 65534  4096 Feb  2 08:16 var

OverlayFS

オーバーレイとは読み取り専用の下層レイヤと読み書き可能な上位レイヤを重ね合わせたもので、下層レイヤは lowerdir、上位レイヤは upperdir と呼ばれる
lowerdir に対する書き込みが発生すると、該当ファイルを upperdir にコピーして書き込みを行うため、lowerdir には変更が反映されない
このような仕組みを Copy on Write (CoW) と呼び、複数のコンテナでファイルシステムを共有することができるため、ディスクスペースの節約やファイルシステムの作成に伴うコンテナの起動時間短縮に繋がっている

lowerdir、upperdir 以外に lowerdir と upperdir を重ね合わせた merged、作業用として利用される work が存在する

実際にやってみる

まずは docker を利用してファイルシステムを用意する

$ OVERLAY=$(mktemp -d)
$ mkdir $OVERLAY/{lower,upper,work,merged}
$ CID=$(sudo docker container create alpine:latest)
$ sudo docker export $CID | tar -x -C $OVERLAY/lower
$ sudo docker rm $CID

早速 OverlayFS としてマウントしてみる

$ sudo mount \
  -t overlay \
  -o lowerdir=$OVERLAY/lower,upperdir=$OVERLAY/upper,workdir=$OVERLAY/work \
  overlay \
  $OVERLAY/merged

$ mount | grep overlay
overlay on /tmp/tmp.ifO7w8f5ym/merged type overlay (rw,relatime,lowerdir=/tmp/tmp.ifO7w8f5ym/lower,upperdir=/tmp/tmp.ifO7w8f5ym/upper,workdir=/tmp/tmp.ifO7w8f5ym/work,nouserxattr)

ディレクトリを確認してみると lower には alpine のファイルシステムが存在し、upper には何も存在せず、当然ながら mergedlower と同じになっている

$ ls -l $OVERLAY/lower
total 68
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 bin
drwxr-xr-x  4 xxxxx xxxxx 4096 Mar 18 07:21 dev
drwxr-xr-x 19 xxxxx xxxxx 4096 Mar 18 07:21 etc
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 home
drwxr-xr-x  7 xxxxx xxxxx 4096 Jan 27 02:55 lib
drwxr-xr-x  5 xxxxx xxxxx 4096 Jan 27 02:55 media
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 mnt
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 opt
dr-xr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 proc
drwx------  2 xxxxx xxxxx 4096 Jan 27 02:55 root
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 run
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 sbin
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 srv
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 sys
drwxrwxr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 tmp
drwxr-xr-x  7 xxxxx xxxxx 4096 Jan 27 02:55 usr
drwxr-xr-x 12 xxxxx xxxxx 4096 Jan 27 02:55 var

$ ls -l $OVERLAY/upper
total 0

$ ls -l $OVERLAY/merged
total 68
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 bin
drwxr-xr-x  4 xxxxx xxxxx 4096 Mar 18 07:21 dev
drwxr-xr-x 19 xxxxx xxxxx 4096 Mar 18 07:21 etc
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 home
drwxr-xr-x  7 xxxxx xxxxx 4096 Jan 27 02:55 lib
drwxr-xr-x  5 xxxxx xxxxx 4096 Jan 27 02:55 media
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 mnt
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 opt
dr-xr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 proc
drwx------  2 xxxxx xxxxx 4096 Jan 27 02:55 root
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 run
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 sbin
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 srv
drwxr-xr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 sys
drwxrwxr-x  2 xxxxx xxxxx 4096 Jan 27 02:55 tmp
drwxr-xr-x  7 xxxxx xxxxx 4096 Jan 27 02:55 usr
drwxr-xr-x 12 xxxxx xxxxx 4096 Jan 27 02:55 var

ここで merged にファイルを作成すると upper にも反映されるが、lower には反映されておらず CoW の振る舞いが確認できる

$ touch $OVERLAY/merged/newfile.txt
$ ls -l $OVERLAY/upper/newfile.txt
-rw-rw-r-- 1 xxxxx xxxxx 0 Mar 18 07:23 /tmp/tmp.brzNgmW39G/upper/newfile.txt

$ ls -l $OVERLAY/lower/newfile.txt
ls: cannot access '/tmp/tmp.brzNgmW39G/lower/newfile.txt': No such file or directory

次に以下のような Dockerfile をイメージして実際にコンテナを作成してみる

# Layer1
FROM alpine:latest

# Layer2
RUN apk add --no-cache curl

ENTRYPOINT ["curl"]

まずは Layer1 で alpine:latest のファイルシステムを用意する

$ LAYER1=$(mktemp -d)
$ mkdir $LAYER1/lower
$ CID=$(sudo docker container create alpine:latest)
$ sudo docker export $CID | tar -x -C $LAYER1/lower
$ sudo docker rm $CID

用意した Layer1 を lowerdir として upperdir に curl をインストールする

$ LAYER2=$(mktemp -d)
$ mkdir $LAYER2/{upper,work,merged}
$ sudo mount \
    -t overlay \
    -o lowerdir=$LAYER1/lower,upperdir=$LAYER2/upper,workdir=$LAYER2/work \
    overlay \
    $LAYER2/merged
$ sudo mount --bind /etc/resolv.conf $LAYER2/merged/etc/resolv.conf
$ unshare -r chroot $LAYER2/merged apk add --no-cache curl
$ sudo umount -R $LAYER2/merged
$ rm -rf $LAYER2/{work,merged}

Layer2 には curl があるが、Layer1 には curl はインストールされていない

$ ls -l $LAYER2/upper/usr/bin/curl
-rwxr-xr-x 1 xxxxx xxxxx 264088 Dec  7 16:32 /tmp/tmp.rxwcLRL0f8/upper/usr/bin/curl

$ ls -l $LAYER1/lower/usr/bin/curl
ls: cannot access '/tmp/tmp.3aXImDPDNv/lower/usr/bin/curl': No such file or directory

最後に Layer1 と Layer2 を重ね合わせたものを lowerdir としてコンテナを起動し、Layer2 でインストールした curl を使って httpbin.org にリクエストする
uuid が返却されていることを確認できる

$ CONTAINER=$(mktemp -d)
$ mkdir $CONTAINER/{upper,work,merged}
$ ROOTFS=$CONTAINER/merged
$ sudo mount \
    -t overlay \
    -o lowerdir=$LAYER2/upper:$LAYER1/lower,upperdir=$CONTAINER/upper,workdir=$CONTAINER/work \
    overlay \
    $ROOTFS
$ sudo mount --bind /etc/resolv.conf $ROOTFS/etc/resolv.conf
$ ARGS="http://httpbin.org/uuid"
$ unshare \
    -uipr \
    --mount-proc \
    --fork \
    chroot $ROOTFS /bin/sh -c "mount -t proc proc /proc && exec curl $ARGS"
{
  "uuid": "ee4dc3f5-afc0-4b98-9077-bc4a430a676c"
}

Network Namespace

通常であればネットワークインターフェースやルーティングテーブル、ソケットなどのネットワークリソースは OS 全体で共有され、コンテナごとにこれらを独立して扱えるようにするためには Namespace の 1 つである Network Namespace で隔離する

隔離された Network Namespace には専用のループバックインターフェースが提供される

$ unshare -nr ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

ネットワークインターフェースは 1 つの Network Namespace にしか存在できないため、Root Namespace (ホスト OS の PID 1 の Namespace) が持つネットワークインターフェースは、隔離された Namespace では利用できない
なので、隔離した Network Namespace で外部と通信するにはネットワークインターフェースを適切に割り当てる必要がある

Docker では veth (Virtual Ethernet Device) ペアを利用し、1 つを Root Namespace に作成したネットワーク Bridge に、もう 1 つを Network Namespace に割り当て、Namespace 間で通信を行う

実際に Network Namespace を利用してプライベートネットワークを作成してみる

Network Namespace 間で通信をおこなうために、ネットワーク Bridge (br0) を作成する

$ sudo ip link add name br0 type bridge
$ sudo ip addr add 192.168.77.1/24 broadcast 192.168.77.255 label br0 dev br0
$ sudo ip link set dev br0 up

$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    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:55:55:8c:b5:0a brd ff:ff:ff:ff:ff:ff
    altname enp0s2
[...]
4: br0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
    link/ether fa:e1:72:58:f5:9e brd ff:ff:ff:ff:ff:ff

Network Namespace (ns0) を作成する

$ sudo ip netns add ns0

$ ip netns list
ns0

veth ペア (veth0veth0_peer) を作成し、片方を Root Namespace の br0 に、もう片方を ns0 に割り当てる

$ sudo ip link add veth0 type veth peer name veth0_peer
$ sudo ip link set dev veth0 master br0
$ sudo ip link set dev veth0 up
$ sudo ip link set dev veth0_peer netns ns0

$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    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:55:55:8c:b5:0a brd ff:ff:ff:ff:ff:ff
    altname enp0s2
[...]
4: br0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
    link/ether fa:e1:72:58:f5:9e brd ff:ff:ff:ff:ff:ff
6: veth0@if5: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue master br0 state LOWERLAYERDOWN mode DEFAULT group default qlen 1000
    link/ether de:4a:b0:c1:31:b6 brd ff:ff:ff:ff:ff:ff link-netns ns0

$ sudo ip netns exec ns0 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: veth0_peer@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:e8:37:f0:a5:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0

ns0 の veth にアドレスを割り当て、デフォルトゲートウェイbr0 (192.168.77.1) に設定する

$ sudo ip netns exec ns0 ip link set dev veth0_peer name eth0
$ sudo ip netns exec ns0 ip addr add 192.168.77.2/24 dev eth0
$ sudo ip netns exec ns0 ip link set dev eth0 up
$ sudo ip netns exec ns0 ip route add default via 192.168.77.1

$ sudo ip netns exec ns0 ip route list
default via 192.168.77.1 dev eth0 
192.168.77.0/24 dev eth0 proto kernel scope link src 192.168.77.2 

ns0 を参照するコンテナを作成し、br0 (192.168.77.1)ping で疎通確認する

$ sudo ip netns exec ns0 ping -c 3 192.168.77.1
PING 192.168.77.1 (192.168.77.1) 56(84) bytes of data.
64 bytes from 192.168.77.1: icmp_seq=1 ttl=64 time=0.434 ms
64 bytes from 192.168.77.1: icmp_seq=2 ttl=64 time=0.224 ms
64 bytes from 192.168.77.1: icmp_seq=3 ttl=64 time=0.229 ms

--- 192.168.77.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2045ms
rtt min/avg/max/mdev = 0.224/0.295/0.434/0.097 ms

最後に

前回、今回とコンテナ要素技術について学んだ
知らないことばかりで、ゼロベースから調べながら進めるととても時間がかかり、正直とても大変だったがやはり有用だったと思う

これを機に低レイヤについて更に学習していこうと思う

今更ながら「コンテナ技術入門」をやってみた (その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 について深ぼってみて、「コンテナ」への理解が進んだと思う
次回はファイルシステムとネットワークについて学ぶ予定

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

はじめに

業務で Docker や Kubernetes などのコンテナ技術を日々利用しているが、その要素技術についてきちんと学んだことがなかったため、今更ながら「コンテナ技術入門」をやってみた

en-ambi.com

環境

M1, 2020
macOS sonoma バージョン 14.2.1

仮想サーバの用意

colima を利用して仮想サーバを立ち上げる
バージョンは 0.6.8 を利用
仮想サーバに割り当てる CPU とメモリはそれぞれ 4CPUs、8GB とする (自身のローカル環境に合わせて適宜調整すること)

$ colima start --cpu 4 --memory 8
$ colima list
PROFILE    STATUS     ARCH       CPUS    MEMORY    DISK     RUNTIME    ADDRESS
default    Running    aarch64    4       8GiB      60GiB    docker

$ colima ssh

起動した仮想サーバは Ubuntu 23.10

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 23.10"
NAME="Ubuntu"
VERSION_ID="23.10"
VERSION="23.10 (Mantic Minotaur)"
VERSION_CODENAME=mantic
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=mantic
LOGO=ubuntu-logo

ローカル環境の cgroup バージョンは v2 となっているが「コンテナ技術入門」では cgroup v1 を利用していると思われるため一部読み替えて実行していく

使用しているLinux環境のcgroupが、v1なのかv2なのかを確認する - CLOVER🍀

$ stat -fc %T /sys/fs/cgroup/
cgroup2fs

まずは必要となるコマンドをインストールする
libcap パッケージのリンクが切れていたのでリポジトリ URL はミラーリポジトリに変更する

$ sudo apt update
$ sudo apt install cgdb cgroup-tools make gcc
$ sudo git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/morgan/libcap /usr/src/libcap
$ cd /usr/src/libcap && sudo make && sudo make install

コンテナを作成する

仮想サーバの準備ができたら Docker コンテナの bash イメージを利用してファイルシステムを用意する

まずはファイルシステムとして利用するテンポラリディレクトリを作成する

$ ROOTFS=$(mktemp -d)

bash イメージのコンテナを作成し、コンテナ ID を取得する

$ CID=$(sudo docker container create bash)

コンテナのファイルシステムを tar アーカイブで出力し、作成したディレクトリに展開する

$ sudo docker container export $CID | tar -x -C $ROOTFS

/usr/local/bin/bashシンボリックリンクを作成する

$ ln -s /usr/local/bin/bash $ROOTFS/bin/bash

不要になったコンテナを削除する

$ sudo docker container rm $CID

これでこれから作成するコンテナのファイルシステムが用意できた

続いて CPU とメモリを制限するサブグループを作成しようとしたところ、uuidgen コマンドが実行できなかった

$ UUID=$(uuidgen)
bash: uuidgen: command not found

util-linux パッケージに含まれているらしいが、既にバージョン 2.39.1 をインストール済みとのこと

Ubuntu Manpage: uuidgen - create a new UUID value

$ dpkg -l | grep util-linux
ii  util-linux                     2.39.1-4ubuntu2                   arm64        miscellaneous system utilities

調べてみたところソースコードを落としてきてビルドすると util-linux に含まれるコマンド諸々が利用できるとのこと
試してみたところ問題なさそう

Linux: util-linux を gdb でデバッグする - CUBE SUGAR CONTAINER

$ cd ~
$ wget -O - https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/v2.39/util-linux-2.39.1.tar.gz   | tar zxvf -
$ cd util-linux-2.39.1/
$ ./configure && make
$ ./uuidgen --version
uuidgen from util-linux 2.39.1

続いて cgcreate で実際にサブグループを作成してみる
いくつかオプションがあるが、-t-a でタスク (プロセスやスレッド) とサブグループおよび関連するファイルのオーナーを指定し、-g でサブシステム (cpu,memory など) とそれらを管理するサブグループを指定している

$ UUID=$(./uuidgen)
$ sudo cgcreate -t $(id -un):$(id -gn) -a $(id -un):$(id -gn) -g cpu,memory:$UUID

作成されたサブグループは /sys/fs/cgroup/<subgroup> で確認できる

$ ls -l /sys/fs/cgroup/$UUID
total 0
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.controllers
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.events
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.freeze
--w------- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.kill
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.max.depth
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.max.descendants
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.pressure
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.procs
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.subtree_control
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.threads
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.type
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.idle
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.max.burst
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.pressure
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.uclamp.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.uclamp.min
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.weight
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.weight.nice
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.cpus
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.cpus.effective
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.cpus.partition
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.mems
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.mems.effective
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.pressure
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.prio.class
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.weight
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.current
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.events
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.events.local
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.high
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.low
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.min
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.numa_stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.oom.group
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.peak
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.pressure
--w------- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.reclaim
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.stat
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.current
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.events
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.high
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.max
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.peak
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.zswap.current
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.zswap.max
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.current
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.events
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.max
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.peak

次に作成したグループに CPU 30%、メモリ 10MB の制限を設けてみる
cgroup v2 では読み替えが必要で memory.limit_in_bytesmemory.maxcfs_period_uscpu.cfs_quota_uscpu.max となる

https://www.kernel.org/doc/Documentation/cgroup-v2.txt

# cgorup v1 のコマンド
$ cgset -r memory.limit_in_bytes=10000000 $UUID
# cgorup v2 のコマンド
$ cgset -r memory.max="10000000" $UUID

# cgorup v1 のコマンド
$ cgset -r cpu.cfs_period_us=1000000 $UUID
$ cgset -r cpu.cfs_quota_us=300000 $UUID
# cgorup v2 のコマンド
$ cgset -r cpu.max="300000 1000000" $UUID

続いて CPU とメモリに制限を設けたサブグループにコンテナを作成しようとしたが、cgroup change of group failed エラーが発生した

$ CMD="/bin/sh"
$ cgexec -g cpu,memory:$UUID \
  unshare -muinpfr /bin/sh -c "
    mount -t proc proc $ROOTFS/proc &&
    touch $ROOTFS$(tty); mount --bind $(tty) $ROOTFS$(tty) &&
    touch $ROOTFS/dev/pts/ptmx; mount --bind /dev/pts/ptmx $ROOTFS/dev/pts/ptmx &&
    ln -sf /dev/pts/ptmx $ROOTFS/dev/ptmx &&
    touch $ROOTFS/dev/null && mount --bind /dev/null $ROOTFS/dev/null &&
    /bin/hostname $UUID &&
    exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'
  "
cgroup change of group failed

以下を参考にログレベルを変えてみたところ /sys/fs/cgroup/$UUID/cgroup.procs への書き込みで Permission denied が発生しているようだ

linux - Using cgroups v2 without root - Unix & Linux Stack Exchange

$ CGROUP_LOGLEVEL=DEBUG cgexec -g cpu,memory:$UUID \
  unshare -muinpfr /bin/sh -c "
    mount -t proc proc $ROOTFS/proc &&
    touch $ROOTFS$(tty); mount --bind $(tty) $ROOTFS$(tty) &&
    touch $ROOTFS/dev/pts/ptmx; mount --bind /dev/pts/ptmx $ROOTFS/dev/pts/ptmx &&
    ln -sf /dev/pts/ptmx $ROOTFS/dev/ptmx &&
    touch $ROOTFS/dev/null && mount --bind /dev/null $ROOTFS/dev/null &&
    /bin/hostname $UUID &&
    exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'
  "
Found cgroup option cpuset, count 0
Found cgroup option cpu, count 1
Found cgroup option io, count 2
Found cgroup option memory, count 3
Found cgroup option hugetlb, count 4
Found cgroup option pids, count 5
Found cgroup option rdma, count 6
Found cgroup option misc, count 7
My euid and egid is: 501,1000
Will move pid 3200 to cgroup '21f28fdd-f5bc-4453-bd03-995bb6dde1e7'
Adding controller cpu
Adding controller memory
cgroup build procs path: /sys/fs/cgroup/21f28fdd-f5bc-4453-bd03-995bb6dde1e7/cgroup.procs
Warning: cannot write tid 3200 to /sys/fs/cgroup/21f28fdd-f5bc-4453-bd03-995bb6dde1e7/cgroup.procs:Permission denied
Warning: cgroup_attach_task_pid failed: 50016
cgroup change of group failed

/sys/fs/cgroup/$UUID/cgroup.procs の Permission を確認してみたが問題はなさそう

$ ls -l /sys/fs/cgroup/$UUID/cgroup.procs
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:59 /sys/fs/cgroup/21f28fdd-f5bc-4453-bd03-995bb6dde1e7/cgroup.procs

なにやら Linux Kernel のドキュメントによると、サブグループの cgorup.procs だけでなく、親となる cgroup.procs への書き込み権限も必要らしい

Control Group v2 — The Linux Kernel documentation

A PID can be written to migrate the process associated with the PID to the cgroup. The writer should match all of the following conditions.
- It must have write access to the "cgroup.procs" file.
- It must have write access to the "cgroup.procs" file of the common ancestor of the source and destination cgroups.

cgroup.procs への書き込み権限を付与して再度コマンドを実行してみると無事コンテナを作成できた

$ sudo chmod o+w /sys/fs/cgroup/cgroup.procs
-rw-r--rw- 1 root root 0 Mar  1 07:26 /sys/fs/cgroup/cgroup.procs

$ cgexec -g cpu,memory:$UUID \
  unshare -muinpfr /bin/sh -c "
    mount -t proc proc $ROOTFS/proc &&
    touch $ROOTFS$(tty); mount --bind $(tty) $ROOTFS$(tty) &&
    touch $ROOTFS/dev/pts/ptmx; mount --bind /dev/pts/ptmx $ROOTFS/dev/pts/ptmx &&
    ln -sf /dev/pts/ptmx $ROOTFS/dev/ptmx &&
    touch $ROOTFS/dev/null && mount --bind /dev/null $ROOTFS/dev/null &&
    /bin/hostname $UUID &&
    exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'
  "

コンテナを作成したコマンドをみていくと・・・
cgexeccgcreate で作成したサブグループ内でプロセスを実行している

cgexec -g cpu,memory:$UUID

実行しているプロセス unshare は隔離した Namespace でプロセスを実行するもので、ここでは /bin/sh -c に続くコマンドを実行している
オプション -muinpfr は Namespace での隔離対象にファイルシステムのマウントポイント、UTS (ホストネームもしくはドメイン名)、IPC (POSIX メッセージキュー、セマフォセット、共有メモリ)、ネットワーク (ルートテーブル、ファイアウォールルール、ソケットなど)、PID を含め、実行するプロセスは unshare の子プロセスとしてフォークし、現在のユーザの UID:GID を新しい Namespace では root ユーザの UID:GIDマッピングするというものらしい

unshare -muinpfr /bin/sh -c

更にコマンドを見ていくと
隔離された Namespace 上の /proc と、仮想サーバ上の $ROOTFS/proc をマウントしている

mount -t proc proc $ROOTFS/proc

仮想サーバ上のファイルシステムtty を作成し、隔離した Namespace 上の tty とマウントする

touch $ROOTFS$(tty); mount --bind $(tty) $ROOTFS$(tty)

同様に ptmx も作成してマウントする

touch $ROOTFS/dev/pts/ptmx; mount --bind /dev/pts/ptmx $ROOTFS/dev/pts/ptmx

マウントした ptmxシンボリックリンクを作成する
これでターミナルからコンテナの操作が実行できそう

ln -sf /dev/pts/ptmx $ROOTFS/dev/ptmx

続いて /dev/null を作成してマウントする

touch $ROOTFS/dev/null && mount --bind /dev/null $ROOTFS/dev/null

ホストネームに $UUID を指定する

/bin/hostname $UUID

capsh はケイパビリティを設定するコマンドで、--chroot オプションでプロセスのルートディレクトリを冒頭で作成したファイルシステムとし、ファイルシステムを隔離している
--drop で子プロセスの chroot ケイパビリティを剥奪している
最後に exec $CMD/bin/sh を実行している

exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'

作成できたコンテナ内でホスト名、ユーザ、プロセステーブル、マウントテーブルを確認してみる

ホスト名は UUID に書き換わっていることを確認できる

# uname -n
21f28fdd-f5bc-4453-bd03-995bb6dde1e7

ユーザは昇格したとおり root になっている

# id
uid=0(root) gid=0(root) groups=65534(nobody),0(root)

プロセスも隔離されており、プロセス /bin/sh が PID 1 となっている

# ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   13 root      0:00 ps aux

マウントした /procttyptmx/dev/null も確認できる

# mount
proc on /proc type proc (rw,relatime)
devpts on /dev/pts/0 type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
devpts on /dev/pts/ptmx type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
devtmpfs on /dev/null type devtmpfs (rw,nosuid,noexec,relatime,size=4034036k,nr_inodes=1008509,mode=755,inode64)

最後にリソースが制限されているかを確認するため yes コマンドを実行してみる

# yes >/dev/null 

別途ターミナルを立ち上げて仮想サーバに接続してからプロセスを確認してみると、unshare/bin/shyes プロセスが確認できる

$ ps f
    PID TTY      STAT   TIME COMMAND
   8339 pts/1    Ss     0:00 /bin/bash --login
   8367 pts/1    R+     0:00  \_ ps f
   2344 pts/0    Ss     0:00 /bin/bash --login
   8053 pts/0    S      0:00  \_ unshare -muinpfr …
   8054 pts/0    S      0:00      \_ /bin/sh
   8323 pts/0    R+     0:02          \_ yes

top コマンドで CPU 使用率を確認してみると 30% に制限されていることも確認できた

$ top -p 8323
top - 07:30:42 up 38 min,  2 users,  load average: 0.00, 0.00, 0.00
Tasks:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):  7.3 us,  0.2 sy,  0.0 ni, 92.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st 
MiB Mem :   7922.4 total,   7127.7 free,    299.2 used,    656.9 buff/cache     
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   7623.2 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                             
   8323 xxxxx     20   0    1704    768    768 R  29.9   0.0   0:21.89 yes

一通り確認できたので、コンテナを終了して

# exit

サブグループとファイルシステムも削除する

$ sudo cgdelete -r -g cpu,memory:$UUID
$ rm -rf $ROOTFS

さいごに

日頃、コンテナを起動する際にどんなことが行われているかをなんとなく理解することはできた
「コンテナ技術入門」のコンテンツはまだ続いているため、引き続きコンテナの要素技術について学び、纏めようと思う

しかし「コンテナ」とは良く言ったものだ

AWS Certified Solutions Architect - Professional 試験 (SAP-C02) を受験してきた

はじめに

AWS Certified Solutions Architect - Professional 認定の有効期限が近づいていたので、再認定のために 2024/2 に認定試験 (SAP-C02) を受験してきました
次回の再認定時に前回どうやって勉強したかを思い出すために勉強方法を記録しておきます

勉強方法

前提事項ですが、筆者の AWS 業務経験は 8 年くらいです
ただ、直近は GCP をメインに扱っていたため、ここ 2 ~ 3 年で発表された新サービスはほとんどキャッチアップできてないという状況です

勉強方法は Udemy の問題集を繰り返し解き、試験の直前に AWS Skill Builder の公式模擬試験に挑戦しました
問題文および解答の選択肢に、そもそも知らないサービスであったり抑えられてない仕様があった場合にはメモを取り、一通り最後まで問題を解きました
間違えた問題は解説や公式ドキュメントを見ながら復習し、知らないサービスや仕様については ChatGPT を駆使しながら公式ドキュメントを読み漁るスタイルで進めました

1 週目は 50% 程度の正答率でしたが、2 週目は問題文と答えを覚えてしまっているのもあり正答率は 90% 程度でした

公式模擬試験を受けた時の正答率は 70% 程度とギリギリ、かつ Udemy では全く触れられなかったサービスが問題として出てきたこともあり非常に焦りましたが、じたばたしても仕方ないと思いそのまま試験に臨みました
次回以降は早めに公式模擬試験を受けた方が良さそうです

www.udemy.com

explore.skillbuilder.aws

勉強時間

試験までの約 2 週間、平日は毎日 1 ~ 2 時間、週末は 3 時間くらい時間を取ったので、勉強時間としては 30 時間くらいでした
最終的なスコアは 810 だったので必要十分な時間だったと思います

勉強時間の捻出方法としてはいつもより 1 時間早く起きるスタイルにしました
終業後の疲れ果てた状態でアルコールを我慢して勉強するというのは私には無理でした・・・

さいごに

試験を受けるたびに思うのですが、試験時間 3 時間は長すぎてとても疲れます (過去、再認定の場合は試験時間が短かった記憶があります、AWS さん当時の仕様に戻していただけないでしょうか・・・)
試験の申し込みが遅くなってしまったため、今回は午後しか空きがなくその枠で予約したのですが、疲労の少ない午前中に試験を受けることをお勧めします

日本語で試験を受けたのですが、問題文や解答の選択肢が良く分からないと感じた問題はだいぶ少なくなったと思います
それでも一部の問題は原文を表示して確認はしました

無事認定試験には合格できたのでよかったですが、やはり認定試験のための勉強となってしまったのが良くなかったなと反省してます
有効期限が迫っており時間にも限りがあるため仕方なかったと割り切ってます (次回の自分に期待します)

試験、お疲れ様でした

Google Domains から Cloudflare にドメインを移管してみた

はじめに

昨年 Google Domains がサービス終了すると連絡があったが、長らく放置していた
先日、ドメインが自動更新されると通知があり、下記を参考に Cloudflare へドメインを移管したので備忘録として残す

developers.cloudflare.com

ドメインの確認

whoisドメインを確認したところ、Registrar が既に Squarespace に切り替わってしまっていた・・・が一旦公式の手順通りに進めてみることに

[...]
Registrar: Squarespace Domains II LLC
[...]

Cloudflare サイトの追加

Cloudflare dashboard にアクセスして Add site をクリックする

Enter your domain に移管対象のドメインを入力して Continue をクリックする

プランの選択画面が表示されるので Free を選択して Continue をクリックし、移管対象ドメインDNS レコードが正しいか確認して問題なければ再び Continue をクリックする

ネームサーバを更新するように促されるので Click to copy から Cloudflare のアドレスを 2 つコピーする

ドメインのネームサーバを変更しようと Google Domains の管理画面 にアクセスしたが、Registrar が Squarespace に切り替わったためか、ドメインが表示されなかった・・・

ダメ元で Google Cloud コンソールの Cloud Domains ページを覗きにいったら、View in Google Domains というリンクがあり、こちらをクリックしたところ Google Domains の管理画面にドメインが表示された

ネームサーバの管理をクリックし、既存のネームサーバを削除して、先ほどコピーした Cloudflare のアドレスを入力したら保存をクリックする

数分待つと Cloudflare からサイトがアクティブになったとメールが届くので、Cloudflare の Websites を見てみると Active になっていることが確認できた

whois からもネームサーバが切り替わっていることを確認できる

[...]
Name Server: NATASHA.NS.CLOUDFLARE.COM
Name Server: SEVKI.NS.CLOUDFLARE.COM
[...]

ドメインの移管

Google Domains の管理画面を見るとドメインはロックされていますと表示されているのでオフにする

同様にプライバシー保護も有効になっているのでオフにする

認証コードを取得してメモする

ここで Cloudflare の Transfer Domains にアクセスするとドメイン移管の手続きを進められる想定だったが、ドメインのロック解除に時間がかかるとのこと
数分待ったらロック解除が反映されたが、数時間かかるとも書かれているので先にドメインのロック解除を実施しておくことをお勧めする

移管対象のドメインを選択し Confirm Domains をクリックする

先ほどメモした認証コードを入力し Confirm and Proceed をクリックする

連作先情報の入力を求められるので、すべて入力して Confirm and Finalize Transfer をクリックする

数分待つと Google Domains からメールが届くので、リンク先のページで移管をクリックする

更に数分待つと今度は Cloudflare から Domain Transfer Complete というメールが届く
Cloudflare の Manage Domains を見にいくと Status が Active となっていることが確認できた
これをもってドメインの移管が完了となる

最後に念の為 whois で確認したところ Registrar が Cloudflare, Inc. に変わっていることも確認できた

[...]
Registrar: Cloudflare, Inc.
[...]

移管後

下記を参考に DNSSEC を有効にする

developers.cloudflare.com

最後に

Registrar が Squarespace に切り替わっていたため Google Domains 側で移管手続きが進められないかと思ったがなんとかなった
計画的な移行を

Istio の Connection idleTimeout について

はじめに

Istio の Connection idleTimeout の初期値および設定値の変更方法について調査した結果を備忘録として纏めておく
対象の Istio バージョンは v1.14.1

Connection idleTimeout

Istio Documentation の DestinationRule [1] によると、idleTimeout が適用されるのは upstream connection pool に対してのみであり、downstream の idleTimeout には触れられていない

The idle timeout for upstream connection pool connections. The idle timeout is defined as the period in which there are no active requests. If not set, the default is 1 hour. When the idle timeout is reached, the connection will be closed. If the connection is an HTTP/2 connection a drain sequence will occur prior to closing the connection. Note that request based timeouts mean that HTTP/2 PINGs will not keep the connection alive. Applies to both HTTP1.1 and HTTP2 connections.

Envoy Documentation の How do I configure timeouts? [2] を見ると、upstream/downstream 共にデフォルト値は 1 hour と読み取れる
そして downstream の idleTimeout を設定するには、envoy.filters.network.http_connection_managercommon_http_protocol_optionsidle_timeout を指定すればよいと思われる

The HTTP protocol idle_timeout is defined in a generic message used by both the HTTP connection manager as well as upstream cluster HTTP connections. The idle timeout is the time at which a downstream or upstream connection will be terminated if there are no active streams. The default idle timeout if not otherwise specified is 1 hour. To modify the idle timeout for downstream connections use the common_http_protocol_options field in the HTTP connection manager configuration. To modify the idle timeout for upstream connections use the common_http_protocol_options field in the Cluster’s extension_protocol_options, keyed by envoy.extensions.upstreams.http.v3.HttpProtocolOptions

Istio では listener.go [3] で common_http_protocol_options に NodeMetadata の idleTimeout をセットしている

idleTimeout, err := time.ParseDuration(lb.node.Metadata.IdleTimeout)
if err == nil {
    connectionManager.CommonHttpProtocolOptions = &core.HttpProtocolOptions{
        IdleTimeout: durationpb.New(idleTimeout),
    }
}

NodeMetadata は context.go [4] で定義されていて、idleTimeout は IDLE_TIMEOUT で指定できる

// IdleTimeout specifies the idle timeout for the proxy, in duration format (10s).
// If not set, default timeout is 1 hour.
IdleTimeout string `json:"IDLE_TIMEOUT,omitempty"`

これは istioctl install 時に Global Mesh Options の ProxyConfig [5] で、proxyMetadata に ISTIO_META_IDLE_TIMEOUT を指定すればよい

Additional environment variables for the proxy. Names starting with ISTIO_META_ will be included in the generated bootstrap and sent to the XDS server.

実際に試してみたいところなのだが、GKE Cluster を立ち上げようとすると GCE_STOCKOUT となってしまったのでまたの機会に・・・

Reference

[1] https://istio.io/latest/docs/reference/config/networking/destination-rule/#ConnectionPoolSettings-HTTPSettings
[2] https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#how-do-i-configure-timeouts
[3] https://github.com/istio/istio/blob/1.14.1/pilot/pkg/networking/core/v1alpha3/listener.go#L1134-L1139
[4] https://github.com/istio/istio/blob/1.14.1/pilot/pkg/model/context.go#L576-L578
[5] https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#ProxyConfig