TT Lab
はじめる
学ぶ 学習パス コース

Rootless Podmanの運用

user namespaceとsubuid/subgid

TT Labで続きを見る

一言でいうと

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です。

podsterに100000から65536個を貸したときのUIDマッピング。コンテナ内の0だけがユーザー本人であるホストの1000に行き、1からが借りた範囲の始まりなので、1は100000、2は100001、1000は100999、65535は165534に対応する

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を作成します。