secrets 管理工具 Vault 的介紹、安裝及使用

於清樂發表於2021-01-25

原文:https://ryan4yin.space/posts/expirence-of-vault/

Vault 是 hashicorp 推出的 secrets 管理、加密即服務與許可權管理工具。它的功能簡介如下:

  1. secrets 管理:支援儲存各種自定義資訊、自動生成各類金鑰,vault 自動生成的金鑰還能自動輪轉(rotate)
  2. 認證方式:支援接入各大雲廠商的賬號許可權體系(比如阿里雲RAM子賬號體系)或者 LDAP 等進行身份驗證,不需要建立額外的賬號體系。
  3. 許可權管理:通過 policy,可以設定非常細緻的 ACL 許可權。
  4. 金鑰引擎:也支援接入各大雲廠商的賬號體系(比如阿里雲RAM子賬號體系),實現 APIKey/APISecret 的自動輪轉。
  5. 支援接入 kubernetes rbac 許可權體系,通過 serviceaccount+role 為每個 Pod 單獨配置許可權。

在使用 Vault 之前,我們是以攜程開源的 Apollo 作為微服務的分散式配置中心。

Apollo 在國內非常流行。它功能強大,支援配置的繼承,也有提供 HTTP API 方便自動化。
缺點是許可權管理和 secrets 管理比較弱,也不支援資訊加密,不適合直接儲存敏感資訊。因此我們現在切換到了 Vault.

目前我們本地的 CI/CD 流水線和雲上的微服務體系,都是使用的 Vault 做 secrets 管理.

一、Vault 基礎概念

首先看一下 Vault 的架構圖:

可以看到,幾乎所有的元件都從屬於「安全屏障(security barrier)」,
Vault 可以簡單地被劃分為 Storage Backend、安全屏障(security barrier) 和 HTTP API 三個部分。

「安全屏障(security barrier)」是 Vault(金庫) 周圍的加密「鋼鐵」和「混凝土」,Storage Backend 和 Vault 之間的所有資料流動都需要經過「屏障(barrier)」。
barrier 確保只有加密資料會被寫入 Storage Backend,加密資料在經過 barrier 的過程中被驗證與解密。
和銀行金庫(bank vault)非常類似,barrier 也必須先解封,才能允許讀取內部的資料。

1. 資料儲存及加密解密

Storage Backend(後端儲存): Vault 自身不儲存資料,因此需要為它配置一個「Storage Backend」。
「Storage Backend」是不受信任的,只用於儲存加密資料。

Initialaztion(初始化): vault 在首次啟動時需要初始化,這一步生成一個「加密金鑰(encryption key)」用於加密資料,加密完成的資料才能被儲存到 Storage Backend.

Unseal(解封): Vault 啟動後,因為不知道「加密金鑰(encryption key)」,它會進入「封印(sealed)」狀態,在「Unseal」前無法進行任何操作。
「加密金鑰」被「master key」保護,我們必須提供「master key」才能完成 Unseal 操作。
預設情況下,vault 使用沙米爾金鑰共享演算法
將「master key」分割成五個「Key Shares(分享金鑰)」,必須要提供其中任意三個「Key Shares」才能重建出「master key」從而完成 Unseal.

「Key Shares」的數量,以及重建「master key」最少需要的 key shares 數量,都是可以調整的。
沙米爾金鑰共享演算法也可以關閉,這樣 master key 將被直接用於 Unseal.

2. 認證系統及許可權系統

在解封完成後,Vault 就可以開始處理請求了。

HTTP 請求進入後的整個處理流程都由 vault core 管理,core 會強制進行 ACL 檢查,並確保審計日誌(audit logging)完成記錄。

客戶端首次連線 vault 時,需要先完成身份認證,vault 的「auth methods」模組有很多身份認證方法可選:

  1. 使用者友好的認證方法,適合管理員使用:username/password、雲服務商、ldap
    1. 在建立 user 的時候,需要為 user 繫結 policy,給予合適的許可權。
  2. 應用友好的方法:public/private keys、tokens、kubernetes、jwt

身份驗證請求流經 Core 並進入 auth methods,auth methods 確定請求是否有效並返回「關聯策略(policies)」的列表。

ACL Policies 由 policy store 負責管理與儲存,由 core 進行 ACL 檢查。
ACL 的預設行為是拒絕,這意味著除非明確配置 Policy 允許某項操作,否則該操作將被拒絕。

