使用 External Secrets Operator 管理 Kubernetes 的 Secret

语言: CN / TW / HK

Kubernetes 的 Secret 机制允许我们将敏感信息存储中央存储库 etcd 中,这是一种比在 Pod 定义或容器镜像中存储信息更安全的方式。然而,Kubernetes 目前还没有能力管理 Secret 的生命周期,所以有时候我们需要使用外部系统来管理这些敏感信息。随着我们需要管理的 Secret 数量的增长,我们可能需要额外的工具来简化和更好地管理它们。在本文中,我们将详细介绍其中的一种工具 External Secrets Operator

什么是 Secret

Secret 是用于管理人到应用程序以及应用程序到应用程序访问权限的数字凭证。它们可以以密码、加密密钥、令牌等形式存在。

什么是 Secret 管理

Secret 管理就是指安全地管理数字凭证的创建、存储、轮换和撤销,同时消除或至少尽量减少人为的参与,并减少潜在的错误来源。

什么是 Kubernetes Secret

容器需要访问敏感数据来执行基本操作,如与数据库、API 和其他系统集成。在 Kubernetes 中,Secret 是包含数字凭证(如密码、令牌或密钥)的对象,使用 Secret 可以避免在 Pod 定义或容器镜像中存储敏感信息。

问题分析

我们都知道如何使用 Secret 连接到外部服务。下面是一个简单的使用 Secret 连接数据库的架构示例。

我们有一个微服务(或者单体,如果你愿意的话),它使用 Secret(用户名和密码)连接数据库。

当你开始支持开发、测试和生产等多种环境时,管理和同步所有这些 Secret 就变得有点困难了。

现在,想象一下你将应用程序拆分为多个服务,每个服务都有自己的外部依赖,比如数据库、第三方 API 等,这会导致架构变得更复杂。

要在 Kubernetes 中搭建上述的多服务环境将面临许多挑战,包括:

  • 你可能需要管理数百个 Secret。

  • 管理 Secret 的生命周期(如创建、存储、轮换和撤销)变得很困难。

  • 引入新服务和具有特定访问权限的用户变得越来越困难。

  • 你必须考虑如何安全地分发 Secret。基于上述的原因,你可以考虑选择第三方 Secret 管理工具来减轻与管理 Kubernetes Secret 相关的工作量。

一些流行的工具和供应商如下:

  • 云供应商:AWS Secrets Manager、Google Secret Manager、Azure Key Vault、IBM Cloud Secrets Manager、Oracle Key Vault;

  • 开源工具:HashiCorp Vault。我们需要的是一个简单的解决方案,至少能够解决其中的一些问题,将存储在外部 Secret 管理工具中的 Secret 带到我们的集群中,并在我们的应用程序中继续使用 Kubernetes 的 Secret。这意味着我们需要一个组件将外部 Secret 信息同步到集群中,而这就是 External Secrets Operator 的亮点所在。

Operator 设计模式

在深入了解 External Secrets Operator 之前,先让我们来快速回顾一下什么是 Kubernetes Operator。

我们已经知道,每个 Kubernetes 集群都有一个理想的状态。这个状态决定了应该运行哪些工作负载(Pod、部署等)、这些工作负载应该使用哪些镜像,以及这些工作负载应该使用哪些资源。控制器是集群中的控制循环,它监控对象的当前状态,将其与期望的状态进行比较,并根据需要对其进行修改。我们也将这些控制循环称为调和循环。

下面是这个过程的一般示意图。

这种使用声明式状态和控制器管理应用程序和基础设施资源的过程被称为 Operator 设计模式。有时候,控制器和 Operator 这两个术语可以互换使用。二者的不同之处在于,Operator 具有特定领域知识,知道如何通过读取所需的定义和使用控制器更新集群来创建和管理资源。

什么是 External Secrets Operator(ESO)

ESO 是一种 Kubernetes Operator,它连接到我们上面提到的外部 Secret 管理系统,读取 Secret 信息并将它们注入到 Kubernetes 的 Secret 中。它是自定义 API 资源的集合,为管理 Secret 生命周期的外部 API 提供了抽象。

