AKS シークレットストアCSIドライバでworkload identityを使用する

# この記事について
AKS上のシークレットをAzure Key Vaultに保存するシークレットストアCSIドライバを設定した。Key Vaultへの認証にworkload identityを使用しようとしたところ、なかなか苦戦したのでメモ。

# 環境
- Kunbernetes
    - Kubernetes Version: 1.26.6
    - Agentpoolのノードサイズと数: Standard_B2ms x 1
- Azure CLI
    - version: 2.52.0

# 手順

基本的に以下のMSの公式の手順を踏んでいるが、一部そのままでは動かなかったため他の手順とマージしている。

https://learn.microsoft.com/ja-jp/azure/aks/csi-secrets-store-driver


## AKSクラスタの設定


AKSクラスタを作成する。ポイントは以下。
- KeyVault シークレットドライバを有効にするために `azure-keyvault-secrets-provider`アドオンを有効化する。
- Workload identityを有効にするために、`--enable-oidc-issuer`と `--enable-workload-identity`を指定する。

```bash
export AKS_CLUSTER_NAME="myAKSCluster"
export AKS_RG_NAME="myResourceGroup"
export LOCATION="japaneast"

az group create -n ${AKS_RG_NAME} -l ${LOCATION}
az aks create -n ${AKS_CLUSTER_NAME} -g ${AKS_RG_NAME} -l ${LOCATION} \
  --enable-addons azure-keyvault-secrets-provider \
  --enable-oidc-issuer \
  --enable-workload-identity \
  --enable-secret-rotation \
  --node-vm-size Standard_B2ms \
  --node-count 1
```

## Azure KeyVaultの設定

Key Vaultを新しく作成する。
```bash
export KEYVAULT_NAME="your keyvault name"
export KEYVAULT_RG_NAME="myResourceGroup"

az keyvault create -n ${KEYVAULT_NAME} -g ${KEYVAULT_RG_NAME} -l ${LOCATION}
```

サンプルのシークレットを登録する
```bash
export KV_SECRET_USERNAME_KEY="username"
export KV_SECRET_USERNAME_VALUE="Obi-Wan"
export KV_SECRET_PASSWORD_KEY="password"
export KV_SECRET_PASSWORD_VALUE="MayThe4thBWU"

az keyvault secret set --vault-name ${KEYVAULT_NAME} \
  -n ${KV_SECRET_USERNAME_KEY} --value ${KV_SECRET_USERNAME_VALUE}
az keyvault secret set --vault-name ${KEYVAULT_NAME} \
  -n ${KV_SECRET_PASSWORD_KEY} --value ${KV_SECRET_PASSWORD_VALUE}
```


## workload idの設定

OIDC IssuerのURLを取得し環境変数に格納する
```bash
export AKS_OIDC_ISSUER="$(az aks show -n "${AKS_CLUSTER_NAME}" -g "${AKS_RG_NAME}" --query "oidcIssuerProfile.issuerUrl" -otsv)"
```

Managed IDを作成し、クライアントIDとテナントIDを環境変数に格納する
```bash
export USER_ASSIGNED_ID_NAME="myIdentity"
export USER_ASSIGNED_ID_RG="myResourceGroup"
export SUBSCRIPTION="$(az account show --query id --output tsv)"

az identity create --name "${USER_ASSIGNED_ID_NAME}" \
  --resource-group "${USER_ASSIGNED_ID_RG}" \
  --location "${LOCATION}" \
  --subscription "${SUBSCRIPTION}"

export USER_ASSIGNED_CLIENT_ID="$(az identity show --resource-group "${USER_ASSIGNED_ID_RG}" --name "${USER_ASSIGNED_ID_NAME}" --query 'clientId' -otsv)"
export IDENTITY_TENANT=$(az aks show --name ${AKS_CLUSTER_NAME} --resource-group ${AKS_RG_NAME} --query identity.tenantId -o tsv)
```

:::note warn
https://learn.microsoft.com/ja-jp/azure/aks/csi-secrets-store-identity-access#configure-workload-identity
上記のサイトには、Managed IDに対し`Key Vault Administrator`のRoleを付与せよとあるが、これではうまく動かなかった。