在通過 auth methods 完成了身份認證,並且返回的「關聯策略」也沒毛病之後,「token store」將會生成並管理一個新的 token,
這個 token 會被返回給客戶端,用於進行後續請求。
類似 web 網站的 cookie,token 也都存在一個 lease 租期或者說有效期,這加強了安全性。

token 關聯了相關的策略 policies,策略將被用於驗證請求的許可權。
請求經過驗證後,將被路由到 secret engine。如果 secret engine 返回了一個 secret(由 vault 自動生成的 secret),
Core 會將其註冊到 expiration manager,並給它附加一個 lease ID。lease ID 被客戶端用於更新(renew)或吊銷(revoke)它得到的 secret.
如果客戶端允許租約(lease)到期,expiration manager 將自動吊銷這個 secret.

Core 負責處理稽核代理(audit brok)的請求及響應日誌,將請求傳送到所有已配置的稽核裝置(audit devices)。

3. Secret Engine

Secret Engine 是儲存、生成或者加密資料的元件,它非常靈活。

有的 Secret Engines 只是單純地儲存與讀取資料,比如 kv 就可以看作一個加密的 Redis。
而其他的 Secret Engines 則連線到其他的服務並按需生成動態憑證。
還有些 Secret Engines 提供「加密即服務(encryption as a service)」 - transit、證照管理等。

常用的 engine 舉例:

  1. AliCloud Secrets Engine: 基於 RAM 策略動態生成 AliCloud Access Token,或基於 RAM 角色動態生成 AliCloud STS 憑據
    • Access Token 會自動更新(Renew),而 STS 憑據是臨時使用的,過期後就失效了。
  2. kv: 鍵值儲存,可用於儲存一些靜態的配置。它一定程度上能替代掉攜程的 Apollo 配置中心。
  3. Transit Secrets Engine: 提供加密即服務的功能,它只負責加密和解密,不負責儲存。主要應用場景是幫 app 加解密資料,但是資料仍舊儲存在 MySQL 等資料庫中。

二、部署 Vault

官方建議通過 Helm 部署 vault,大概流程:

  1. 使用 helm/docker 部署執行 vault.
  2. 初始化/解封 vault: vault 安全措施,每次重啟必須解封(可設定自動解封).

1. docker-compose 部署

推薦用於本地開發測試環境,或者其他不需要高可用的環境。

docker-compose.yml 示例如下:

version: '3.3'
services:
  vault:
    # 文件:https://hub.docker.com/_/vault
    image: vault:1.6.0
    container_name: vault
    ports:
      # rootless 容器,內部不能使用標準埠 443
      - "443:8200"
    restart: always
    volumes:
      # 審計日誌儲存目錄,預設不寫審計日誌,啟用 `file` audit backend 時必須提供一個此資料夾下的路徑
      - ./logs:/vault/logs
      # 當使用 file data storage 外掛時,資料被儲存在這裡。預設不往這寫任何資料。
      - ./file:/vault/file
      # 配置目錄,vault 預設 `/valut/config/` 中所有以 .hcl/.json 結尾的檔案
      # config.hcl 檔案內容,參考 cutom-vaules.yaml
      - ./config.hcl:/vault/config/config.hcl
      # TLS 證照
      - ./certs:/certs
    # vault 需要鎖定記憶體以防止敏感值資訊被交換(swapped)到磁碟中
    # 為此需要新增如下能力
    cap_add:
      - IPC_LOCK
    # 必須手動設定 entrypoint,否則 vault 將以 development 模式執行
    entrypoint: vault server -config /vault/config/config.hcl

config.hcl 內容如下:

ui = true

// 使用檔案做資料儲存(單節點)
storage "file" {
  path    = "/vault/file"
}

listener "tcp" {
  address = "[::]:8200"

  tls_disable = false
  tls_cert_file = "/certs/server.crt"
  tls_key_file  = "/certs/server.key"
}

將如上兩份配置儲存在同一非資料夾內,同時在 ./certs 中提供 TLS 證照 server.crt 和私鑰 server.key

然後 docker-compose up -d 就能啟動執行一個 vault 例項。