External Secrets Operator 的结构

与所有其他 Kubernetes Operator 一样,ESO 由以下几个主要部分组成:

  • 自定义资源定义(Custom Resource Definitions,CRD)——它们定义了 Operator 可用的配置选项的数据模式,在我们的示例中是 SecretStoreExternalSecret 定义。

  • 可编程结构——它们使用所选的编程语言(在我们的例子中是 Go)定义与上面的 CRD 相同的数据模式。

  • 自定义资源(Custom Resource,CR)——它们包含 CRD 定义的值,并描述 Operator 的配置。

  • 控制器——控制器操作自定义资源,并负责创建和管理资源。它们可以用任何编程语言构建,ESO 的控制器是用 Go 构建的。

外部 Secret 提供程序

ESO 使用不同的提供程序连接到外部 Secret 管理系统,并将 Secret 拉入集群。这些提供程序是通过 SecretStore 和 ExternalSecret 资源配置的,稍后我们将介绍它们。你可以在 这里 找到我们所使用的提供程序的源代码。

Secret 提供程序的结构其实很简单:

type Provider interface{  //通过NewClient构造一个SecretsManagerProvider  NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error)
//ValidateStore方法检查提供的Secret存储是否有效 ValidateStore(store GenericStore) error}

复制代码

正如你在上面看到的,每个提供程序都提供了用于验证存储配置和实例化 SecretsClient 对象的函数。

SecretsClient 实例负责验证 Secret 配置,并以各种形式提取 Secret:

type SecretsClient interface{  GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error)
Validate() (ValidationResult, error)
GetSecretMap(ctx context.Context, ref ExternalSecretDataRemoteRef) (map[string][]byte, error)
GetAllSecrets(ctx context.Context, ref ExternalSecretFind) (map[string][]byte, error)
Close(ctx context.Context) error}

复制代码

让我们来看看之前提到的资源类型是如何同步外部 Secret 的。

SecretStore 资源

你可以通过 SecretStore 资源配置想要访问的外部 Secret 管理服务,并通过指定身份验证所需的配置来访问它。

下面是访问 AWS Secrets Manager 的配置示例:

apiVersion: external-secrets.io/v1beta1kind: SecretStoremetadata:  name: secretstore-samplespec:  provider:    aws:    service: SecretsManager    region: us-east-1    auth:      secretRef:        accessKeyIDSecretRef:          name: awssm-secret          key: access-key        secretAccessKeySecretRef:          name: awssm-secret          key: secret-access-key

复制代码

ExternalSecret 资源

SecretStore 定义了如何访问 Secret,ExternalSecret 资源则定义应该获取哪些 Secret。它持有 SecretStore 引用,因此 ESO 的控制器可以使用 ExternalSecret 资源(调用 SecretStore 资源指定的配置)来创建 Kubernetes Secret。

下面是使用 secretStoreRef 属性连接这两个资源的示例:

apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata:  name: examplespec:  refreshInterval: 1h  secretStoreRef:    name: secretstore-sample    kind: SecretStore  target:    name: secret-to-be-created    creationPolicy: Owner  data:  - secretKey: secret-key-to-be-managed    remoteRef:      key: provider-key      version: provider-key-version      property: provider-key-property  dataFrom:  - extract:      key: remote-key-in-the-provider

复制代码

在应用程序开始启动时,所有的提供程序都将自己注册到 ESO。注册动作就是将提供程序对象及其配置信息添加到 Map 中。当 ESO 控制器需要访问 Secret 存储时,它就使用这个 Map 来查找存储。在创建自己的 Secret 提供程序时,我们也会遵循同样的实现规范。

ESO 如何同步 Secret

