作者:CODING - 王炜

1. 背景

如果对 Kubernetes 集群安全特别关注,那么我们可能想要实现这些需求:

  • 如何实现 Kubernetes 集群的两步验证,除了集群凭据,还需要提供一次性的 Token 校验?
  • 如何验证部署的镜像是否安全合规,使得仅允许部署公司内部镜像仓库的 Docker 镜像?
  • 如何实现对每一个 Deployment 动态注入 sidecar ,满足特定安全或业务需求?
  • 如何实现集群级的 imagePullSecrets ,当创建新的命名空间的时候,自动将 imagePullSecrets 注入到新的命名空间?

本文以实现 Kubernetes 两步验证为例,利用 Kubernetes Admission 动态准入控制,同时借助 Serverless 实现一个两步验证的 Demo,使读者对动态准入控制Serverless有较深入的了解。

1.2 实现效果

Token 两步验证失败,不允许部署

Token 两步验证成功,允许部署

2. 什么是 Admission

Admission 是在用户执行 kubectl 通过认证之后,在将资源持久化到 ETCD 之前的步骤,Kubernetes 为了将这部分逻辑解耦,通过调用 Webhook 的方式来实现用户自定义业务逻辑的补充。而以上过程,都是在用户执行 kuberctl 并等待 API Server 同步返回结果的生命周期内。

上图标注的 ① 和 ② 是 Admission 介入的工作流程,我们会发现有这些特点:

  1. Admission 工作在集群认证通过之后
  2. Admission 一共有两种:MutatingValidating
  3. 这两种具体的实现方式都是以 Webhook 实现的
  4. Admission 的操作对象可以是当前部署的用户、Yaml 内容等

2.2 Admission Mutating

Mutating 的字面理解是“变异”的意思,真正的含义是,在资源持久化到 ETCD 之前,Mutating 控制器可以修改所部署的资源文件,比如给特定的 POD 动态增加 Labels,动态注入 sidecar 等。

细心的读者会发现,Admission Mutating 在很多产品都被用到,比如 Istio 里面就是使用它来动态的给每一个容器注入 sidecar Envoy 容器来实现流量的劫持和管理。

2.3 Admission Validating

Validating 比较好理解,也就是“验证”,它在 Mutating 之后,我们可以将自定义的验证逻辑放在这个阶段实现。本文我们就是利用它来实现一个简单的两步验证机制。

3. 什么是 Admission Webhook

Admission Webhook 其实就是 Mutating ControllersValidating Controllers 的具体实现方式,也就是说,我们需要给 Kubernetes 集群提供一个外部 Webhook Endpoint,API Server 执行到对应流程时,会调用我们预定义的 Webhook 来实现我们预定义的业务逻辑,通过返回规定的数据结构,来实现对 Yaml 文件的变更或者验证。

4. 动手实践

4.1 集群条件

根据官方文档,先决条件有以下几点:

  • Kubernetes 集群版本至少为 v1.16
  • 启用了 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook 控制器

如果不确定,可以通过以下命令查询:

kubectl get pods kube-apiserver -n kube-system -o yaml | grep MutatingAdmissionWebhook,ValidatingAdmissionWebhook

如果你使用的是托管集群,那么请使用以下命令查询:

kubectl api-versions | grep admission

如果出现 admissionregistration.k8s.io/v1beta1 说明集群支持,进行下一步。

4.2 其他条件

4.3 部署腾讯 Serverless 服务

  1. 登陆 CODING,并在配置 Serverless 身份授权,记录凭据 ID(类似:b68948cb-2ad9-4b67-8a49-ad7ba910ed92),稍后使用

  2. 克隆代码仓库 admission-webhook-example