2. 通過 helm 部署 vault {#install-by-helm}

推薦用於生產環境

通過 helm 部署:

# 新增 valut 倉庫
helm repo add hashicorp https://helm.releases.hashicorp.com
# 檢視 vault 版本號
helm search repo hashicorp/vault -l | head
# 下載某個版本號的 vault
helm pull hashicorp/vault --version  0.9.0 --untar

參照下載下來的 ./vault/values.yaml 編寫 custom-values.yaml
部署一個以 mysql 為後端儲存的 HA vault,配置示例如下:

global:
  # enabled is the master enabled switch. Setting this to true or false
  # will enable or disable all the components within this chart by default.
  enabled: true
  # TLS for end-to-end encrypted transport
  tlsDisable: false

injector:
  # True if you want to enable vault agent injection.
  enabled: true

  replicas: 1

  # If multiple replicas are specified, by default a leader-elector side-car
  # will be created so that only one injector attempts to create TLS certificates.
  leaderElector:
    enabled: true
    image:
      repository: "gcr.io/google_containers/leader-elector"
      tag: "0.4"
    ttl: 60s

  # If true, will enable a node exporter metrics endpoint at /metrics.
  metrics:
    enabled: false

  # Mount Path of the Vault Kubernetes Auth Method.
  authPath: "auth/kubernetes"

  certs:
    # secretName is the name of the secret that has the TLS certificate and
    # private key to serve the injector webhook. If this is null, then the
    # injector will default to its automatic management mode that will assign
    # a service account to the injector to generate its own certificates.
    secretName: null

    # caBundle is a base64-encoded PEM-encoded certificate bundle for the
    # CA that signed the TLS certificate that the webhook serves. This must
    # be set if secretName is non-null.
    caBundle: ""

    # certName and keyName are the names of the files within the secret for
    # the TLS cert and private key, respectively. These have reasonable
    # defaults but can be customized if necessary.
    certName: tls.crt
    keyName: tls.key

server:
  # Resource requests, limits, etc. for the server cluster placement. This
  # should map directly to the value of the resources field for a PodSpec.
  # By default no direct resource request is made.

  # authDelegator enables a cluster role binding to be attached to the service
  # account.  This cluster role binding can be used to setup Kubernetes auth
  # method.  https://www.vaultproject.io/docs/auth/kubernetes.html
  authDelegator:
    enabled: true

  # Enables a headless service to be used by the Vault Statefulset
  service:
    enabled: true
    # clusterIP controls whether a Cluster IP address is attached to the
    # Vault service within Kubernetes.  By default the Vault service will
    # be given a Cluster IP address, set to None to disable.  When disabled
    # Kubernetes will create a "headless" service.  Headless services can be
    # used to communicate with pods directly through DNS instead of a round robin
    # load balancer.
    # clusterIP: None

    # Configures the service type for the main Vault service.  Can be ClusterIP
    # or NodePort.
    #type: ClusterIP

    # If type is set to "NodePort", a specific nodePort value can be configured,
    # will be random if left blank.
    #nodePort: 30000

    # Port on which Vault server is listening
    port: 8200
    # Target port to which the service should be mapped to
    targetPort: 8200


  # This configures the Vault Statefulset to create a PVC for audit
  # logs.  Once Vault is deployed, initialized and unseal, Vault must
  # be configured to use this for audit logs.  This will be mounted to
  # /vault/audit
  # See https://www.vaultproject.io/docs/audit/index.html to know more
  auditStorage:
    enabled: false
    # Size of the PVC created
    size: 10Gi
    # Name of the storage class to use.  If null it will use the
    # configured default Storage Class.
    storageClass: null
    # Access Mode of the storage device being used for the PVC
    accessMode: ReadWriteOnce
    # Annotations to apply to the PVC
    annotations: {}

  # Run Vault in "HA" mode. There are no storage requirements unless audit log
  # persistence is required.  In HA mode Vault will configure itself to use Consul
  # for its storage backend.  The default configuration provided will work the Consul
  # Helm project by default.  It is possible to manually configure Vault to use a
  # different HA backend.
  ha:
    enabled: true
    replicas: 3

    # Set the api_addr configuration for Vault HA
    # See https://www.vaultproject.io/docs/configuration#api_addr
    # If set to null, this will be set to the Pod IP Address
    apiAddr: null

    # config is a raw string of default configuration when using a Stateful
    # deployment. Default is to use a Consul for its HA storage backend.
    # This should be HCL.
    
    # Note: Configuration files are stored in ConfigMaps so sensitive data 
    # such as passwords should be either mounted through extraSecretEnvironmentVars
    # or through a Kube secret.  For more information see: 
    # https://www.vaultproject.io/docs/platform/k8s/helm/run#protecting-sensitive-vault-configurations
    config: |
      ui = true

      listener "tcp" {
        address = "[::]:8200"
        cluster_address = "[::]:8201"

        tls_disable = false
        tls_cert_file = "/etc/certs/vault.crt"
        tls_key_file  = "/etc/certs/vault.key"
      }

      storage "mysql" {
        address = "<host>:3306"
        username = "<username>"
        password = "<password>"
        database = "vault"
        ha_enabled = "true"
      }

      service_registration "kubernetes" {}

      # Example configuration for using auto-unseal, using Google Cloud KMS. The
      # GKMS keys must already exist, and the cluster must have a service account
      # that is authorized to access GCP KMS.
      #seal "gcpckms" {
      #   project     = "vault-helm-dev-246514"
      #   region      = "global"
      #   key_ring    = "vault-helm-unseal-kr"
      #   crypto_key  = "vault-helm-unseal-key"
      #}

# Vault UI
ui:
  # True if you want to create a Service entry for the Vault UI.
  #
  # serviceType can be used to control the type of service created. For
  # example, setting this to "LoadBalancer" will create an external load
  # balancer (for supported K8S installations) to access the UI.
  enabled: true
  publishNotReadyAddresses: true
  # The service should only contain selectors for active Vault pod
  activeVaultPodOnly: false
  serviceType: "ClusterIP"
  serviceNodePort: null
  externalPort: 8200

現在使用自定義的 custom-values.yaml 部署 vautl:

kubectl create namespace vault
# 安裝/升級 valut
helm upgrade --install vault ./vault --namespace vault -f custom-values.yaml

3. 初始化(initalize)並解封(unseal) vault

官方文件:Initialize and unseal Vault - Vault on Kubernetes Deployment Guide

通過 helm 部署 vault,預設會部署一個三副本的 StatefulSet,但是這三個副本都會處於 NotReady 狀態(docker 方式部署的也一樣)。
接下來還需要手動初始化(initalize)並解封(unseal) vault,才能 Ready:

  1. 第一步:從三個副本中隨便選擇一個,執行 vault 的初始化命令:kubectl exec -ti vault-0 -- vault operator init
    1. 初始化操作會返回 5 個 unseal keys,以及一個 Initial Root Token,這些資料非常敏感非常重要,一定要儲存到安全的地方!
  2. 第二步:在每個副本上,使用任意三個 unseal keys 進行解封操作。
    1. 一共有三個副本,也就是說要解封 3*3 次,才能完成 vault 的完整解封!
# 每個例項都需要解封三次!
## Unseal the first vault server until it reaches the key threshold
$ kubectl exec -ti vault-0 -- vault operator unseal # ... Unseal Key 1
$ kubectl exec -ti vault-0 -- vault operator unseal # ... Unseal Key 2
$ kubectl exec -ti vault-0 -- vault operator unseal # ... Unseal Key 3

這樣就完成了部署,但是要注意,vault 例項每次重啟後,都需要重新解封!也就是重新進行第二步操作!

4. 設定自動解封

每次重啟都要手動解封所有 vault 例項,實在是很麻煩,在雲上自動擴縮容的情況下,vault 例項會被自動排程,這種情況就更麻煩了。

為了簡化這個流程,可以考慮配置 auto unseal 讓 vault 自動解封。

自動解封目前有兩種方法:

  1. 使用阿里雲/AWS/Azure 等雲服務提供的金鑰庫來管理 encryption key,阿里雲的相關配置方法:alicloudkms Seal
  2. 如果你不想用雲服務,那可以考慮 autounseal-transit

簡單起見,也可以寫個 crontab 或者在 CI 平臺上加個定時任務去執行解封命令,以實現自動解封。

三、Vault 自身的配置管理

Vault 本身是一個複雜的 secrets 工具,它提供了 Web UICLI 用於手動管理與檢視 Vault 的內容。

但是作為一名 DevOps,我們當然更喜歡自動化的方法,這有兩種選擇:

Web UI 適合手工操作,而 sdk/terraform-provider-vault 則適合用於自動化管理 vault.

我們的測試環境就是使用 pulumi-vault 完成的自動化配置 vault policy 和 kubernetes role,然後自動化注入所有測試用的 secrets.

1. 使用 pulumi 自動化配置 vault

使用 pulumi 管理 vault 配置的優勢是很大的,因為雲上資源的敏感資訊(資料庫賬號密碼、資源 ID、RAM子賬號)都是 pulumi 建立的。

再結合使用 pulumi_valut,就能實現敏感資訊自動生成後,立即儲存到 vault 中,實現完全自動化。

後續微服務就可以通過 kubernetes 認證,直接從 vault 讀取敏感資訊。

或者是寫入到本地的 vault 中留做備份,在需要的時候,管理員能登入進去檢視相關敏感資訊。

1.1 Token 的生成

pulumi_vault 本身挺簡單的,宣告式的配置嘛,直接用就是了。

但是它一定要求提供 VAULT_TOKEN 作為身份認證的憑證(實測 userpass/approle 都不能直接使用,會報錯 no vault token found),而且 pulumi 還會先生成臨時用的 child token,然後用這個 child token
進行後續的操作。

首先安全起見,肯定不應該直接提供 root token!root token 應該封存,除了緊急情況不應該啟用。

那麼應該如何生成一個許可權有限的 token 給 vault 使用呢?
我的方法是建立一個 userpass 賬號,通過 policy 給予它有限的許可權。
然後先手動(或者自動)登入獲取到 token,再將 token 提供給 pulumi_vault 使用。

這裡面有個坑,就是必須給 userpass 賬號建立 child token 的許可權:

path "local/*" {
  capabilities = ["read", "list"]
}

// 允許建立 child token
path "auth/token/create" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

不給這個許可權,pulumi_vault 就會一直報錯。。

四、在 Kubernetes 中使用 vault 注入敏感配置

1. 部署並配置 vault agent

前面提到過 vault 支援通過 Kubernetes 的 ServiceAccount + Role 為每個 Pod 單獨分配許可權。

首先啟用 Vault 的 Kubernetes 身份驗證:

# 配置身份認證需要在 vault pod 中執行,啟動 vault-0 的互動式會話
kubectl exec -n vault -it vault-0 -- /bin/sh
export VAULT_TOKEN='<your-root-token>'
export VAULT_ADDR='http://localhost:8200'
 
# 啟用 Kubernetes 身份驗證
vault auth enable kubernetes

# kube-apiserver API 配置,vault 需要通過 kube-apiserver 完成對 serviceAccount 的身份驗證
vault write auth/kubernetes/config \
    token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
    kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
    kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

1.1 使用叢集外部的 valut 例項

如果你沒這個需求,請跳過這一節。

詳見 Install the Vault Helm chart configured to address an external Vault

kubernetes 也可以和外部的 vault 例項整合,叢集中只部署 vault-agent.

這適用於多個 kubernetes 叢集以及其他 APP 共用一個 vault 例項的情況,比如我們本地的多個開發測試叢集,就都共用著同一個 vault 例項,方便統一管理應用的 secrets.

首先,使用 helm chart 部署 vault-agent,接入外部的 vault 例項。使用的 custom-values.yaml 示例如下:

global:
  # enabled is the master enabled switch. Setting this to true or false
  # will enable or disable all the components within this chart by default.
  enabled: true
  # TLS for end-to-end encrypted transport
  tlsDisable: false

injector:
  # True if you want to enable vault agent injection.
  enabled: true

  replicas: 1

  # If multiple replicas are specified, by default a leader-elector side-car
  # will be created so that only one injector attempts to create TLS certificates.
  leaderElector:
    enabled: true
    image:
      repository: "gcr.io/google_containers/leader-elector"
      tag: "0.4"
    ttl: 60s

  # If true, will enable a node exporter metrics endpoint at /metrics.
  metrics:
    enabled: false

  # External vault server address for the injector to use. Setting this will
  # disable deployment of a  vault server along with the injector.
  # TODO 這裡的 https ca.crt 要怎麼設定?mTLS 又該如何配置?
  externalVaultAddr: "https://<external-vault-url>"

  # Mount Path of the Vault Kubernetes Auth Method.
  authPath: "auth/kubernetes"

  certs:
    # secretName is the name of the secret that has the TLS certificate and
    # private key to serve the injector webhook. If this is null, then the
    # injector will default to its automatic management mode that will assign
    # a service account to the injector to generate its own certificates.
    secretName: null

    # caBundle is a base64-encoded PEM-encoded certificate bundle for the
    # CA that signed the TLS certificate that the webhook serves. This must
    # be set if secretName is non-null.
    caBundle: ""

    # certName and keyName are the names of the files within the secret for
    # the TLS cert and private key, respectively. These have reasonable
    # defaults but can be customized if necessary.
    certName: tls.crt
    keyName: tls.key

部署命令和 通過 helm 部署 vault 一致,只要更換 custom-values.yaml 就行。

vault-agent 部署完成後,第二步是為 vault 建立 serviceAccount、secret 和 ClusterRoleBinding,以允許 vault 審查 kubernetes 的 token, 完成對 pod 的身份驗證. yaml 配置如下:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: vault-auth
  namespace: vault
---
apiVersion: v1
kind: Secret
metadata:
  name: vault-auth
  namespace: vault
  annotations:
    kubernetes.io/service-account.name: vault-auth
type: kubernetes.io/service-account-token
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: role-tokenreview-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
  - kind: ServiceAccount
    name: vault-auth
    namespace: vault

現在在 vault 例項這邊,啟用 kubernetes 身份驗證,在 vault 例項內,執行如下命令:

vault 例項內顯然沒有 kubectl 和 kubeconfig,簡便起見,下列的 vault 命令也可以通過 Web UI 完成。

export VAULT_TOKEN='<your-root-token>'
export VAULT_ADDR='http://localhost:8200'
 
# 啟用 Kubernetes 身份驗證
vault auth enable kubernetes
 
# kube-apiserver API 配置,vault 需要通過 kube-apiserver 完成對 serviceAccount 的身份驗證
# TOKEN_REVIEW_JWT: 就是我們前面建立的 secret `vault-auth`
TOKEN_REVIEW_JWT=$(kubectl -n vault get secret vault-auth -o go-template='{{ .data.token }}' | base64 --decode)
# kube-apiserver 的 ca 證照
KUBE_CA_CERT=$(kubectl -n vault config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}' | base64 --decode)
# kube-apiserver 的 url
KUBE_HOST=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.server}')

