Istio私钥管理利器SDS浅析

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: ## 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 在[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相关能力,敬请期待。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
Kubernetes 负载均衡 网络协议
【Kubernetes中Gateway和ServiceEntry使用、SDS认证授权等使用】
【Kubernetes中Gateway和ServiceEntry使用、SDS认证授权等使用】
|
存储 运维 Kubernetes
Kubernetes密钥管理安全方案和最佳实践
众所周知,Kubernetes作为编排引擎为应用开发者提供了Secrets模型用于在应用Pod中加载和使用敏感信息(如数据库密码、应用证书、认证token等)。Secrets的使用对于K8s开发者来说应该已经比较熟悉了,下面是一些Secrets相关的基本概念:Secrets是一个namespace维度的模型,结合K8s RBAC访问控制可以实现集群内namespace维度的读写隔离Secrets可
1006 0
Kubernetes密钥管理安全方案和最佳实践
|
3月前
|
Kubernetes 安全 数据安全/隐私保护
Kubernetes 安全性最佳实践
【8月更文第29天】随着容器化和微服务架构的普及,Kubernetes 已成为管理容器化应用的标准平台。然而,随着 Kubernetes 的广泛采用,其安全性问题也日益受到关注。本文将深入探讨 Kubernetes 的安全最佳实践,并通过具体的代码示例来展示如何保护 Kubernetes 集群免受攻击。
166 2
|
4月前
|
Kubernetes 负载均衡 调度
Kubernetes的主要用途是什么?
【7月更文挑战第2天】Kubernetes的主要用途是什么?
167 1
|
Kubernetes 前端开发 API
Kubernetes 认证机制学习
# 引言 Kubernetes API Server 组件是 Kubernetes 具有网关性质的组件,它是 Kubernetes 集群资源操作的唯一入口,它通过 HTTP RESTful 的形式暴露服务,允许不同的用户、外部组件等访问它。我们使用 curl 命令去模拟访问 apisever 请求过程中,发生了什么。 ```bash iZj6ccqyhc7xduup9vl8mvZ :: ~ »
|
存储 Kubernetes 安全
一文搞懂基于 Kubescape 进行 Kubernetes 安全加固
Hello folks! 今天我们介绍一款开源容器平台安全扫描工具 - Kubescape。作为第一个用于测试 Kubernetes 集群是否遵循 NSA-CISA 和 MITREATT&CK 等多个框架安全部署规范的开源工具,Kubescape 在整容器编排生态中具有举足轻重的意义。在这篇文章中,我们将解析什么是 Kubernetes 加固以及如何基于 Kubescape 工具进行 Kubernetes 生态体系加固。
302 0
|
自然语言处理 Kubernetes 网络协议
在阿里云安装初试 Istio| 学习笔记
快速学习在阿里云安装初试 Istio
在阿里云安装初试 Istio| 学习笔记
|
存储 Kubernetes API
kubernetes代码阅读-apiserver基础篇
apiserver是整个kubernetes的核心模块,做的事情多,代码量也较大。市面上已经有不少apiserver代码解读的文章了,但问题在于,由于k8s的代码变化很快,想写一篇长久能用的未必能做到。
2080 0
|
存储 Kubernetes 安全
Kubernetes:从4个方面增强Kubernetes环境的安全性
Kubernetes:从4个方面增强Kubernetes环境的安全性
326 0
Kubernetes:从4个方面增强Kubernetes环境的安全性
|
存储 Kubernetes API
十一、kubernetes 核心技术-Secret
kubernetes 核心技术-Secret
158 1