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

CKS — Kubernetesセキュリティスペシャリスト

CKS模擬試験B

TT Labで続きを見る

目標

B回です。A回と重なる問題は1つもありません。同じドメインを同じ割合で問いますが、確認する能力はすべて異なります。Aを解いたあとに続けて受けると、どのドメインがまだ弱いかが明らかになります。

実際のCKSと同じ条件で、課題17個を120分以内に解きます。合格ラインは67%で、部分点制なので、17個のうち12個に合格すれば完了として扱われます。

模擬試験です。ヒントと正解を見ずに、まず最後まで解いてみてください。 行き詰まった課題は印を付けて飛ばし、残った時間に戻ってくるほうがよいです。 採点はいつ押してもよく、何度押しても結果は変わりません。

なぜ重要なのか

CKSは、CKAへの合格が前提条件になっている唯一の試験です。Kubernetesを扱う手はすでに身に付いているものとされているため、問題は「何かを作りなさい」よりも、「この設定の何が危険かを見つけて直しなさい」に近いものです。防御と診断が試験の対象で、攻撃の手法は扱いません。

この回は、特に境界を絞る方法を問います。権限を名前1つに絞り、トークンに対象と寿命を付け、ルールは1回だけ書いて、効力は1つのネームスペースに限定します。広く開けておいてあとで絞るということは、実務ではほとんど起きないため、最初から絞って開ける手こそが実力です。

試験環境(実際の試験で確認された事実)

この模擬試験環境での違い

このラボのクラスターは、Podの中で動く1人用のクラスターです。kube-apiserverは本物なので、マニフェスト、RBAC、Pod Security Admission、アドミッションポリシーは、実際に動作し、実際に拒否します。ただし、コンテナを実際に動かすランタイムがないため、次の3つは確認できません。

残りはすべて、稼働中のクラスターで再計算して採点します。権限はkubectl auth can-iで、アドミッションポリシーとPod Security Admissionはサーバーのdry-runで、その時点の判定を改めて問い合わせます。

ステップ

Cluster Setup

  1. ネームスペースstagingを作成し、その中でapp=webではなくapp=apiラベルが付いたPodに適用されるNetworkPolicyapi-allowを作成してください。ingressは、env=frontendラベルが付いたネームスペースの中の、role=webラベルが付いたPodから来るTCP 8080だけを許可し、残りのingressはすべて塞ぎます。egressはUDP 53とTCP 53だけを許可し、残りはすべて塞ぎます。
  2. /usr/local/bin/kubectlと/usr/local/bin/helmのSHA-256ハッシュを、sha256sumの出力形式のまま/root/exam/verify/binaries.sha256に保存してください。パスは絶対パスである必要があり、sha256sum -cで検証したときに、2つの項目とも通過する必要があります。
  3. /root/exam/kubelet/config.yamlにkubeletの構成を書いてください。apiVersionはkubelet.config.k8s.io/v1beta1、kindはKubeletConfigurationです。匿名アクセスをオフにし、Webhook認証をオンにし、認可方式をWebhookにし、認証なしの読み取り専用ポートを閉じ、カーネルのデフォルト値の保護をオンにしてください。

Cluster Hardening

  1. ネームスペースstagingで、ユーザーci-deployerがDeploymentwebの1つだけをget、patch、updateできるように、Roleweb-deployerとRoleBindingci-deployer-webを作成してください。同じネームスペースのほかのDeploymentには触れられず、Deploymentの一覧を見ることもできない必要があります。
  2. ネームスペースstagingにServiceAccountagentを作成し、Podcollectorを作成してください。PodはServiceAccountagentを使い、デフォルトのトークンの自動マウントはオフにして、代わりにprojectedボリュームtokenで、対象がvaultで有効期間が3600秒のServiceAccountトークンを、ファイル名tokenで発行してもらい、/var/run/secrets/vaultにマウントしてください。コンテナ名はappです。
  3. ClusterRolenamespace-readerを作成して、コアグループのpods、services、configmapsをget、list、watchできるようにしてください。そして、ユーザーoncallがネームスペースstagingの中でだけその権限を持つように、RoleBindingoncall-readerを作成してください。ほかのネームスペースでは何も読み取れず、stagingの中でもSecretは読み取れない必要があります。