vault write auth/kubernetes/config \
        token_reviewer_jwt="$TOKEN_REVIEW_JWT" \
        kubernetes_host="$KUBE_HOST" \
        kubernetes_ca_cert="$KUBE_CA_CERT"

這樣,就完成了 kubernetes 與外部 vault 的整合!

2. 關聯 k8s rbac 許可權系統和 vault

接下來需要做的事:

  1. 通過 vault policy 定義好每個 role(微服務)能訪問哪些資源。
  2. 為每個微服務生成一個 role,這個 role 需要繫結對應的 vault policy 及 kubernetes serviceaccount
    1. 這個 role 是 vault 的 kubernetes 外掛自身的屬性,它和 kubernetes role 沒有半毛錢關係。
  3. 建立一個 ServiceAccount,並使用這個 使用這個 ServiceAccount 部署微服務

其中第一步和第二步都可以通過 vault api 自動化完成.
第三步可以通過 kubectl 部署時完成。

方便起見,vault policy / role / k8s serviceaccount 這三個配置,都建議和微服務使用相同的名稱。

上述配置中,role 起到一個承上啟下的作用,它關聯了 k8s serviceaccount 和 vault policy 兩個配置。

比如建立一個名為 my-app-policy 的 vault policy,內容為:

# 命名規則:"<engine-name>/data/<path>/*"
path "my-app/data/*" {
   # 只讀許可權
   capabilities = ["read", "list"]
}

