KMS 加密kubernetes secrets的正確姿勢


背景

在kubernetes中, secrets默認是明文存儲在etcd中,具有很大的安全風險,可以配置KMS provider進行加密。但引入KMS provider是否會對apiserver造成影響,需要從性能和可用方面進行仔細考量。

架構

目前kubernetes調用kms進行加解密,我們需要提供一個kms-provider(或稱kms-plugin),其利用公司內部的kms服務來實現加解密:

kms-grpc-deployment-diagram

可以閱讀官方文檔Using a KMS provider for data encryption了解更多。

每次用戶創建secrets的時候會請求apisever,apisever采用信封加密模型加密該secetes,具體流程為: 

  • apiserver生成隨機 Data Encryption Key (DEK),用該DEK加密用戶secrets;
  • 然后調用kms-plugin加密該DEK,生成EncryptedDEK,在kms-plugin內部會繼續調用kms服務進行加密;
  • 加密完成后,EncryptedDEK被附加到secrets前面作為header,然后存入ETCD,以便后續讀取時能夠通過該header找到對應DEK解密數據。 

當用戶讀取該secrtes的時候,執行該逆過程。apiserver與kms-plugin通過local domain socket gRPC通信。

kubernetes為了提高性能,允許用戶在apiserver中使用LRU算法緩存DEK與對應的EncryptedDEK,對於secrets讀取請求,每次先去cache中查找,如果找不到對應記錄,才去調用kms-plugin。

可用性

  • 根據上述流程,對於可能產生異常的點主要是kms-plugin 和kms服務。由於kms被kms-pluigin調用,當kms異常的時候我們可以使kms-plugin隨之拋出異常, 所有問題轉換為如何使kms-plugin掛掉的時候,自動failover。
  • 要實現kms-plugin的高可用,可以設置多個副本,apiserver通過LoadBalancer訪問該多個副本。但是遺憾的是目前kubernetes不支持對kms-plugin的遠程調用,只能通過domain socket與本地kms-plugin通信。如果修改kubernetes代碼實現遠程調用,內部維護代碼差異成本較大,且需要考慮調用直接如何進行認證授權。
  • 如果非要讓apisever通過本地調用kms-plugin, 那么在kms-plugin掛掉的時候,就做到服務降級。服務降級: 在服務不可用的時候不能影響核心功能的正常使用,核心功能定義為secrets的讀取。服務降級是利用apiserver內部cache實現: 設置足夠大的cache size,使每個key都能緩存在其中,每次secrets讀取操作都能夠被cache命中,即使kms-plugin掛掉下,也不影響secrets的正常讀取。此時寫入/更新操作會失敗,但secrets寫入操作較少。設置足夠大的cache size帶來的性能和內存空間占用問題參見下文性能部分
  • 如果利用cache進行服務降級,也是有一些問題:目前apiserve每次寫入ETCD都會重新生成一個DEK,即使同一個secrets update也會生成新的DEK,該DEK緩存到cache中。那么就會出現如果一個secrets在短時間內多次更新,該DEK會迅速占慢整個緩存,導致其他secrets DEK被擠出去。當kms-plugin掛掉的時候,如果該DEK又沒在cache中,如果用戶請求這些secrets就會失敗。解決方式 可以短期內先上線緩存的方式, 通過配置報警監測: 1). 短時間內大量的secret寫請求,2). cache size的空間變化。通過運維方式解決該問題。 長期方案需要收集更多的場景再決定是否需要支持遠程調用,從根本上解決問題。

性能

性能問題需要弄清楚當前集群內secrets的:使用方式,使用場景,使用規模,apiserver內存overhead,etcd存儲空間overhead,QPS,請求時延等情況。

kubernetes中的內部使用方式

當前kubernetes用戶使用secrets用來存放serviceAccount token,docker鏡像的拉取token, 證書以及用戶自定義的其他secrets。secrets在k8s中使用方式如下:

kubelet側使用

secrets用來存放用戶敏感信息,在容器啟動后會注入到容器中, 目前kubelet會在pod啟動前拉取secrets, 由kubelet中secretsManager負責。無論是imagePullSecrets 還是serviceAccountsecrets,不同種類的 secrets 處理方式相同。kubelet中secretsManager可以通過三種方式管理secrets:

  • get: 每次需要時kubelet都會重新調用get請求獲取
  • cache: 每次獲取之后緩存起來,在失效時間內可以重復使用
  • watch: 會使用informer監聽secrets變化,使用informer內置的cache。使用watch機制可以避免大規模的get請求,減輕apiserver的負

