Overview
A container started with docker run shows up in the host’s ps as an ordinary process, right next to everything else. It still cannot see the outside because Docker wraps it in several layers of Linux features: namespaces, capabilities, cgroups, AppArmor or SELinux, and seccomp. This video goes through the layers from the inside out and looks into each one with standard Linux commands instead of the docker command.
Note that the steps can’t be followed as is on Docker Desktop for Mac or Windows, where containers run inside a Linux virtual machine.
Key Points
- An nginx container appears on the host as a master process and four workers, hanging off the same process tree as everything else through containerd-shim up to systemd (PID 1). The host can even read the container’s files through
/proc/<PID>/root, and every container’s files live under/var/lib/docker, so access to that directory should be restricted and audited. - Linux has eight kinds of namespaces, and Docker gives each container six of them by default (mount, PID, network, IPC, UTS and cgroup).
lsnslists them, andnsenterenters one from the host: joining only the network namespace lets you run the host’sipcommand against a slim image that lacks it. Sharing a PID or network namespace with--pid=container:or--network=container:gives a debugging container, orkubectl debug, a view of the target. - A user namespace makes a process root inside while it stays an ordinary user on the host; Docker’s
userns-remapmaps the container’s root to UID 100000. Letting unprivileged users create user namespaces exposes root-only kernel features, and bugs there have led to real privilege escalations. - Capabilities split root’s powers into 41 pieces. A container’s root gets only 14 of them by default, so changing the clock with
datefails.--cap-drop ALLstill lets most apps run, andnet_rawandnet_bind_servicecan usually go, because Docker lets containers ping and listen on low ports without them. - cgroups cap resources:
--cpus 0.5held a two-core stress test to half a core, and--pids-limit 10stopped a fork bomb while the host kept responding. Docker sets neither limit by default. - AppArmor’s
docker-defaultprofile is applied to every container, and a custom profile can stop even root from writing to/etc. seccomp filters the system calls a process may make: Docker’s default allowlist blocksunshare, but Kubernetes turns seccomp off by default, so setseccompProfiletoRuntimeDefaultand test your app with it. - Along the way, the video introduces the people behind these features, including Solomon Hykes, Eric Biederman, Michael Kerrisk, Crispin Cowan, Stephen Smalley, Andrea Arcangeli, Kees Cook and Jessie Frazelle.
Chapters
- 0:00 Introduction
- 0:38 Chapter 1: A container is just a process
- 2:17 Chapter 2: Namespaces and mounts
- 4:36 Chapter 3: The PID namespace
- 5:43 Chapter 4: The network namespace
- 6:55 Chapter 5: The user namespace
- 8:00 Chapter 6: Capabilities
- 10:27 Chapter 7: cgroups
- 11:28 Chapter 8: AppArmor and SELinux
- 12:46 Chapter 9: seccomp
- 14:40 Summary
概要
docker runで起動したコンテナは、ホストのpsで見ると、ほかのプロセスと同じ一覧に並ぶ普通のプロセスです。それでも中から外が見えないのは、Dockerが名前空間、ケーパビリティ、cgroups、AppArmorやSELinux、seccompといったLinuxの機能を何重にもかぶせているからです。この動画では、内側から順にそれぞれの層を、dockerコマンドではなくLinuxの標準のコマンドで覗いていきます。
なお、MacやWindowsのDocker Desktopではコンテナが裏で動くLinuxの仮想マシンの中にあるため、この手順はそのままでは試せません。
ポイント
- nginxのコンテナは、ホストからはマスターと4つのワーカーとして見え、containerd-shimを経てsystemd(PID 1)につながる同じプロセスの木にぶら下がっています。ホストからは
/proc/<PID>/rootでコンテナの中のファイルまで読めます。全部のコンテナの中身は/var/lib/dockerの下にあるので、このディレクトリに触れる人を絞り、アクセスを監査しておくべきです。 - Linuxの名前空間は8種類あり、Dockerは最初からそのうち6つ(マウント、PID、ネットワーク、IPC、UTS、cgroup)をコンテナごとに分けます。
lsnsで一覧でき、nsenterを使えばホストから名前空間に入れます。ネットワークの名前空間にだけ入れば、ipコマンドの入っていない小さなイメージでもホストのipを持ち込んで調べられます。--pid=container:や--network=container:で名前空間を共有すれば、デバッグ用のコンテナやkubectl debugから対象を調べられます。 - ユーザーの名前空間を使うと、中ではrootでもホストでは普通のユーザーのままという状態を作れます。Dockerの
userns-remapでは、中のrootがホストではUID 100000になります。ただし、普通のユーザーがユーザーの名前空間を自由に作れる設定では、本来rootしか使えないカーネルの機能に手が届き、そこにある脆弱性で実際に権限昇格が起きています。 - ケーパビリティはrootの権限を41種類に分けたものです。コンテナのrootが持つのは既定で14個だけなので、
dateで時刻を変えようとしても断られます。--cap-drop ALLで全部外しても多くのアプリは動きます。Dockerはコンテナの中で特権なしのpingや1024番より下のポートでの待ち受けを許しているので、net_rawとnet_bind_serviceもたいてい外せます。 - cgroupsは資源の量を制限します。2コアを使い切るストレステストを
--cpus 0.5で半コアに抑え、--pids-limit 10でフォーク爆弾を止めている間もホストは普通に応答しました。Dockerはどちらも既定では制限しません。 - AppArmorの
docker-defaultプロファイルは全部のコンテナに当たり、自作のプロファイルを使えばrootでも/etcに書き込めなくできます。seccompはプロセスが呼べるシステムコールを絞る仕組みで、Dockerの既定の許可リストはunshareを止めます。一方Kubernetesでは標準でseccompが外れているので、seccompProfileにRuntimeDefaultを指定し、アプリが止まらないかテストで確かめます。 - 途中で、ソロモン・ハイクス、エリック・ビーダーマン、マイケル・ケリスク、クリスピン・コーワン、スティーブン・スモーリー、アンドレア・アルカンジェリ、キース・クック、ジェシー・フラゼルといった、これらの機能に関わった人たちも紹介します。