然後在 vault 中建立 k8s role my-app-role:

  1. 關聯 k8s default 名字空間中的 serviceaccount my-app-account,並建立好這個 serviceaccount.
  2. 關聯 vault token policy,這就是前面建立的 my-app-policy
  3. 設定 token period(有效期)

這之後,每個微服務就能通過 serviceaccount 從 vault 中讀取 my-app 中的所有資訊了。

3. 部署 Pod

參考文件:https://www.vaultproject.io/docs/platform/k8s/injector

下一步就是將配置注入到微服務容器中,這需要使用到 Agent Sidecar Injector。
vault 通過 sidecar 實現配置的自動注入與動態更新。

具體而言就是在 Pod 上加上一堆 Agent Sidecar Injector 的註解,如果配置比較多,也可以使用 configmap 儲存,在註解中引用。

需要注意的是 vault-inject-agent 有兩種執行模式:

  1. init 模式: 僅在 Pod 啟動前初始化一次,跑完就退出(Completed)
  2. 常駐模式: 容器不退出,持續監控 vault 的配置更新,維持 Pod 配置和 vualt 配置的同步。

示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app
  name: my-app
  namespace: default
spec:
  minReadySeconds: 3
  progressDeadlineSeconds: 60
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    rollingUpdate:
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-configmap: my-app-vault-config  # vault 的 hcl 配置
        vault.hashicorp.com/agent-init-first: 'true'  # 是否提前初始化
        vault.hashicorp.com/agent-inject: 'true'
        vault.hashicorp.com/agent-limits-cpu: 250m
        vault.hashicorp.com/agent-requests-cpu: 100m
        vault.hashicorp.com/secret-volume-path: /app/secrets
      labels:
        app: my-app
    spec:
      containers:
      - image: registry.svc.local/xx/my-app:latest
        imagePullPolicy: IfNotPresent
        # 此處省略若干配置...
      serviceAccountName: my-app-account

