Post

用 Google Secret Manager 與 External Secret Operator 管理機敏資料

用 Google Secret Manager 與 External Secret Operator 管理機敏資料

TL;DR

  • Google Secret Manager(GSM) 集中管理機敏資料
  • Workload Identity(WI) 的方式賦予 External Secret Operator(ESO) 訪問 GSM 的權限
  • ESO 同步 GKE & GSM 的 secret

背景

有些服務會使用到環境變數 .envkind: secret 的方式部署,在管理上不便

GSM 機敏資料建立

在 Secret value 的部分,是以 json 的格式存放 key-value GSM example

WI 授予權限

WI 是透過 Google ServiceAccount(GSA) 與 Kubernetes ServiceAccount(KSA) 的綁定,讓 KSA 擁有 GSA 的權限同時,間接授予擁有該 KSA 的服務享有相同權限

👉 先賦予 GSA 權限同時,綁定 KSA 這邊是透過 IaC(terragrunt) 的方式,建立 GSA 賦予權限,並且綁定 KSA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# main.tf

resource "google_service_account" "sa" {
  project      = var.project_id
  account_id   = var.name
  display_name = var.display_name
  description  = var.description
}

resource "google_project_iam_member" "roles" {
  for_each = toset(var.project_roles)
  project  = var.project_id
  role     = each.value
  member   = google_service_account.sa.member
}

# Workload Identity 綁定:允許 K8s SA impersonate GSA
resource "google_service_account_iam_member" "workload_identity" {
  service_account_id = google_service_account.sa.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${var.project_id}.svc.id.goog[${var.k8s_namespace}/${var.k8s_sa_name}]"
}
1
2
3
4
5
6
7
8
# terragrunt.hcl

  project_roles = [
    "roles/secretmanager.secretAccessor"                  # 視實際需求調整
  ]

  k8s_namespace   = "external-secrets"                    # 視實際部署調整
  k8s_sa_name     = "external-secret-external-secrets"    # 視實際部署調整

ESO 部署

這邊要指明 KSA 所對應的 GSA 權限來源

可以參考 ArtifactHubGithub 找到怎麼設定

1
2
3
4
serviceAccount:
  create: true
  annotations:
    iam.gke.io/gcp-service-account: "${GSA-name}@${GCP-project-id}.iam.gserviceaccount.com"

👉 接著,我們會需要一個 ClusterSecretStore 來讓 ESO 知道 secret 來源

會需要設置

  1. project: GCP project-id
  2. clusterLocation: GKE location
  3. clusterName: GKE cluster name
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    # ClusterSecretStore.yaml
    apiVersion: external-secrets.io/v1
    kind: ClusterSecretStore
    metadata:
      name: 
      labels:
     project: 
    spec:
      provider:
     gcpsm:
       projectID: 
       auth:
         workloadIdentity:
           clusterLocation: 
           clusterName: 
           serviceAccountRef:
             name: external-secret-external-secrets
             namespace: external-secrets
    

到這邊,應該可以看到上面這個 ClusterSecretStore 的狀態要是 True

1
2
3
4
5
6
# 範例

kubectl get clustersecretstores.external-secrets.io 

NAME                 AGE    STATUS   CAPABILITIES   READY
gcp-secret-manager   117m   Valid    ReadWrite      True

服務取得 GSM secret

服務端則是建立好 ExternalSecret 後,剩下的 dirty work ESO 會幫我們處理好

ExternalSecret 中會有幾項需要 define:

  1. refreshInterval: 更新時間
  2. secretStoreRef: 參考的類型(文章使用 ClusterSecretStore),以及建立時的名字
  3. target: 建立的 secret 名稱
  4. dataFrom: 資料來源(GSM 的 secret name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: 
  namespace: 
spec:
  # 這將「每1分鐘」刷新一次金鑰。可以根據自己的要求保留時間。
  refreshInterval: 
  secretStoreRef:
    name: 
    kind: 
  target:
    name: 
    creationPolicy: 
  dataFrom:
    - extract:
        key: 

後續 ESO 會透過上述設定,到 GSM 取值,並依照 ExternalSecret 的設定,到指定 ns 建立 secret

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 範例

kubectl -n cosparks describe secret kk   
Name:         kk
Namespace:    ${namespace}
Labels:       app.kubernetes.io/managed-by=Helm
              reconcile.external-secrets.io/managed=true
Annotations:  meta.helm.sh/release-name: ${server}
              meta.helm.sh/release-namespace: ${namespace}

Type:  Opaque

Data
====
kk:    12 bytes
ping:  4 bytes

👉 並確認服務有吃到參數 demo

1
2
3
4
5
6
const dotenv = require('dotenv');
dotenv.config();

app.get('/', (req, res) => {
    res.send(`Hello CoSparks, ${process.env.kk}`);
});

同時 GSM 若更新, ESO 會依照 refreshInterval 更新 K8s 內的 secret 後續服務只需要重啟,就可以吃到新後的 secret

This post is licensed under CC BY 4.0 by the author.