はじめに
以前#8で、研究室サーバーのDocker関連のユーザー権限を見直しました。
複数ユーザーが研究室サーバーのDockerを利用するケースでは、docker
グループにユーザーを追加する方法が一般的だと思います(私が所属した研究室は2/2がこの方法でした)。
$ id -nG azuma nas_access docker $ cat /etc/group | grep docker docker:x:999:***,***,infra,azuma,***,***
一方、docker
グループの権限はroot
ユーザーに匹敵するので、ホストのファイルシステム全体をコンテナにマウントすることで任意のファイルを読み書きできてしまうことがよく知られています。
本記事では、セキュリティ重視のコンテナ管理ツールと謳われているPodmanに入門しつつ、rootlessコンテナの有用性を探ります。
0. 参考書籍
『Podman in Action』という書籍が大変参考になりました。Chapter 1を読むだけでもPodmanの概要が掴めるのでオススメです。
英語版は無料でダウンロードできますが、日本語版の『Podmanイン・アクション』も秀和システムから刊行されているのでぜひ参考にしてください。
1. Podmanとは
Podmanはコンテナを開発・管理・実行するためのdaemonlessなコンテナエンジンです。「daemonless」については後ほど説明します。
まず「コンテナエンジン」についてですが、似たような言葉で「コンテナランタイム」というものがあります。『Podman in Action』では以下のような分類がなされていました。
コンテナオーケストレーター (e.g., Kubernetes, Docker Swarm)
コンテナエンジン(e.g., Docker, Podman, CRI-O, containerd)
OCI コンテナランタイム(e.g., runc, crun, Kata, gVisor)
コンテナエンジンはシングルノードでコンテナを実行するために利用され、ユーザーが直接利用するものとしてはDockerとPodman、Kubernetes関連でCRI-Oやcontainerdを目にしたことがあるかもしれません。
コンテナランタイムは、Linuxカーネルのさまざまな部分を設定してコンテナ化されたアプリケーションを起動します。ちなみに、CRI-Oやcontainerdは高レベルコンテナランタイムに分類されることもあるそうです。
Podman vs. Docker Difference - Which One to Choose? | Codicaより引用
話を戻すと、PodmanはDockerの代替手段とされます。実際にalias docker=podman
と設定することで簡単に切り替えられるので、両者を組み合わせて利用することも可能です。2章で実例を示しますが、DockerのCLIとPodmanのCLIはとても似ているため移行のハードルは低そうです。
これらの知識を踏まえ、改めてDockerとPodmanを比較し「rootless」と「daemonless」について理解を深めます。
※ 理解を深めるために手元でコマンドを実行しながら読み進めたい場合は、先にPodmanをインストールしてください(2章)
1-1. rootlessとは
Podmanはコンテナ実行に(デフォルトで)rootアクセスを必要としません。
一般ユーザーがDockerクライアントを実行したい場合、そのユーザーをdocker
グループに追加するケースが多いと思います。しかし、このユーザーは非特権ユーザーであるにも関わらず、コンテナ上で以下のようなコマンドを実行できることが予想できます。
# 実行しないでください $ docker run -it --rm -v /:/tmp ubuntu rm -rf /tmp
また『Podman in Action』では以下のようなコマンドが紹介されていました。--privileged
オプションとchroot
コマンドによって、コンテナ内からホストシステムにフルアクセスできるようになり、データの破壊・漏洩・改竄などあらゆる行為が可能になってしまいます。
# 実行しないでください $ docker run -ti --name hacker --privileged -v /:/host ubi8 chroot /host #
(Dockerがデフォルトのlogging driverを使用している場合、)ハッカーは終了後にdocker rm
コマンドを実行することでログも削除できるため非常に危険です。
Dockerと異なり、Podmanはデフォルトでrootlessです。つまり、Podmanで実行したプロセスの所有者は全てそのユーザーであり、ホストに対してそのユーザー以上の権限を持つことはありません。これによって、間違ったコマンドを1つ実行しただけでホストマシンがダウンするような恐怖に怯えず、安全にコンテナを実行することができます。
$ podman run -it --rm -v /:/tmp ubuntu touch /tmp/hoge.txt touch: cannot touch '/tmp/hoge.txt': Permission denied
最後に勘違いされがちな点として、Dockerにもrootlessモードという機能があります。
ドキュメントにある通り、ユーザー自身がインストールスクリプトを実行し、システム起動時にデーモンが起動される必要があります。管理者目線でもユーザー目線でも面倒だと感じました。これは後述するdaemonlessにも繋がる話題です。
2-2. daemonlessとは
Podmanでは、バックグラウンドで常時実行されるようなデーモンを必要としません。
Dockerは、次に示すような複雑なアーキテクチャになっています。
- ユーザーがDockerクライアントを起動
- DockerクライアントがDockerデーモンに接続
- Dockerデーモンがcontainerdデーモンに接続
- containerdがOCIランタイムを実行
- OCIランタイムがコンテナを起動
一方でPodmanは、ユーザーが起動したPodmanがOCIランタイムを実行し、OCIランタイムがコンテナを起動します。結果として、コンテナはコンテナエンジンの子プロセスとして起動します。
実際にコンテナを起動してプロセスを確認します。
$ id uid=2401(azuma) gid=2401(azuma) groups=2401(azuma),995(nas_access),998(docker) $ podman run -d --rm --name myubuntu ubuntu sleep 600 f31b9fa695f919d9afd9e29a5f182fd792ffef66edb9fee86435b37cb38701bc $ podman top myubuntu hpid,huser,comm HPID HUSER COMMAND 2326328 2401 sleep $ pstree -ps 2326328 systemd(1)───conmon(2326325)───sleep(2326328) $ ps -p 2326325 -o pid,cmd -ww PID CMD 2326325 /usr/bin/conmon --api-version 1 # 後略 $ ps -p 2326328 -o pid,cmd PID CMD 2326328 sleep 600
ホストから見たコンテナのプロセスIDは2326328
、プロセス所有者は2401
(自分)です。systemd
が親プロセスであり、その下でconmon
(プロセスID:2326325
)が動作していて、さらにその下でsleep
プロセスが実行されていることもプロセスツリーから分かりました。
検索したところ、conmon
はOCIコンテナランタイムモニターのことで、以下のような説明がありました。どうやら、コンテナを見守ってくれる世話役のようです。
Conmon is a monitoring program and communication tool between a container manager (like Podman or CRI-O) and an OCI runtime (like runc or crun) for a single container.
Upon being launched, conmon (usually) double-forks to daemonize and detach from the parent that launched it. It then launches the runtime as its child. This allows managing processes to die in the foreground, but still be able to watch over and connect to the child process (the container).
GitHub - containers/conmon: An OCI container runtime monitor.
少し遠回りしてしまいましたが、Podmanがdaemonlessであることも確認できました。
2. Podmanの導入
今回はUbuntu 22.04にPodmanをインストールします。
$ sudo apt-get update $ apt-cache show podman | grep Version Version: 3.4.4+ds1-1ubuntu1.22.04.2 Version: 3.4.4+ds1-1ubuntu1 $ sudo apt-get -y install podman $ podman version Version: 3.4.4 API Version: 3.4.4 Go Version: go1.18.1 Built: Thu Jan 1 09:00:00 1970 OS/Arch: linux/amd64
Ubuntu 22.04では利用可能なバージョンが3.4.4
のみ(執筆時点での最新は5.2.4
)だったので、以下のURLからopenSUSEのリポジトリを追加してパッケージをインストールしました。
Install package devel:kubic:libcontainers:unstable / podman
# 省略 $ podman -v podman version 4.6.2
公式ドキュメントに沿って、試しにPodmanでコンテナを実行してみます。
$ podman images REPOSITORY TAG IMAGE ID CREATED SIZE root@DLBox-II:/# podman run -dt -p 8080:80/tcp docker.io/library/httpd Trying to pull docker.io/library/httpd:latest... # 省略 841318295bc1434e3d9de4aac527f39e40da45d854cebf7c30d256aa3296eaec $ podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 841318295bc1 docker.io/library/httpd:latest httpd-foreground 39 seconds ago Up 39 seconds 0.0.0.0:8080->80/tcp bold_galileo $ curl localhost:8080 <html><body><h1>It works!</h1></body></html> $ podman rm -f bold_galileo bold_galileo
docker
コマンドがpodman
コマンドになったくらいで、サブコマンドやオプションは噂通り瓜二つでした。
Dockerの場合は短縮名を利用すると常にhttps://docker.io
からイメージがpullされます。『Podman in Action』ではこれを
The Docker engine gives docker.io an advantage over other container registries as the preferred registry.
と表現していました。Podmanの場合は、/etc/containers/registriesconf
のunqualified-search-registries
として記載されたレジストリーから、その順序でイメージが検索されます。
$ cat /etc/containers/registries.conf | grep unqualified-search-registries unqualified-search-registries = ["registry.fedoraproject.org", "registry.access.redhat.com", "docker.io", "quay.io"] # $ podman info -f json | jq '.registries["search"]' # [ # "registry.fedoraproject.org", # "registry.access.redhat.com", # "docker.io", # "quay.io" # ]
移行をイメージしてalias docker=podman
というLinuxのエイリアスも設定してみました。
$ alias docker=podman $ docker run hello Resolved "hello" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf) Trying to pull quay.io/podman/hello:latest... # 省略 !... Hello Podman World ...! .--"--. / - - \ / (O) (O) \ ~~~| -=(,Y,)=- | .---. /` \ |~~ ~/ o o \~~~~.----. ~~ | =(X)= |~ / (O (O) \ ~~~~~~~ ~| =(Y_)=- | ~~~~ ~~~| U |~~ Project: https://github.com/containers/podman Website: https://podman.io Desktop: https://podman-desktop.io Documents: https://docs.podman.io YouTube: https://youtube.com/@Podman X/Twitter: @Podman_io Mastodon: @Podman_io@fosstodon.org $ unalias docker
本来のDockerでdocker run hello
を実行するとイメージが見つからずエラーが発生するはずです。
Linuxのエイリアスによってpodman run hello
が実行されたこと、それからコンテナレジストリのエイリアスによって"hello" = "quay.io/podman/hello"
と名前解決されたことが分かります。
3. Podmanとユーザー名前空間
「Podmanのrootlessコンテナでは、デフォルトでユーザーのUIDがユーザー名前空間のrootとしてコンテナにマッピングされるが、podman run
の--userns
オプションでこのデフォルトのマッピングを変更できる」という内容がこの公式ブログには書かれています。
公式ブログではコンテキストが省略されているので、まずはユーザー名前空間について復習します。名前空間(Namespace)はLinuxカーネルの機能で、ホストとNamespace内のプロセスのリソースを隔離します。その中でもユーザー名前空間(User Namespace)は、UID / GIDを隔離するための機能です。これにより、例としてコンテナ内では特権を持ちつつホスト側では特権を持たないユーザーが作成できます。
ユーザー名前空間については、以下の記事が大変分かりやすいです。
実際にコンテナ内とホスト側でUID / GIDがどのようにマッピングされているか、研究室サーバーに戻ってイメージを膨らませていきます。
まずは、Docker(デフォルト)のUIDマッピングを確認しましょう。
$ id uid=2401(azuma) gid=2401(azuma) groups=2401(azuma),995(nas_access),998(docker) $ docker run -d --rm --name myubuntu ubuntu sleep 100 bfa21d2b2026a63d27353ad3f19c411a3c6f946069c58a82479c72afddc941f1 $ docker exec myubuntu ps -e -o user,pid,cmd USER PID CMD root 1 sleep 100 root 7 ps -e -o user,pid,cmd $ docker inspect --format '{{.State.Pid}}' myubuntu 792876 $ ps -o user= -p 792876 root
これは、コンテナ内のroot
ユーザーがホスト側から見てもroot
ユーザーであることを示しています。ちなみにオプションとして--user $(id -u):$(id -g)
を使用するとコンテナ内でUID2401
、ホスト側から見るとazuma
となりますが、容易に他ユーザーのなりすましができてしまいます。
次に、PodmanのUIDマッピングをpodman top
コマンドで確認します。
$ podman run -d --rm --name myubuntu ubuntu sleep 100 5ac4bdc991214c8100e9621f2fc2a055173f00305791ccfc3e049cc02b71128b # The "l"ast created container $ podman top -l user huser USER HUSER root 2401
コンテナ内のユーザー(USER
)はroot
ですが、ホスト側から見たユーザー(HUSER
)はUID2401
のユーザーであることが分かります。万が一コンテナエスケープされても一般ユーザーであることから、影響を最小限にすることができます。
Podmanでは、このUID / GIDのデフォルトマッピングを--userns
オプションを利用して簡単に変更できます。4種類のマッピングの違いを、ユーザーazuma
(UID:2401
)を例に図示してみました。
公式ドキュメントを読んでいた際に、自身のUIDの対応だけでなく全体的なマッピングのイメージを掴みたいと思って頑張って分析して描きました。似たような図はどこにも見当たらなかったので、もし間違っていたらコメントで教えていただけると幸いです。
それぞれのオプションの詳細な説明は公式ドキュメントに委ねます。
なお、図中の1083040...1148575
という数字はサブUIDであり、以下のコマンドで確認できます。
$ cat /etc/subuid | grep azuma azuma:1083040:65536
Ubuntuではユーザーが新しく追加された際に自動で割り当てられるそうです。さらに詳しく知りたい方は以下の記事を参照してください。
研究室サーバーでは、自身のホームディレクトリ以下の特定のディレクトリをコンテナにマウントして結果を書き込むケースが多いので--userns=keep-id
が便利だと思いました。『Podman in Action』では、隔離されたコンテナが固有のユーザー名前空間で実行される(他のコンテナやホストシステム上のUIDと分離される)ように--userns=auto
の使用を推奨していました。
【参考】
--userns
オプションによる/proc/self/uid_map
の違い
# (名前空間内の最初のID) (名前空間外の最初のID) (範囲) $ podman run --userns="" ubi9 cat /proc/self/uid_map 0 2401 1 1 1083040 65536 $ podman run --userns=keep-id ubi9 cat /proc/self/uid_map 0 1 2401 2401 0 1 2402 2402 63135 $ podman run --userns=auto ubi9 cat /proc/self/uid_map 0 1 1024 $ podman run --userns=nomap ubi9 cat /proc/self/uid_map 0 1 65536
4. NASマウントと補足グループ
前回#8で一旦解決した、「コンテナ上からマウントしたNASに書き込みできない」問題を再訪します。
コンテナ起動コマンドは以下のようなものでした。
$ docker run -it --rm \ --name nas-write-test \ -u $(id -u):$(id -g) \ --group-add $(getent group nas_access | awk -F: '{print $3}') \ -v $NAS_MOUNT_PATH:/tmp/nas/azuma \ azuma-ubuntu:latest \ /bin/bash
NASにアクセス可能なnas_access
グループのGIDを取得して--group-add
オプションとして指定しています。これはnas_access
グループのGIDがマシンによって異なることへの対策でしたが、スマートな解決策であるとは言えません。
Podmanの場合、以下のようにシンプルかつスマートに書けます。
$ podman run -it --rm \ --name nas-write-test \ --userns=keep-id \ --group-add=keep-groups \ -v $NAS_MOUNT_PATH:/tmp/nas \ ubuntu \ /bin/bash ########## azuma@3f34c3dabeec:/$ id uid=2401(azuma) gid=2401(azuma) groups=2401(azuma),65534(nogroup) azuma@3f34c3dabeec:/$ ls -ld /tmp/nas drwxrwxr-x 2 nobody nogroup 0 Nov 15 04:06 /tmp/nas azuma@3f34c3dabeec:/$ echo "Hello World!" > /tmp/nas/hello.txt azuma@3f34c3dabeec:/$ cat !$ cat /tmp/nas/hello.txt Hello World!
--group-add=keep-groups
を指定すると、プライマリユーザーの補足グループ(supplementary group)を保持してくれます。
実際にコンテナ内でnas_access
というグループが作成されている訳ではありませんが、--group-add
オプション無しで実行するとPermission denied
と表示されることから正しく適用されていることが分かります。
低レベルコンテナラインタイムであるcrunやOCI Runtime Specificaitonについても興味が湧いてきました。
5. まとめ
Podman入門からrootlessコンテナの有用性まで探ることができました。
「研究室サーバーのDockerはPodmanに移行しないの?」
と思われる方も居るかもしれませんが、現時点では移行に消極的です。
理由としては、Podmanに興味がある管理者に引き継げるとは限らないことと、Dockerと完全なCLI互換ではないため混乱する人が増加しそうなことがあります。また、研究室に入って初めてコンテナに触れる学生が多く、ソフトウェアエンジニアや研究者として社会に出たときにメジャーなDockerを実は一度も触ったことがなかったという状態は避けたいです。
ただし、alias docker=podman
を指定するのは個人的には大賛成なので、まず私はエイリアスを指定して安心して研究を行いたいと思いますし、興味があるメンバーにも勧めたいと思います。
個人的にかなり好きになってしまったPodmanですが、つい先ほどCNCF入りが発表されたので、今後はさらに注目が集まるかもしれません。楽しみです。
Big news! As you may have seen elsewhere, @Podman_io, @Buildah_io, Skopeo, Podman Desktop, and other container tools are moving to the CNCF! https://t.co/QGRuU8pn8m #opensource
— Podman (@Podman_io) 2024年11月14日