由於kubelet對於每一個pod都會定期resync, resync時會由kubelet中volume Manager判斷是否需要remount secrets volume, resync周期默認是1~1.5min。
對於volume 文件形式使用的secrets, 在secrets發生更新后會及時同步到容器內,以env使用的secrets則會在下次重啟才能生效。

此外社區增加了imutable secrets,其實現也比較簡單, 只是在kubelet中做判斷,如果是imutable的類型,在采用watch機制時,第一次拉取過來后就會停止這個secrets的reflector 不再監聽隨后的更新事件。

apiserver側使用

apiserver側處理流程和其他的資源處理流程相同,只是多了一個加解密過程,此處不再贅述。除此之外一些細節包括:

  • apiserve在啟動的時候會用informer獲取所有的secrets用來做authentication,一個serviceAccout Token請求過來之后,會判斷對應的secrets是否已經刪除,信息是否發生改變,該參數可以由--service-account-lookup指定是否開啟。 kms-plugin掛掉可能會導致authentication功能有部分缺失。該authendication功能會在apiserver啟動的時候用informer list所有的secrets資源, 此時就會初始化apiserver中的key cache。
  • apiserver 暴露的/healthz 接口里可以查看kms-plugin是否正常, kube-controller-manager在啟動的時候會請求該接口,如果接口返回成功才會啟動。

使用場景

kubernetes中secrets的使用場景主要是service account token, docker images token及用戶自己創建的各種token,這里主要介紹一下service account token,其他token較為簡單。

service Account

默認每個service Acount都會關聯一個secrets,當namespace創建完成后, controller-manager中service account controller會自動創建一個service account, 同時token controller會自動創建一個token關聯該service accout並存儲在secrets中。 該token為jwt token, 包含了service account的信息, 用戶可以用該token請求apisever, apiserver 收到該token后在authentication模塊中校驗該jwt token是否有效,然后取出token中的身份信息,該認證過程並不涉及secrets 的訪問。該token只做認證並不會授權,如果用戶希望有特定的權限,需要為該serviceAcount綁定到對應的Role上進行授權。 該service account對應的token一般創建后變更較少,serviceAccout不刪除則對應的sercrets就不會變化。

pod中使用的service account的關聯secrets 是在apiserver admission controller中自動注入的, 會以volume的形式掛載進容器中,然后動態同步更新變化。如果想關閉該secrets的自動掛載,可以1.從pod中單獨關閉,2.可以在service account的定義中關閉, 3.也可以在apiserver中關閉這個admission plugin。關閉自動掛載secrets功能對於已經有的正常運行的pod沒有影響, 新創建pod如果要訪問apiserver就需要用戶手動掛載service account對應的secrets。

對於service account的token 如果開啟了 Service Account Token VolumesBound Service Account Tokens新feature之后, 使用的方式會發生變化, token是帶有失效期的, 每次需要重新請求token request的接口來重新生成service account。

空間占用:

空間占用我們需要從兩方面衡量: ETCD內存儲空間overhead,apiserver key cache內存空間overhead。

ETCD:

要想弄明白ETCD的存儲空間overhead,我們首先得明白secrets在etcd中的存儲格式。由於kms在加密數據的時候可能會造成數據長度發生變化,這部分長度變化也需要仔細衡量,根據所采用的kms而略有不同。

secrets被寫入ETCD時,格式為: prefix + kms插件名 + key-len + encrpt(key) + aes<key>(raw-data)

  • key: 32 bytes, 隨機生成
  • encrpt(): keycenter加密函數,根據不同的加密方式略有差異
  • key-len: 標識key的長度, 目前占用兩個字節來存放信息
  • prefix: 固定為: k8s:enc:kms:v1:
  • kms插件名: 自定義,在配置文件中指定

舉例來說: 當執行kubectl create secret generic secret1 -n default --from-literal=mykey=mydata 命令時,創建secrets原始數據raw-data大小為: 221 byte。假設encrpy函數調用kms-plugin之后增加大小為2byte,則總共大小為

  1. prefix: 15bytes,
  2. kms插件名: 此處假設為myKmsPlugin, 長度為11bytes+1bytes(冒號),
  3. key-len: 2bytes
  4. encrpt(key)為32+2=34 bytes
  5. aes (raw-data)分為: blockSize+len(data)+paddingSize = 16+221+3 = 240byte
    則上述總共加起來大小為: 303 bytes。


