Kubernetesの脆弱性でクラスタに乗っ取りの危険性(CVE-2020-8558)

A conceptual image illustrating research into Kubernetes security.

This post is also available in: English (英語)

概要

Kubernetesノード上で動作するネットワークコンポーネントkube-proxyで、セキュリティ上の問題(CVE-2020-8558)が最近発見されました。この脆弱性により、認証なしで実行されることが多いKubernetesノードの内部サービスが危険にさらされます。特定のKubernetesデプロイメントでは、この問題によりapi-serverが危険にさらされ、認証されていない攻撃者がクラスタを完全に制御できる可能性があります。このようなアクセスが可能な攻撃者は、機密情報を盗み出し、クリプトマイナー(仮想通貨採掘マルウェア)を展開し、既存のサービスを完全に削除することができます。

この脆弱性が原因で、ノードのlocalhostサービス(ノード自体からのアクセスのみが意図されたサービス)が、ローカルネットワーク上のホストとノード上で動作しているポッドに晒されます。localhost専用のサービスでは、信頼できるローカルなプロセスによるアクセスのみを想定しているため、多くの場合、認証なしで要求に応答します。ノードで認証を実施せずにlocalhostサービスを実行している場合、この問題の影響を受けます。

問題の詳細は2020年4月18日に公開され、2020年6月1日にパッチがリリースされました。弊社でKubernetesクラスタへのその他の影響を評価した結果、一部のKubernetesデプロイメントでは、通常はマスターノード内からしかアクセスできないapi-serverのinsecure-portが無効化されていないことがわかりました。攻撃者はCVE-2020-8558を悪用することにより、insecure-portにアクセスし、クラスタを完全に制御することが可能です。

弊社は、この脆弱性の潜在的な影響について、Kubernetesのセキュリティチームに警告しました。これを受け、同セキュリティチームは、api-serverのinsecure-portが有効になっているクラスタでは脆弱性の影響が高く、有効になっていない場合は影響は中程度と評価しました。幸い、Azure Kubernetes Service (AKS)、AmazonのElastic Kubernetes Service (EKS)、Google Kubernetes Engine (GKE)などの大半のKubernetesホスティングサービスでは、CVE-2020-8558の影響は多少抑えられています。CVE-2020-8558は、Kubernetesのv1.18.4、v1.17.7、およびv1.16.11 (2020年6月17日にリリース)で修正されています。すべてのユーザーにアップデートが推奨されます。

弊社のPrisma Cloudをご利用中のお客様は、「結論」で解説している機能により同脆弱性から保護されています。

kube-proxy

kube-proxyは、Kubernetesクラスタの各ノードで動作するネットワークプロキシで、ポッド−サービス間の接続を管理しています。Kubernetesサービスで公開されるのは単一のclusterIPですが、複数の支援ポッドを構成して負荷分散できるようにする場合もあります。たとえば1つのサービスがそれぞれ専用のIPアドレスを持つ3つのポッドから構成されていても、10.0.0.1など単一clusterIPのみが公開されます。サービスにアクセスするポッドは、そのclusterIPの10.0.0.1にパケットを送信しますが、ここから何らかの方法でサービス抽象化の背後にあるポッドの1つにリダイレクトしてやる必要があります。

そこでkube-proxyの登場です。kub-proxyが各ノード上にルーティングテーブルを作成することで、サービスを支援するポッドの1つに特定サービスへの要求をきちんとルーティングできるようにするのです。このkube-proxyは、一般に静的ポッドかDaemonSetの一部としてデプロイされることが多いです。

興味があれば詳細についてGKEのドキュメントを参照してみてください。

なお、Ciliumなどのネットワーキングソリューションを設定すれば、kube-proxyを完全に置き換えることもできます。

原因はroute_localnet

kube-proxyにはsysctlファイルを使って複数のネットワークパラメータを設定するという役割もあるのですが、そこで設定される内容の1つがnet.ipv4.conf.all.route_localnetで、これが今回の脆弱性の原因です。Sysctlドキュメントには「route_localnet: Do not consider loopback addresses as martian source or destination while routing. This enables the use of 127/8 for local routing purposes. default FALSE.」(route_localnet: ルーティング時ループバックアドレスを「ありえない(Martian)」送信元アドレス/宛先アドレスと見なさない。これによりローカルルーティング時127.0.0.0/8が使用可能になる。デフォルトはFALSE。)と記載されています。

この意味について説明しましょう。IPv4では、ループバックアドレスは127.0.0.0/8アドレスブロック(127.0.0.1~127.255.255.255)で構成されますが、一般には127.0.0.1のみが使用され、ホスト名「localhost」がこのアドレスにマッピングされています。これらのアドレスは、マシンが自身を参照するために使用するもので、ローカルサービスを対象とするパケットは、ループバック ネットワーク インターフェイスを介してIP 127.0.0.1に送信され、送信元IPも127.0.0.1に設定されます。