https://learn.microsoft.com/ja-jp/azure/aks/workload-identity-deploy-cluster#optional---grant-permissions-to-access-azure-key-vault
上記のサイトには、Key Vaultの`set-policy`の方を設定せよと書いてあり、こっちが正解。
:::


Managed IDからKey Vaultへのアクセスを許可する。
```bash
az keyvault set-policy --name "${KEYVAULT_NAME}" \
  --secret-permissions get --spn "${USER_ASSIGNED_CLIENT_ID}"
```


## Kubernetesのサービスアカウントの作成

kubeconfigを取得する
```bash
az aks get-credentials -n ${AKS_CLUSTER_NAME} -g "${AKS_RG_NAME}"
```

ネームスペースを作成する。サービスアカウントはネームスペース毎に独立なので、このあとの手順で作成する各種Kubernetesリソースはすべて同じネームスペースを指定すること。
```bash
export MY_NAMESPACE="csidrivertest"

kubectl create namespace ${MY_NAMESPACE}
```

サービスアカウントを作成する
- `azure.workload.identity/client-id`のannotationで、先程作成したManaged IDと紐付ける

```bash
export SERVICE_ACCOUNT_NAME="workload-identity-sa"

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    azure.workload.identity/client-id: "${USER_ASSIGNED_CLIENT_ID}"
  name: "${SERVICE_ACCOUNT_NAME}"
  namespace: "${MY_NAMESPACE}"
EOF
```

フェデレーションID資格情報を確立する。やっていることは、Managed ID側へのサービスアカウントの情報の登録。

```bash
export FEDERATED_IDENTITY_CREDENTIAL_NAME="myFedIdentity"

az identity federated-credential create \
  --name ${FEDERATED_IDENTITY_CREDENTIAL_NAME} \
  --identity-name "${USER_ASSIGNED_ID_NAME}" \
  --resource-group "${USER_ASSIGNED_ID_RG}" \
  --issuer "${AKS_OIDC_ISSUER}" \
  --subject system:serviceaccount:"${MY_NAMESPACE}":"${SERVICE_ACCOUNT_NAME}" --audience api://AzureADTokenExchange
```

## Secret Provider Classの設定

Secret Provider Classを作成する。
- Podから環境変数としてクレデンシャルを参照する場合には、`secretObjects`の設定が必要。

```bash
export SECRET_PROVIDER_CLASS_NAME="azure-kv-spc"
export SECRET_NAME="azure-kv-secret"

export SECRET_USERNAME_KEY="username"
export SECRET_PASSWORD_KEY="password"

cat <<EOF | kubectl apply -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: ${SECRET_PROVIDER_CLASS_NAME}
  namespace: ${MY_NAMESPACE}
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "false"
    clientID: "${USER_ASSIGNED_CLIENT_ID}"
    keyvaultName: ${KEYVAULT_NAME}
    cloudName: ""
    objects:  |
      array:
        - |
          objectName: ${KV_SECRET_USERNAME_KEY}
          objectType: secret
          objectVersion: ""
        - |
          objectName: ${KV_SECRET_PASSWORD_KEY}
          objectType: secret
          objectVersion: ""
    tenantId: "${IDENTITY_TENANT}"
  secretObjects:
  - data:
    - key: ${SECRET_USERNAME_KEY}
      objectName: ${KV_SECRET_USERNAME_KEY}
    - key: ${SECRET_PASSWORD_KEY}
      objectName: ${KV_SECRET_PASSWORD_KEY}
    secretName: ${SECRET_NAME}
    type: Opaque 
EOF
```


## シークレットを参照するPodをデプロイする

以下、ポイント。
- シークレットを取得するためには、ボリュームとしてマウントする方法と、環境変数として参照する方法がある。
- 環境変数として参照する場合にも、ボリュームのマウントは必要。

