Secretの参照・Podの作成・実行時の安全性は別の境界
一言でいうと
Secret APIを読む権限、Podを作成する権限、Podの実行のセキュリティ設定は、それぞれ別の境界です。 RestrictedなPodも、同じネームスペースのSecretをボリュームとして読めます。
なぜ必要なのか
例として挙げるシナリオです。デプロイ用のサービスアカウントからget secretsを削除しました。
kubectl auth can-iもnoと答えたため、シークレットにはアクセスできないと報告しました。
ところが、そのアカウントにはcreate podsが残っていました。攻撃者は、Secretを読むAPIの代わりに、
Secretボリュームを持つPodを作成するAPIを選びました。kubeletがボリュームを用意すると、
コンテナは通常のファイル読み取りで値を得ました。直接のアクセスの拒否と、間接的なアクセスの遮断は別物です。
どう動くのか
3つの問い
| 境界 | 尋ねる質問 | このラボで見ること |
|---|---|---|
| 認証 | リクエスト元は誰ですか | 管理者の証明書と、代理でリクエストしたrunner SAを区別する |
| 認可 | この動詞・リソース・範囲を許可していますか | Secretの取得は拒否、appでのPod作成は許可 |
| アドミッション | 許可された作成リクエストのオブジェクトを受け入れますか | Restrictedの実行条件を満たすPodかを検査 |
Pod作成リクエストの主体と、PodのserviceAccountNameは、同じ概念ではありません。
管理者がrunnerを指定したPodを作成したからといって、runnerの作成権限を使ったことにはなりません。
次のラボでは、--as=system:serviceaccount:app:runnerで実際の作成リクエストを送ります。
これは管理者のimpersonate権限を利用した教育用の代理リクエストであり、SAトークンの窃取や偽造ではありません。
Restrictedが防ぐものと残すもの
Pod Security StandardsのRestrictedは、privilege escalationやroot実行など、コンテナの
実行設定を制限します。baselineの段階から、hostPath・hostPID・hostNetwork・privilegedのような
危険な設定を制限します。しかし、Restrictedが許可するボリュームにはsecretとconfigMapが
含まれています。このポリシーは、あるユーザーが特定のSecretを業務上使ってよいかを判断しません。
runAsNonRoot: true、capabilitiesのdrop ALL、allowPrivilegeEscalation: false、
seccompProfile: RuntimeDefaultをすべて入れても、コンテナが正当にマウントされたファイルの
読み取りまでは禁止されません。automountServiceAccountToken: falseも、APIトークンの自動
マウントを無効にするオプションであって、Secretボリュームを防ぐオプションではありません。
そのため、create podsは常にノードのrootになるという説明も間違いです。同じ権限でも、
アドミッションポリシー、選択できるSA、ネームスペースの範囲、ボリュームの種類によって、到達できる範囲が異なります。
pods/execは、入れる既存のコンテナの権限とデータの影響を受けます。
bind・escalate・impersonateも、許可されたリソースと主体の範囲を確認する必要があり、
この動詞が1つあるというだけで、無条件にcluster-adminだと断定してはいけません。
防御は、データとワークロードの境界をあわせて決める
互いに信頼しない作業を、機微なSecretと同じネームスペースに置かないことが
出発点です。ワークロードの作成権限と、選択できるサービスアカウントを最小化し、必要なら
別のアドミッションポリシーで、許可されたSecret参照・SAの選択を制限します。直接のget権限に
resourceNamesを設定しても、Pod作成によって参照されるボリュームまで自動で制限されるわけではありません。
NetworkPolicyはネットワーク接続の統制なので、すでにマウントされたローカルファイルの読み取りは防ぎません。
PSAは、その上でやはり必要です。ノードへ越えていく危険な実行設定を減らす統制と、 ネームスペース内の業務データを誰が使えるかを決める統制を、あわせて適用する必要があります。 どちらか1つを有効にしたというチェックマークで、別の境界の検証を省略しないことが核心です。
保存時の暗号化も別の境界
SecretのAPIのdataの値はbase64の表現であり、暗号化ではありません。保存時の暗号化を構成して
いない場合、保存先のデータは機密性を保証されません。今回のk3s/kine環境では、
SQLite内のシリアライズされたSecretで、合成マーカーを確認します。すべてのディストリビューションが同じファイル・
シリアライズ形式を使うという意味ではなく、マネージドサービスの暗号化のデフォルト値も別に確認する必要があります。
EncryptionConfigurationは、最初のプロバイダーで新しい値を書き、後ろのプロバイダーで以前の値を読みます。 identityを先頭に置くと、新しいデータも暗号化されません。設定を有効にしたり、キーを変更したりしても、 既存のオブジェクトが自動で保存し直されるわけではありません。書き直し・検証・古い読み取り経路の削除が必要です。 正常に認可されたAPIリクエストは復号された値を受け取るため、保存時の暗号化が、上のマウント経路を 遮断するわけではありません。キーとバックアップをあわせて露出すると、暗号化の効果も失われかねません。
現場での姿
点検の順序の例は、次のとおりです。まず特定のSAについて、ネームスペースを指定して
can-i get secretsとcan-i create podsをそれぞれ尋ねます。次に、どんなPodを作成できるか、
アドミッションポリシーとSAの選択範囲を見ます。最後に、作成されたワークロードがどんな
データをマウントするかを確認します。can-i --listは出発点であって、外部の認可・アドミッションの
結果をすべて完全に説明する証拠ではありません。実際のリクエストと失敗の原因をあわせて見る必要があります。
サービスアカウントの自動トークンマウントを無効にしても、Podに明示した設定が優先されることがあります。 このオプションを変更しても、すでに実行中のすべてのPodがただちに変わるわけではないので、影響を確認して 新しいPodで適用します。トークンが不要かどうかと、Secretが不要かどうかは、別々に判断します。
401と403も、区別して読みます。匿名認証が許可されたパスで資格のないリクエストは、
system:anonymousユーザーとsystem:unauthenticatedグループとして処理されることがあります。
したがって、匿名のリクエストが常に401になるわけではありません。パスごとの匿名の許可、無効な認証情報、
認可の結果によって変わります。403だけを見て、どのアドミッション統制まで通過したのかを断定せず、
レスポンスの理由と監査記録をあわせて確認します。
次のラボですること
合成Secretで保存時の暗号化と書き直しを確認したあと、Restrictedが適用されたappで、 runnerが作成した非rootのPodのSecretファイルのハッシュを比較します。実際のシークレットの値は出力しません。 APIでの直接の取得の拒否、Pod作成の成功、Secretボリュームの読み取りの成功を、別々の証拠として記録します。 そのあと、認証の境界と、本文のない監査ログを確認します。このVMは教育用のシングルノードであり、 この結果を、ほかのテナントや本番クラスター全体のセキュリティの保証に拡大解釈しません。