Rootless dockerデーモンから学ぶ、プロセスID/ユーザーID、そしてユーザーネームスペースの関係性、彼らは運命共同体だった・・
初めに
あえてDocker Desktopを使わず、Rootless Dockerで運用しているぜ
なんて、ちょっと尖ったエンジニアに出会ったことはありませんか?
私はというと、社内の先輩から「Rootless入れてみて」と言われて導入したクチですが、使ってみると、これは間違いなく学ぶ価値のある仕組みだと実感しました。
今回は、そんなRootless Dockerを使ってハマったエラーから、「プロセス」や「ユーザーネームスペース」について深掘りした学習ログをまとめます。
rootless dockerのメリット
まず、Rootless Dockerのメリットを簡単にまとめると:
- Desktopに比べ、容量が段違いに少ないこと
- Desktopは3~4GB
- rootlessは30KB
- root権限が不要でセキュア
- 実行ユーザー権限で動作するため、OS全体に影響を与えにくい
この投稿の発端、それは権限エラーとの出会いでした。
まず、Rails8とbootstarpの学習をしたく、docker composeのためのファイルを作成していました。
ERRO[0041] error waiting for container: context canceled
Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "./entrypoint.sh": permission denied: unknown
おやおや、entrypoint.sh の実行権限不足とのこと。
やれやれ😥 chmod x+ entrypoint.shっと・・・
ちょっと待った~とchatgpt君が私の歩を止めました。
なんでそれやるの???
うーーーん、権限エラーと言われたから?
なら、実行ユーザーだけに配布すればよくないか?
今回は、ファイル作成者がdockerを動かすのだから、作成者・グループ・他ユーザーに付与は必要なくないか?
うーーーん、シラン・・・。
ということで、本題へ。
ファイルの作成者 ≠ ファイルの実行者(by Docker)
なぜなら、dockerコマンドを動かす際の、実行ユーザーはファイル作成者ではないから。
ここがかなりミソ。もうカニみそです。
お子様がみんな嫌いで、大人になればなるほど、味を理解するアレです。
なぜdockerコマンドを動かす際の、実行ユーザーはファイル作成者ではないのか
前提として、下記の情報のインプットが必要です。
- プロセス
- ユーザーネームスペース
プロセスとは
簡単に言うと、
プロセス = 実行中のプログラムの“実体”
コマンドを叩くと、裏で何かしらのバイナリが起動され、それがメモリ上で「プロセス」として動きます。このとき、カーネルがプロセスID(PID)を割り当てて、プロセスを管理しています。
ユーザーネームスペースとは
簡単に言うと、
そのプロセスだけのためのユーザーIDマッピングの空間
Rootless Dockerは、Linuxの「ユーザーネームスペース(User Namespace)」機能を使って、ホストOSと異なるUIDを仮想的に割り当てた空間でプロセスを実行しています。
このネームスペースとは、ホストOSがもつユーザーID(uid)が、プロセスを実行する際に、プロセス間で採番される疑似的uidです。
要するに、ホスト内のuidとプロセス内の疑似uidのマッピングを管理する空間です。
たとえば、ホストOS内で、docker rootlessを使ってまーす。という方がいたとしましょう。
docker xxxxをした際には、下記の空間が生成されます。
[ ホストOS空間 ]
└─ UID 0 (root)
└─ UID 1000 (あなた)
└─ ...
└─ [ Rootless Dockerデーモンの空間 ]
└─ USER NS: UID 0(仮想root)
└─ マッピング: 0〜65535 → 100000〜165535
└─ 他のUID(たとえば1000)はマッピングされてない
ホストOS内では1000のあなたはrootless dockerデーモン内では、uidは1000ではありません。
別のuidとなります。ここが大事です。rootless dockerがセキュアと称される特大重要ポイントです。
冒頭に話した、作成者が実行しているのに権限エラーとなるのは、dockerプロセス内で栽培されたあなたのuidはホストOSから見れば、誰やねん状態なんです。だから権限エラーとなる。
そもそものプロセス立ち上がりまでの流れ
# プロセス下準備作成(=型を作るとでもいうのかな?)
clone(..., CLONE_NEWUSER | CLONE_NEWNS, ...); or unshare --user --fork --pid --mount-proc bash
↓
# 作成したプロセスにユーザーネームスペースを作成。(これがいわゆるuidmapかな?)
newuidmap <プロセスID> <ネームスペース内のUID> <ホストOS上のUID> <数>
↓
# 作成したプロセスの実行
setuid(0)
exec bash
プロセスを動かすための下地を作り、そこにマッピングとなるユーザーネームスペースを作り、最後にプロセス実行。という流れ。
ユーザーネームスペースを作成の際のコマンドについて、
<プロセスID>は作成したpid。
<ネームスペース内のUID>は対象プロセスの疑似的に採番するuidの開始番号。
<ホストOS上のUID>はホスト上のuidの開始番号。
<数>は疑似的に採番するuidの数。
例として挙げるならば、
newuidmap 12345 0 100000 65536
<プロセスID> -> 12345
<ネームスペース内のUID> -> 0
<ホストOS上のUID> -> 100000
<数> -> 65536
つまり、
・ネームスペースの中では「UID 0〜65535」は使える
・でもそれはホスト側では「UID 100000〜165535」にマッピングされる
Discussion