なぜ監査は設定ファイルから、ポリシーはデフォルト拒否から始めるのか
一言でいうと
CISベンチマークが/etc/kubernetes/manifestsのテキストファイルから調べ始める理由と、ネットワークポリシーを「デフォルト拒否」から始めなければならない理由は同じです。どちらも、デフォルトが危険な側に開いているからです。
なぜ必要なのか
Kubernetesは、開発者にとって扱いやすいデフォルト値を選びました。Podはどのような別のPodにも話しかけられます。APIサーバーは匿名リクエストをsystem:anonymousというユーザーとして受け付けます。kubeletは認証なしの読み取り専用ポートを開いたままにできました。これらのデフォルト値には「設定しなければ開いている」という共通の性質があります。そのため、セキュリティ点検の最初の問いは「何を塞いだか」ではなく「何を塞いでいないか」になります。
CISベンチマークとkube-benchが設定ファイルの監査から始めるのは、このためです。コントロールプレーンのコンポーネントは、ほとんどが静的Podマニフェスト1枚のcommand配列で動作が決まります。その配列に--anonymous-auth=falseがないという事実1つで、クラスターに到達できる誰もが認証なしでAPIを叩けるということになります。実行中のプロセスを観察するより、テキストを1行読むほうが速く、何より証拠として残ります。
どう動くのか
NetworkPolicyは許可リストです。あるPodに対するポリシーが1つもなければ、そのPodはすべて許可された状態です。ポリシーが1つでもそのPodを選択した瞬間、そのポリシーのpolicyTypesに書かれた方向は「明示されたものだけを許可」に切り替わります。そのため、実務での順序はいつも次のとおりです。
| 順序 | ポリシー | 効果 |
|---|---|---|
| 1 | podSelector: {} + policyTypes: [Ingress, Egress]、ルールなし |
ネームスペース全体を遮断 |
| 2 | DNS egressだけを開放(kube-dnsへのUDP/TCP 53) | 名前解決を復旧 |
| 3 | 必要なPodの組だけingressを開放 | 最小限の接続 |
| 4 | ipBlockのexceptでノードのメタデータを遮断 |
認証情報を盗まれる経路を遮断 |
2つ目を抜かしてしまうのが、最もよくある事故です。デフォルト拒否を適用すると、DNSの問い合わせもegressなので一緒に塞がれ、アプリケーションは「コネクション拒否」ではなく「名前が見つからない」というエラーで落ちます。原因がネットワークポリシーだと気づくまでに時間がかかる理由です。
ipBlock.exceptはCKSで繰り返し出題される箇所です。クラウドのインスタンスメタデータのアドレス169.254.169.254は、認証なしでノードのIAM認証情報を返すことがあります。Podが1つ破られると、そのノードのクラウド権限がそっくり奪われます。egressを0.0.0.0/0で開放する場合でも、exceptでこのアドレスだけを切り取るのが定石です。
現場での姿
筆者の7ノードのホームラボは、Cilium 1.20.1をeBPFモードで動かしており、kube-proxyをそもそもインストールしていません。ここで確認したことが1つあります。同じIPの同じポート80なのに、GETは200、POSTは403と結果が分かれました。サイドカーは注入しておらず、アプリケーションコードもそのままでした。L3/L4だけを見る標準のNetworkPolicyでは不可能なことであり、CNIの選択がそのままセキュリティの表現力になるということです。逆に、Flannelの標準設定のようにNetworkPolicy自体を実装していないCNIの上では、ポリシーオブジェクトをどれだけ丁寧に書いても何も遮断されません。オブジェクトが作成されたという事実と、トラフィックが遮断されたという事実は別のものです。
同じホームラボで、もっと痛かった事故は設定ファイル側でした。DHCPがコントロールプレーンのIPを10.0.0.111から10.0.0.120に変えてしまい、apiserver証明書のSANには新しいアドレスがありませんでした。etcdとkube-apiserverは存在しないアドレスにバインドしようとしてCrashLoopBackOffに陥りましたが、kube-schedulerとcontroller-managerは127.0.0.1にバインドするため、プロセスは問題なく動いていました。この「半分は生きている」状態が、診断を最も難しくします。ファイル1枚の値がクラスター全体の生死を分けることを、身をもって学んだ出来事でした。
試験で実際に手が動くところ
CKSのクラスターセットアップ分野は、「設定ファイルを直してコンポーネントを復活させる」形で出題されます。手が覚えておくべきことは、いくつか決まっています。
静的Podマニフェストを直すと、kubeletが自動的に立ち上げ直します。
/etc/kubernetes/manifests/の下のファイルを変更した瞬間に反映されます。文法を間違えるとそのコンポーネントがまったく起動しなくなるため、修正する前にコピーを残します。
cp /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/
# 고친 뒤
crictl ps | grep apiserver # 새 컨테이너가 떴는지
journalctl -u kubelet -f # 안 뜨면 이유가 여기 남는다
APIサーバーの修正を誤るとkubectl自体が使えなくなるため、確認はcrictlとjournalctlで行います。これを知らないと、試験会場で身動きが取れなくなります。
よく出るフラグは、いくつか暗記しておきます。
| 目的 | フラグ |
|---|---|
| 匿名アクセスの遮断 | --anonymous-auth=false |
| 監査ログ | --audit-policy-file、--audit-log-path、--audit-log-maxage |
| etcdの暗号化 | --encryption-provider-config |
| アドミッション制御 | --enable-admission-plugins=NodeRestriction,... |
監査ポリシーは、ファイルも一緒にマウントする必要があります。フラグだけを入れてvolumes/volumeMountsを追加しないと、APIサーバーがファイルを見つけられず起動しません。静的Podで最もよくあるミスです。
etcdの暗号化は、有効にしたあとで既存のデータを書き直して初めて適用されます。
kubectl get secrets -A -o json | kubectl replace -f -
kubelet側も確認します。/var/lib/kubelet/config.yamlのauthentication.anonymousとauthorization.modeが出題されます。ここを直したら、kubeletを自分で再起動する必要があります(systemctl restart kubelet)。
変更したあとは、必ず確認のコマンドを1行実行します。問題が求めた内容が実際に適用されたかを確認するコマンドまでが答えです。
次のラボですること
cks-netネームスペースにデフォルト拒否ポリシーを適用し、DNSだけを例外として開けるegressルールを重ね、ノードのメタデータへ出る経路をipBlock.exceptで切り取ります。Ingress用のTLS Secretをkubernetes.io/tlsタイプで作成し、最後にCISの推奨を反映したkube-apiserverマニフェストを自分で書いて、どのフラグがあるべきで、どのフラグがあってはならないかを手で確認します。