akv://<vault-name>/<secret-name> 形式の文字列を Azure Key Vault secrets で差し替えるツールを作った

akv というツールを作りました。次のように標準入力に含まれる akv://<vault-name>/<secret-name> 形式の文字列や、同形式の環境変数を Azure Key Vault secret(AWS だと parameter store に相当)の値で差し替えることができます。

$ az keyvault secret set --vault-name example --name password --value 'C@6LWQnuKDjQYHNE'
$ echo 'password: akv://example/password' | akv inject
password: C@6LWQnuKDjQYHNE
$ env PASSWORD=akv://example/password akv exec -- printenv PASSWORD
C@6LWQnuKDjQYHNE

inject subcommand は 1Password CLI の inject subcommand みたいなもので、exec subcommand は berglas の exec subcommand が begras://sm:// で始まる環境変数を差し替えたり、1Password CLI の run subcommandop:// で始まる環境変数を差し替えるのと同じような感じです。

背景

Argo Workflows の events API では、GitHub の webhook のように Authorization ヘッダをリクエストに含めることができないケースのための仕組みがあります。
cf. Webhooks - Argo Workflows - The workflow engine for Kubernetes

これは、予め argo-workflows-webhook-clients という名前で service account ごとに type, secret 情報を Secret オブジェクトに保存しておけば、Authorization ヘッダがないリクエストを受け取った場合にそれらの情報を使ってリクエストの signature を検証し、検証に成功した service account の token を Authorization ヘッダに付与してくれるというものです。
cf. addWebhookAuthorization

なので、Kubernetes manifest で Secret オブジェクトを管理しつつ、webhook secret の値は manifest を見ただけではわからない形にしたいという思いがありました。
次の記事ややり取りを見ても色んな人が Secret オブジェクトの管理には頭を悩ませている印象です。

どちらでも言及されている external-secretssealed-secrets が広く使われてそうですが、今回は手軽に実現したかったのでどちらも大仰に感じました。
また、秘匿情報の管理は Azure Key Vault secrets に集約したいという思いもありました。Azure Kubernetes Service では Azure Key Vault secrets を CSI volume として利用できるんですが、前述のとおり events API で利用するには Secret オブジェクトでなければいけません。

というわけで、akv://<vault-name>/<secret-name> といった形式の文字列を Azure Key Vault secret の値で差し替えるツールを作って、Kustomize の exec KRM function から利用するのが手軽と考えました。
Kustomize のプラグイン機能はアルファのまま何年も進展がないのが気になるところではありますが…

Kustomize の exec KRM function による secret の差し替え

exec KRM function の詳細についてはドキュメントに譲りますが、最低限次のようなファイルが必要です。ファイル名は kustomization.yaml 以外は好きな名前で大丈夫です。

$ tree .
.
├── key-vault-secrets.yaml
├── kustomization.yaml
├── plugins
│   └── inject-key-vault-secrets.sh
└── secret.yaml

2 directories, 4 files

それぞれのファイルの内容は以下のとおりで、secret.yaml が差し替え対象の Secret オブジェクト、key-vault-secrets.yaml が KRM function の定義です。

kustomization.yaml
resources:
- secret.yaml

transformers:
- key-vault-secrets.yaml
secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: some-secret
stringData:
  # This secret is replaced by KeyVaultSecretsTransformer
  password: akv://example/password
key-vault-secrets.yaml
apiVersion: abicky.net/v1alpha1 # どんな値でも OK
kind: KeyVaultSecretsTransformer # どんな値でも OK
metadata:
  name: key-vault-secrets # どんな値でも OK
  annotations:
    config.kubernetes.io/function: |
      exec:
        # kustomization.yaml からの相対パス
        path: ./plugins/inject-key-vault-secrets.sh
plugins/inject-key-vault-secrets.sh
#!/bin/bash
akv inject --quote

このディレクトリに対して --enable-alpha-plugins --enable-exec オプションを付けて実行すると secret の内容が差し替わります。

$ kustomize build --enable-alpha-plugins --enable-exec .
apiVersion: v1
kind: Secret
metadata:
  name: some-secret
stringData:
  password: C@6LWQnuKDjQYHNE

なお、akv inject--quote オプションを付けているのは secret に改行が含まれるケースのためです。例えば SSH の private key を Secret オブジェクトで管理するケースが当てはまります。1

もし --quote オプションがないと、YAML が壊れてしまうので次のようなエラーになります。

$ kustomize build --enable-alpha-plugins --enable-exec .
Error: couldn't execute function: MalformedYAMLError: yaml: line 20: could not find expected ':'

既にダブルクォートで囲まれているケースのために --escape オプションもあるんですが、仮に次のように Secret オブジェクトの定義の方でダブルクォートで囲っていたとしても Kustomize が不要なダブルクォートとみなして KRM function を実行するまでに取り除いてしまうので、Kustomize で利用する際には --quote オプションにする必要があります。

apiVersion: v1
kind: Secret
metadata:
  name: some-secret
stringData:
  password: "akv://example/password"
  1. Argo Workflows の git artifact で GitHub App の private key から token を生成して checkout できると良いんですが、現状は artifact を利用せずに自前で checkout するか、deploy key または machine user の private key を使うしかなさそうです cf. Git artifact using GitHub App Credentials · argoproj/argo-workflows · Discussion #11028