はじめに
前回はコンテナを構成する要素技術の Namespace、cgroup、Capability について学んだが、今回はファイルシステム、ネットワークについて深ぼっていく
ファイルシステムの分離
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
と /lib
が usr/bin
と usr/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 を利用するには以下の条件を満たす必要がある
- 新しいファイルシステム (new_root) と元のファイルシステムの移動先 (put_old) はディレクトリではなくてはならない
- new_root と put_old は現在のルートディレクトリと同じマウントポイントにあってはならない
- put_old は new_root 配下になければならない
- new_root はマウントポイントへのパスでなければならないが、ルートは利用できない
- new_root の親マウントおよび現在のルートディレクトリの親マウントの伝搬タイプは MS_SHARED であってはならない
- 現在のルートディレクトリはマウントポイントでなくてはならない
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
には何も存在せず、当然ながら merged
は lower
と同じになっている
$ 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 ペア (veth0
、veth0_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
最後に
前回、今回とコンテナ要素技術について学んだ
知らないことばかりで、ゼロベースから調べながら進めるととても時間がかかり、正直とても大変だったがやはり有用だったと思う
これを機に低レイヤについて更に学習していこうと思う