```bash
export MY_POD_NAME="busybox-secrets-store-inline-wi"

cat <<EOF | kubectl apply -f -
kind: Pod
apiVersion: v1
metadata:
  name: ${MY_POD_NAME}
  namespace: ${MY_NAMESPACE}
spec:
  serviceAccountName: ${SERVICE_ACCOUNT_NAME}
  containers:
    - name: busybox
      image: registry.k8s.io/e2e-test-images/busybox:1.29-4
      command:
        - "/bin/sleep"
        - "10000"
      volumeMounts:
      - name: secrets-store01-inline
        mountPath: "/mnt/secrets-store"
        readOnly: true
      env:
      - name: SECRET_USERNAME
        valueFrom:
          secretKeyRef:
            name: ${SECRET_NAME}
            key: ${SECRET_USERNAME_KEY}
      - name: SECRET_PASSWORD
        valueFrom:
          secretKeyRef:
            name: ${SECRET_NAME}
            key: ${SECRET_PASSWORD_KEY}
  volumes:
    - name: secrets-store01-inline
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: ${SECRET_PROVIDER_CLASS_NAME}
EOF
```

:::note warn
https://learn.microsoft.com/ja-jp/azure/aks/csi-secrets-store-identity-access#configure-workload-identity
上記のサイトにはmetadata.labelsに`azure.workload.identity/use=true`を指定せよと記載されているが、今回は**記載しなくても問題なく動作した**。
（通常、workload identityを有効にするためには、Podのmetadata.labelsに`azure.workload.identity/use=true`が必要。）
今回`azure.workload.identity/use=true`を省略しても動作した理由は不明だが、SecretProviderClassがシークレットを取得してくれている（Podは認証通す必要がない？）からかもしれない。
:::


## 動作確認

シークレットを取得。
```bash
k get secret -n ${MY_NAMESPACE} 
```

以下のように、自動的にシークレットが作成されていることを確認。
```bash
NAME              TYPE     DATA   AGE
azure-kv-secret   Opaque   2      2s
```

次に、PodをDescribeしてみる。
```bash
k describe po -n ${MY_NAMESPACE} ${MY_POD_NAME}
```

以下、表示の抜粋。
- `/mnt/secrets-store`にシークレットがマウントされている
- 環境変数として`SECRET_USERNAME`、`SECRET_PASSWORD`が登録されている。

```bash
Name:             busybox-secrets-store-inline-wi
Namespace:        csidrivertest
Service Account:  workload-identity-sa
Labels:           <none>
Annotations:      <none>
Status:           Running
Containers:
  busybox:
--- 中略 ---
    Ready:          True
    Environment:
      SECRET_USERNAME:             <set to the key 'username' in secret 'azure-kv-secret'>  Optional: false
      SECRET_PASSWORD:             <set to the key 'password' in secret 'azure-kv-secret'>  Optional: false
    Mounts:
      /mnt/secrets-store from secrets-store01-inline (ro)
      /var/run/secrets/azure/tokens from azure-identity-token (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-wt9hp (ro)
--- 中略 ---
Volumes:
  secrets-store01-inline:
    Type:              CSI (a Container Storage Interface (CSI) volume source)
    Driver:            secrets-store.csi.k8s.io
    FSType:            
    ReadOnly:          true
    VolumeAttributes:      secretProviderClass=azure-kv-spc
```

シークレットがマウントされているパスをlsしてみる。

```bash
k exec -n ${MY_NAMESPACE} ${MY_POD_NAME} -- ls -l /mnt/secrets-store
```

以下の様に、ファイルとしてシークレットが格納されている。

```bash
lrwxrwxrwx  1 root   root       15 Sep 17 13:36 password -> ..data/password
lrwxrwxrwx  1 root   root       15 Sep 17 13:36 username -> ..data/username
```

# まとめ

AKSのシークレットをAzure Key Vaultに保存し、認証をworkload identityで行うことができた。シークレットの管理を外部に任せつつ、namespace単位でアクセス権限を分けられるので、シークレットを安全に運用できそう。



# 参考にしたサイト
- https://learn.microsoft.com/ja-jp/azure/aks/csi-secrets-store-driver
- https://learn.microsoft.com/ja-jp/azure/aks/csi-secrets-store-identity-access
- https://learn.microsoft.com/ja-jp/azure/aks/workload-identity-deploy-cluster
- https://secrets-store-csi-driver.sigs.k8s.io/