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-projection
BoundServiceAccountTokenVolume
在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相关能力,敬请期待。