使用 External Secrets Operator 安全管理 Kubernetes Secrets
关键要点
· Kubernetes Secret 管理有助于将 Secret 与应用程序代码分离,并在需要时在集群中启用它们。
· 默认情况下,Secret 以不安全的 base64 形式存储,这是一种编码方法而不是加密。
· 第三方 Secret 管理系统是拥有集中、强大的 Secret 管理机制的更好的选择。
· ESO 是一个 Kubernetes Operator,它集成了外部 Secret 管理系统,例如 AWS Secrets Manager、HashiCorp Vault、Google Secret Manager、Azure Key Vault 等。
· External Secrets Operator 的目标是将来自外部 API 的 Secret 同步到 Kubernetes。
Kubernetes Secret 未加密地存储在 API 服务器的底层数据存储(etcd)中。任何拥有 API 访问权限的人都可以检索或修改 Secret,任何有权访问 etcd 的人也可以。Secret 对象类型用来保存敏感信息,例如密码、OAuth 令牌和 ssh key。
将这些信息放在 Secret 中比放在 pod 的定义中或者 docker 镜像中来说更加安全和灵活。然而,Kubernetes 尚不具备管理 Secret 生命周期的能力,因此有时我们需要外部系统来管理这些敏感信息。一旦我们需要管理的 Secret 信息量增加,我们可能需要额外的工具来简化和更好地管理流程。在本文中,我们将详细介绍其中一个工具 External Secrets Operator (https://external-secrets.io/)。
什么是Secret?
Secret 是用于管理用户到应用程序和应用程序到应用程序级别的访问权限的数字凭证。它们可以是密码、加密密钥、令牌等形式。
什么是Secrets Management?
Secret Management 是关于安全地管理数字凭证的创建、存储、轮换和撤销,同时消除或至少最大限度地减少人为参与并减少潜在的风险。
什么是 Kubernetes Secret?
容器需要访问敏感数据才能执行基本操作,例如与数据库、API 和其他系统集成。就 Kubernetes 而言,Secret 是一个包含数字凭证的对象,例如密码、令牌或密钥。使用Secret消除了将敏感信息存储在 pod 规范或容器映像中的需要。
问题
我们都知道使用 Secret 连接到外部服务的典型模式。下面是一个简单的解决方案架构,展示了我们如何使用 Secret 连接到数据库。
· 您可能需要管理数百个 Secret。
· 管理 Secret 的生命周期变得很困难,例如创建、存储、轮换和撤销。
· 加入新服务和具有特定访问权限的人员变得困难。
· 您必须考虑安全分发 Secret。
由于上述原因,您可以考虑选择第三方 Secret 管理工具来卸载与控制 Kubernetes Secret 相关的一些工作。
· 云提供的工具:AWS Secrets Manager、Google Secret Manager、Azure Key Vault、IBM Cloud Secrets Manager、Oracle Key Vault
· 开源工具:HashiCorp Vault
Operator设计模式
· 使用 Operator 可以自动化的事情包括:
· 按需部署应用
· 获取/还原应用状态的备份
· 处理应用代码的升级以及相关改动。例如,数据库 schema 或额外的配置设置
· 发布一个 service,要求不支持 Kubernetes API 的应用也能发现它
· 模拟整个或部分集群中的故障以测试其稳定性
· 在没有内部成员选举程序的情况下,为分布式应用选择首领角色
什么是External Secrets Operator (ESO)?
External Secrets Operator的结构
· 自定义资源定义 (CRD):这些定义了 Operator 可用设置的数据架构,在我们的例子中是SecretStore (https://github.com/external-secrets/external-secrets/blob/main/config/crds/bases/external-secrets.io_secretstores.yaml)和 ExternalSecret (https://github.com/external-secrets/external-secrets/blob/main/config/crds/bases/external-secrets.io_externalsecrets.yaml) 定义。
· 程序结构:这些结构使用选择的编程语言定义与上述 CRD 相同的数据模式,在我们的例子中是 (https://github.com/external-secrets/external-secrets/tree/main/pkg/controllers) Go。
· 自定义资源 (CR):它们保存由 CRD 定义的设置的值,并描述 Operator 的配置。
· 控制器:这是实际工作发生的地方。控制器作用于自定义资源并负责创建和管理资源。它们可以用任何编程语言创建,ESO 控制器是用 Go 创建的。
External Secret Providers
type Provider interface{
//NewClient constructs a SecretsManagerProvider
NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error)
//ValidateStore checks if the provided store is valid
ValidateStore(store GenericStore) error
}
SecretsClient
实例负责验证 Secre t配置并以各种形式提取 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
}
SecretStore 资源
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: secretstore-sample
spec:
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
资源定义应该检索什么。它具有对 SecretStore 的引用,以便 ESO Operator 的控制器可以使用 ExternalSecret 资源通过使用 SecretStore 资源指定的配置来创建 Kubernetes Secret。secretStoreRef
属性连接两个资源的方式:apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: example
spec:
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 如何同步 secrets?
· 循环不停的读取外部 Secret 配置
· 通过 secretStoreRef 属性检索外部密钥配置引用的 SecretStore
· 使用 store 规范中的 Provider 名称查找上述 Provider 映射以查找与 Secret 关联的 Provider
· 使用 store Provider 名称实例化 Secret 客户端
· 使用 Secret 客户端从外部系统检索 Secret 数据
· 如果没有返回 secret 数据并且删除策略设置为 Delete,则从集群中删除 secret。如果删除策略设置为保留,则密钥保持原样。
· 假设已成功检索到外部密钥,则会在集群中创建 Kubernetes 密钥,并应用指定的模板。
创建一个简单的 ESO Provider
· 为新的 Secret Provider 添加配置模式
· 创建类型定义以将 CRD 定义映射到 Go 结构
· 添加 Provider 实现
· 更新 register.go 以包含新的 Provider
· 创建和部署
一个简单的Secrets-Management服务
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 定义
express:
description: Configuration to sync secrets using Express provider
properties:
host:
type: string
required:
- host
type: object
deploy/crds/bundle.yaml
。新的 Provider 只有一个配置属性:host
,它告诉 Provider Secret 服务在哪里运行。为 Provider 配置创建类型
package v1beta1
type ExpressProvider struct {
Host string `json:"host"`
}
实现 Provider
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 returns
type 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 use
func (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 secret
func (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 errors
func (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 loop
func init() {
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
Express: &esv1beta1.ExpressProvider{},
})
}
将 Provider 注册到 Provider 列表
package register
import (
…
_ "github.com/external-secrets/external-secrets/pkg/provider/express"
…
)
部署 ESO 进行测试
make crds.install
make run
使用 Secret 测试 Provider
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: secretstore-express
spec:
provider:
express:
host: http://express-secrets-service
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: express-external-secret
spec:
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
kubectl get secret my-express-secret -o yaml
apiVersion: v1
data:
secretKey: dGhpcy1pcy1hLXNlY3JldA==
immutable: false
kind: Secret
结论
推荐阅读
推荐视频
微信扫码关注该文公众号作者