コントロールプレーンのハードニング点検
目標
コントロールプレーンコンポーネントの危険な設定を一覧に整理し、etcdの保存時の暗号化設定を自分で書き、
ネームスペースの分離と匿名アクセスの範囲をkubectl auth can-iで測定して確認したうえで、
その根拠をまとめたハードニングレポートを書きます。
なぜ重要なのか
セキュリティ点検が信頼を得る瞬間は、「こうするのがよい」ではなく、「今このクラスターはこうなっている」
を示せるときです。kubectl auth can-i --as=は、その証拠を作る道具です。
RBACマニフェストを読んで頭の中で計算する代わりにapiserverへ直接尋ねれば、
バインディングが重なっていたりグループの継承があったりする複雑な場合でも、正確な答えが返ってきます。
EncryptionConfigurationを自分で書いてみるのは、providers配列の順序がすべてだからです。
identityを先頭に置くと、暗号化を有効にしたつもりで平文のまま保存することになります。
このミスはエラーを出さないため、気づきにくいのです。
このラボ環境では、apiserverプロセスの実際のフラグを変更できません。 そのためステップ2・3・4は、点検の成果物を作成する形になっています。現場でCISベンチマークへの対応が まさにこのような形であることも、あわせて身につけてください。設定を変えるよりも、根拠を文書に残すほうに 時間がかかります。
ステップ
- ディレクトリ
/root/kcsa-hardとネームスペースkcsa-hardを作成します。 /root/kcsa-hard/risky-flags.txtに、本番のapiserverにあってはならない設定5行を、正確にこの形で書きます。--anonymous-auth=true、--authorization-mode=AlwaysAllow、--insecure-port=8080、--profiling=true、--service-account-lookup=falseです。ほかの行は入れません。/root/kcsa-hard/encryption-config.yamlにEncryptionConfigurationを書きます。apiVersionはapiserver.config.k8s.io/v1、対象リソースはsecrets、providersはちょうど2つで、aescbc(キー名はkey1、secretはbase64でエンコードした実際の値)とidentityを正しい順序で配置します。/root/kcsa-hard/kubelet-checklist.txtに、kubeletのハードニング項目4行を正確にこの形で書きます。authentication.anonymous.enabled=false、authentication.webhook.enabled=true、authorization.mode=Webhook、readOnlyPort=0です。ほかの行は入れません。- ネームスペース
kcsa-team-aとkcsa-team-bを作成し、それぞれにServiceAccountappを作成します。kcsa-team-aにRolepod-reader(コアグループのpodsに対するgetとlist)とRoleBindingapp-pod-reader(対象はkcsa-team-aのSAapp)を作成します。そのうえでkubectl auth can-i list podsを--as=system:serviceaccount:kcsa-team-a:appで2つのネームスペースそれぞれに実行し、答えが分かれるかを確認します。 - 匿名ユーザーとして3つのことを尋ね、結果を
/root/kcsa-hard/anon.txtに키=값の3行(プレースホルダーはキーと値です)で保存します。get-pods=(ネームスペースkcsa-hardでのPodのget)、list-secrets=(全ネームスペースでのsecretsのlist)、healthz=(非リソースパス/healthzに対するget)です。匿名の主体は--asによるなりすましで尋ねることができないため(下の参考を参照)、管理者権限でSubjectAccessReviewを作成して.status.allowedを読み、trueはyes、falseはnoに書き換えます。 /root/kcsa-hard/hardening-report.mdを作成します。次の5行がセクション見出しとして正確に入っている必要があります。## apiserver、## etcd、## kubelet、## namespace、## anonymousです。さらに本文に、次の5つの根拠が記載されている必要があります。--anonymous-auth=false、aescbc、readOnlyPort=0、kcsa-team-b、system:anonymousです。
参考
kubectl auth can-i list pods -n kcsa-team-b --as=system:serviceaccount:kcsa-team-a:appのように、--asで別の主体になりすまして尋ねられます。なりすましが許可されるには管理者権限が必要です。- 匿名の主体だけは
--asで尋ねられません。kubectl auth can-iはSelfSubjectAccessReviewを作成しますが、その権限はClusterRolesystem:basic-userにあり、バインディングはsystem:authenticatedグループにしか設定されていません。system:anonymousになりすますと、apiserverがグループをsystem:unauthenticatedに差し替えるため、リクエスト自体がForbiddenで終わります。 - そのためステップ6では
SubjectAccessReviewを使います。これは管理者が「この主体はこれができるか」を代わりに尋ねるオブジェクトなので、specにuserとgroupsを直接書きます。リソースに関する質問ならresourceAttributes、/healthzのような非リソースパスに関する質問ならnonResourceAttributesを入れ、答えは.status.allowedにtrue/falseで返ってきます。 - base64エンコードは、
head -c 32 /dev/urandom | base64のような方法で作れます。 - よくあるミス1: ステップ3で
identityをproviders配列の1番目に置くことです。そうすると、暗号化を有効にしたつもりで平文のまま保存することになります。 - よくあるミス2: ステップ6で標準的な答えを暗記して書くことです。採点はこのクラスターに直接尋ねた結果と照合するため、実際に実行して出た値を書き写す必要があります。
作業スペースとネームスペースを準備する
ディレクトリ/root/kcsa-hardとネームスペースkcsa-hardを作成してください。
成果物をまとめるディレクトリ1つと、ラボ用のネームスペース1つを作成します。ディレクトリには、途中のパスまで一度に作成するオプションがあります。
apiserverの危険なフラグの一覧を書く
/root/kcsa-hard/risky-flags.txtに、本番のapiserverにあってはならない設定5行を、正確にこの形で書いてください。--anonymous-auth=true、--authorization-mode=AlwaysAllow、--insecure-port=8080、--profiling=true、--service-account-lookup=falseです。ほかの行は入れないでください。
各フラグがどの段階(認証/認可/情報の露出/トークン検証)を無効にするのかを考えながら書いてください。ファイルには指示された5行だけがあり、値まで含めた形そのままである必要があります。
EncryptionConfigurationを書く
/root/kcsa-hard/encryption-config.yamlにEncryptionConfigurationを書いてください。apiVersionはapiserver.config.k8s.io/v1、対象リソースはsecrets、providersはちょうど2つで、aescbc(キー名はkey1、secretはbase64でエンコードした実際の値)とidentityを正しい順序で配置します。
providers配列は順序に意味があります。1番目が書き込みに使われ、すべてが読み取りに使われるという事実から、2つのプロバイダーの順序が決まります。secretの値はbase64でエンコードされた実際の文字列である必要があり、プレースホルダーのままだと通りません。
kubeletの認証/認可の点検項目を整理する
/root/kcsa-hard/kubelet-checklist.txtに、kubeletのハードニング項目4行を正確にこの形で書いてください。authentication.anonymous.enabled=false、authentication.webhook.enabled=true、authorization.mode=Webhook、readOnlyPort=0です。ほかの行は入れないでください。
kubeletの設定ファイル(KubeletConfiguration)のフィールドのパスを、ドット記法で書きます。匿名アクセス、Webhook認証、認可モード、読み取り専用ポートの4つです。認可モードにAlwaysAllowを使うと何が意味を失うのかを考えてみてください。
ネームスペースの分離をcan-iで証明する
ネームスペースkcsa-team-aとkcsa-team-bを作成し、それぞれにServiceAccountappを作成してください。kcsa-team-aにRolepod-reader(コアグループのpodsに対するgetとlist)とRoleBindingapp-pod-reader(対象はkcsa-team-aのSAapp)を作成します。そのうえでkubectl auth can-i list podsを--as=system:serviceaccount:kcsa-team-a:appで2つのネームスペースそれぞれに実行し、答えが分かれるかを確認してください。
Roleは自分のネームスペースでしか有効ではありません。その事実を、主張ではなく測定で示す必要があります。同じServiceAccountで2つのネームスペースをそれぞれ尋ねると、答えが分かれます。--asで別の主体になりすまして尋ねられます。
匿名ユーザーに何ができるかを観察する
匿名ユーザーとして3つのことを尋ね、結果を/root/kcsa-hard/anon.txtに키=값の3行(プレースホルダーはキーと値です)で保存してください。get-pods=(ネームスペースkcsa-hardでのPodのget)、list-secrets=(全ネームスペースでのsecretsのlist)、healthz=(非リソースパス/healthzに対するget)です。匿名の主体は--asによるなりすましで尋ねることができないため(下の参考を参照)、管理者権限でSubjectAccessReviewを作成して.status.allowedを読み、trueはyes、falseはnoに書き換えます。
正解を暗記して書くステップではありません。このクラスターに直接尋ねて出た答えをそのまま書き写す必要があります。ただし、匿名の主体はkubectl auth can-i --as=system:anonymousでは尋ねられません。そのコマンドが作るレビューオブジェクトを、匿名ユーザーが作成する権限を持たないからです。管理者が代わりに尋ねるためのレビューオブジェクトが別にあり、リソースではないパスもそれで尋ねられます。
ハードニングレポートにまとめる
/root/kcsa-hard/hardening-report.mdを作成してください。次の5行がセクション見出しとして正確に入っている必要があります。## apiserver、## etcd、## kubelet、## namespace、## anonymousです。さらに本文に、次の5つの根拠を記載してください。--anonymous-auth=false、aescbc、readOnlyPort=0、kcsa-team-b、system:anonymousです。
前の5つのステップの成果物を根拠にした文書を作成します。セクション見出しは指示された形そのままである必要があり、本文には各セクションの結論を裏づける具体的な値を入れる必要があります。