做者:CODING - 王炜html
若是对 Kubernetes 集群安全特别关注,那么咱们可能想要实现这些需求:linux
本文以实现 Kubernetes 两步验证为例,利用
Kubernetes Admission
动态准入控制,同时借助Serverless
实现一个两步验证
的 Demo,使读者对动态准入控制
和Serverless
有较深刻的了解。git
Token 两步验证失败,不容许部署github
Token 两步验证成功,容许部署web
Admission 是在用户执行 kubectl 经过认证以后,在将资源持久化到 ETCD 以前的步骤,Kubernetes 为了将这部分逻辑解耦,经过调用 Webhook 的方式来实现用户自定义业务逻辑的补充。而以上过程,都是在用户执行 kuberctl 并等待 API Server 同步返回结果的生命周期内。sql
上图标注的 ① 和 ② 是 Admission 介入的工做流程,咱们会发现有这些特色:数据库
Mutating
和 Validating
Webhook
实现的Mutating
的字面理解是“变异”的意思,真正的含义是,在资源持久化到 ETCD 以前,Mutating
控制器能够修改所部署的资源文件,好比给特定的 POD 动态增长 Labels,动态注入 sidecar
等。
细心的读者会发现,Admission Mutating
在不少产品都被用到,好比 Istio
里面就是使用它来动态的给每个容器注入 sidecar Envoy
容器来实现流量的劫持和管理。npm
Validating
比较好理解,也就是“验证”,它在 Mutating
以后,咱们能够将自定义的验证逻辑放在这个阶段实现。本文咱们就是利用它来实现一个简单的两步验证机制。json
Admission Webhook
其实就是 Mutating Controllers
和 Validating Controllers
的具体实现方式,也就是说,咱们须要给 Kubernetes 集群提供一个外部 Webhook Endpoint,API Server 执行到对应流程时,会调用咱们预约义的 Webhook 来实现咱们预约义的业务逻辑,经过返回规定的数据结构,来实现对 Yaml 文件的变动或者验证。bootstrap
根据官方文档,先决条件有如下几点:
若是不肯定,能够经过如下命令查询:
kubectl get pods kube-apiserver -n kube-system -o yaml | grep MutatingAdmissionWebhook,ValidatingAdmissionWebhook
若是你使用的是托管集群,那么请使用如下命令查询:
kubectl api-versions | grep admission
若是出现 admissionregistration.k8s.io/v1beta1
说明集群支持,进行下一步。
登录 CODING,并在配置 Serverless 身份受权,记录凭据 ID(相似:b68948cb-2ad9-4b67-8a49-ad7ba910ed92),稍后使用
克隆代码仓库 admission-webhook-example
git clone https://e.coding.net/wangweicoding/admission-webhook-example.git
VPC_ID
和 SUBNET_ID
,这两项能够在腾讯云控制台“私有网络”找到;若是没有私有网络和子网,则能够本身新建一个,注意地域选择“广州”CODING Git
代码仓库使用“空白模板”建立构建计划,选择“使用代码仓库的 Jenkinsfile”
运行构建计划,部署 Serverless 服务
运行完成后,点击“输出 Endpoint”阶段,查看输出的 URL (相似:https://service-faeax9cy-1301578102.gz.apigw.tencentcs.com/release/index), 此 URL 即为 Serverless 服务对外提供服务的 URL 。记录供下一个阶段使用
至此,腾讯云 Serverless 服务已部署完成。
由于 Admission Webhook 只容许 https 协议而且须要提供证书信息,因此须要咱们提早生成,代码仓库已经提供脚本,运行便可配置集群证书。
$ ./deployment/webhook-create-signed-cert.sh creating certs in tmpdir /var/folders/mt/965plkfs62v6wqx2839qthz40000gq/T/tmp.i1imELSt Generating RSA private key, 2048 bit long modulus (2 primes) ...................+++++ ....+++++ e is 65537 (0x010001) certificatesigningrequest.certificates.k8s.io/admission-webhook-example-svc.default created NAME AGE REQUESTOR CONDITION admission-webhook-example-svc.default 1s admin Pending certificatesigningrequest.certificates.k8s.io/admission-webhook-example-svc.default approved secret/admission-webhook-example-certs configured (base)
修改 deployment/deployment.yaml
文件,将 serverlessURL
替换为上一个阶段记录下的 Endpoint
(相似:https://service-faeax9cy-1301578102.gz.apigw.tencentcs.com/release/index)
证书建立成功后,部署 Deployment 和 Services
$ kubectl create -f deployment/deployment.yaml deployment.apps "admission-webhook-example-deployment" created $ kubectl create -f deployment/service.yaml service "admission-webhook-example-svc" created
至此咱们用来接收 Validating 请求的服务已经部署完成,最后配置 ValidatingWebhookConfiguration
,运行如下命令:
cat ./deployment/validatingwebhook.yaml | ./deployment/webhook-patch-ca-bundle.sh > ./deployment/validatingwebhook-ca-bundle.yaml
执行完成后,能够看到 validatingwebhook-ca-bundle.yaml
的 caBundle
字段已经被替换。
脚本运行依赖于 jq (Shell 读取 JSON 工具),若是你尚未安装,请移步:https://www.ibm.com/developerworks/cn/linux/1612_chengg_jq/index.html
Mac 系统能够直接使用:brew install jq 进行安装。
接下来,咱们为 default
命名空间打标签,由于咱们的 ValidatingWebhookConfiguration
使用了 namespaceSelector
只对包含特定 labels 的命名空间作两步验证。
$ kubectl label namespace default admission-webhook-example=enabled namespace "default" labeled
最后,建立 ValidatingWebhookConfiguration
$ kubectl create -f deployment/validatingwebhook-ca-bundle.yaml validatingwebhookconfiguration.admissionregistration.k8s.io "validation-webhook-example-cfg" created
这样,一旦在 default
命名空间建立资源,咱们部署的服务(Deployment) 将会拦截请求,并进行二次校验。
至此,咱们已经成功部署了两步验证的 Demo,总体架构图如今变成了:
如今,咱们能够尝试部署
$ kubectl apply -f deployment/sleep.yaml Error from server (Token 错误,不容许部署): error when creating "deployment/sleep.yaml": admission webhook "required-labels.coding.net" denied the request: Token 错误,不容许部署
因为咱们在建立 Serverless 服务的时候,预先向数据库配置了四组 token,分别是:11十一、222二、333三、4444,因此咱们能够修改 sleep.yaml
,将注解metadata.annotations.token
修改成 1111
,再次尝试部署
$ kubectl apply -f deployment/sleep.yaml deployment.apps/sleep created
部署成功,若是重复使用此 token,是没法验证经过的。至此,基于 Serverless 的两步验证已经完成。
当执行 kubectl apply 以后, API Server 将请求转发到咱们部署的 POD ,核心代码在项目根目录下,主要是 main.go
和 webhook.go
main.go 主要是启动了一个 HTTP 服务,并从命令行读取了咱们建立的证书以及 Serverless Endpoint
// main.go flag.IntVar(¶meters.port, "port", 443, "Webhook server port.") flag.StringVar(¶meters.certFile, "tlsCertFile", "/etc/webhook/certs/cert.pem", "File containing the x509 Certificate for HTTPS.") flag.StringVar(¶meters.keyFile, "tlsKeyFile", "/etc/webhook/certs/key.pem", "File containing the x509 private key to --tlsCertFile.") flag.StringVar(¶meters.serverlessURL, "serverlessURL", "https://example.com", "serverless endpoint URL.")
webhook.go 主要是转发 API Server 发送的请求,咱们将 validate
从新改写,将全部请求转发到 Serverless Endpoint。
// webhook.go glog.Infof("parameters.serverlessURL is %v", whsvr.parameters.serverlessURL) res, _ := Post(whsvr.parameters.serverlessURL, req) // 初始化请求变量结构 jsonData := make(map[string]string) // 调用json包的解析,解析请求body _ = json.NewDecoder(res.Body).Decode(&jsonData) glog.Infof("res is %v", jsonData) allowed := false reason := &metav1.Status{ Reason: "Token 错误,不容许部署", } if jsonData["allow"] == "true" { allowed = true } return &v1beta1.AdmissionResponse{ Allowed: allowed, Result: reason, }
POD 将请求转发到咱们的 Serverless 函数以后,由它来作业务逻辑判断是否容许准入。随后,POD 将 Serverless 的结果从新格式化以后返回给 API Server。
咱们部署的 Serverless 服务,主要包含了四个部分:
咱们使用 CODING DevOps 在腾讯云部署了以上几个 Serverless 服务,Jenkinsfile 核心代码:
stage('部署 Serverless 服务') { steps { withCredentials([string(credentialsId:"b68948cb-2ad9-4b67-8a49-ad7ba910ed92", variable:'tencent_serverless')]) { sh 'echo "${tencent_serverless}" > .tmp' sh ''' SecretId=$(cat .tmp | jq -r .SecretId) SecretKey=$(cat .tmp | jq -r .SecretKey) token=$(cat .tmp | jq -r .token) AppId=$(cat .tmp | jq -r .AppId) echo "TENCENT_SECRET_ID=${SecretId}" >> ./serverless/.env echo "TENCENT_SECRET_KEY=${SecretKey}" >> ./serverless/.env echo "TENCENT_APP_ID=${AppId}" >> ./serverless/.env echo "TENCENT_TOKEN=${token}" >> ./serverless/.env ''' sh 'cd serverless && cat .env' sh 'cd serverless && npm run bootstrap && sls deploy --all | tee log.log' sh 'rm ./serverless/.env' } echo '部署完成' } } stage('输出 Endpoint') { steps { sh 'cd serverless && cat log.log | grep apigw.tencentcs.com' } }
这里主要是使用临时凭据,以及使用 Serverless SDK 对预约义的 serverless.yml 进行部署。
API Gateway 负责对外提供外网访问
# ./serverless/api/serverless.yml API Gateway 部署文件 events: - apigw: name: k8sAdmission parameters: protocols: - http - https serviceName: description: Based on Tencent Cloud Serverless, it provides dynamic access control for K8S environment: release endpoints: - path: /index method: POST
Postgresql 负责存储预约义的 tokens
# ./serverless/db/serverless.yml 数据库部署文件 org: k8sAdmission app: k8sAdmission-db stage: dev component: postgresql name: fullstackDB inputs: region: ${env:REGION} zone: ${env:ZONE} dBInstanceName: ${name} vpcConfig: vpcId: ${output:${stage}:${app}:serverlessVpc.vpcId} subnetId: ${output:${stage}:${app}:serverlessVpc.subnetId} extranetAccess: false
VPC 实现将云函数和 Postgresql 网络互通
# ./serverless/vpc/serverless.yml VPC部署文件 org: k8sAdmission app: k8sAdmission-db stage: dev component: vpc # (required) name of the component. In that case, it's vpc. name: serverlessVpc # (required) name of your vpc component instance. inputs: region: ${env:REGION} zone: ${env:ZONE} vpcName: serverless subnetName: serverless
云函数负责准入逻辑判断,能够看到 handler: api_service.main_handler
,也就是说云函数的入口函数是 main_handler
,当有外部请求过来时,将会执行 main_handler
函数
# ./serverless/api/serverless.yml 云函数部署文件 org: k8sAdmission component: scf # (必填) 引用 component 的名称,当前用到的是 tencent-scf 组件 name: k8s # (必填) 该组件建立的实例名称 app: k8sAdmission-db # (可选) 该 SCF 应用名称 stage: dev # (可选) 用于区分环境信息,默认值是 dev inputs: src: ./ name: ${name} description: 基于腾讯云 Serverless 的 K8S 动态准入控制 handler: api_service.main_handler # 入口函数 runtime: Python3.6 # 云函数的运行时环境。除 Nodejs10.15 外,可选值为:Python2.七、Python3.六、Nodejs6.十、Nodejs8.九、PHP五、PHP七、Golang一、Java8。 region: ${env:REGION} vpcConfig: vpcId: ${output:${stage}:${app}:serverlessVpc.vpcId} subnetId: ${output:${stage}:${app}:serverlessVpc.subnetId} timeout: 10 environment: variables: PG_CONNECT_STRING: ${output:${stage}:${app}:fullstackDB.private.connectionString} PG_DN_NAME: ${output:${stage}:${app}:fullstackDB.private.dbname} events: - apigw: name: k8sAdmission parameters: protocols: - http - https serviceName: description: Based on Tencent Cloud Serverless, it provides dynamic access control for K8S environment: release endpoints: - path: /index method: POST
云函数关键代码
咱们将在首次触发(请求)时建立 TOKENS 表,并将 4 组预约义的 tokens 插入到表内。并检查咱们在执行 kubectl apply yaml 文件
annotations(注解)
内携带的 tokens 是否合法,并将 token 和 Postgresql 数据库存储的 token 进行比对。
# ./serverless/api/api_service.py 云函数业务逻辑 def main_handler(event,content): logger.info('start main_handler') logger.info('got event{}'.format(event)) logger.info('got content{}'.format(content)) # 链接数据库 print('Start Serverlsess DB SDK function') conn = psycopg2.connect(DB_HOST) print("Opened database successfully") cur = conn.cursor() cur.execute('''CREATE TABLE IF NOT EXISTS TOKENS (ID INT PRIMARY KEY NOT NULL, tokens TEXT NOT NULL);''') conn.commit() cur.execute("select * from TOKENS") myresult = cur.fetchall() for row in myresult: print("ID = " + str(row[0])) print("tokens = " + row[1]) if not bool(cur.rowcount): print("insert default tokens") cur.execute("INSERT INTO TOKENS (ID,tokens) \ VALUES (1, '1111')") cur.execute("INSERT INTO TOKENS (ID,tokens) \ VALUES (2, '2222')") cur.execute("INSERT INTO TOKENS (ID,tokens) \ VALUES (3, '3333')") cur.execute("INSERT INTO TOKENS (ID,tokens) \ VALUES (4, '4444')") conn.commit() json_dict = json.loads(event["body"]) if json_dict["object"]["metadata"]["annotations"]["token"] == "": return {"errorCode":0,"errorMsg":"","allow":"false"} cur.execute("SELECT * FROM TOKENS where tokens=%s",[json_dict["object"]["metadata"]["annotations"]["token"]]) myresult = cur.fetchall() allow = "false" if len(myresult) > 0: allow = "true" query_id = myresult[0][0] cur.execute("DELETE FROM TOKENS where ID=%s",[query_id]) conn.commit() conn.close() return {"errorCode":0,"errorMsg":json_dict["object"]["metadata"]["annotations"]["token"],"allow":allow}
若是 token 在数据库内存在,则从数据库删除本次使用的 token,并返回 JSON 给咱们在集群内部署的POD
{"errorCode":0,"errorMsg":"tokens","allow":"true"}
POD 根据 Serverless 返回的结果从新组装信息,返回以下 JSON 给 Kubernetes API Server
{ "UID":"b24ab5f7-8b6b-4ea2-83ff-6f9834a9937e", "Allowed":false, "Result":{ "ListMeta":{ "SelfLink":"", "ResourceVersion":"", "Continue":"" }, "Status":"", "Message":"", "Reason":"Token 错误,不容许部署", "Details":"", "Code":0 }, "Patch":"", "PatchType":"" }
其中,Allowed
字段为本次 kubectl apply
是否准入关键,Reason
信息将做为结果展现。
这里可能有同窗会问,为啥要经过咱们部署的 POD 再调用 Serverless 服务?让 API Server 直接请求 Serverless Endpoint 不行吗?答案是不行的,由于 API Server 请求的 webhook URL 要求双向 TLS 验证,咱们须要建立 Kubernetes CA 签名的 TLS 证书,确保 Webhook 和 Api Server 之间通讯的安全,因此咱们采用这种方式来实现。
至此,咱们实现了简单的 Kubernetes 两步验证。若是想实现更多的逻辑,好比判断 image 合规性、对于来源于非公司内部仓库的镜像拒绝部署,均可以在 Serverless 云函数内实现。
在生产实践中,如本例的 token,属于动态的 yaml 制品类型部署,咱们能够结合 CODING 持续部署
来为制品文件提供动态的参数绑定。
若是想要实现对 Deployment 动态注入 sidecar,能够利用 Mutating Webhook 监听部署的 Deployment,将须要注入的 sidecar 动态 Patch 注入。
若是想要实现集群级的 imagePullSecrets ,一个可行的思路是利用 Mutating Webhook 监听建立 namespaces 行为,自动将已存在的 imagePullSecrets Patch 到新的 namespaces 内。
实现 Mutating Webhook ,请留意项目根目录的 webhook.go 文件的 mutate
函数,原理与 Validating Webhook 相似,不一样点在于其主要经过 Patch 来实现。
Kubernetes admission
经过 Webhook 的方式解耦了 kubectl 的过程,使得咱们本身的业务逻辑可以动态加入到用户执行 kubectl 到返回结果的过程中,本文的两步验证只是一个简单的 Demo,想要更加深刻了解,能够浏览“参考资料”的连接。