System Hardening

  1. /root/exam/seccomp/net-deny.jsonにseccompプロファイルを書いてください。デフォルトの動作はSCMP_ACT_LOGで、socketとconnectのシステムコールはSCMP_ACT_ERRNOで塞ぐ必要があります。そして、ネームスペースstagingにPodsensorを作成しますが、PodレベルにはRuntimeDefaultプロファイルを置き、コンテナappでだけ、このプロファイルをLocalhost方式で、profiles/net-deny.jsonのパスから使うようにしてください。
  2. /root/exam/sudoers.d/deployにsudoルールを書いてください。ユーザーdeployが、パスワードを聞かれずに、/usr/bin/systemctl restart kubeletの1つだけをrootとして実行できる必要があります。コマンドの位置にALLを書いたルールは置かないでください。ファイルは、visudo -cfの文法チェックを通過する必要があります。

Minimize Microservice Vulnerabilities

  1. ネームスペースpaymentsを作成し、Pod Security Admissionを段階的に設定してください。enforceはbaselineで、バージョンはv1.31に固定します。auditとwarnはrestrictedで、バージョンはlatestです。結果として、baselineに違反するPodは実際に拒否され、restrictedにだけ違反するPodは、警告を受けながら作成される必要があります。
  2. ネームスペースpaymentsにSecretdb-credを作成してください。タイプはデフォルト(Opaque)で、キーはusernameとpasswordです。そして、Podreporterを作成して、このSecretをボリュームcredとして/etc/dbに読み取り専用でマウントしますが、ファイルの権限は8進数の0400である必要があります。Secretの値を環境変数としては入れないでください。コンテナ名はappです。
  3. /root/exam/encryption/config.yamlに、保存時の暗号化の構成を書いてください。apiVersionはapiserver.config.k8s.io/v1、kindはEncryptionConfigurationです。暗号化の対象はsecretsで、プロバイダーはaescbcが1つ目、identityが最後である必要があります。aescbcのキーは名前を持ち、値は32バイトをbase64で書いたものである必要があります。

Supply Chain Security

  1. ネームスペースsupplyを作成し、レジストリregistry.internalにユーザーdeployerでアクセスするための認証情報を、Secretregcredとして作成してください。ServiceAccountpullerがそのSecretでイメージをダウンロードするように連携させ、DeploymentcatalogがそのServiceAccountを使うようにしてください。コンテナ名はappで、イメージはregistry.internal/catalog:1.4.2です。
  2. ネームスペースsupplyで、動くタグを止めてください。ValidatingAdmissionPolicyno-latest-tagとValidatingAdmissionPolicyBindingno-latest-tagを作成し、Podのすべてのコンテナとすべてのinitコンテナのイメージが、:latestで終わっていないことを要求するようにしてください。バインディングのvalidationActionsはDenyで、適用範囲はネームスペースsupplyだけです。ほかのネームスペースは影響を受けてはいけません。
  3. /root/exam/imagepolicy/admission-config.yamlにアドミッション構成ファイルを書いてください。apiVersionはapiserver.config.k8s.io/v1、kindはAdmissionConfigurationで、プラグインはImagePolicyWebhookの1つです。その設定のimagePolicyに、kubeConfigFileとして/etc/kubernetes/imagepolicy/kubeconfig.yamlを書き、allowTTLは50、denyTTLは50、retryBackoffは500とし、Webhookに到達できなかったときにイメージが許可されないように、defaultAllowをfalseにしてください。

