本文介紹阿里雲ACK如何通過ack-kms-agent-webhook-injector組件整合KMS Agent。
概述
架構說明
阿里雲Container Service for Kubernetes (ACK)提供了ack-kms-agent-webhook-injector組件,通過配置特定註解(kms-agent-webhook-injector/inject),將KMS Agent作為Sidecar容器注入Pod,從而允許容器中的應用通過本地HTTP介面,藉助KMS Agent從KMS執行個體擷取憑據並緩衝在記憶體中,避免敏感資訊寫入程式碼,保障資料安全。此外,KMS Agent 的緩衝機制,能提升高並發及網路不穩定情境下訪問KMS的穩定性,最佳化使用體驗。架構圖如下所示:
使用限制
ACK叢集類型限制:支援ACK託管與專有叢集、ACK Serverless叢集。
地區限制:ACK叢集與KMS執行個體需要在同一地區。
效能限制:由於每個Pod獨立運行KMS Agent Sidercar容器,若業務部署大量Pod,在身分識別驗證過程中如果STS Token請求每分鐘超過500次則會觸發限流,進而對KMS Agent的正常工作產生影響。
費用說明
KMS側的費用:
ACK側的費用:
ack-kms-agent-webhook-injector組件本身是完全免費的,但使用該組件的過程中,可能會產生額外的費用。
安裝ack-kms-agent-webhook-injector組件後,會產生一個Webhook服務工作負載,該負載將佔用一定計算資源併產生費用,您可以在設定檔中,對該負載的 CPU 和記憶體使用量進行限制。
當您建立或更新合格工作負載時,ack-kms-agent-webhook-injector將會把KMS Agent以 Sidecar的形式注入到您的容器中,KMS Agent會使用一定計算資源併產生費用。
身份認證方式
支援RRSA和Worker RAM角色兩種認證方式。
(推薦)方式一:RRSA方式
可以實現Pod維度許可權隔離。適用於1.22及以上版本的ACK託管叢集和ACK Serverless叢集。
重要實現Pod維度許可權隔離需要ack-kms-agent-webhook-injector的版本大於等於v0.2.0。
例如,通過RRSA方式,您可以讓部署在app1-dev中的應用,使用app1-rrsa這個角色,訪問KMS中帶有secret:app1標籤的憑據,。部署在app2-dev中的應用,使用app2-rrsa這個角色,訪問KMS中帶有secret:app2標籤的憑據。
方式二:Worker RAM角色方式
可以實現Worker維度許可權隔離。由於ACK Serverless叢集不支援綁定Worker RAM角色,該方式只適用於ACK託管叢集和ACK專有叢集。
例如,您可以通過Worker RAM角色,設定app1-dev和app2-dev中的應用訪問KMS中管理的憑據。同一個Worker裡的應用使用同一個Worker RAM角色,即您無法針對app1-dev和app2-dev設定不同的許可權。
前提條件
已建立KMS執行個體,並將憑據託管在KMS執行個體中。詳細介紹,請參見購買和啟用KMS執行個體、憑據管理快速入門。
已建立ACK叢集,且叢集與您的KMS執行個體在同一地區。請參見建立ACK託管叢集、建立ACK Serverless叢集。
步驟一:配置認證資訊,使KMS Agent可以訪問特定的憑據
方式一:RRSA方式授權
以您有兩個應用app1和app2為例,app1需要訪問KMS執行個體中打標籤secret: app1的憑據,app2需要訪問KMS執行個體中打標籤secret: app2的憑據。
app1的NameSpace為app1-dev,serviceAccountName為app1-service,RAM角色為app1-rrsa。
app2的NameSpace為app2-dev,serviceAccountName為app2-service,RAM角色為app2-rrsa。
啟用ACK RRSA功能。
建立叢集時開啟
建立ACK託管叢集和ACK Edge叢集時,您可以在叢集配置的進階選項(選填)地區,選中開啟RRSA功能。

在叢集資訊頁面開啟
登入Container Service管理主控台,在左側導覽列選擇叢集列表。
在叢集列表頁面,單擊目的地組群名稱,然後在左側導覽列,選擇叢集資訊。
在基本資料頁簽的安全與審計地區,單擊RRSA OIDC右側的開啟。

在彈出的啟用RRSA對話方塊,單擊確定。
在基本資料地區,當叢集狀態由更新中變為運行中後,表明該叢集的RRSA特性已變更完成。
開啟叢集詳情頁,在基本資料頁簽的安全與審計地區,將滑鼠懸浮至RRSA OIDC右側已開啟上面,查看供應商的URL連結和ARN資訊。

為app1建立一個可信實體為身份供應商的RAM角色,並授權其可以訪問標籤為secret: app1的憑據。具體操作,請參見建立可信實體為身份供應商的RAM角色。
登入RAM控制台。
在左側導覽列,選擇,然後在角色頁面,單擊建立角色。
在建立角色面板,選擇可信實體類型為身份供應商,並單擊切換編輯器。

在建立角色頁面的可視化編輯,配置如下角色資訊後,單擊確定。
配置項
描述
效果
選擇允許。
主體
選擇身份供應商。
身份供應商類型:選擇OIDC。
身份供應商:開啟RRSA後,ACK叢集會預設建立身份供應商,命名格式為ack-rrsa-<cluster_id>。其中,<cluster_id>為您的叢集ID。
操作
保持預設。即勾選sts:AssumeRole。
條件
在預設的oidc:iss和oidc:aud限制條件基礎上,新增一個限制條件:
條件鍵:選擇oidc:sub。
運算子:選擇StringEquals。
條件值:system:serviceaccount:<namespace>:<serviceAccountName>。本文樣本為
system:serviceaccount:app1-dev:app1-service。<namespace>:希望注入KMS Agent的應用所在的命名空間。
<serviceAccountName>:希望注入KMS Agent的應用所使用的服務賬戶名稱。服務賬戶名稱為Pod提供身份標識,可以通過RRSA機制與RAM角色動態綁定。
說明若您需要為多個不同命名空間中的不同服務賬戶配置,您可以配置多個值。
在建立角色對話方塊中,設定角色名稱,然後單擊確定。本文角色名稱以app1-rrsa為例。
(可選)查看app1-rrsa這個RAM角色的信任策略。
信任策略表示允許服務賬戶app1-service通過阿里雲RRSA(RAM Roles for Service Accounts) ,在滿足OIDC身分識別驗證條件後,擔任某個RAM角色。

建立權限原則。
權限原則名稱以app1-rrsa-kms-policy為例,策略內容為訪問標籤為secret: app1的憑據。

{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "kms:Decrypt", "kms:GetSecretValue" ], "Resource": "*", "Condition": { "StringEqualsIgnoreCase": { "kms:tag/secret": [ "app1" ] } } } ] }將app1-rrsa-kms-policy權限原則,授權給app1-rrsa角色。具體操作,請參見為RAM角色授權。

為app2建立一個可信實體為身份供應商的RAM角色,並授權其可以訪問標籤為secret: app1的憑據。具體操作,請參見建立可信實體為身份供應商的RAM角色。
登入RAM控制台。
在左側導覽列,選擇,然後在角色頁面,單擊建立角色。
在建立角色面板,選擇可信實體類型為身份供應商,並單擊切換編輯器。
在建立角色頁面的可視化編輯,配置如下角色資訊後,單擊確定。
配置項
描述
效果
選擇允許。
主體
選擇身份供應商。
身份供應商類型:選擇OIDC。
身份供應商:開啟RRSA後,ACK叢集會預設建立身份供應商,命名格式為ack-rrsa-<cluster_id>。其中,<cluster_id>為您的叢集ID。
操作
保持預設。即勾選sts:AssumeRole。
條件
在預設的oidc:iss和oidc:aud限制條件基礎上,新增一個限制條件:
條件鍵:選擇oidc:sub。
運算子:選擇StringEquals。
條件值:system:serviceaccount:<namespace>:<serviceAccountName>。本文樣本為
system:serviceaccount:app2-dev:app2-service。<namespace>:希望注入KMS Agent的應用所在的命名空間。
<serviceAccountName>:希望注入KMS Agent的應用所使用的服務賬戶名稱。服務賬戶名稱為Pod提供身份標識,可以通過RRSA機制與RAM角色動態綁定。
說明若您需要為多個不同命名空間中的不同服務賬戶配置,您可以配置多個值。
在建立角色對話方塊中,設定角色名稱,然後單擊確定。本文角色名稱以app2-rrsa為例。
(可選)查看app2-rrsa這個RAM角色的信任策略。
信任策略表示允許服務賬戶app2-service通過阿里雲RRSA(RAM Roles for Service Accounts) ,在滿足OIDC身分識別驗證條件後,擔任某個RAM角色。
建立權限原則。
權限原則名稱以app2-rrsa-kms-policy為例,策略內容為訪問標籤為secret: app2的憑據。
{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "kms:Decrypt", "kms:GetSecretValue" ], "Resource": "*", "Condition": { "StringEqualsIgnoreCase": { "kms:tag/secret": [ "app2" ] } } } ] }將app2-rrsa-kms-policy權限原則,授權給app2-rrsa角色。具體操作,請參見為RAM角色授權。
方式二:Worker RAM角色方式授權
Worker RAM角色屬於普通服務角色,您可以為Worker節點池指定預設角色或自訂角色。
預設角色:ACK託管叢集會自動建立一個所有節點共用的預設Worker RAM角色。當您通過預設的Worker RAM角色授權時,許可權將會共用給叢集內所有的節點,可能會存在非預期的許可權擴散的風險。
(推薦)自訂角色:需要您提前在存取控制台建立一個普通服務角色,並為節點池指定這個角色。通過為不同的節點池分配特定的角色,可以將每個節點池的許可權隔離開,降低叢集內所有節點共用相同許可權的風險。本文以使用自訂角色為例。
建立普通服務角色。角色名稱以ack-secret-manager為例。
登入RAM控制台,在左側導覽列,選擇。
在角色頁面,單擊建立角色。
在建立角色頁面,選擇信任主體類型為雲端服務,信任主體名稱請選擇Container ServiceKubernetes版,最後單擊確定。

在建立角色對話方塊,輸入角色名稱,單擊確定。
建立如下自訂權限原則。策略名稱稱以ack-secret-manager-policy為例。具體操作,請參見建立自訂權限原則。

{ "Version": "1", "Statement": [ { "Action": [ "kms:GetSecretValue", "kms:Decrypt" ], "Resource": [ "*" ], "Effect": "Allow" } ] }將自訂權限原則ack-secret-manager-policy,授權給自訂角色ack-secret-manager。

建立節點池時,為節點池指定Worker RAM角色。
Worker RAM角色選擇自訂,然後選擇ack-secret-manager。

建立叢集時同步建立節點池,詳細介紹,請參見建立ACK託管叢集、建立ACK Serverless叢集。為已有叢集建立節點池,請參見建立和管理節點池。
步驟二:在ACK叢集中建立Namespace和Service Account
Namespace將ACK叢集劃分為邏輯隔離的虛擬空間,用於區分開發、測試、生產等環境,不同Namespace中的應用預設無法互訪資源,為RAM角色綁定提供物理邊界。
分別建立兩個Namespace,app1-dev和app2-dev。對應的serviceAccountName分別為app1-service和app2-service。
通過YAML檔案建立名為app1-dev的Namespace。
建立YAML,檔案名稱以app1-namespace.yaml為例。
apiVersion: v1 kind: Namespace metadata: name: app1-dev執行如下命令在ACK叢集中建立名為 app1-dev 的 Namespace。
kubectl apply -f app1-namespace.yaml查看Namespace是否建立成功。
kubectl get namespaces若輸出包含
app1-dev,即代表建立成功。
通過YAML檔案建立名為app1-service的Service Account。
建立YAML,檔案名稱以app1-serviceaccount.yaml為例。
apiVersion: v1 kind: ServiceAccount metadata: name: app1-service namespace: app1-dev執行如下命令在
app1-dev中建立一個名為 app1-service 的ServiceAccount。kubectl apply -f app1-serviceaccount.yaml查看 ServiceAccount 是否建立成功。
kubectl get serviceaccount -n app1-dev若輸出包含
app1-service,即代表建立成功。
重複上述操作,建立命名空間app2-dev和服務賬戶app2-service。
步驟三:在ACK叢集中安裝ack-kms-agent-webhook-injector組件
登入Container Service管理主控台,在左側導覽列選擇叢集列表。
在叢集列表頁面,單擊目的地組群名稱,然後在左側導覽列,選擇。
在Helm頁面,單擊建立,配置基本資料,單擊下一步。
配置項
說明
應用程式名稱
請輸入您的應用程式名稱。建議您使用預設應用程式名稱,此處無需輸入,單擊下一步後會提示使用預設應用程式名稱ack-kms-agent-webhook-injector,選擇是即可。
命名空間
選擇應用所在的命名空間。建議您使用預設命名空間,此處無需選擇保持預設,單擊下一步後會提示使用預設命名空間kube-system,選擇是即可。
一個ACK叢集中安裝到一個命名空間即可,無需重複安裝。
來源
預設為應用市場,不支援修改。
Chart
搜尋並選中ack-kms-agent-webhook-injector。
在彈出的對話方塊中確認無誤後,單擊是。
選擇是即使用預設安裝路徑,組件預設安裝在kube-system命名空間中,並以組件名稱發布應用。

在參數配置頁面,完成各項配置,然後單擊確定。
配置項
說明
Chart 版本
建議您選擇最新版本。
參數
agent.auth.roleArn和agent.auth.roleArnMapping參數配置說明:
採用RRSA方式授權時:agent.auth.roleArn為空白,agent.auth.roleArnMapping格式為
<NameSpace>:<serviceAccountName>:<RAM Role ARN>。agent: auth: roleArn: roleArnMapping: app1-dev:app1-service: acs:ram::190325303126****:role/app1-rrsa app2-dev:app2-service: acs:ram::190325303126****:role/app2-rrsa採用Worker RAM角色方式授權:agent.auth.roleArn和agent.auth.roleArnMapping均設定為空白。
請等待約30 秒,進入命名空間(預設為kube-system),在無狀態工作負載中檢查組件狀態,確認組件是否已經就緒。
步驟四:為工作負載注入KMS Agent
通過配置註解將KMS Agent注入Pod,根據您是為建立工作負載的Pod還是為現存工作負載的Pod進行配置,具體操作不同。
以無狀態工作負載Deployment為例。本文樣本中,請為app1-dev和app2-dev分別執行如下操作,進行設定。
進入到目的地組群,在左側導覽列選擇。
建立或修改工作負載時,為添加如下Pod註解:名稱為kms-agent-webhook-injector/inject,值為true(除此之外,填入值1,T,t,True,TRUE亦可視為有效值)。
情境一:通過鏡像建立工作負載
單擊使用鏡像建立,完成各項配置,單擊建立。
填寫進階配置時,請在標籤與註解地區,添加如下Pod註解:名稱填入kms-agent-webhook-injector/inject,值填入true。

情境二:通過 YAML 建立工作負載
單擊使用YAML建立資源,完成各項配置,單擊建立。
編輯 YAML 檔案中的 spec.template.metadata.annotations 一項(若不存在,您需要自主建立),向其中添加索引值 kms-agent-webhook-injector/inject: "true"。其他參數如何配置,請參見工作負載YAML樣本。

