Kubernetes 兩步驗證 - 使用 Serverless 實現動態准入控制

CODING_DevOps發表於2020-06-30

作者: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. 參考資料

相關文章