ところがここでroute_localnetを設定すると、カーネルに対して、127.0.0.1/8 IPアドレスをMartianとして定義しないよう指示することになります。このコンテキストでは「Martian」(火星人)とは何を意味するのでしょうか? ネットワークインターフェイスで受信する一部のパケットは、その送信元または宛先IPについて、意味をなさない主張を行います。たとえば、送信元IPが255.255.255.255であるパケットを受信したとします。このパケットは存在してはなりません。255.255.255.255はブロードキャストを示すために使用される予約済みアドレスであり、ホストを識別することはできません。では、何が起きているのでしょうか? カーネルにはその判断はできないため、パケットがMars (火星)から来たものであると結論付け、廃棄するしかありません。

Martianパケットはたいてい、ネットワーク上の悪意のある誰かが攻撃しようとしていることを示しています。前述の例では、攻撃者は、サービスにIP 255.255.255.255に応答させることにより、ルーターに応答をブロードキャストさせようとしている可能性があります。怪しい宛先IPが原因で、パケットがMartianであると見なされることもあります。たとえば、宛先IP 127.0.0.1で外部ネットワークインターフェイスに着信するパケットです。このパケットも意味をなしません。127.0.0.1は、ループバックインターフェイスを介した内部通信用に使用されるもので、ネットワーク側インターフェイスから着信するものではありません。Martianパケットの詳細については、RFC 1812を参照してください。

一部の複雑なルーティングシナリオでは、カーネルに特定のMartianパケットの通過を許可させた方がよい場合があります。route_localnetはこのために使用します。これは、127.0.0.0/8をMartianアドレスと見なさないようにカーネルに指示します(前述のように、通常はMartianアドレスと見なされるため)。ここでは詳しく説明しませんが、kube-proxyは、さまざまなルーティング技をサポートするようにroute_localnetを有効にします。ただし、route_localnetは理由があってデフォルトでは無効になっています。適切な対策が用意されていない限り、ローカルネットワーク上の攻撃者は、route_localnetを悪用して複数の攻撃を実行することができてしまいます。最も影響が大きいのが、localhost専用サービスへのアクセスです。

localhost専用サービス

Linuxでは、各プロセスが自身をネットワークインターフェイスのアドレスにバインドできるように、特定のIPアドレスのみをリッスンすることが可能です。内部サービスはたいていこの機能を使用して、127.0.0.1のみをリッスンします。通常これにより、127.0.0.1に到達できるのはローカルプロセスのみであるため、ローカルプロセスのみがサービスにアクセスできるようになります。しかし、route_localnetが127.0.0.0/8宛ての外部パケットを許可することでこの前提は崩れます。外部パケットが到達することはないという前提があるがゆえに内部サービスは認証をしていないことが多く、これは大きな問題となります。

内部サービスへのアクセス

攻撃対象の内部サービスへのアクセスを試みる攻撃者は、宛先IPアドレスを127.0.0.1に、宛先MACアドレスを被害者のMACアドレスに設定した悪意のあるパケットを作成する必要があります。ただし意味のない宛先IPが指定されているので、攻撃者のパケットが攻撃対象に到達するのにレイヤー2 (MACベースの)ルーティングしか使用できません。したがって、攻撃はローカルネットワーク内に限定されます。つまり、攻撃対象マシンがroute_localnetを有効にしていたとしても、そのlocalhostサービスにアクセスできるのは、攻撃者が同じローカルネットワーク上にいる場合だけということになります。

route_localnetが有効なので、悪意のあるパケットを受信した被害マシンはこのパケットを通過させます。そしてパケットの宛先IPは127.0.0.1なので、localhostサービスへのアクセスが許可されます。表1に、悪意のあるパケットの例を示します。攻撃者のIPは10.0.0.1、MACアドレスはXXXであり、宛先IPは10.0.0.0.2、MACアドレスはYYYです。標的はポート1234上でlocalhost専用サービスを実行しています。

src mac

XXX dst mac YYY
src ip 10.0.0.1 dst ip 127.0.0.1
src port random dst port 1234

図1: route_localnetを悪用するパケット

標的の応答を受信できるように、攻撃者はパケットに自身のIPアドレスを含めて送信します。以上をまとめると、route_localnetを有効にしている場合、ローカルネットワーク内の攻撃者が、図1のようなパケットで、攻撃対象ホストの内部サービスにアクセスできるようになります。

ここで再びKubernetesの脆弱性について(CVE-2020-8558)

ということで、kube-proxyが原因でクラスタ内のノードはすべてroute_localnetが有効になっています。つまり当該ノードのローカルネットワーク上の全ホストが、ノードの内部サービスにアクセスすることができるので、ノードが認証なしで内部サービスを実行していれば、この問題の影響を受けることになります。

ノードのローカルネットワーク上の隣接するホストのほか、ノード上で動作しているポッドも内部サービスにアクセスできます。ただしポッドがアクセスできるのは、そのポッドをホスティングしているノードの内部サービスのみです。攻撃実行にはポッドにCAP_NET_RAW機能が必要です。そして残念ながらデフォルトでKubernetesはこの機能を付与しています。