Monitoring, Logging and Runtime Security

  1. /root/exam/falco/rules.yamlにランタイム検知ルールを書いてください。ファイルは項目のリストで、その中に、trusted_writersというlist項目と、Write below etcというrule項目が1つずつ必要です。リストには、項目が最低1つ必要です。ルールのpriorityはWARNINGで、descとtagsが空であってはならず、conditionにはopen_write、/etc、trusted_writersがすべて現れ、outputには%proc.nameと%fd.nameが必要です。
  2. /root/exam/audit/quiet-policy.yamlに監査ポリシーを書いてください。apiVersionはaudit.k8s.io/v1、kindはPolicyで、ルールは正確に4つで、順序が重要です。1つ目は、ユーザーsystem:kube-proxyのwatchが、コアグループのendpointsとservicesに対して行われるものをNoneで捨てます。2つ目は、グループsystem:authenticatedの非リソースパス/api*と/versionへのアクセスをNoneで捨てます。3つ目は、ネームスペースpaymentsのコアグループsecretsをRequestResponseで残します。4つ目は、対象を書かないMetadataルールで、残りのすべてを受けます。
  3. ネームスペースruntimeを作成し、その中で作成されるPodのすべてのコンテナが、readOnlyRootFilesystem: trueを持つように強制してください。ValidatingAdmissionPolicyimmutable-rootとValidatingAdmissionPolicyBindingimmutable-rootを作成し、バインディングのvalidationActionsはDenyで、適用範囲はネームスペースruntimeだけです。ほかのネームスペースは影響を受けてはいけません。

参考

2つのセレクターを併せ持つ許可リスト

ネームスペースstagingを作成し、app=apiラベルが付いたPodに適用されるNetworkPolicyapi-allowを作成してください。ingressは、env=frontendラベルが付いたネームスペースの中の、role=webラベルが付いたPodから来るTCP 8080だけを許可し、残りのingressはすべて塞ぎます。egressはUDP 53とTCP 53だけを許可し、残りはすべて塞ぎます。

from項目1つの中にnamespaceSelectorとpodSelectorを併せて書くと、2つの条件をどちらも満たすPodだけが選ばれます。2つの項目に分けて書くと、どちらか一方を満たすだけで開くため、範囲が広がります。policyTypesにIngressとEgressの両方を書かないと、egressも制御されません。egressルールにportsだけを書いてtoを書かないと、宛先は制限せずにポートだけを開ける意味になります。

デプロイ前のバイナリの検証

/usr/local/bin/kubectlと/usr/local/bin/helmのSHA-256ハッシュを、sha256sumの出力形式のまま/root/exam/verify/binaries.sha256に保存してください。パスは絶対パスである必要があり、sha256sum -cで検証したときに、2つの項目とも通過する必要があります。

sha256sumの出力が、そのままsha256sum -cの入力形式です。ファイルを複数まとめて渡すと、行が複数出ます。手でハッシュを書き写すと、1文字違うだけで検証に失敗するため、出力をリダイレクトでそのまま保存してください。相対パスで書くと、検証するときに作業ディレクトリによってファイルが見つかりません。

kubelet構成の強化

/root/exam/kubelet/config.yamlにkubeletの構成を書いてください。apiVersionはkubelet.config.k8s.io/v1beta1、kindはKubeletConfigurationです。匿名アクセスをオフにし、Webhook認証をオンにし、認可方式をWebhookにし、認証なしの読み取り専用ポートを閉じ、カーネルのデフォルト値の保護をオンにしてください。

匿名アクセスとWebhook認証は、authenticationの下に別々にあり、認可方式はauthorization.modeです。認可をAlwaysAllowにすると、認証をどれほど厳しくしても、誰でも何でもできます。認証なしの読み取りポートはreadOnlyPortで、閉じる値は0です。カーネルのデフォルト値の保護はprotectKernelDefaultsで、kubeletがカーネルパラメーターを勝手に変更できないようにします。

名前1つに絞った権限

ネームスペースstagingで、ユーザーci-deployerがDeploymentwebの1つだけをget、patch、updateできるように、Roleweb-deployerとRoleBindingci-deployer-webを作成してください。同じネームスペースのほかのDeploymentには触れられず、Deploymentの一覧を見ることもできない必要があります。