常見錯誤:

  • vault-agent(sidecar) 報錯: namespace not authorized
    • auth/kubernetes/config 中的 role 沒有繫結 Pod 的 namespace
  • vault-agent(sidecar) 報錯: permission denied
    • 檢查 vault 例項的日誌,應該有對應的錯誤日誌,很可能是 auth/kubernetes/config 沒配對,vault 無法驗證 kube-apiserver 的 tls 證照,或者使用的 kubernetes token 沒有許可權。
  • vault-agent(sidecar) 報錯: service account not authorized
    • auth/kubernetes/config 中的 role 沒有繫結 Pod 使用的 serviceAccount

4. vault agent 配置

vault-agent 的配置,需要注意的有:

  1. 如果使用 configmap 提供完整的 config.hcl 配置,注意 agent-init

vautl-agent 的 template 說明:

目前來說最流行的配置檔案格式應該是 json/yaml,以 json 為例,
對每個微服務的 kv 資料,可以考慮將它所有的個性化配置都儲存在 <engine-name>/<service-name>/ 下面,然後使用如下 template 注入配置(注意這裡使用了自定義的左右分隔符 [[]]):

{
    [[ range secrets "<engine-name>/metadata/<service-name>/" ]]
        "[[ printf "%s" . ]]": 
        [[ with secret (printf "<engine-name>/<service-name>/%s" .) ]]
        [[ .Data.data | toJSONPretty ]],
        [[ end ]]
    [[ end ]]
}