git clone https://e.coding.net/wangweicoding/admission-webhook-example.git
  1. 修改根目录下的文件
  • 根目录下的 Jenkinsfile,将上一步获取的凭据 ID 替换光标处的凭据 ID

  • 修改 serverless/.env 的 VPC_IDSUBNET_ID,这两项可以在腾讯云控制台“私有网络”找到;如果没有私有网络和子网,则可以自己新建一个,注意地域选择“广州”



  • 修改完成后,将代码推送到你自己的 CODING Git 代码仓库
  1. 使用“空白模板”创建构建计划,选择“使用代码仓库的 Jenkinsfile”

  2. 运行构建计划,部署 Serverless 服务



    运行完成后,点击“输出 Endpoint”阶段,查看输出的 URL (类似:https://service-faeax9cy-1301578102.gz.apigw.tencentcs.com/release/index), 此 URL 即为 Serverless 服务对外提供服务的 URL 。记录供下一个阶段使用

至此,腾讯云 Serverless 服务已部署完成。

4.4 Kubernetes 集群部署 Validating Webhook

因为 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.yamlcaBundle 字段已经被替换。

脚本运行依赖于 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) 将会拦截请求,并进行二次校验。

4.5 尝试两步验证

至此,我们已经成功部署了两步验证的 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,分别是:1111、2222、3333、4444,所以我们可以修改 sleep.yaml ,将注解metadata.annotations.token 修改为 1111,再次尝试部署

$ kubectl apply -f deployment/sleep.yaml
deployment.apps/sleep created

部署成功,如果重复使用此 token,是无法验证通过的。至此,基于 Serverless 的两步验证已经完成。

5. 源码分析

5.1 我们部署的 Deployment 做了什么

当执行 kubectl apply 之后, API Server 将请求转发到我们部署的 POD ,核心代码在项目根目录下,主要是 main.gowebhook.go

main.go 主要是启动了一个 HTTP 服务,并从命令行读取了我们创建的证书以及 Serverless Endpoint

// main.go

