はじめに

今年の夏も酷暑続きでしたねー。 仕事が忙しくて自宅ラボに触る時間もなかったり、マシンの熱が心配なのもあって夏の間は自宅ラボの電源を落としてたり、ということもあり前回の投稿から 4 ヶ月も空いてしまいました。 忙しさのピークも一段落して季節も秋に変わってきたので、また自宅ラボという名の盆栽いじりを再開してます。

前回の投稿では、cert-manager を使って Ingress 用の証明書を発行するようにしました。 Let’s Encrypt の ACME DNS-01 チャレンジ用に cert-manager が Azure DNS に TXT レコードを作成してくれます。

このとき、cert-manager は Microsoft Entra ID のサービスプリンシパルを使います。 サービスプリンシパルのクライアントシークレットは有効期限が最大でも 2 年間 (24 ヶ月) なので、HashiCorp Vault と Vault Secrets Operator を使ってクライアントシークレットを動的に生成して、クライアントシークレットの有効期限を意識せずにサービスプリンシパルを使えるようにします。

自宅ラボ用途なので 2 年間でもいいっちゃいいんですがね。

全体像

今回は色々なサービスが連携していて流れが分かりづらいため、最初に全体像を把握します。 シーケンス図のほうが分かりやすかったかも?

サービスプリンシバルを含め、Secret リソース以外は事前に作成しておきます。

全体像

今回の投稿でメインになってくるのは Vault と Vault Secrets Operator (VSO) です。

サービスプリンシバル

あらかじめ Vault 用と cert-manager 用のサービスプリンシバルを作成しておきます。 cert-manager サービスプリンシバルは前回作成したものを使います。

Vault サービスプリンシバルも同様に作成したあと、他のサービスプリンシバルを操作するためのアクセス許可を設定します。 必要なものは以下の 2 つです。

  • Application.ReadWrite.All
  • GroupMember.ReadWrite.All

アクセス許可を追加したあとに忘れず「プライベートテナントに管理者の同意を与えます」を押しておきます。 設定後のアクセス許可はこんな感じです。

API のアクセス許可

Vault サービスプリンシバルは Azure リソースに対して操作しないため、Azure RBAC での権限付与はしていません。

HashiCorp Vault

cert-manager サービスプリンシパルのクライアントシークレットを動的生成するために HashiCorp Vault を使います。 自宅ラボでは Kubernetes 上に Vault をデプロイしています。

Vault には Azure シークレットエンジンというものがあり、Microsoft Entra ID のサービスプリンシパルを動的に作成したり、既存のサービスプリンシパルのクライアントシークレットを動的に生成することができます。 今回は後者のクライアントシークレット動的生成パターンを採用しました(理由は後ほど)。

Azure シークレットエンジン

Vault からサービスプリンシパルを動的生成するために、Vault の Azure シークレットエンジンを有効化します。 作業は Vault Pod 内からおこなっていきます。

kubectl exec -it vault-0 -- sh
vault login
vault secrets enable azure

あらかじめ作成しておいた Vault サービスプリンシバルを使用するように Azure シークレットエンジンを設定します。 Vautl サービスプリンシバルにはルート認証情報としてクライアントシークレットを作成しておきます。

vault write azure/config \
     subscription_id=$SUBSCRIPTION_ID \
     client_id=$CLIENT_ID \
     client_secret=$CLIENT_SECRET \
     tenant_id=$TENANT_ID \
     use_microsoft_graph_api=true

Description も設定しておきます。

vault secrets tune -description="Vault on K8s から使用するサービスプリンシパル" azure

Vault には上記で設定したルート認証情報としてのクライアントシークレットをローテーションする機能があるようです(実際にローテーションされるところを確認できていないですが)。 クライアントシークレットをローテーションすることで、Vault サービスプリンシバルのシークレットは Vault しか知らない状態になり、よりセキュアな環境を構築できます。

vault write -f azure/rotate-root

ローテーションを設定すると事前に作成していたルート認証情報としてのクライアントシークレットは削除されて、新たに Vault がクライアントシークレットを作成します。 このクライアントシークレットの有効期限は半年です。

証明書

クライアントシークレットの有効期限は Valut CLI からも確認できます。

$ vault read azure/config
Key                              Value
---                              -----
client_id                        00000000-0000-0000-0000-000000000000
environment                      n/a
root_password_expiration_date    2024-04-09T13:31:26.572713Z
root_password_ttl                4380h
subscription_id                  00000000-0000-0000-0000-000000000000
tenant_id                        00000000-0000-0000-0000-000000000000