template 的詳細語法參見: https://github.com/hashicorp/consul-template#secret

注意:v2 版本的 kv secrets,它的 list 介面有變更,因此在遍歷 v2 kv secrets 時,
必須要寫成 range secrets "<engine-name>/metadata/<service-name>/",也就是中間要插入 metadata
官方文件完全沒提到這一點,我通過 wireshark 抓包除錯,對照官方的 KV Secrets Engine - Version 2 (API) 才搞明白這個。

這樣生成出來的內容將是 json 格式,不過有個不相容的地方:最後一個 secrets 的末尾有逗號 ,
渲染出的效果示例:

{
    "secret-a": {
  "a": "b",
  "c": "d"
},
    "secret-b": {
  "v": "g",
  "r": "c"
},
}

因為存在尾部逗號(trailing comma),直接使用 json 標準庫解析它會報錯。
那該如何去解析它呢?我在萬能的 stackoverflow 上找到了解決方案:yaml 完全相容 json 語法,並且支援尾部逗號!

以 python 為例,直接 yaml.safe_load() 就能完美解析 vault 生成出的 json 內容。

五、使用 vault 管理阿里雲的 RAM 賬號體系

vault 可以接入各大雲廠商的賬號體系,顯然可以用於管理阿里雲的, 實現 ACCESS_KEY/SECRET_KEY 的自動輪轉。

具體配置方法,待續。。。

相關文章