Kubernetesディストリビューション — 自分で立てる
OpenShift の任意 UID と SCC — まずイメージが備えていなければならない
一言でいうと
OpenShiftは、コンテナをイメージのUSERではなく、プロジェクトごとに割り当てられた任意のUIDで動かし、そのユーザーをrootグループ(GID 0)に入れます。そのため、書き込むディレクトリをrootグループ所有でグループ書き込み可能にしたイメージだけが、そのまま移行できます。
なぜ必要なのか
通常のKubernetesでは、イメージにUSERがなければrootで、あればそのユーザーで動きます。そのため、多くのイメージが「rootが作ったディレクトリにrootが書き込む」という前提の上に成り立っています。OpenShiftは、この前提を受け入れません。OCP 4.21のイメージ作成ガイドラインは、デフォルトで任意に割り当てたUIDでコンテナを動かすと述べ、その理由を、コンテナエンジンの脆弱性でプロセスがエスケープしても、ホストで高い権限を得られないようにするためだと説明しています。
UIDは毎回変わりうるので、イメージの作成者は「どのユーザーで動くか」を事前に知ることができません。そのためガイドラインは、所有者の代わりにグループを基準にします。コンテナのユーザーは常にrootグループのメンバーなので、書き込むディレクトリとファイルをrootグループ所有にし、グループに読み取り・書き込み(実行ファイルなら実行)の権限を与えれば、どのUIDが来ても動きます。ガイドラインの例がchgrp -R 0 <디렉터리> && chmod -R g=u <디렉터리>です(プレースホルダーはディレクトリです)。また、このユーザーには特権がないので、1024未満のポートは開けません。
先にお断りしておくことが1つあります。OpenShiftはシングルノードのインストールでも、最低8 vCPU・16GB RAM・120GBのストレージが必要とドキュメントに書かれていて、8GiBのこのラボのVMでは動かせません。この記事のOpenShiftの動作はドキュメントで確認したもので、次のラボはk3sで同じ条件を手で作り、症状を再現します。
どう動くのか
UIDを選ぶのはSCC(Security Context Constraints)です。OCP 4.19 SCCドキュメントによると、認証されたユーザーがデフォルトで使うSCCはrestricted-v2で、次のような性質を持ちます。
runAsUser MustRunAsRange 범위를 네임스페이스 주석에서 가져온다
seLinuxContext MustRunAs MCS 레벨도 네임스페이스 주석에서
fsGroup MustRunAs supplemental-groups 주석, 없으면 uid-range 로
capabilities ALL 을 떨어뜨림 NET_BIND_SERVICE 만 명시적으로 더할 수 있다
seccomp runtime/default
allowPrivilegeEscalation 설정하지 않거나 false 여야 한다
このコードブロックの韓国語の部分は、順に、範囲をネームスペースのアノテーションから取得すること、MCSレベルもネームスペースのアノテーションから取得すること、supplemental-groupsのアノテーションを使い、なければuid-rangeを使うこと、ALLをドロップし、NET_BIND_SERVICEだけを明示的に追加できること、allowPrivilegeEscalationは設定しないかfalseでなければならないことを述べています。
範囲は、プロジェクト(ネームスペース)のopenshift.io/sa.scc.uid-rangeアノテーションで、<시작>/<길이>のブロックを1つ受け取ります(プレースホルダーは開始値と長さです)。PodがrunAsUserを要求しなければ、範囲の最小値がデフォルト値になります。バージョンによる違いもあります。4.19のドキュメントではrestricted-v2がデフォルトですが、4.20と4.21 SCCドキュメントには、ユーザーネームスペース(hostUsers: false)を強制するrestricted-v3が、新規インストールのデフォルトとして追加されました。どちらの場合も、範囲のUIDで動くという点は同じです。
SCCとは別に、Pod Security Admissionも動きます。4.21 Pod Security Admissionドキュメントは、両者を独立した2つの仕組みとして説明しています。グローバルにはprivilegedを強制し、restrictedは警告と監査にだけ使い、ネームスペースの警告・監査ラベルは、サービスアカウントが使えるSCCに合わせて自動的に同期されます。ワークロードは両方を通過する必要があります。
現場での姿
k3sのVMで、restricted-v2が埋める値(uid 1000680000、gid 0、fsGroup)を直接入れ、よくあるレガシーイメージ(rootが作った/app/data、権限755)を動かした結果です(実測)。
id: uid=1000680000 gid=0(root) groups=0(root),1000680000
whoami: whoami: unknown uid 1000680000
HOME=/
startup failed: cannot write /app/data/started
/app/run.sh: line 6: can't create /app/data/started: Permission denied
ここでよく試される回避策を3つ、同じVMで測ってみました(実測)。
- fsGroupだけを指定する: 上のPodにはすでにfsGroupがありましたが、イメージ内の
/app/dataはそのままroot 755でした。fsGroupは、Podにマウントされるボリュームのグループを変える仕組みです。 - emptyDirを重ねてマウントする: グループ1000680000、権限2777で作られ、書き込みはできました。その代わり、Podが消えるとデータも消え、イメージがそのパスに入れておいたファイルは隠れます。
- root initContainerでchownする:
violates PodSecurity "restricted:latest": runAsUser=0となり、作成さえされませんでした。
イメージをchgrp -R 0 /app && chmod -R g=u /appで直すと、範囲の最初のUIDと最後のUID(1000689999)のどちらでも起動しました。このとき、もう一度引っかかりました。スクリプトファイルに実行ビットがなかった最初のビルドは、exec: "/app/run.sh": permission deniedでRunContainerErrorになりました。ガイドラインが実行ファイルにグループの実行権限を求める理由です。
whoamiの失敗とHOME=/は、containerdで見た姿です。OpenShiftのドキュメントは、CRI-Oが任意のUIDをコンテナの/etc/passwdに入れてくれることがあると説明しているので、OpenShiftでは名前の参照ができることがあります。ただし4.21のドキュメントは、同じ箇所で、イメージに/etc/passwdが入っているとCRI-Oがその注入に失敗して、実行中のUIDを解決できないことがあるとも書き、そのファイルの権限を開放する方法は危険だと警告しています。そのため、「うちのk3sでwhoamiが動かない」をOpenShiftの症状として書き写してはいけません。一方、HOMEにキャッシュを書き込むツールは、どこでもHOMEを書き込み可能なパスに向けておくほうが安全です。
実務で本当に大切なこと
- 移行する前に、イメージを任意のUIDで動かしてみます。通常のクラスターでも、runAsUserを大きな値に、runAsGroupを0にして指定すれば、OpenShiftで起きる権限の問題のほとんどが事前に明らかになります。範囲内の異なる2つのUIDで動かしてみるのが確実です。
- 直すのはイメージです。特定のUIDにchownする代わりに、rootグループとg=uを使い、USERは数値で書きます(ドキュメントは、S2Iイメージに数値のUSERがないとビルドが失敗すると説明しています)。
- anyuidのような広いSCCを与えるのは、最後の手段です。ドキュメントはデフォルトのSCCを変更しないよう警告していて、広いSCCは、任意のUIDが防ごうとした危険を復活させます。
- 読み取り専用のルートを一緒に有効にすると、イメージがどこに書き込むかが明らかになります。書き込むパスごとにボリュームを与える一覧が、そのまま運用ドキュメントになります。
次のラボですること
k3sのVMにOpenShiftのプロジェクトを模したネームスペースを作り、レガシーイメージが任意のUIDで権限拒否により止まることを記録します。ユーザー情報の調査、buildahでのイメージの修正とk3sへの移行、2つのUIDでの起動、fsGroup・emptyDir・root initContainerの比較、読み取り専用のルートまで試したうえで、レポートにまとめます。