[root@k8s-test-master01 ~]# etcdctl  get /registry/secrets/default/secret1 -w=json | jq .
{
  "header": {
    "cluster_id": 14841639068965180000,
    "member_id": 10276657743932975000,
    "revision": 5688234,
    "raft_term": 20
  },
  "kvs": [
    {
      "key": "L3JlZ2lzdHJ5L3NlY3JldHMvZGVmYXVsdC9zZWNyZXQx",
      "create_revision": 5688228,
      "mod_revision": 5688228,
      "version": 1,
      "value": "azhzOmVuYzprbXM6djE6bXlLbXNQbHVnaW46ACJlbiZ71Al+94uK9wUqKhrKzCoykswbx6mgdeL/9OPuj774yFIS06TsmxTf4qYMzWhirz3jz4w9ttBl8eqZZXtqwpH/jUWrRus8uoC4jH7Ezy7nn3tFXZ+ykPb6xfnje0lr9ZsWJ11QHu6wfP27p96tydL84TfG9dgHGYLRYblW5XZU3kNO+YDjlm/ybaDCbn22t6qG2OhDhbEbIpiv/UZuye9NbEIPyHtEFFJHC9QRX+XjjW/kdZUqzgZqMbsHaXa0VqePWpwJH84r+KsDdqZnldiC1qfQ83vdTp1IKtwyEeozkhFiYA4z/0LX6K38jvS3hUT80tacQehn664LeEgHiBGsRPB7M+rSmU6aneUzkqQp"
    }
  ],
  "count": 1
}


[root@k8s-test-master01 ~]# echo azhzOmVuYzprbXM6djE6bXlLbXNQbHVnaW46ACJlbiZ71Al+94uK9wUqKhrKzCoykswbx6mgdeL/9OPuj774yFIS06TsmxTf4qYMzWhirz3jz4w9ttBl8eqZZXtqwpH/jUWrRus8uoC4jH7Ezy7nn3tFXZ+ykPb6xfnje0lr9ZsWJ11QHu6wfP27p96tydL84TfG9dgHGYLRYblW5XZU3kNO+YDjlm/ybaDCbn22t6qG2OhDhbEbIpiv/UZuye9NbEIPyHtEFFJHC9QRX+XjjW/kdZUqzgZqMbsHaXa0VqePWpwJH84r+KsDdqZnldiC1qfQ83vdTp1IKtwyEeozkhFiYA4z/0LX6K38jvS3hUT80tacQehn664LeEgHiBGsRPB7M+rSmU6aneUzkqQp | base64 -d | wc -c
303

綜上所述,使用該測試kms加密secrets對於etcd中每一個secrets在空間上的overhead大小約為160個字節左右。

apiserver 內存overhead

衡量apiserver中用來存放secrets key的cache占用內存大小較為困難,由於只是存放key的明文和密文的對應關系,初步估計不會占用太多空間。因為我們無法區分內存的增長是由於cache內數據增加了還是由於其他的操作申請了更多的內存,此處只能粗略計算。一種合理的測試方式為: 啟動apiserver,不請求secrets,保持該cache為空,等到apiserver內存平穩之后,請求一定數目的secrets, 此時會填充該cache, 對比前后內存的占用量。但是前面提到,apisever內部會有一個secrets informer,在啟動的時候就會list一遍所有的secrets,這導致該cache在啟動是就被填滿了。為了防止啟動時就填充該cache,筆者修改了apiserver代碼,關閉了所有k8s組件請求secrets地方,最后通過壓測模擬,10w 個secrets cache初始化前后的apiserver內存占用,發現cache size大概占用小於200Mib/10w secrets。這個內存占用量還是可以接收的。

請求延時

請求secrets時,如果secrets key時沒有被cache命中,就需要重新獲取加解密的key, 需要重新調用kms服務,所以請求延時主要在於kms的請求延時,這部分也根據不同的kms服務略有差異。此外對於CRUD請求延遲影響較小,影響最大的當屬list請求,不過這些請求訪問量較小,不必特別在意。

結語

雖然是一個小小的改動,但是在上線之前還是要充分弄請求其原理,進行必要的功能測試,壓力測試等,設置合適充分的報警,這樣才能防患於未然。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM