Why SDS
傳統方式下Envoy證書是通過secret卷挂載的方式以檔案挂載到sidecar容器中,當證書發生輪轉時需要重新開機服務讓Envoy重新加載證書;同時證書私鑰在secret中存儲并在服務節點外跨節點傳輸的方式也存在明顯的安全漏洞。為此Istio1.1版本後增加了SDS(Secret Discovery Service)API,在Citadel服務的基礎上,增加了nodeagent元件,以ds的形式部署在每個節點上,而SDS服務由nodeagent管理,在每一個節點上會啟動實作了SecretDiscoveryService 這個gRPC服務的SDS服務端,同時在一個節點上的sds server和client之間通過Unix domian socker通訊。nodeagent除了支援對接Citadel發送證書簽發請求外,還可以對接Vault,GoogleCA等證書簽發元件。
通過如上設計給整個mesh系統帶來了如下好處:
• envoy可以動态擷取輪轉後的證書而無需重新開機
• 安全性提升:私鑰不出節點傳輸;證書在memory中傳遞而無需落盤
sds服務請求簽發證書的流程如下:

- Pilot将SDS相關config發送至istio sidecar,比如envoy
- 使用指定的serviceaccount向NodeAgent發送請求
- nodeagent發送CSR到citadel請求證書簽發,支援自定義CA,這裡同樣使用sa作為通路憑證
- Citadel向apiserver認證sa
- 如果認證通過,Citadel向NodeAgent傳回簽發後的證書
- SDS傳回證書内容給sidecar
SDS安裝
Enable Service Account Token Volumes
1.12版本前應用pod中使用的serviceaccount中對應的JWT token是永不過期的,也就是說直到sa被删除前該token都可以被用來請求apiserver,也就是說如果sa發生洩漏,應用管理者需要删除所有關聯的secret并重新開機服務。除此之外,傳統方式下每一個serviceaccount都需要存儲在一個對應的secret,而具有secret讀取權限的元件或人員可以擷取到所有其可見範圍内的sa token,比如ingress controller需要由路由TLS相關secret的讀取權限,但是同時它也能檢視所有應用中使用到的sa token。這樣的情況導緻sa的洩露變得難以防範,是以k8s 1.12後ServiceAccountTokenVolumeProjection成為了beta特性,開啟了該特性的叢集允許kubelet将sa以projected volume的形式挂載到pod中,當pod删除時,相應的sa token也會同時删除。同時kubelet的token manager會自動重新整理臨近過期的token,以及增加對token audiences的校驗。該特性大大增強了pod在使用sa作為apiserver通路憑證的安全性。
有關ServiceAccountTokenVolumeProjection特性的開啟和使用可參見
https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projectionBoundServiceAccountTokenVolume
在1.13後成為alpha的feature-gate,注意在apiserver和controller-manager中均需要開啟該feature-gate,相關配置如下所示:
apiserver:
controller-manager:
helm安裝
這裡參考
官方文檔進行sds的定制化helm安裝,這裡通過将sds中的enable置為true開啟sds的安裝,同時需要指定
udsPath
和token中用于認證的
audience
參數
helm repo add istio.io https://storage.googleapis.com/istio-release/releases/1.4.2/charts/
kubectl apply -f install/kubernetes/helm/helm-service-account.yaml
helm init --service-account tiller
helm install install/kubernetes/helm/istio-init --name istio-init --namespace istio-system
kubectl -n istio-system wait --for=condition=complete job --all
helm install install/kubernetes/helm/istio --name istio --namespace istio-system --values install/kubernetes/helm/istio/values-istio-sds-auth.yaml
SDS源碼解析
下圖描述了啟用sds後證書簽發和輪轉時元件之間的互動流程:
這裡我們結合代碼來分析一下sds具體的工作流程:首先sds的啟動代碼在
security/cmd/node_agent_k8s/main.go
中,這裡首先會根據sds配置類型建立secretCache執行個體并在
NewServer
方法中啟動服務端。
workloadSecretCache, gatewaySecretCache := newSecretCache(serverOptions)
if workloadSecretCache != nil {
defer workloadSecretCache.Close()
}
if gatewaySecretCache != nil {
defer gatewaySecretCache.Close()
}
server, err := sds.NewServer(serverOptions, workloadSecretCache, gatewaySecretCache)
if err != nil {
log.Errorf("failed to create sds service: %v", err)
return fmt.Errorf("failed to create sds service")
}
在初始化SecretCache的過程中,首先會根據sds服務的兩種模式(預設Workload,選裝IngressGateway)執行個體化不同的SecretFetcher,這裡的SecretFetcher相當于sds的用戶端,可以從不同的目标secret中擷取到對應的證書内容;之後基于SecretFetcher封裝一個secretCache執行個體,用于在memory中以
sync.map
的形式緩存不同應用或ingressgateway的證書内容,同時還存儲了根證書,證書變更需要觸發的回調函數和一些證書相關變更的計數統計等資訊。SecretCache的初始化函數如下所示:
// newSecretCache creates the cache for workload secrets and/or gateway secrets.
// Although currently not used, Citadel Agent can serve both workload and gateway secrets at the same time.
func newSecretCache(serverOptions sds.Options) (workloadSecretCache, gatewaySecretCache *cache.SecretCache) {
if serverOptions.EnableWorkloadSDS {
wSecretFetcher, err := secretfetcher.NewSecretFetcher(false, serverOptions.CAEndpoint,
serverOptions.CAProviderName, true, []byte(serverOptions.VaultTLSRootCert),
serverOptions.VaultAddress, serverOptions.VaultRole, serverOptions.VaultAuthPath,
serverOptions.VaultSignCsrPath)
...
workloadSecretCache = cache.NewSecretCache(wSecretFetcher, sds.NotifyProxy, workloadSdsCacheOptions)
} else {
workloadSecretCache = nil
}
if serverOptions.EnableIngressGatewaySDS {
gSecretFetcher, err := secretfetcher.NewSecretFetcher(true, "", "", false, nil, "", "", "", "")
if err != nil {
log.Errorf("failed to create secretFetcher for gateway proxy: %v", err)
os.Exit(1)
}
gatewaySecretChan = make(chan struct{})
gSecretFetcher.Run(gatewaySecretChan)
gatewaySecretCache = cache.NewSecretCache(gSecretFetcher, sds.NotifyProxy, gatewaySdsCacheOptions)
} else {
gatewaySecretCache = nil
}
return workloadSecretCache, gatewaySecretCache
}
首先我們來看下SecretFetcher的執行個體化流程,如果是ingressGateway模式的agent,這裡會在
InitWithKubeClient
方法建立一個指定ns下的secret的informer,同時啟動對應的controller,在eventHandler中注冊相應的處理函數用于證書在fetcher執行個體中證書緩存對應的動态更新。如果是在預設的workload模式下,會調用
nodeagent/caclient/client.go
中的
NewCAClient
方法去初始化證書擷取的用戶端,這裡會根據nodeagent對接的不同的後端CA簽發機構去處理,支援包括Vault,googleCA和預設的citadel。
// NewSecretFetcher returns a pointer to a newly constructed SecretFetcher instance.
func NewSecretFetcher(ingressGatewayAgent bool, endpoint, caProviderName string, tlsFlag bool,
tlsRootCert []byte, vaultAddr, vaultRole, vaultAuthPath, vaultSignCsrPath string) (*SecretFetcher, error) {
ret := &SecretFetcher{}
if ingressGatewayAgent {
ret.UseCaClient = false
cs, err := kube.CreateClientset("", "")
...
ret.FallbackSecretName = ingressFallbackSecret
ret.InitWithKubeClient(cs.CoreV1())
} else {
caClient, err := ca.NewCAClient(endpoint, caProviderName, tlsFlag, tlsRootCert,
vaultAddr, vaultRole, vaultAuthPath, vaultSignCsrPath)
...
ret.UseCaClient = true
ret.CaClient = caClient
}
return ret, nil
}
我們以預設的citadel為例,這裡會從指定的configmap istio-security中讀取根證書,之後基于指定的grpc協定建立與Citadel連接配接的用戶端,代碼如下:
case citadelName:
cs, err := kube.CreateClientset("", "")
if err != nil {
return nil, fmt.Errorf("could not create k8s clientset: %v", err)
}
controller := configmap.NewController(namespace, cs.CoreV1())
rootCert, err := getCATLSRootCertFromConfigMap(controller, retryInterval, maxRetries)
if err != nil {
return nil, err
}
return citadel.NewCitadelClient(endpoint, tlsFlag, rootCert)
在完成了secretFetcher的執行個體化後,我們再來看下如何建構sds服務端對應的secretCache,在secretCache中封裝了一組重要的接口
SecretManager
,包含了sds服務端進行證書管理的主要邏輯,接口如下:
// SecretManager defines secrets management interface which is used by SDS.
type SecretManager interface {
// GenerateSecret generates new secret and cache the secret.
GenerateSecret(ctx context.Context, connectionID, resourceName, token string) (*model.SecretItem, error)
// ShouldWaitForIngressGatewaySecret indicates whether a valid ingress gateway secret is expected.
ShouldWaitForIngressGatewaySecret(connectionID, resourceName, token string) bool
// SecretExist checks if secret already existed.
// This API is used for sds server to check if coming request is ack request.
SecretExist(connectionID, resourceName, token, version string) bool
// DeleteSecret deletes a secret by its key from cache.
DeleteSecret(connectionID, resourceName string)
}
這裡我們主要了解一下證書簽發的邏輯,首先接口實作GenerateSecret中會調動
generateSecret
函數,該函數包含了證書簽發的主要邏輯,包括CSR的建立,然後通過
sendRetriableRequest
方法去嘗試通過之前執行個體化後的CaClient中的CSRSign方法完成證書的簽發,我們可以在
nodeagent/caclient/providers/
下找到對接不同後端的CaClient對應實作。
精簡後的SecretCache的初始化方法如下,對于預設workload模式下的服務端,這裡同樣需要為fetcher中的informer controller實作不同的事件處理函數。
// NewSecretCache creates a new secret cache.
func NewSecretCache(fetcher *secretfetcher.SecretFetcher, notifyCb func(ConnKey, *model.SecretItem) error, options Options) *SecretCache {
ret := &SecretCache{
fetcher: fetcher,
closing: make(chan bool),
notifyCallback: notifyCb,
rootCertMutex: &sync.Mutex{},
configOptions: options,
randMutex: &sync.Mutex{},
}
...
fetcher.AddCache = ret.UpdateK8sSecret
fetcher.DeleteCache = ret.DeleteK8sSecret
fetcher.UpdateCache = ret.UpdateK8sSecret
...
go ret.keyCertRotationJob()
return ret
}
在函數的最後啟動了一個證書輪轉的任務,任務中的rotate方法會以固定的間隔時間不斷地周遊secretCache中緩存map的所有證書内容,如果有即将過期的證書(預設1小時前),同樣會觸發上面提到的
generateSecret
函數去重新簽發證書并更新到緩存中。
在完成了secretCache的執行個體化後就可以啟動sds服務端,這裡secretCache執行個體會被傳入到Server端對應的sdsService中,在service中會基于sbs_pb.go中的grpc協定實作對應的證書Secret請求方法。最後根據不同的啟動模式在相應的initService方法中啟動grpc服務端并開始服務。
func NewServer(options Options, workloadSecretCache, gatewaySecretCache cache.SecretManager) (*Server, error) {
s := &Server{
workloadSds: newSDSService(workloadSecretCache, false, options.UseLocalJWT, options.RecycleInterval),
gatewaySds: newSDSService(gatewaySecretCache, true, options.UseLocalJWT, options.RecycleInterval),
}
if options.EnableWorkloadSDS {
if err := s.initWorkloadSdsService(&options); err != nil {
sdsServiceLog.Errorf("Failed to initialize secret discovery service for workload proxies: %v", err)
return nil, err
}
sdsServiceLog.Infof("SDS gRPC server for workload UDS starts, listening on %q \n", options.WorkloadUDSPath)
}
if options.EnableIngressGatewaySDS {
if err := s.initGatewaySdsService(&options); err != nil {
sdsServiceLog.Errorf("Failed to initialize secret discovery service for ingress gateway: %v", err)
return nil, err
}
sdsServiceLog.Infof("SDS gRPC server for ingress gateway controller starts, listening on %q \n",
options.IngressGatewayUDSPath)
}
version.Info.RecordComponentBuildTag("citadel_agent")
if options.DebugPort > 0 {
s.initDebugServer(options.DebugPort)
}
return s, nil
}
使用小結
Istio官方文檔中介紹了sds 在
workload和
IngressGateway兩種工作模式下的使用介紹,我們可以在開啟了SDS的istio叢集中手工驗證。這裡我們總結下使用SDS模式後ingress gateway agent支援了如下特性:
• 不需要重新開機ingress gateway即可增加,删除或更新其對應使用的證書
• 無需使用挂載volume的方式引用證書,seceret内容不落盤
• gateway agent支援配置多host證書對
對于開啟了SDS後的應用負載sidecar也帶來了如下優點:
• 應用私鑰隻存在于Citadal agent和Envoy記憶體中,絕不會出節點傳輸
• 無需依賴Kubernetes Secret使用挂載volume的方式引用證書
• Sidecar Envoy會通過SDS API動态輪轉證書而無需重新開機
在後續容器服務Istio叢集也會支援開啟SDS相關能力,敬請期待。