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

Kubernetesディストリビューション — 自分で立てる

OpenShift に移したら権限エラーで落ちた

TT Labで続きを見る

このラボは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)は、ほとんどがポリシーに阻まれるか、イメージのディレクトリには効果がありません。

ステップ

  1. ネームスペース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)です。
  2. 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を含む行をそのまま)です。
  3. レガシーイメージで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に書き込めるかどうか)です。
  4. /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)です。
  5. 修正したイメージで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の形式)です。
  6. レガシーイメージを直さずに耐える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が受け入れられたかどうか)です。
  7. 修正したイメージに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に書き込めるかどうか)です。
  8. 次のフィールドを書いてください(書き込み先: /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をソートした配列)です。

参考

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が埋められているかどうかで確認できます。採点ツールも、同じ方法と前のステップの記録からもう一度計算します。