はじめに
今年の夏も酷暑続きでしたねー。 仕事が忙しくて自宅ラボに触る時間もなかったり、マシンの熱が心配なのもあって夏の間は自宅ラボの電源を落としてたり、ということもあり前回の投稿から 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
アクセス許可を追加したあとに忘れず「プライベートテナントに管理者の同意を与えます」を押しておきます。 設定後のアクセス許可はこんな感じです。
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 で動的生成するようにしました。 構成が複雑になった割にほとんど恩恵がないですが、好きな構成を組めるのも自宅ラボのいいところ。
あと、この記事の作成中にブログのコードブロックが等倍フォントになってないのに気づいたのでいつか直す。