做者: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,想要更加深刻了解,能夠瀏覽「參考資料」的連接。