正如我们在上面的 Operator 设计模式小节中所讲的那样,控制器通过无限循环来调和集群的当前状态和期望状态之间的漂移。ESO 控制器也不例外。在每一次调和循环中, 外部Secret控制器 会执行以下这些操作。

  1. 为当前调和循环读取外部 Secret 配置;

  2. 通过 secretStoreRef 属性获取被外部 Secret 配置引用的 SecretStore;

  3. 使用存储定义中的提供程序名称查找上面提到的提供程序 Map,找到与 Secret 关联的提供程序;

  4. 使用存储提供程序名称实例化一个 Secret 客户端;

  5. 使用 Secret 客户端从外部系统获取 Secret 数据;

  6. 如果没有 Secret 数据返回,且删除策略被设置为“Delete”,就会从集群中删除 Secret 数据。如果删除策略被设置为“Retain”,则 Secret 将保持原样;

  7. 假设成功获取到了外部 Secret,就会在集群中创建 Kubernetes 密钥,并被应用到任意指定的模板中。

创建一个简单的 ESO 提供程序

本小节的目标是创建一个非常简单的 ESO 提供程序。请记住,我们在这里所做的绝对不适合用在生产环境中。要获得更优雅的、可用于生产环境的解决方案,可以在理解了如何添加提供程序之后查看提供程序的源代码。

以下是向 ESO 中添加新 Secret 提供程序的步骤。

  1. 为新的 Secret 提供程序添加配置模式;

  2. 创建类型定义,将 CRD 定义映射到 Go 语言的结构体;

  3. 添加提供程序实现;

  4. 在 register.go 中注册新的提供程序。

  5. 创建并部署。

一个简单的 Secret 管理服务

为了让本教程尽可能简单,并且考虑到 ESO 已经涵盖了大多数用于管理 Secret 的常见外部系统,我们将在本教程中使用 Node.js Express 作为 Secret 服务器。

下面是服务的实现。

const express = require('express');const router = express.Router(); const keys = []; /* GET keys listing as a JSON array */router.get('/', (req, res, next) => {   res.send(keys);}); /* GET a single key as a JSON object. */router.get('/:key', (req, res) => {   const key = keys.find(k => k.key === req.params.key);   res.send(key);}) module.exports = router;

复制代码

添加新的 CRD 定义

我们需要让 Kubernetes 知道新提供程序的配置。这是自定义资源的最小定义。

express:  description: Configuration to sync secrets using Express provider  properties:    host:      type: string  required:    - host  type: object

复制代码

这个定义应该与其他 CRD 一样添加到 deploy/crds/bundle.yaml。新的提供程序只有一个配置属性 host,它告诉提供程序 Secret 服务在哪里。

为提供程序配置创建类型

为了让提供程序从控制器获取配置,我们还需要添加必要的类型,将配置转换为 Go 语言的结构体。

package v1beta1 type ExpressProvider struct {   Host string `json:"host"`}

复制代码

可以看到,CRD 的配置与上面的结构体是相匹配的。在运行时,提供程序将接收到上述结构的配置。

实现提供程序

我们的提供程序需要实现 Provider 和 SecretClient 接口。基本上,我们需要创建一个 SecretClient 实例并将它返回。我们需要实现 SecretClient 的 GetSecret 函数。我们还可以添加验证逻辑来检查存储的配置是否正确。下面是提供程序的基本实现。

