Kubernetesディストリビューション — 自分で立てる
OpenShift に移したら権限エラーで落ちた
このラボはOpenShiftではなくk3sで模擬します
OpenShiftは、シングルノードでインストールしても最低8 vCPU・16GBのメモリ・120GBのストレージを要求するため(OCP 4.21のドキュメント)、このVM(8GiB)では動かせません。そこで、VMのk3s v1.36.4+k3s1の上で、OpenShiftのrestricted-v2 SCCがPodに埋める値(範囲内の任意のUID、rootグループ、fsGroup)を直接入れて、同じ症状を作ります。k3sはSCCもプロジェクトのアノテーションも知らないので、アノテーションは計算の根拠としてだけ使います。最後のステップで、その事実も確認します。
目標
rootが所有するディレクトリに書き込むイメージが、任意のUIDで権限拒否により止まることを再現し、イメージをrootグループとg=uに直して、範囲内のどのUIDでも起動するようにし、fsGroup・root initContainer・読み取り専用のルートが、この問題でそれぞれ何をして、何をできないのかを区別します。
なぜ重要なのか
Kubernetesでうまく動いていたイメージが、OpenShiftで最初に止まる理由のほとんどは、ユーザーです。OpenShiftはイメージのUSERを信用せず、プロジェクトごとに割り当てた大きなUIDでコンテナを動かします。コンテナエスケープの脆弱性があっても、ホストで意味のあるユーザーになれないようにするための設計です。そのユーザーは事前にわからないので、イメージ内のファイルの「所有者」に合わせることはできず、代わりに常に所属するrootグループに権限を与えるのが、公式のガイドラインです。Podの設定で回避しようとする試み(fsGroup、rootでchownするinitContainer)は、ほとんどがポリシーに阻まれるか、イメージのディレクトリには効果がありません。
ステップ
- ネームスペース
ocp-simを作成し、ラベルpod-security.kubernetes.io/enforce=restricted・pod-security.kubernetes.io/warn=restrictedと、アノテーションopenshift.io/sa.scc.uid-range=1000680000/10000・openshift.io/sa.scc.supplemental-groups=1000680000/10000を付けてください。そのあと次のフィールドを書いてください(書き込み先:/root/ocp/project.json)。uid_range(アノテーションの値そのまま)、default_uid(restricted-v2のMustRunAsRangeがデフォルトで選ぶUID)、max_uid(範囲の最後のUID)です。 - Pod
legacyを作成してください(ファイルは/root/ocp/legacy.yaml)。イメージはlocalhost/ocp-app:legacy(VMにあらかじめ入れてあります)、Podのsecurity contextはrunAsNonRoot true・runAsUserはステップ1のdefault_uid・runAsGroup 0・fsGroup 1000680000・seccompProfile RuntimeDefault、コンテナはallowPrivilegeEscalation false・capabilities drop ALL・terminationMessagePolicy: FallbackToLogsOnErrorです。再起動が1回以上起きたら、次のフィールドを書いてください(書き込み先:/root/ocp/crash.json)。uid、restart_count、error(終了メッセージのうちPermission deniedを含む行をそのまま)です。 - レガシーイメージで
sleep 86400だけを実行するPodprobeを、ステップ2と同じsecurity contextで作成し(ファイルは/root/ocp/probe.yaml)、Readyにしておいてください。その中でユーザー情報を調べ、次のフィールドを書いてください(書き込み先:/root/ocp/identity.json)。uid、gid、groups(id -Gの数値をソートした配列)、whoami_ok(whoamiが成功するかどうか)、home(HOME環境変数の値)、home_writable(そのHOMEに書き込めるかどうか)です。 /root/ocp/app/Containerfileを作成し、レガシーイメージと同じベースとスクリプトで、/appをrootグループ(GID 0)所有に変更して、グループの権限を所有者の権限と同じ(g=u)にしたうえで、数値のUSER(0以外の値)を指定してください。buildahでlocalhost/ocp-app:fixedをビルドしてk3sのcontainerd(k8s.ioネームスペース)に入れ、次のフィールドを書いてください(書き込み先:/root/ocp/build.json)。tool、image、image_id(k3s crictl inspectiのstatus.id)、user(イメージ設定のUser)です。- 修正したイメージでPod
fixed-a(runAsUserはdefault_uid)とfixed-b(runAsUserはmax_uid)を、ステップ2と同じsecurity contextで作成し(ファイルは/root/ocp/fixed-a.yamlと/root/ocp/fixed-b.yaml)、両方ともReadyにしてください。次のフィールドを書いてください(書き込み先:/root/ocp/arbitrary.json)。Pod名をキーにして、uid(コンテナ内のid -u)とdata(/app/dataの소유자UID:그룹GID:권한8진수。プレースホルダーは所有者UID、グループGID、8進数の権限です。例: 0:0:755の形式)です。 - レガシーイメージを直さずに耐える2つの方法を試してください。(1) Pod
legacy-ed(ステップ2と同じで、/app/dataにemptyDirdataをマウント)を作成し(ファイルは/root/ocp/legacy-emptydir.yaml)、Readyにしておきます。(2) rootで動く(runAsUser 0)initContainerfix-permsが/app/dataをchownするPodlegacy-initを適用し(ファイルは/root/ocp/legacy-init.yaml)、出力を保存します(保存先:/root/ocp/init-denied.txt)。次のフィールドを書いてください(書き込み先:/root/ocp/alternatives.json)。fsgroup_fixed_image_dir(fsGroupがあるlegacy Podがイメージのディレクトリへの書き込みに成功したかどうか)、emptydir_data(legacy-ed内の/app/dataの그룹GID:권한8진수。プレースホルダーはグループGIDと8進数の権限です)、root_init_admitted(legacy-initが受け入れられたかどうか)です。 - 修正したイメージに
readOnlyRootFilesystem: trueだけを加えたPodro-bareを作成して(ファイルは/root/ocp/ro-bare.yaml)失敗を確認し、同じ設定でemptyDirを/app/data(名前はdata)と/tmp(名前はtmp)にマウントして環境変数HOME=/tmpを与えたPodfixed-roを作成し(ファイルは/root/ocp/fixed-ro.yaml)、Readyにしておいてください。次のフィールドを書いてください(書き込み先:/root/ocp/readonly.json)。bare_error(ro-bareの終了メッセージのうちRead-onlyを含む行をそのまま)、home(fixed-ro内のHOME)、home_writable、etc_writable(fixed-ro内の/etcに書き込めるかどうか)です。 - 次のフィールドを書いてください(書き込み先:
/root/ocp/report.json)。root_cause(ステップ2の失敗の原因:image-dir-owner、missing-capability、selinuxのいずれか)、whoami_ok_here(このk3sで任意のUIDでwhoamiが動くかどうか)、openshift_runtime_adds_passwd_entry(OpenShiftのドキュメントが、CRI-Oが任意のUIDを/etc/passwdに入れてくれると述べているかどうか)、fsgroup_fixes_image_dir、k3s_applies_uid_range(このk3sがrunAsUserのないPodにアノテーションのUIDを埋めるかどうか)、uid_range_default(アノテーションから計算したデフォルトのUID)、running_uids(現在Readyのfixed-aとfixed-bのUIDをソートした配列)です。
参考
- kubeconfig:
/etc/rancher/k3s/k3s.yaml。ビルドツールはbuildahだけです(dockerとpodmanはありません)。 - イメージの移行:
buildah push <이미지> docker-archive:<파일>:<이름>→k3s ctr -n k8s.io images import <파일>→k3s crictl images(プレースホルダーはイメージ、ファイル、名前です) - 終了メッセージ:
kubectl -n ocp-sim get pod <이름> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.message}'(プレースホルダーはPod名です) - よくあるミス: イメージの中で
chown 1001のように特定のUIDに合わせること。OpenShiftはそのUIDでは動かしません。 - よくあるミス: 同じタグで再ビルドしたのに、k3sに取り込まないこと。Podはcontainerdにある古いイメージを使います。
- OCP 4.21のイメージ作成ガイドライン・OCP 4.19 SCCの管理
OpenShiftのプロジェクトのようにネームスペースを整える
ネームスペースocp-simを作成し、ラベルpod-security.kubernetes.io/enforce=restricted・pod-security.kubernetes.io/warn=restrictedと、アノテーションopenshift.io/sa.scc.uid-range=1000680000/10000・openshift.io/sa.scc.supplemental-groups=1000680000/10000を付けてください。そのあと次のフィールドを書いてください(書き込み先: /root/ocp/project.json)。uid_range(アノテーションの値そのまま)、default_uid(restricted-v2のMustRunAsRangeがデフォルトで選ぶUID)、max_uid(範囲の最後のUID)です。
uid-rangeアノテーションは、<시작>/<길이>のブロックを1つだけ受け取ります(プレースホルダーは開始値と長さです)。MustRunAsRangeは、範囲の最小値をデフォルト値として使います。このVMのk3sはこのアノテーションを読まないので、以降のステップで、その値をPodに直接入れます。
OpenShiftに移したら権限拒否で止まった
Podlegacyを作成してください(ファイルは/root/ocp/legacy.yaml)。イメージはlocalhost/ocp-app:legacy(VMにあらかじめ入れてあります)、Podのsecurity contextはrunAsNonRoot true・runAsUserはステップ1のdefault_uid・runAsGroup 0・fsGroup 1000680000・seccompProfile RuntimeDefault、コンテナはallowPrivilegeEscalation false・capabilities drop ALL・terminationMessagePolicy: FallbackToLogsOnErrorです。再起動が1回以上起きたら、次のフィールドを書いてください(書き込み先: /root/ocp/crash.json)。uid、restart_count、error(終了メッセージのうちPermission deniedを含む行をそのまま)です。
restricted-v2 SCCがOpenShiftで埋める値を、ここでは手で入れます。レガシーイメージのContainerfileは、/root/ocp/app/Containerfile.legacyにあります。FallbackToLogsOnErrorを使うと、ログの末尾がPodの状態の終了メッセージとして残り、コンテナが再び起動しても消えません。
/etc/passwdにないユーザーとして動くということ
レガシーイメージでsleep 86400だけを実行するPodprobeを、ステップ2と同じsecurity contextで作成し(ファイルは/root/ocp/probe.yaml)、Readyにしておいてください。その中でユーザー情報を調べ、次のフィールドを書いてください(書き込み先: /root/ocp/identity.json)。uid、gid、groups(id -Gの数値をソートした配列)、whoami_ok(whoamiが成功するかどうか)、home(HOME環境変数の値)、home_writable(そのHOMEに書き込めるかどうか)です。
containerdは、イメージの/etc/passwdにないUIDでコンテナを起動しても止めませんが、名前とホームディレクトリを教えることはできません。ドキュメントによると、OpenShiftのCRI-Oは任意のUIDのエントリを/etc/passwdに入れてくれます。ここで見える姿が、その違いです。書き込み可能かどうかは、test -wで確認してください。
イメージ側で直す: rootグループとg=u
/root/ocp/app/Containerfileを作成し、レガシーイメージと同じベースとスクリプトで、/appをrootグループ(GID 0)所有に変更して、グループの権限を所有者の権限と同じ(g=u)にしたうえで、数値のUSER(0以外の値)を指定してください。buildahでlocalhost/ocp-app:fixedをビルドしてk3sのcontainerd(k8s.ioネームスペース)に入れ、次のフィールドを書いてください(書き込み先: /root/ocp/build.json)。tool、image、image_id(k3s crictl inspectiのstatus.id)、user(イメージ設定のUser)です。
buildahが作ったイメージは、buildahのストレージにしかありません。buildah push <이미지> docker-archive:<파일>:<이름>で取り出してから(プレースホルダーはイメージ、ファイル、名前です)、k3s ctr -n k8s.io images import <파일>で入れます(プレースホルダーはファイル名です)。実行ファイルもグループの実行権限がないと、任意のUIDが実行できません。
範囲内のどのUIDでも起動するか
修正したイメージでPodfixed-a(runAsUserはdefault_uid)とfixed-b(runAsUserはmax_uid)を、ステップ2と同じsecurity contextで作成し(ファイルは/root/ocp/fixed-a.yamlと/root/ocp/fixed-b.yaml)、両方ともReadyにしてください。次のフィールドを書いてください(書き込み先: /root/ocp/arbitrary.json)。Pod名をキーにして、uid(コンテナ内のid -u)とdata(/app/dataの소유자UID:그룹GID:권한8진수。プレースホルダーは所有者UID、グループGID、8進数の権限です。例: 0:0:755の形式)です。
OpenShiftは、同じイメージをプロジェクトごとに異なるUIDで動かします。特定のUID 1つでしか動かないイメージは、移行した瞬間にまた壊れます。stat -c '%u:%g:%a'で確認します。
fsGroupとroot initContainerはなぜ答えではないのか
レガシーイメージを直さずに耐える2つの方法を試してください。(1) Podlegacy-ed(ステップ2と同じで、/app/dataにemptyDirdataをマウント)を作成し(ファイルは/root/ocp/legacy-emptydir.yaml)、Readyにしておきます。(2) rootで動く(runAsUser 0)initContainerfix-permsが/app/dataをchownするPodlegacy-initを適用し(ファイルは/root/ocp/legacy-init.yaml)、出力を保存します(保存先: /root/ocp/init-denied.txt)。次のフィールドを書いてください(書き込み先: /root/ocp/alternatives.json)。fsgroup_fixed_image_dir(fsGroupがあるlegacy Podがイメージのディレクトリへの書き込みに成功したかどうか)、emptydir_data(legacy-ed内の/app/dataの그룹GID:권한8진수。プレースホルダーはグループGIDと8進数の権限です)、root_init_admitted(legacy-initが受け入れられたかどうか)です。
fsGroupは、Podにマウントされるボリュームのグループ所有を変える仕組みで、イメージのファイルシステムは変えません。emptyDirは書き込みはできますが、Podが消えると一緒に消えます。restrictedの基準とrestricted-v2 SCCは、どちらもUID 0を許可しません。
読み取り専用のルートで、書き込む場所だけを開ける
修正したイメージにreadOnlyRootFilesystem: trueだけを加えたPodro-bareを作成して(ファイルは/root/ocp/ro-bare.yaml)失敗を確認し、同じ設定でemptyDirを/app/data(名前はdata)と/tmp(名前はtmp)にマウントして環境変数HOME=/tmpを与えたPodfixed-roを作成し(ファイルは/root/ocp/fixed-ro.yaml)、Readyにしておいてください。次のフィールドを書いてください(書き込み先: /root/ocp/readonly.json)。bare_error(ro-bareの終了メッセージのうちRead-onlyを含む行をそのまま)、home(fixed-ro内のHOME)、home_writable、etc_writable(fixed-ro内の/etcに書き込めるかどうか)です。
読み取り専用のルートは、イメージがどこに書き込むかを明らかにしてくれます。書き込むパスごとにボリュームを与え、HOMEのようにツールがデフォルトで使う場所も、書き込み可能なパスに向けておきます。ro-bareは削除せず、証拠として残します。
移行前にイメージを点検する一覧
次のフィールドを書いてください(書き込み先: /root/ocp/report.json)。root_cause(ステップ2の失敗の原因: image-dir-owner、missing-capability、selinuxのいずれか)、whoami_ok_here(このk3sで任意のUIDでwhoamiが動くかどうか)、openshift_runtime_adds_passwd_entry(OpenShiftのドキュメントが、CRI-Oが任意のUIDを/etc/passwdに入れてくれると述べているかどうか)、fsgroup_fixes_image_dir、k3s_applies_uid_range(このk3sがrunAsUserのないPodにアノテーションのUIDを埋めるかどうか)、uid_range_default(アノテーションから計算したデフォルトのUID)、running_uids(現在Readyのfixed-aとfixed-bのUIDをソートした配列)です。
k3sがアノテーションを使うかどうかは、runAsUserを除いたPodを--dry-run=server -o jsonで送り、返ってきたspecにrunAsUserが埋められているかどうかで確認できます。採点ツールも、同じ方法と前のステップの記録からもう一度計算します。