この問題の調査時に、私たちはKubernetesにもともとデプロイされているlocalhostサービスを特定しようとしました。その際、Kubernetesのapi-serverはデフォルトで、insecure-portと呼ばれるポートを介して、localhostで認証されていない要求を処理していることがわかりました。insecure-portは、マスター上で動作するその他のコントロール プレーン コンポーネント(etcdなど)がapi-serverと簡単に対話できるように設定されています。このポートでは、ロールベースのアクセス制御(RBAC)やその他の認証メカニズムは実施されていません。Kubernetesデプロイメントでは、api-serverがマスターノード上のポッドとして実行されることがよくあります。つまり、route_localnetが有効になっているkube-proxyと一緒に実行されます。

恐ろしいことに、これはKubernetesデプロイメントでinsecure-portを無効にしていないと、マスターノードのローカルネットワーク上のホストがCVE-2020-8558を悪用することにより、api-serverを操り、クラスタを完全に制御できることを意味します。

マネージドKubernetes

GKE、EKS、AKSなどのマネージドKubernetesプラットフォームは、CVE-2020-8558からより強力に保護されています。

まず、Microsoft Azureなどの一部のクラウド サービス プロバイダ(CSP)の仮想ネットワークでは、レイヤー2のセマンティクスおよびMACベースのルーティングはサポートされていません。わかりやすく言うと、たとえばすべてのAKSマシンは、同じMACアドレス12:34:56:78:9A:BCを持ちます。これにより、ノードのローカルネットワーク上の他のホストによるCVE-2020-8558の悪用の可能性を軽減できますが、CAP_NET_RAW機能を持つ悪意のあるポッドは引き続き攻撃を実行することができます。

また、多くのクラウドホスティング型Kubernetesサービスでは、Kubernetesのコントロールプレーンおよびapi-serverの管理を代行し、クラスタのその他の部分とは別のネットワーク上で実行します。クラスタのその他の部分にはさらされないため、これによりapi-serverが保護されます。CSPがinsecure-portを無効にせずにapi-serverを実行しても、同じローカルネットワーク内で実行されていないため、クラスタ内の攻撃者はapi-serverにアクセスすることはできません。

ただし、CSPの仮想ネットワークでレイヤー2ルーティングがサポートされている場合、クラスタのネットワーク内における悪意のあるホストは、ワーカーノード上のlocalhostサービスにアクセスできてしまいます。

修正

最初の修正は実はkubelet内に存在し、route_localnet:ルーティングルールに対策を追加します。これにより、ノードは127.0.0.1宛ての外部パケットを廃棄するようになります。この記事の執筆時には、route_localnetはまだkube-proxyにより有効になっています。これを無効にすることについては、継続的に議論が行われています

このような対策を実施しても、ローカルネットワークの攻撃者がノードの内部UDPサービスにパケットを送信できる特別なケースがまだ存在します。ただしこの攻撃は、被害者ノードでリバース パス フィルタリングが無効になっている(通常は有効になっている)場合にのみ可能であり、有効になっている場合は攻撃者は応答を得ることはできません。パッチ開発中ですが、攻撃が不確かな設定(rp_filter=0)に依存しているため、中止されるかもしれません。

影響の有無

以下の項目にすべて該当するクラスタはCVE-2020-8558に対して脆弱です。

  • クラスタで脆弱なバージョン(v1.18.4、v1.17.7、およびv1.16.11より前のバージョン)を実行している。
    • この問題はkube-proxyで発生していますが、パッチはkubelet内に含まれることに注意してください。
  • ノード上でkube-proxyを実行している。
  • ノード(またはhostnetworkポッド)で、追加の認証を要求しないlocalhost専用サービスを実行している。

また、以下の項目も該当する場合、api-serverのinsecure-portを介して、クラスタが完全に乗っ取られる可能性があります。

  • クラスタで、--insecure-port=0により、api-serverのinsecure-portを無効にしていない。
  • api-serverがポッド自体であるデプロイメントなどで、api-serverがノード上でkube-proxyと一緒に実行されている。

ノードは、ローカルネットワーク上の悪意のあるホスト、またはノード上で実行されている、CAP_NET_RAW機能を持つ悪意のあるポッドにより攻撃される可能性があります。

結論

CVE-2020-8558は深刻な結果をもたらす可能性があります。できるだけ早くクラスタにパッチを適用してください。またCVE-2020-8558は、セキュリティのベストプラクティスは役に立ち、攻撃対象領域を大幅に縮小してくれることに気付かせてもくれます。api-serverのinsecure-portは無効にするべきです。また、ポッドにCAP_NET_RAWが必要ない場合は、この機能を有効にしておくべき理由はありません。この特定の問題には関係ありませんが、ロールベースアクセス制御などのセキュリティの推奨事項を実施し、またコンテナをルート以外のユーザーで実行することをお勧めします。

パロアルトネットワークスのPrisma Cloudをご利用中のお客様は、この脆弱性から保護されています。コンプライアンスルールによりクラスタは安全に設定されており、以下については、ブロックされているか、警告が発せられます。

  • api-serverのinsecure port
  • CAP_NEW_RAW機能を持つポッド