攻撃面の地図 — どこから入ってくるのか
一言でいうと
Kubernetesのエントリーポイントは、思ったより少ないものです。APIサーバー、kubelet、etcd、コンテナレジストリ、 そしてワークロード自身。残りの大半は、この5つのどれかに至る経路です。
なぜ必要なのか
「クラスターを保護しよう」という言葉が漠然としているのは、守るべきもののリストがないからです。 攻撃者はリストを持って動きます。開いているポートを走査し、匿名アクセスを試し、すでに持っている認証情報で 何ができるかを数えます。防御側も同じリストを持って初めて、釣り合いが取れます。
どう動くのか
5つのエントリーポイント
1) kube-apiserver(6443)
クラスターの唯一の入口であり、最も価値の高い標的です。ここで見るのは3つです。
匿名アクセスが開いているか(--anonymous-auth)、認可モードは何か(AlwaysAllowは致命的です)、
そして誰がどんな認証情報を持っているか。
2) kubelet(10250、10255)
最も忘れられやすい標的です。kubeletは自分のAPIを持っており、そこにはPodの実行・exec・ログがあります。
--anonymous-auth=trueであるか、認可モードがAlwaysAllowなら、ノードにネットワークで到達できる誰もが
そのノードのすべてのコンテナでコマンドを実行できます。読み取り専用ポート(10255)にも認証がないため、
Podスペックや環境変数がそのまま露出します。そのためreadOnlyPort=0がハードニングの標準項目です。
3) etcd(2379/2380) クラスターの状態がすべてここにあります。etcdを読めればすべてのSecretを読めますし、 書き込めればapiserverのあらゆる認可チェックを迂回してクラスターを所有できます。 デフォルト設定ではSecretは事実上平文で保存されるため、etcdのバックアップファイル・スナップショット・ディスクイメージを 手に入れた人も同じものを手に入れます。
4)コンテナレジストリとイメージのサプライチェーン
攻撃者がクラスターに直接接続する必要はなく、こちらが自分でそのコードを取得して実行するよう仕向ける経路です。
タグは動きます。myapp:1.4.2が昨日と今日で別のイメージを指すことがあります。
ダイジェストで固定しなければ、何をデプロイしたのか特定できません。
5)ワークロード自身
すでにクラスター内で動いているPodは、すでに内側にいます。ここから外へ出る経路は複数あります。
マウントされたServiceAccountトークンでのAPI呼び出し、hostPathによるノードのファイルシステムへのアクセス、
privilegedコンテナによるノードの乗っ取り、hostNetworkによるノードのネットワークネームスペースへの侵入です。
見落としやすい攻撃面
- ServiceAccountトークンの自動マウント: デフォルトで有効なので、すべてのPodがAPIサーバーと話す資格を
持っています。不要なら
automountServiceAccountToken: falseで無効にする必要があります。 - 監査ログがない状態: 攻撃を防げないだけでなく、起きたかどうかさえわかりません。 拒否を記録しなければ、権限探索の試みというシグナル自体が存在しません。
- アドミッションWebhook: 防御手段ですが、同時に攻撃面でもあります。Webhookが落ちると、
failurePolicyに応じて デプロイ全体が止まるか(Fail)、ポリシーが丸ごと迂回されます(Ignore)。 - CI/CDの認証情報: パイプラインは通常、クラスターへデプロイする権限を持ちます。そのトークンがそのまま クラスターへのアクセス権限です。
クラウドネイティブ特有の性質2つ
動的です。Podは絶えず終了しては新しく起動し、IPが変わります。IPベースのファイアウォールルールは意味を失い、 そのためラベルやアイデンティティ(SPIFFEなど)に基づくポリシーへ移っていきます。
証拠が揮発します。侵害されたPodはすでに消えているかもしれません。ログと監査記録をクラスターの外へ 出しておかなければ、調査する対象が残りません。
現場での姿
著者のブログがまとめた失敗パターンのうち、KCSAの観点で特に価値のあるものを1つ挙げます。KubernetesのSecretの実体です。
etcdctl get /registry/secrets/default/app-db | hexdump -Cを実行すると、キーのパスが平文のまま見え、
kubectl get secret ... -o jsonpath='{.data.password}' | base64 -dの1行でパスワードが出てきます。
base64にはキーがなく、元に戻すのにコマンド1つで足りるので、暗号化ではありません。
ここにRBACの問題が重なります。あるネームスペースでget secretsの権限があれば、
そのネームスペースのすべてのSecretを読めます。著者の表現では、「開発者に利便のために与えた編集権限が
実質的に本番の認証情報の閲覧権限になっていることが非常によくあります。」
もう1つ、環境変数についての誤解があります。.envファイルを読み込んだあとに消しても意味がありません。
値はすでにカーネルがプロセスごとに保持していて、/proc/PID/environで読めるからです。
同じPodのサイドカー、ノードにアクセスできる人、kubectl debug --target=appで接続したデバッグコンテナが
すべて同じものを見ます。環境変数は保管場所ではなく受け渡しの方式だというのが要点です。
次のクイズで確認すること
このモジュールはクイズで締めくくります。次のモジュールでは、apiserverの認証チェーンからkubeletの認可まで コンポーネントごとに掘り下げ、ラボで危険なフラグの一覧とEncryptionConfigurationを自分で書きます。