ルールにresourceNamesを書くと、その名前のオブジェクトにだけルールが適用されます。一覧の取得は名前のないリクエストなので、resourceNamesで絞ったルールでは許可されません。Deploymentはコアグループではなく、appsグループにあります。サブジェクトの種類はUserで、apiGroupはrbac.authorization.k8s.ioです。確認は、リソースの後ろに名前を付けて、kubectl auth can-i patch deployments/webのように問い合わせます。

対象と寿命が決まったトークン

ネームスペースstagingにServiceAccountagentを作成し、Podcollectorを作成してください。PodはServiceAccountagentを使い、デフォルトのトークンの自動マウントはオフにして、代わりにprojectedボリュームtokenで、対象がvaultで有効期間が3600秒のServiceAccountトークンを、ファイル名tokenで発行してもらい、/var/run/secrets/vaultにマウントしてください。コンテナ名はappです。

projectedボリュームのsourcesにserviceAccountTokenを置くと、kubeletが短い寿命のトークンを発行して入れます。audienceを書くと、その対象にだけ使えるトークンになり、expirationSecondsで寿命を決めます。ボリューム内のファイル名は、pathで決めます。デフォルトのトークンをオフにするのはPodスペックのautomountServiceAccountTokenで、これをオフにしても、明示的に作成したprojectedトークンはそのまま入ります。

ルールは1回、効力は1つのネームスペース

ClusterRolenamespace-readerを作成して、コアグループのpods、services、configmapsをget、list、watchできるようにしてください。そして、ユーザーoncallがネームスペースstagingの中でだけその権限を持つように、RoleBindingoncall-readerを作成してください。ほかのネームスペースでは何も読み取れない必要があります。

RoleBindingのroleRefは、RoleだけでなくClusterRoleも指すことができ、そのとき、ルールの効力は、そのRoleBindingが置かれたネームスペースの中に限定されます。同じClusterRoleをClusterRoleBindingで結び付けると、クラスター全体に広がるため、ほかのネームスペースでnoが出ることを確認しないと、2つを区別できません。サブジェクトの種類はUserで、apiGroupはrbac.authorization.k8s.ioです。

コンテナレベルのseccompによる上書き

/root/exam/seccomp/net-deny.jsonにseccompプロファイルを書いてください。デフォルトの動作はSCMP_ACT_LOGで、socketとconnectのシステムコールはSCMP_ACT_ERRNOで塞ぐ必要があります。そして、ネームスペースstagingにPodsensorを作成しますが、PodレベルにはRuntimeDefaultプロファイルを置き、コンテナappでだけ、このプロファイルをLocalhost方式で、profiles/net-deny.jsonのパスから使うようにしてください。

seccompプロファイルは、defaultActionで既定の判定を決め、syscallsの配列で例外を書きます。既定が記録(SCMP_ACT_LOG)なら、塞ぐものだけを選んでSCMP_ACT_ERRNOで書けば構いません。securityContextはPodにもコンテナにもあり、コンテナ側が優先されます。Localhost方式のlocalhostProfileは、ノードのseccompルートを基準にした相対パスです。

ホストアカウントの権限の最小化

/root/exam/sudoers.d/deployにsudoルールを書いてください。ユーザーdeployが、パスワードを聞かれずに、/usr/bin/systemctl restart kubeletの1つだけをrootとして実行できる必要があります。コマンドの位置にALLを書いたルールは置かないでください。ファイルは、visudo -cfの文法チェックを通過する必要があります。

ルール1行の形は、「ユーザー」「ホスト=(実行アカウント)」「タグ: コマンド」の順に並べたものです。パスワードを聞かないようにするタグはNOPASSWD:で、コマンドの位置にALLを書くと、すべてのコマンドが開きます。コマンドは絶対パスで書き、引数まで書くと、その引数だけに制限されます。文法は、visudo -cfにファイル名を渡して確認します。

段階的に引き上げるPod Security Admission

ネームスペースpaymentsを作成し、Pod Security Admissionを段階的に設定してください。enforceはbaselineで、バージョンはv1.31に固定します。auditとwarnはrestrictedで、バージョンはlatestです。結果として、baselineに違反するPodは実際に拒否され、restrictedにだけ違反するPodは、警告を受けながら作成される必要があります。