情境三:現存工作負載
定位到目標工作負載,單擊操作列的詳情。
在工作負載詳情頁,單擊右上方的YAML編輯按鈕,尋找到 spec.template.metadata.annotations 一項(若不存在,您需要自主建立),向其中添加索引值
kms-agent-webhook-injector/inject: "true"。
單擊更新,等待工作負載重新就緒。
返回無狀態頁面,進入工作負載,可以觀察到容器組頁簽下各個容器,在鏡像列即可看到 KMS Agent 已經作為 Sidecar被注入到您的工作負載中。
重要在容器組頁簽下查看鏡像時,您可能注意到Pod被注入了兩次KMS Agent鏡像,這是因為我們使用了初始化容器(initContainer)來進行必要的初始化工作,該初始化容器在初始化完成後就會終止(Terminated),不會對您的應用產生負面影響,也不會持續佔用您的計算資源。

如果您使用RRSA方式授權,請修改YAML設定檔,將serviceAccountName參數改為步驟一中授權的服務賬戶。
本文樣本為app1-service和app2-service。修改後該應用和KMS Agent能被正確授權訪問KMS憑據。
說明Worker RAM角色方式授權時無需修改設定檔,服務賬戶可自動扮演Worker RAM角色擷取許可權。
步驟五:在應用程式容器中擷取 KMS 憑據
當工作負載注入了KMS Agent後,您可在應用程式容器中,通過HTTP協議向KMS Agent發起請求,擷取儲存在KMS中的憑據。
樣本說明:
本地主機HTTP連接埠號碼取預設值2025,如果您設定為其他連接埠號碼,請將樣本中的2025修改為實際使用連接埠號碼。
token路徑取預設路徑
file:///var/run/kmstoken/token,如果您設定其他路徑,例如file:///var/run/path1/path2,請將樣本中的/var/run/kmstoken/token替換為/var/run/path1/path2。
KMS Agent預設擷取憑據的ACSCurrent 版本。要擷取其他版本的憑據值,您可以設定 versionStage 或 versionId。
KMS Agent只監聽127.0.0.1,即僅允許同一台機器上的應用或進程與其通訊,外部網路裝置無法串連。訪問地址僅支援localhost或127.0.0.1,不支援改為應用的本地IP。以下樣本以localhost為例。
使用curl
實際使用時請將範例程式碼中的<SecretId>替換為您實際的憑據名稱。
# 從檔案讀取 token
curl -v -H "X-KMS-Token:$(</var/run/kmstoken/token)" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>'
# 直接寫 token
curl -v -H "X-KMS-Token:<token>" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>'您可以指定versionStage 或 versionId以擷取特定憑據值。以擷取指定versionId的憑據值為例,使用時請將0a7513ee719da740807b15b77500****替換為您實際的憑據版本。
# 從檔案讀取 token
curl -v -H "X-KMS-Token:$(</var/run/kmstoken/token)" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>&versionId=0a7513ee719da740807b15b77500****'
# 直接寫 token
curl -v -H "X-KMS-Token:<token>" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>&versionId=0a7513ee719da740807b15b77500****'Go程式碼範例
使用時請將範例程式碼中的agent-test替換為您實際的憑據名稱。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
//支援指定versionStage或versionId以擷取特定憑據值。
//以擷取指定versionId的憑據值為例,url := fmt.Sprintf("http://localhost:2025/secretsmanager/get?secretId=%s&versionId=%s", "agent-test", "version-id")。
url := fmt.Sprintf("http://localhost:2025/secretsmanager/get?secretId=%s", "agent-test")
token, err := ioutil.ReadFile("/var/run/kmstoken/token")
if err != nil {
fmt.Printf("error reading token file: %v\n", err)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Printf("error creating request: %v\n", err)
}
req.Header.Add("X-KMS-Token", string(token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("error sending request: %v \n", err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("status code %d - %s \n", resp.StatusCode, string(body))
}