cert-manager 用ロール

Azure シークレットエンジンの設定ができたら cert-manager サービスプリンシパルのシークレットを作成するためのロールを設定します。

Vault は新しくサービスプリンシパルを作成することも既存のサービスプリンシパルを指定することもできますが、今回は後者を選択します。 なぜかと言うと、新規作成したサービスプリンシパルのクライアント ID を cert-manager が参照できないからです。

cert-manager でサービスプリンシパルを指定する場合、Issuer リソース (または ClusterIssuer リソース) にクライアント ID とクライアントシークレットを記述します。 クライアントシークレットは Secret リソースを参照する形式なので、VSO で動的に Secret リソースとして生成して cert-manager に渡せます。 しかし、クライアント ID はマニフェストにベタ書きする必要があるため、Vault でサービスプリンシパル自体を動的生成してしまうとクライアント ID も変わってしまい、cert-manager から参照できなくなってしまいます。

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
(snip)
    solvers:
      - dns01:
          azureDNS:
            clientID: 00000000-0000-0000-0000-000000000000     <- クライアント ID
            clientSecretSecretRef:                             <- クライアントシークレットを記述した Secret の指定
              name: azure-service-principal-secret
              key: client_secret
(snip)

そのため、Vault で cert-manager 用のサービスプリンシパルを扱う場合は ロールで既存サービスプリンシパルを指定してクライアントシークレットだけローテーションします。

vault write azure/roles/certmanager-role \
    application_object_id=00000000-0000-0000-0000-000000000000 \
    ttl=1h

ちなみに、サービスプリンシパルを新規作成するロールは Azure RBAC でサービスプリンシパルに付与するロールも指定してあげます。 たとえば、サブスクリプション全体に共同作成者ロールを付与したサービスプリンシパルを作成する場合は以下のようになります。

vault write azure/roles/new-principal-role ttl=1h azure_roles=-<<EOF
    [
        {
            "role_name": "Contributor",
            "scope":  "/subscriptions/00000000-0000-0000-0000-000000000000"
        }
    ]
EOF

「ロール」という名称としても既存サービスプリンシパルを使い回すより、要件に沿った Azure ロールが付与されたサービスプリンシパルを使い分けるほうがしっくり来ますね。

以上で Vault の準備は完了です。

Vault Secrets Operator

Vault 経由で cert-manager サービスプリンシパルのクライアントシークレットをローテーションする設定はできたので Vault Secrets Operator (VSO) を使ってクライアントシークレットを Kubernetes 側に反映させます。

使用するマニフェストはこんな感じです。

apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
  name: vaultconnection
  namespace: cert-manager
spec:
  address: http://vault.vault.svc.cluster.local:8200
  skipTLSVerify: true
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
  name: vaultauth
  namespace: cert-manager
spec:
  vaultConnectionRef: vaultconnection
  method: kubernetes
  mount: kubernetes
  kubernetes:
    role: azure-role
    serviceAccount: cert-manager
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
  name: vaultdynamicsecret
  namespace: cert-manager
spec:
  vaultAuthRef: vaultauth
  mount: azure
  path: creds/certmanager-role
  rolloutRestartTargets:
    - kind: Deployment
      name: cert-manager
  destination:
    name: azure-service-principal-secret
    create: true

VSO に関しては会社のブログにも色々書いたし、ここでは割愛。 こうして個人ブログの内容が薄くなっていくのである…。

このマニフェストをデプロイすると、Vault 側の設定がちゃんとできていれば VSO がサービスプリンシパルのクライアントシークレットを含む Secret リソースを作成してくれます。 今回に限らず Vault と VSO の連携は一発で成功したことない、ほんと難しい。

cert-manager と Ingress

Vault と VSO の設定ができたら、残りは cert-manager と公開するサービス毎の Ingress の設定です。 これらは前回の投稿とほとんど同じ内容です。 違う点は、cert-manager の ClusterIssuer リソースが参照する Secret リソースを VSO が動的生成したものに変更しているくらい。

おわりに

ということで、cert-manager が使う Azure サービスプリンシパルのシークレットを Vault Secrets Operator で動的生成するようにしました。 構成が複雑になった割にほとんど恩恵がないですが、好きな構成を組めるのも自宅ラボのいいところ。

あと、この記事の作成中にブログのコードブロックが等倍フォントになってないのに気づいたのでいつか直す。