flag.IntVar(&parameters.port, "port", 443, "Webhook server port.")
flag.StringVar(&parameters.certFile, "tlsCertFile", "/etc/webhook/certs/cert.pem", "File containing the x509 Certificate for HTTPS.")
flag.StringVar(&parameters.keyFile, "tlsKeyFile", "/etc/webhook/certs/key.pem", "File containing the x509 private key to --tlsCertFile.")
flag.StringVar(&parameters.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。

5.2 Serverless 做了什么?

我们部署的 Serverless 服务,主要包含了四个部分:

  • API Gateway
  • 云函数
  • Postgresql
  • VPC

我们使用 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.7、Python3.6、Nodejs6.10、Nodejs8.9、PHP5、PHP7、Golang1、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 之间通信的安全,所以我们采用这种方式来实现。

6. 结束语

至此,我们实现了简单的 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,想要更加深入了解,可以浏览“参考资料”的链接。

7. 参考资料

Kubernetes 两步验证 - 使用 Serverless 实现动态准入控制的更多相关文章

  1. 使用KeePass管理两步验证

    目录 使用KeePass管理两步验证 两步验证 KeePass中管理两步验证 KeeTrayTOTP插件使用 使用KeePass管理两步验证 文:铁乐与猫 2018-9-9 KeePass 是一款管理 ...

  2. OPTAUTH 两步验证详解

    先贴图: 在对外网开放的后台管理系统中,使用静态口令进行身份验证可能会存在如下问题: (1) 为了便于记忆,用户多选择有特征作为密码,所有静态口令相比动态口令而言,容易被猜测和破解: (2) 黑客可以 ...

  3. Authenticator App 两步验证会不会造成亚马逊账号关联?

    今天听人说,因为用Authenticator App做亚马逊两步验证造成了帐号关联…… 我给大家解释一下Authenticator的实现原理,作为计算机专业科班出身的我,此次从各方面了解并经过自己亲测 ...

  4. 两步验证Authy时间同步问题

    Authy是我常用的软件之一,通常用于Google的两步验证,或者是其他基于Google两步验证的原理的衍生程序.比如Namesilo.印象笔记等均有使用. 先说说什么是两步验证. 两步验证 两步验证 ...

  5. Google 推出全新的两步验证机制

    近日 Google 在官方的 Apps Updates 博客公布了全新的两步验证功能--Google 提示,新的功能通过与 Google App 联动,进一步将验证确认工作缩减到仅有两步,同时支持 A ...

  6. SecureCRT两步验证自动登录脚本

    简介 用于解决 Google Authenticator 的两步验证登录.涉及到密码,不建议脚本保存到公共环境. 安装oathtool Mac $ brew install oath-toolkit ...

  7. 两步验证杀手锏:Java 接入 Google 身份验证器实战

    两步验证 大家应该对两步验证都熟悉吧?如苹果有自带的两步验证策略,防止用户账号密码被盗而锁定手机进行敲诈,这种例子屡见不鲜,所以苹果都建议大家开启两步验证的. Google 的身份验证器一般也是用于登 ...

  8. 为Linux服务器的SSH登录启用Google两步验证

    对于Linux服务器而言使用密钥登录要比使用密码登录安全的多,毕竟当前网上存在多个脚本到处进行爆破. 这类脚本都是通过扫描IP端的开放端口并使用常见的密码进行登录尝试,因此修改端口号也是非常有必要的. ...

  9. kubernetes高级之动态准入控制

    系列目录 动态准入控制器文档介绍了如何使用标准的,插件式的准入控制器.但是,但是由于以下原因,插件式的准入控制器在一些场景下并不灵活: 它们需要编译到kube-apiserver里 它们仅在apise ...

  10. kubernetes集群的认证、授权、准入控制

    一.kubernetes集群安全架构 用户使用kubectl.客户机或通过REST请求访问API.可以授权用户和Kubernetes服务帐户进行API访问.当一个请求到达API时,它会经历几个阶段,如 ...

随机推荐

  1. Eclipse 使用maven创建Dynamic Web Project

    今天看到spring的官网首页已经没有下载连接了,建议使用maven构建spring项目.虽然maven已经不是新技术,但是这个我还没用过,谷歌走起! 通常创建项目都是用Eclipse,前几年还是My ...

  2. CentOS7 续续

    1.配置网络,虚拟机为桥接模式,IP地址为 192.168.100+学号/24,配置完成后可以通过ping物理机192.168.100段,或者ping 192.168.100.140验证2.通过临时与 ...

  3. Django 的 CSRF 保护机制(转)

    add by zhj:假设用户登录了网站A,而在网站B中有一个CSRF攻击标签,点击这个标签就会访问网站A,如果前端数据(包括sessionid)都放在本地存储的话, 当在网站B点击CSRF攻击标签时 ...

  4. 分页功能的实现——Jdbc && JSP

    @目录 什么是分页 ? 两个子模块功能的问题分析 和 解决方案 有条件查和无条件查询的影响 和 解决方案 项目案例: mysql + commons-dbutils+itcast-tools+Base ...

  5. 4.16 反射和jvm

  6. android手机短信获取

    关于Android中对短信的一些相关操.我看到一个文章下面我就从标题中的三个方面来对Android系统中的短信操作进行一个简单地学习. 短信发送: 由于Android中对短信发送方法的优良封装,之后对 ...

  7. 十四、文件和目录——文件时间和utime函数

    14.1 文件时间 存在于 stat 结构体中 14.2 文件时间函数 14.2.1 介绍 utime(修改文件的存取时间和更改时间) 相关函数 utimes,stat #include <sy ...

  8. shell中的dd命令使用详解

    一.dd命令的解释 dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换. 注意:指定数字的地方若以下列字符结尾,则乘以相应的数字:b=512:c=1:k=1024:w=2 参数注释: 1. ...

  9. python_的面向对象编程

    废话不多说,先弄个对象来看看 class Student(object): def __init__(self, name, score): self.name = name self.score = ...

  10. xorm的sql builder

    最近在使用xorm,并使用了sql builder来构建sql查询没想到升级后原来可以使用的代码居然报错了. 0x00 代码 sql, args, _ := builder.Select(" ...