package express import (   "context"   "encoding/json"   "fmt"   esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"   "io/ioutil"   "log"   "net/http"   "net/url"   "sigs.k8s.io/controller-runtime/pkg/client"   "time") const (   errNilStore              = "nil store found"   errMissingStoreSpec      = "store is missing spec"   errMissingProvider       = "storeSpec is missing provider"   errInvalidProvider       = "invalid provider spec. Missing express field in store %s"   errInvalidExpressHostURL = "invalid express host URL") // this struct will hold the keys that the service returnstype keyValue struct {   Key   string `json:"key"`   Value string `json:"value"`} type Provider struct {   config  *esv1beta1.ExpressProvider   hostUrl string}
// NewClient this is where we initialize the SecretClient and return it for the controller to usefunc (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) { config := store.GetSpec().Provider.Express return &Provider{ config: config, hostUrl: config.Host, }, nil} func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { return nil, fmt.Errorf("GetAllSecrets not implemented")} // GetSecret reads the secret from the Express server and returns it. The controller uses the value here to// create the Kubernetes secretfunc (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { expressClient := http.Client{ Timeout: time.Second * 5, } req, err := http.NewRequest(http.MethodGet, p.hostUrl+"/keys/"+ref.Key, nil) if err != nil { log.Fatal(err) } fmt.Printf("Sending request to: %s\n", p.hostUrl+"/keys/"+ref.Key) res, getErr := expressClient.Do(req) if getErr != nil { return nil, fmt.Errorf("error getting the secret %s", ref.Key) } if res.Body != nil { defer res.Body.Close() } body, readErr := ioutil.ReadAll(res.Body) if readErr != nil { return nil, fmt.Errorf("error reading secret %s", ref.Key) } fmt.Printf("body: %s\n", body) secret := keyValue{} jsonErr := json.Unmarshal(body, &secret) if jsonErr != nil { return nil, fmt.Errorf("bad key format: %s", ref.Key) } return []byte(secret.Value), nil} // ValidateStore validates the store configuration to prevent unexpected errorsfunc (p *Provider) ValidateStore(store esv1beta1.GenericStore) error { if store == nil { return fmt.Errorf(errNilStore) }
spec := store.GetSpec() if spec == nil { return fmt.Errorf(errMissingStoreSpec) } if spec.Provider == nil { return fmt.Errorf(errMissingProvider) } provider := spec.Provider.Express if provider == nil { return fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String()) } hostUrl, err := url.Parse(provider.Host) if err != nil { return fmt.Errorf(errInvalidExpressHostURL) } if hostUrl.Host == "" { return fmt.Errorf(errInvalidExpressHostURL) } return nil}
// registers the provider object to process on each reconciliation loopfunc init() { esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{ Express: &esv1beta1.ExpressProvider{}, })}

复制代码

将提供程序注册到提供程序列表中

下一步是在 register.go 中导入提供程序模块,用于初始化它的函数。

package register import (   _ "github.com/external-secrets/external-secrets/pkg/provider/express")

复制代码

部署用于测试的 ESO

ESO 文档描述了将 ESO 部署到 Kubernetes 集群所需的步骤。不过,因为我们是在本地运行,所以可以通过手动运行 Makefile 中定义的任务来加快开发和测试过程。

首先部署 CRD。

make crds.install

复制代码

然后在本地运行 ESO。

make run

复制代码

用 Secret 来测试提供程序

为了测试提供程序,我们需要将 SecretStore 和 ExternalSecret 配置部署到集群中。SecretStore 配置将指向 Express 服务器,ExternalSecret 配置将把存储在 Express 服务器中的 Secret 映射成 Kubernetes Secret。

apiVersion: external-secrets.io/v1beta1kind: SecretStoremetadata: name: secretstore-expressspec: provider:   express:     host: http://express-secrets-service---apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: express-external-secretspec: refreshInterval: 1h  secretStoreRef:   kind: SecretStore   name: secretstore-express  target:   name: my-express-secret   creationPolicy: Owner  data:   - secretKey: secretKey # Key given to the secret to be created on the cluster     remoteRef:       key: my-secret-key

复制代码

部署上面的清单。

kubectl apply -f secret.yaml 

复制代码

如果一切都进展得很顺利,这个 Secret 应该会出现在 Kubernetes 集群中。

kubectl get secret my-express-secret -o yam

复制代码

下面是 Kubernetes API 的输出。

apiVersion: v1data:  secretKey: dGhpcy1pcy1hLXNlY3JldA==immutable: falsekind: Secret

复制代码

总结

在本文中,我们解释了为什么要使用 External Secrets Operator,并展示了如何开发外部 Secret 提供程序。External Secrets Operator 是一个用于在多租户和多服务环境中管理 Secret 的强大工具,许多 组织 都在生产环境中使用它。

作者简介:

Önsel Akin 是一名拥有 25 年软件开发经验的软件架构师。他曾身兼数职,与开发、设计思维和产品开发团队密切合作。他曾在许多大型软件开发公司工作,担任软件工程师和软件架构师。他喜欢玩《万智牌》,有时间也会设计手机游戏。他是 Container Solutions 的云原生工程师。

原文链接:

Managing Kubernetes Secrets with the External Secrets Operator