使用 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