user namespaceとsubuid/subgid
一言でいうと
rootlessコンテナが可能なのは、新しいuser namespaceを作ると、その中でUID 0になるからです。外から見れば、相変わらずごく普通のユーザーです。
なぜ必要なのか
コンテナを作るには、mount、pivot_root、ネットワークネームスペースの作成といった特権作業が必要です。そのため、長いあいだコンテナデーモンはrootで動いてきました。その結果、/var/run/docker.sockは事実上rootの権限そのものになり、これを狙う攻撃が絶えませんでした。そのソケットにアクセスできるユーザーは、ホストのルートファイルシステムをマウントしたコンテナを起動するだけで、ホストを乗っ取れます。
user namespaceは、この問題を根本から変えました。
どう動くのか
マッピングの原理
unshare(CLONE_NEWUSER)で新しいuser namespaceを作ると、その中で自分のUIDが0に見えるようにマッピングできます。そして、新しいnamespaceの中では、すべてのcapabilityを持ちます。そのため、その中でmountやネットワークネームスペースの作成といった作業ができるようになります。
重要なのは、その権限がnamespaceの中でのみ有効であるという点です。外のファイルに対しては、相変わらず元のUIDの権限しか持ちません。コンテナがエスケープしても、手に入るのは一般ユーザーの権限だけです。
なぜUID1つでは足りないのか
コンテナイメージの中には、さまざまなUIDのファイルがあります。root(0)、nobody(65534)、アプリケーションのアカウント(1000)などです。UIDを1つだけマッピングしても、そのイメージを正しく展開できません。
そこで、UIDの範囲を貸し出します。/etc/subuidと/etc/subgidが、その台帳です。
# /etc/subuid
podster:100000:65536
読み方: ユーザーpodsterは、ホストのUID 100000から65536個を、自分のnamespaceの中で自由に使えます。
マッピングの結果は、次のようになります。
| コンテナ内のUID | ホストのUID |
|---|---|
| 0 (root) | podster自身のUID(例: 1000) |
| 1 | 100000 |
| 2 | 100001 |
| 1000 | 100999 |
| 65535 | 165534 |
コンテナ内のUID 1がホストの100000に対応するという点に注意が必要です。0はユーザー本人にマッピングされ、1からがsubuid範囲の始まりです。そのため、コンテナ内のUID N(N≥1)のホストUIDは、100000 + N - 1です。
65536個を渡す理由は、コンテナイメージが使うUIDの大半が、その範囲に収まるからです。ユーザーごとに重ならない範囲を渡す必要があるので、通常は100000、165536、231072のように、65536ずつ空けて割り当てます。
範囲を実際に適用するには、newuidmap/newgidmapというsetuidヘルパーが必要です(uidmapパッケージ)。このバイナリがなかったり、file capabilityがなかったりすると、範囲のマッピングが失敗して、単一UIDモードにフォールバックします。その状態でもコンテナは起動しますが、複数のUIDを使うイメージで権限エラーが出ます。
確認する方法
grep '^podster:' /etc/subuid /etc/subgid
podman unshare cat /proc/self/uid_map
podman info --format '{{.Host.Security.Rootless}}'
podman info --format '{{.Store.GraphRoot}}'
podman unshareは、podmanが使うものと同じuser namespaceの中でコマンドを実行します。ファイルの所有権の問題をデバッグするときに、欠かせないツールです。たとえば、ボリュームディレクトリの所有者がおかしく見えるとき、podman unshare ls -l <경로>で見ると、コンテナから見た所有者が出てきます(プレースホルダーはパスです)。
rootlessの制約
| 制約 | 理由 | 回避策 |
|---|---|---|
| 1024未満のポートをバインドできない | 特権ポート | net.ipv4.ip_unprivileged_port_startを調整するか、高いポート + リバースプロキシ |
| ネットワークがユーザー空間のスタック | pasta/slirp4netns経由 | 超高性能が必要ならrootful |
| 一部のストレージドライバーに制限 | overlayのマウントは特権が必要 | fuse-overlayfsまたはvfs |
| cgroupの制御に制限 | cgroup v2の委譲が必要 | systemdのユーザースライスの委譲を設定 |
| pingが通らないことがある | ICMPソケットの権限 | ping_group_rangeを調整 |
現場での姿
docker system dfの感覚でディスクを探して戸惑います。rootless podmanのイメージは、/var/lib/dockerではなく、次の場所に積まれます(保存先: ~/.local/share/containers/storage)。ホームパーティションが小さいサーバーでは、これだけでディスクが埋まります。そのため、次のモジュールのgraph rootを移す場面が、実務でよく出てきます。
podman infoでrootlessがfalseと表示されます。rootで実行したか、マッピング設定がなくてrootfulにフォールバックしたのです。どちらなのかを確認しないと、「なぜファイルの所有者がおかしいのか」で何日も迷います。
次の確認で見ること
続くクイズでは、user namespaceの権限の範囲、subuid/subgidのマッピング、rootlessのネットワークとストレージの制約を区別します。その基準を確認したあと、次のモジュールで、ユーザーpodsterのマッピングと、storage.conf・registries.confを作成します。