Pod Security Admissionは、ネームスペースのラベルで有効にし、モードごとにレベルを別々に決められます。止めるのはenforceだけで、auditとwarnは記録と警告を残すだけです。そのため、今後引き上げるレベルをauditとwarnに先に設定しておき、どれだけ引っかかるかを見てからenforceを上げるのが、実務の順序です。バージョンは、同じ接頭辞に-versionを付けたラベルです。ラベルは6つになります。

Secretはファイルとしてのみ

ネームスペースpaymentsにSecretdb-credを作成してください。タイプはデフォルト(Opaque)で、キーはusernameとpasswordです。そして、Podreporterを作成して、このSecretをボリュームcredとして/etc/dbに読み取り専用でマウントしますが、ファイルの権限は8進数の0400である必要があります。Secretの値を環境変数としては入れないでください。コンテナ名はappです。

環境変数として入れたSecretは、プロセスの一覧やクラッシュダンプ、そしてkubectl describeにも一緒に出てきます。ファイルとして入れれば、そのパスを読めるプロセスだけが見られます。ボリュームのファイルの権限はdefaultModeで決め、YAMLで0から始まる数は8進数として読まれます。ボリュームマウントにも、readOnlyが別にあります。

保存時の暗号化構成

/root/exam/encryption/config.yamlに、保存時の暗号化の構成を書いてください。apiVersionはapiserver.config.k8s.io/v1、kindはEncryptionConfigurationです。暗号化の対象はsecretsで、プロバイダーはaescbcが1つ目で、identityが最後である必要があります。aescbcのキーは名前を持ち、値は32バイトをbase64で書いたものである必要があります。

プロバイダーのリストは、順序がそのままポリシーです。書き込みには最初のプロバイダーを使い、読み取りには上から順番に試します。そのため、identityを前に置くと、ファイルは問題なく見えるのにSecretは平文で保存され、identityをそもそも外すと、すでに平文で保存されていたものを読めません。aescbcのキーの長さは正確に32バイトである必要があり、head -c 32 /dev/urandom | base64で作成します。

プライベートレジストリの認証情報

ネームスペースsupplyを作成し、レジストリregistry.internalにユーザーdeployerでアクセスするための認証情報を、Secretregcredとして作成してください。ServiceAccountpullerがそのSecretでイメージをダウンロードするように連携させ、DeploymentcatalogがそのServiceAccountを使うようにしてください。コンテナ名はappで、イメージはregistry.internal/catalog:1.4.2です。

kubectl create secret docker-registryは、--docker-server、--docker-username、--docker-passwordを受け取って、kubernetes.io/dockerconfigjsonタイプのSecretを作成します。一般的なSecretとして作成すると、タイプがOpaqueになるため、kubeletが認証情報として認識しません。ServiceAccountにはimagePullSecretsフィールドがあり、PodはserviceAccountNameでそのアカウントを使います。Podごとに書く代わりにアカウントに1回付けておけば、そのアカウントを使うすべてのPodに引き継がれます。

動くタグを止める

ネームスペースsupplyで、動くタグを止めてください。ValidatingAdmissionPolicyno-latest-tagとValidatingAdmissionPolicyBindingno-latest-tagを作成し、Podのすべてのコンテナとすべてのinitコンテナのイメージが、:latestで終わっていないことを要求するようにしてください。バインディングのvalidationActionsはDenyで、適用範囲はネームスペースsupplyだけです。ほかのネームスペースは影響を受けてはいけません。

CELのall()は、リストのすべての項目が条件を満たすかを調べます。ところが、object.spec.initContainersは、initコンテナがないPodではそもそも存在しないフィールドなので、そのまま参照すると評価が失敗します。has()で囲んで、ないときも一緒に扱ってください。実際に止めるには、バインディングのvalidationActionsにDenyが必要で、Warnだけだと、警告が出るだけでPodは作成されます。範囲は、バインディングのmatchResourcesで絞ります。

