回線上の暗号化、時間の上の脆弱性、ノードの上の攻撃面
一言でいうと
Pod間のトラフィックを暗号化する方法は2つあります。Ciliumの透過的暗号化はノードのカーネルでWireGuardやIPsecを使って包み込み、IstioのmTLSはPod内のサイドカーで終端されます。サポートが終わったバージョンを動かすことは、CVEの修正を受け取らないという意味になるため、アップグレードそのものがセキュリティ項目であり、ノードのOSはkubelet・ランタイム・CNIを動かすのに必要なものだけを残すのが原則です。
なぜ必要なのか
NetworkPolicyが決めるのは、誰が誰に接続できるかだけです。接続が許可されたあとで回線上を流れるバイト列は平文です。同じノードの中なら、ノードですべて見えるので暗号化は無意味ですが、ノードとノードの間は、物理スイッチ、クラウドの仮想ネットワーク、ほかのテナントの機器を通ります。セキュリティチェックリストは、「すべてのCNIプラグインが転送中の暗号化を提供するわけではなく、選んだプラグインにその機能がなければ、サービスメッシュが代替手段になる」と書いています。そのため、CKSはCiliumとIstioの2つの道を問います。
アップグレードがセキュリティ項目である理由は、時間にあります。脆弱性は、修正されたバージョンでしか防げず、プロジェクトは直近3つのminorにしかパッチを提供しません。その範囲を外れたクラスターは、CVEが公開されても受け取れる修正版がありません。ノードのOSも同じ原理です。インストールされているサービス・パッケージ・カーネルモジュールの1つ1つが、攻撃者に叩かれうる扉であり、使わない扉をなくすのが最も安上がりな防御です。
どう動くのか
Cilium透過的暗号化: ノードのカーネルで
Cilium透過的暗号化のドキュメントによると、CiliumはCiliumが管理するエンドポイント間のトラフィックを、IPsecまたはWireGuardで(およびベータ版としてztunnelで)暗号化します。アプリケーションは何も知りません。暗号化と復号がノードのカーネルのデータパスで行われるからです。
WireGuardを有効にすると、各ノードのエージェントが自分の鍵ペアを作成し、公開鍵をCiliumNodeオブジェクトのnetwork.cilium.io/wg-pub-keyアノテーションで知らせます。ほかのノードは、その公開鍵を使って、そのノードとのトンネルを確立します。トンネルのエンドポイントはUDP 51871なので、ファイアウォールがある環境ではノード間でこのポートを開ける必要があり、トンネルルーティングモードでは、VXLANやGeneveで一度包んだパケットを、WireGuardがもう一度包みます。Helmの値はencryption.enabled=true、encryption.type=wireguardで、ノード間・ノードとPodの間のトラフィックまで含めるには、encryption.nodeEncryption=trueを追加します。カーネルがWireGuardをサポートしている必要があります(Linux 5.6以降は組み込み)。
IPsecは、鍵をKubernetes Secretとして配布し、ノード間でESPトラフィックが流れるため、セキュリティグループやファイアウォールでESPを開ける必要があります。Cilium 1.18からは、トンネルモードでオーバーレイのカプセル化のあとにIPsecを適用するため、ポリシー用のセキュリティ識別子まで回線上で暗号化されます。ドキュメントが明記している制限もあります。他のCNIの上にチェイニングした構成ではサポートされず、IPsecの復号はトンネルあたりCPUコア1つに制限されます。
2つの方式に共通する性質が2つあります。1つ目は、同じノードに向かうパケットは暗号化しないことです。ノードで原文を見られるので意味がない、という設計です。2つ目は、暗号化するかどうかが、ポリシーの強制と同じ方式で「宛先がリモートのCiliumエンドポイントか」を判断して決まるため、新しいエンドポイントの情報がまだ広まっていない短い間は、最初のパケットが平文で出ていくことがあります。ドキュメントは対策として、reserved:worldへのegressを塞ぐポリシーや、encryption.strictModeを挙げています。
Istio mTLS: Pod内のサイドカーで
Istioのセキュリティの概念のドキュメントの流れはこうです。クライアントのリクエストはPod内のサイドカーEnvoyに振り向けられ、クライアントのEnvoyがサーバーのEnvoyとmTLSハンドシェイクを行いながら、secure namingの検査で、サーバー証明書のサービスアカウントがそのサービスを運用する権限を持っているかを確認します。接続が確立すると、サーバーのEnvoyがリクエストを認可し、通過すればローカルのTCPでバックエンドに渡します。最小のTLSバージョンは1.2です。つまり、暗号化は両側のサイドカーの間でのみ有効で、アプリケーションコンテナと自分のサイドカーの間は、Pod内の平文のloopbackです。
デフォルトのモードはPERMISSIVEで、平文とmTLSの両方を受け付けます。サイドカーのないクライアントを段階的に移すための設計です。すべて移したあとで、PeerAuthenticationをSTRICTに変更して、平文を拒否します。
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: foo
spec:
mtls:
mode: STRICT
メッシュ全体に適用するにはルートネームスペースに置き、ワークロードごとにはselectorを、ポートごとの例外にはportLevelMtlsを使います。mTLS移行のタスクドキュメントの結果が、この違いを示しています。STRICTを設定したあと、サイドカーのないlegacyネームスペースから来たcurlだけが失敗します。アンビエントモードでは、サイドカーの代わりにノードのztunnelがHBONEプロトコルでmTLSを担い、そのため、DISABLEモードはサポートされません。
Istioのセキュリティのベストプラクティスは、mTLSの限界をはっきり書いています。mTLSは認証であって認可ではないため、有効な証明書を持つ誰もがサービスに到達でき、閉じるにはAuthorizationPolicyをdefault-denyのパターンで置く必要があります。サイドカーはTCPだけを横取りし、UDP・ICMPは素通りし、ポート22のようないくつかのポートはinboundのキャプチャから外れ、アプリケーションとサイドカーが同じネットワーク・プロセスのネームスペースにあるため、アプリケーションがリダイレクションのルールを削除してサイドカーを迂回できます。そのため、ドキュメントはNetworkPolicyを併用する多層防御を勧めています。
| 比較 | Cilium透過的暗号化 | Istio mTLS |
|---|---|---|
| 終端点 | ノードのカーネル(WireGuard/IPsec) | Pod内のサイドカーEnvoy(アンビエントはノードのztunnel) |
| アプリケーションの変更 | なし | なし(サイドカーの注入が必要) |
| アイデンティティ | ノードの鍵(ノード単位) | ワークロードのServiceAccount証明書(ワークロード単位) |
| 認可との結合 | NetworkPolicyとは別 | AuthorizationPolicyでワークロード・リクエスト単位の認可が可能 |
| 範囲 | Cilium管理のエンドポイント間のすべてのL3トラフィック | サイドカーが横取りしたTCPトラフィック |
アップグレードがセキュリティ項目である理由
パッチリリースのドキュメントによると、パッチは通常、毎月出て、各minorは約14か月間サポートされます。12か月の標準期間のあと、2か月のメンテナンスモードでは、CVEが割り当てられた脆弱性・依存関係・コアコンポーネントの問題だけが修正されます。バージョンスキューポリシーは、直近3つのminorブランチにだけセキュリティ修正がバックポートされると書いています。サポート期間を外れたクラスターは、脆弱性が公開されても受け取れる修正版がなく、その状態で残っていること自体が欠陥です。クラスターのセキュリティのドキュメントは、セキュリティ告知を受け取るためにkubernetes-announceグループに参加するよう述べており、kubeadmの証明書のドキュメントは、最新のパッチにすぐ上げ、サポートされているminorを維持することが安全を守る方法だと書いています。
スキューポリシーがアップグレードの順序を決めることも、セキュリティにつながります。minorを飛ばせないため、2バージョン遅れたクラスターは2回のアップグレードを経る必要があり、先送りするほど、追いつくコストが大きくなります。アップグレードを定期的な作業にしておく必要がある理由です。
ホストOSの最小化
KubernetesのドキュメントがノードのOSについて直接述べているのは、境界に関することです。クラスターのセキュリティのドキュメントは、kubeletがデフォルトで認証なしのアクセスを許可するため、本番クラスターではkubeletの認証・認可を有効にし、etcdはAPIサーバーだけが到達できるようにファイアウォールの内側に隔離し、クラウドのメタデータAPIへのPodのアクセスを制限し、使わないアルファ・ベータ機能は無効にし、認証情報は短い寿命で頻繁にローテーションするよう述べています。セキュリティチェックリストは、APIサーバー・kubeletのAPI・etcdをインターネットに公開せず、機密度の異なるワークロードはノードを分けて配置するよう書いています。
その上に重ねるOSハードニングの原則は、kubelet・コンテナランタイム・CNIを動かすのに必要ないものは置かないことです。不要なサービスとパッケージは削除します。動いていないデーモンは、脆弱性があっても攻撃面にはならず、インストールされていないパッケージはパッチを当てる必要もありません。SSHは、OpenSSHのsshd_configのマニュアルによると、PermitRootLoginのデフォルト値がprohibit-passwordで、PasswordAuthenticationのデフォルト値はyesなので、ノードではパスワード認証を無効にして鍵認証だけを残すのが通常の設定です。カーネルモジュールは、カーネルのsysctlのドキュメントのkernel.modules_disabledでロードを防げますが、一度1にするとモジュールを入れることも外すこともできず、元に戻すこともできないため、必要なモジュール(ランタイム・CNIが使うもの)をすべてロードしてからでなければ有効にしません。この節のSSHとカーネルモジュールの項目は、Kubernetesのドキュメントではなく、各ツールの公式マニュアルを根拠にしたものであり、どのパッケージを削除するかはディストリビューションとCNIによって異なるため、ここでは一覧を定めません。
現場での姿
STRICTを有効にしたら、モニタリングが途切れました。サイドカーのないPrometheusがメッシュ内のワークロードをスクレイプしていた場合、STRICTへの切り替えと同時にスクレイプが失敗します。Istioの移行ドキュメントが説明する手順、つまりPERMISSIVEの状態で平文で入ってくるクライアントをダッシュボードで見つけ出し、すべて移したあとにネームスペース単位でロックするという手順を、飛ばした結果です。
Cilium WireGuardを有効にしたら、ノード間の通信が途切れました。クラウドのセキュリティグループにUDP 51871がないと、トンネルが確立しません。IPsecならESPです。暗号化を有効にするのはHelmの値2行ですが、その値が要求するポートをネットワークチームと調整しないと、Podネットワーク全体が止まります。
次のクイズで確認すること
クイズでは、2つの暗号化方式の終端点の違い、同じノード内のトラフィックの扱い、PERMISSIVEとSTRICTの違い、mTLSが代わりにはできないこと、パッチのサポート期間、そしてmodules_disabledの性質を問います。