Kubernetes 两步验证 - 使用 Serverless 实现动态准入控制

做者:CODING - 王炜html

1. 背景

若是对 Kubernetes 集群安全特别关注,那么咱们可能想要实现这些需求:linux

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

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

1.2 实现效果

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

Token 两步验证成功,容许部署web

2. 什么是 Admission

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

上图标注的 ① 和 ② 是 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 容器来实现流量的劫持和管理。npm

2.3 Admission Validating

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

3. 什么是 Admission Webhook

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

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,分别是:11十一、222二、333三、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.七、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 之间通讯的安全,因此咱们采用这种方式来实现。

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. 参考资料

相关文章
相关标签/搜索