Istio私钥管理利器SDS浅析

简介: ## Why SDS 传统方式下Envoy证书是通过secret卷挂载的方式以文件挂载到sidecar容器中,当证书发生轮转时需要重启服务让Envoy重新加载证书;同时证书私钥在secret中存储并在服务节点外跨节点传输的方式也存在明显的安全漏洞。为此Istio1.1版本后增加了SDS(Secret Discovery Service)API,在Citadel服务的基础上,增加了nodeag

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服务请求签发证书的流程如下:

image.png

  1. Pilot将SDS相关config发送至istio sidecar,比如envoy
  2. 使用指定的serviceaccount向NodeAgent发送请求
  3. nodeagent发送CSR到citadel请求证书签发,支持自定义CA,这里同样使用sa作为访问凭证
  4. Citadel向apiserver认证sa
  5. 如果认证通过,Citadel向NodeAgent返回签发后的证书
  6. 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:

image.png

controller-manager:

image.png

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后证书签发和轮转时组件之间的交互流程:

image.png

这里我们结合代码来分析一下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 在workloadIngressGateway两种工作模式下的使用介绍,我们可以在开启了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相关能力,敬请期待。

相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
消息中间件 Dubbo NoSQL
云原生系列一:Aeraki --- 管理 Istio 服务网格中任何 7 层协议
​ 今天由叶秋学长来介绍如何通过 Aeraki 来在服务网格中为 Dubbo、Thrift 等协议的服务提供七层流量路由、本地限流、全局限流,以及如何基于 Aeraki Protocol快速开发一个自定义协议,并在 Istio 服务网格中对采用自定义协议的服务进行管理。 本篇文章作为系列教程的先导篇,叶秋学长将从全局视角带您了解 Aeraki Mesh。 Aeraki [Air-rah-ki]是希腊语中“微风”的意思。虽然 Istio 在中连接微服务,但 Aeraki 提供了一个框架,允许 Istio 支持更多的第 7 层协议,而不仅仅是 HTTP 和 gRPC。我们希望这股"微风"
306 1
云原生系列一:Aeraki --- 管理 Istio 服务网格中任何 7 层协议
|
Prometheus Kubernetes 负载均衡
如何使用 Istio 进行多集群部署管理:多控制平面
本文摘自于阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实战》一书,讲述了如何使用 Istio 进行多集群部署管理来阐述服务网格对多云环境、多集群即混合部署的支持能力。
如何使用 Istio 进行多集群部署管理:多控制平面
|
Kubernetes Cloud Native Shell
如何使用 Istio 进行多集群部署管理:单控制平面 Gateway 连接拓扑
本文摘自于由阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实践》一书,讲述了如何使用 Istio 进行多集群部署管理来阐述服务网格对多云环境、多集群即混合部署的支持能力。
如何使用 Istio 进行多集群部署管理:单控制平面 Gateway 连接拓扑
|
Kubernetes 负载均衡 Cloud Native
如何使用 Istio 进行多集群部署管理:多控制平面
本文摘自于阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实战》一书,讲述了如何使用 Istio 进行多集群部署管理来阐述服务网格对多云环境、多集群即混合部署的支持能力。
如何使用 Istio 进行多集群部署管理:多控制平面
|
Kubernetes Shell API
如何使用 Istio 进行多集群部署管理(2): 单控制平面 Gateway 连接拓扑
本文摘自于由阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实践》一书,讲述了如何使用 Istio 进行多集群部署管理来阐述服务网格对多云环境、多集群即混合部署的支持能力。
1912 0
如何使用 Istio 进行多集群部署管理(2): 单控制平面 Gateway 连接拓扑
|
Kubernetes 负载均衡 Cloud Native
如何使用 Istio 进行多集群部署管理(1): 单控制平面 VPN 连接拓扑
本文摘自于由阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实践》一书,在展望服务网格未来的同时,讲述了如何使用 Istio 进行多集群部署管理,来阐述服务网格对多云环境、多集群即混合部署的支持能力。
1611 0
如何使用 Istio 进行多集群部署管理(1): 单控制平面 VPN 连接拓扑
|
Kubernetes Cloud Native Serverless
如何使用 Istio 进行多集群部署管理:单控制平面 Gateway 连接拓扑
本文摘自于由阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实践》一书,讲述了如何使用 Istio 进行多集群部署管理来阐述服务网格对多云环境、多集群即混合部署的支持能力。
如何使用 Istio 进行多集群部署管理:单控制平面 Gateway 连接拓扑
|
Kubernetes 负载均衡 Cloud Native
如何使用 Istio 进行多集群部署管理:单控制平面 VPN 连接拓扑
本文摘自于由阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实践》一书,在展望服务网格未来的同时,讲述了如何使用 Istio 进行多集群部署管理,来阐述服务网格对多云环境、多集群即混合部署的支持能力。
如何使用 Istio 进行多集群部署管理:单控制平面 VPN 连接拓扑
|
网络安全 Shell 开发工具
带你读《Istio入门与实战》之三:使用Vagrant管理虚拟机
本书系统化介绍Istio技术要点与应用技巧,可帮助读者快速搭建微服务架构并进行管理。主要内容包括:service mesh基本概念与使用,Istio架构设计与主要功能,快速搭建一个微服务实验,介绍如何让服务流量控制更简单,让服务更具弹性,让服务故障测试更容易,让服务通信更安全可控,让服务更易观测与监控,以及istio维护方案。本书内容丰富、案例讲解,实用性强,非常适合入门级读者快速掌握Istio技术。
|
运维 负载均衡 Kubernetes
从架构到组件,深挖istio如何连接、管理和保护微服务2.0?
近几年我一直从事于微服务系统的设计以及实现方面的工作,属于微服务架构一线实践者。之前做过一些单体系统的微服务改造,在微服务拆分、治理等方面都有一定的经验。 本人比较特殊一点的经历是既做过 IT 领域的微服务,也做过 CT(通讯领域)的微服务,微服务架构在这两个领域的具体形态和要求是不太一样的,但其中一些思想是互通且是很有借鉴意义的。
1892 0