イメージポリシーWebhookの構成

/root/exam/imagepolicy/admission-config.yamlにアドミッション構成ファイルを書いてください。apiVersionはapiserver.config.k8s.io/v1、kindはAdmissionConfigurationで、プラグインはImagePolicyWebhookの1つです。その設定のimagePolicyに、kubeConfigFileとして/etc/kubernetes/imagepolicy/kubeconfig.yamlを書き、allowTTLは50、denyTTLは50、retryBackoffは500とし、Webhookに到達できなかったときにイメージが許可されないように、defaultAllowをfalseにしてください。

defaultAllowは、Webhookに到達できなかったときの判定です。trueにすると、Webhookが落ちた瞬間にすべてのイメージが通過してしまい、ポリシーの意味が逆転します。デフォルト値に頼らず、明示してください。プラグインの設定は、pluginsの配列の項目のconfigurationにインラインで書け、その中のキー名はimagePolicyです。

ランタイム検知ルール

/root/exam/falco/rules.yamlにランタイム検知ルールを書いてください。ファイルは項目のリストで、その中に、trusted_writersというlist項目と、Write below etcというrule項目が1つずつ必要です。リストには、項目が最低1つ必要です。ルールのpriorityはWARNINGで、descとtagsが空であってはならず、conditionにはopen_write、/etc、trusted_writersがすべて現れ、outputには%proc.nameと%fd.nameが必要です。

Falcoのルールファイルは項目のリストで、項目の種類は最初のキーで決まります。listは名前とitemsを持ち、ruleはdesc、condition、output、priority、tagsを持ちます。条件の中でリスト名をかっこで囲むと、そのリストの項目のどれかであるかを問う意味になります。outputの%で始まるものは、イベントから値を取り出す場所で、何がどのファイルに書き込んだかがそこに残っていないと、あとで調査できません。

監査ログからノイズを取り除く

/root/exam/audit/quiet-policy.yamlに監査ポリシーを書いてください。apiVersionはaudit.k8s.io/v1、kindはPolicyで、ルールは正確に4つで、順序が重要です。1つ目は、ユーザーsystem:kube-proxyのwatchが、コアグループのendpointsとservicesに対して行われるものをNoneで捨てます。2つ目は、グループsystem:authenticatedの非リソースパス/api*と/versionへのアクセスをNoneで捨てます。3つ目は、ネームスペースpaymentsのコアグループsecretsをRequestResponseで残します。4つ目は、対象を書かないMetadataルールで、残りのすべてを受けます。

監査ポリシーは、上から走査して、最初に一致したルールで止まります。そのため、捨てるものを前に置かないと、後ろの包括ルールがそれまですべて記録します。ルールは、リソースだけでなく、users、userGroups、verbs、namespaces、nonResourceURLsでも対象を絞れます。levelがNoneなら、そのリクエストは記録しません。非リソースパスは、リソースと一緒には書かず、別に書きます。

読み取り専用のルートをアドミッションの段階で強制する

ネームスペースruntimeを作成し、その中で作成されるPodのすべてのコンテナが、readOnlyRootFilesystem: trueを持つように強制してください。ValidatingAdmissionPolicyimmutable-rootとValidatingAdmissionPolicyBindingimmutable-rootを作成し、バインディングのvalidationActionsはDenyで、適用範囲はネームスペースruntimeだけです。ほかのネームスペースは影響を受けてはいけません。

1つのワークロードを読み取り専用にすることと、今後入ってくるすべてのPodをそうすることは、別のことです。後者はアドミッションの段階で行う必要があります。securityContextは存在しないことがあり、その中のフィールドも存在しないことがあるため、CELでhas()によって2つの段階をどちらも確認しないと、何も書いていないPodが評価エラーですり抜けたり、まるごと塞がれたりします。適用範囲は、バインディングのmatchResourcesで絞り、ネームスペースを1つだけ選ぶときは、すべてのネームスペースに自動で付くkubernetes.io/metadata.nameラベルを使えば構いません。