|
技术分享|Kubernetes 安全风险加固手册
技术分享|Kubernetes 安全风险加固手册
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2024年01月29日 15:31
北京
一、 Kubernetes 安全风险现状随着云计算的快速发展,越来越多的企业开始将应用程序迁移到云平台上。而在云平台中,Kubernetes 成为了最受欢迎的容器编排工具之一。然而,随着 Kubernetes 的广泛应用,安全风险也逐渐凸显出来。本文将从 Cloud、Cluster、Container 角度出发,以一种由下至上的方式,列举 Kubernetes 的安全风险,并提供相应的加固建议。二、基础设施安全风险(Cloud)2.1 最大程度减少具有容器集群管理权限的 RAM 账户(高风险)公有云中对容器集群具有高权限的 Action 如下所示:当云中的账户直接或间接的拥有上述权限,可对容器集群具有管理权限。除了上述针对于容器集群的高风险权限以外,还需要关注本身是管理员权限的账户权限以及可以通过提权到公有云平台管理员权限的账户权限,比如ram:CreateAccessKey权限、ram:AttachPolicyToUser权限、ram:UpdateRole结合sts:AssumeRole权限等都可以提升到云平台管理员权限,这块属于另一部分的内容,不在这篇文章中进行过多的介绍,这里说明一些云平台默认的具有管理员权限的权限策略:公有云服务权限策略腾讯云CAMQcloudCAMPFullAccess腾讯云CAMAdministratorAccess阿里云RAMAdministratorAccess阿里云RAMAliyunRAMFullAccess建议依照最小权限原则分配 RAM 账户权限,针对于不需要管理 Kubernetes 集群的 RAM 账户,移除其高权限,或使用 Resource 指定权限作用资源也能一定程度降低权限影响。2.2 容器集群需限制高权限角色扮演当容器集群中 CVM 实例设置了角色绑定,则运行在该 CVM 的 Pod 如果存在 SSRF 或 RCE 等漏洞,则可能造成信息泄露或提升到云管理员权限。腾讯云中可以使用 cvmescribeInstances 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.813.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 NameNamespacestatefulset-controllerkube-systempersistent-volume-binderkube-systemdaemon-set-controllerkube-systemdeployment-controllerkube-systemsystem:kube-controller-managerkube-systemexpand-controllerkube-systemtoken-cleanerkube-systemgeneric-garbage-collectorkube-systemhorizontal-pod-autoscalerkube-systemnamespace-controllerkube-systembootstrap-signerkube-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.crt3.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 # 不应该配置为 trueauthorization: mode: Webhook # 默认为 Webhook, 不应该配置为 AlwaysAllow3.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 的名字由三部分组成-token-,其中 service account name 是已知的,我们只需要选取 kube-system 命名空间中默认生成的高权限 service account 即可,随机值通过查看相关源码也是可穷举的,相关代码如下:// https://github.com/kubernetes/kubernetes/blob/2fef630dd216ddefd051ef5a2dda3fe1fdf7439a/pkg/controller/serviceaccount/tokens_controller.go#L227func (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#L361func (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#L49const ( // 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-L80C8const ( alphanums = "bcdfghjklmnpqrstvwxz2456789" alphanumsIdxBits = 5 alphanumsIdxMask = 1<
|
|