
Kubernetesでは自動化とコード重複の排除が基本方針です。シークレットマネージャーからのシークレット取得もこの原則に沿うべきもの。本記事では、その具体的な実現方法を解説します。
本記事は、Vaultを利用してKubernetes Podへのシークレット注入を自動化する方法を扱った前回の記事の続編です。
Kubernetes上のシークレットは、結局のところbase64エンコードされた平文に過ぎません。
保存時(at rest)の暗号化は可能ですが、Pod上では暗号化されておらず、暗号化キーは_Etcd_(シークレットの保管先)全体で共通です。すべてのシークレットが同じグローバルキーで暗号化されるため、マルチテナントクラスターではこれがリスクになります。
AWS、GCP、VaultからKubernetes Podへ安全にシークレットを注入する
GCP KMSを使えば、この課題を解決するためのエンベロープアプリケーションレイヤーキーを作成できます。とはいえ最終的に、Podは平文のままシークレットを利用することになります。
Hashicorp Vault以外にも、AWSとGCPがそれぞれシークレットマネージャーを提供しています。
AWSとGCPは、認証バックエンド、シークレットアクセス監査、失効、動的シークレットといったVaultの豊富な機能こそ備えていませんが、バージョン管理付きでシークレットを保存するという点では同様の仕組みを提供しています。
課題:Kubernetes Podでのシークレット利用
これらのシークレットをKubernetes Podで利用するには、ひとつ厄介な問題があります。認証処理と環境変数としてのシークレット取り込みを担うカスタムラッパースクリプトを自前で書かなければならないのです。さらに、すべての環境変数を引き継ぎPID:1となるよう、ラッパースクリプトをラッパープロセスに置き換える必要もあります。これは、コンテナで終了シグナルを正しく処理するうえで欠かせない要件です。
Kubernetesでは、こうした付帯的な作業の管理に必要となるコード重複を、自動化によって最小限に抑えるのが定石です(DNSや証明書の自動化も同じ発想です)。シークレットマネージャーからのシークレット取得についても、同じ考え方が当てはまります。
そもそもVaultの運用管理を避けたい企業もあり、その場合はAWSやGCPのシークレットマネージャーを採用する十分な理由になります。
押さえておきたいKubernetes Webhook
Kubernetesには、認証・認可後でオブジェクトの永続化前にKubernetes APIサーバーへのリクエストをインターセプトできるアドミッションコントローラーが標準で組み込まれています。
kube-apiserverバイナリには、次の2つの特別なコントローラーが組み込まれています。
- MutatingAdmissionWebhook
- ValidatingAdmissionWebhook
これらは認証・認可の後に、リクエストの変更(Mutation)と検証(Validation)を実行します。
Mutatingコントローラーは受け入れたオブジェクトを書き換えることができ、ValidatingコントローラーはAPIサーバーへのオブジェクトの受け入れを許可するかどうかを判定します。
これらのwebhookオブジェクトは、Kubernetesの通常のオブジェクトと同じくkubectlでインストールできます(実体は、webhookロジックを提供するhttpsエンドポイントです)。
インストールすると、以降のすべてのリクエストがこのwebhookロジックを通過するようになります。
Secrets Consumer Mutation Webhook
以下のコードと知見は、Banzai Cloudの優れた取り組みに大きく依拠しています。功績はすべて彼らに帰すべきものです(Banzai Cloudのブログは秀逸なので、ぜひご一読ください)。
Banzai Cloudのwebhookと本webhookの主な違いは次のとおりです。
- AWS、GCP、Vaultのシークレットマネージャーに対応。
- Banzai CloudのwebhookからconsulTemplatesと動的シークレット機能を削除。
- VaultをKubernetesバックエンドに加え、GCPバックエンドでも認証可能。
- シークレットマネージャーから明示的にシークレットを指定するか、すべてを取得するかを選択可能。
- シークレットパスをディレクトリとして扱い、その配下のシークレットをまとめて取得(第1階層に限定)。
- シークレットパスが
/で終わる場合、シークレット名をキーとして利用し、各値は単一値として扱う。
このユースケースの狙いは、シークレットの中身をより扱いやすくすること、すなわち追加・編集時のエラーを減らし、必要な手順数(読み取り、追記、更新)を短縮することにあります。 7. ワイルドカードのシークレットパスに対応。例えば、シークレットパスが/で終わり、db_で始まるシークレットが複数ある場合、/secret/path/db_*のように指定できます。 8. シークレットがKV1かKV2かを自動判定(シークレットパスに/dataを付ける必要はもうありません)。
Mutation Webhookの仕組み
APIサーバーへのすべての新規リクエストは、このwebhookを通過します。
webhookはオブジェクトがPodかどうかを確認し、特定のPodアノテーションが付いている場合のみオブジェクトを変更します。例えば次のようなアノテーションです。
https://gist.github.com/innovia/3e58eeb38668c9f4da4f1f76a736fbca
これらのアノテーションを検出すると、webhookはPodオブジェクトに対して以下の変更を加えます。
- 共有のインメモリボリュームを追加
- vault-envバイナリと、それを共有ボリュームへコピーするコマンドを持つinitコンテナを追加
- Podのコマンドを vault-env に変更
- Vault操作を簡素化するためのVault環境変数(ROLE、CA_PATH、SECRET_PATH)を追加
重要なポイント:
このwebhookはシークレットを環境変数としてPodに付与することはありません。機密データがディスクに永続化されることは一切なく、一時的にも保存されません。変更後のPodオブジェクト上から閲覧することもできません。
すべてのシークレットはメモリ上にのみ保持され、それを要求したプロセスからしか参照できません。
container.commandおよびcontainer.argsを明示しないチャートの利用
本webhookは、イメージレジストリから取得したイメージメタデータをもとに、コンテナのENTRYPOINTとCMDを判別できるようになりました。このデータはwebhook Podが再起動されるまでキャッシュされます。レジストリが認証なしで公開されている場合は特に設定は不要ですが、認証が必要なレジストリの場合は、PodのimagePullSecretsセクションに認証情報を用意しておく必要があります。
ほぼすべてのチャートでpodAnnotationsを設定できます。利用可能なアノテーションはwebhookのREADMEページでご確認ください。
Kubernetes外でのシークレット利用
secrets-consumer-envツールは、いくつかの環境変数を設定するだけで実行できます。利用可能なオプションの一覧はREADME.mdをご覧ください。
Webhookのインストール
このチャートをインストールする前に、専用のnamespaceを作成する必要があります。これは、チャート内のリソースが適用される順序に起因するものです(Helmは指定されたチャートとその依存関係のリソースをすべて収集し、リソースタイプごとにグループ化したうえで、定義済みの順序でインストールします。詳細はこちら — Helm 2.10をご参照ください)。
MutatingWebhookConfigurationは、webhook本体として動作する実際のバックエンドPodより先に作成されます。Kubernetesはそのwebhook Pod自体もMutateしようとしますが、まだMutateの準備ができていないため、無限再帰に陥ってしまいます。
export WEBHOOK_NS=`<namepsace>`WEBHOOK_NS=${WEBHOOK_NS:-vault-secrets-webhook}kubectl create namespace "${WEBHOOK_NS}"kubectl label ns "${WEBHOOK_NS}" name="${WEBHOOK_NS}"チャートの取得:
git clone https://github.com/innovia/secrets-consumer-webhook.gitチャートのインストール:
helm upgrade --namespace ${WEBHOOK_NS} --install secrets-consumer-webhook secrets-consumer-webhook --wait注:Helmのタイミングに関する問題があるため--waitが必須です。詳しくはこのissueをご参照ください。
GKEプライベートクラスターについて
Googleはプライベートクラスター用のコントロールプレーンを構成する際、Kubernetesクラスターのネットワークと、別のGoogle管理プロジェクトとの間にVPCピアリングを自動的に設定します。
自動生成されるルールでは、マスターとノード間のポートはわずかに10250と443しか開きません。そのため、GKEプライベートクラスターでwebhookコンポーネントを利用するには、マスターのCIDRがポート8443経由でwebhook Podにアクセスできるよう、追加のファイアウォールルールを構成する必要があります。
GKEコントロールプレーンノード向けのファイアウォールルールの追加方法については、GKEのドキュメントに詳しい解説があります。
明示的指定 vs. 非明示的(全取得)シークレット
プロセスに公開するシークレットを個別に選ぶことも、シークレットパス/名前配下のすべてを取得することもできます。
シークレットマネージャーから明示的にシークレットを選択する場合は、次のルールに従ってPodに環境変数を追加します。
env:- name: <エクスポートする変数名> value: vault:<シークレット内のVaultキー名>VaultでKubernetesバックエンド認証をセットアップする
Vaultは、Kubernetesサービスアカウントを利用してKubernetesに対して認証を行えます。
具体的には、auth-delegator権限を持つvault-reviewerという別のサービスアカウントを介して実現します。これにより、別のサービスアカウントトークンを認証用にKubernetesマスターへ渡せるようになります。
Kubernetesへの認証が成功すると、Vaultはクライアントトークンを返します。これを使ってVaultにログインできます。Vaultは、vault role、サービスアカウント、namespace、ポリシーのマッピングを参照し、アクセスの許可・拒否を判定します。
このvault-reviewerサービスアカウントトークンは、Vault CLIを使ってVault内に設定します。
では、vault-reviewer用のサービスアカウントを作成しましょう。
https://gist.github.com/innovia/5435f2336e4dd0045dbb5842880b3334
注:Vaultを別のnamespaceに構築している場合は、それに合わせてこのファイルを更新してください。
kubectl apply -f vault-reviewer.yamlKubernetes認証バックエンドを有効化:
$ vault loginToken (will be hidden):$ vault auth enable kubernetesSuccess! Enabled kubernetes auth method at: kubernetes/Vault-reviewerトークンとCAでVaultを設定:
注:Vaultを別のnamespaceに構築している場合は、各_kubectl_コマンドの後に_-n_フラグを指定してください。
$ VAULT_SA_TOKEN_NAME=$(kubectl get sa vault-reviewer -o jsonpath="{.secrets[*]['name']}")$ SA_JWT_TOKEN=$(kubectl get secret "$VAULT_SA_TOKEN_NAME" -o jsonpath="{.data.token}" | base64 --decode; echo)$ SA_CA_CRT=$(kubectl get secret "$VAULT_SA_TOKEN_NAME" -o jsonpath="{.data['ca\.crt']}" | base64 --decode; echo)$ vault write auth/kubernetes/config token_reviewer_jwt="$SA_JWT_TOKEN" kubernetes_host=https://kubernetes.default kubernetes_ca_cert="$SA_CA_CRT"Success! Data written to: auth/kubernetes/configVaultにおける、サービスアカウントとnamespaceに対するロールマッピングの例
vault write auth/kubernetes/role/tester bound_service_account_names=tester bound_service_account_namespaces=default policies=test_policy ttl=1hVaultでGCPバックエンド認証をセットアップする
Google Cloud認証メソッドを有効化:
$ vault auth enable gcp認証メソッドの認証情報を設定:
$ vault write auth/gcp/config \ credentials=@/path/to/credentials.jsonインスタンスの認証情報を利用する場合や、環境変数で認証情報を指定したい場合は、このステップをスキップしてかまいません。
名前付きロールを作成します。
iam-タイプのロールの場合:
$ vault write auth/gcp/role/my-iam-role \ type="iam" \ policies="dev,prod" \ bound_service_accounts="[email protected]"gce-タイプのロールの場合:
$ vault write auth/gcp/role/my-gce-role \ type="gce" \ policies="dev,prod" \ bound_projects="my-project1,my-project2" \ bound_zones="us-east1-b" \ bound_labels="foo:bar,zip:zap" \ bound_service_accounts="[email protected]"必要となるGCP権限
Vaultサーバーの権限
iam-タイプのVaultロールでは、Vaultに次のロールを付与できます。
roles/iam.serviceAccountKeyAdmingce-タイプのVaultロールでは、Vaultに次のロールを付与できます。
roles/compute.viewer必要最小限のGCP権限のみを持つカスタムロールを作成したい場合は、次の権限リストを利用してください。
iam.serviceAccounts.getiam.serviceAccountKeys.getcompute.instances.getcompute.instanceGroups.listVaultに対する認証用の権限
上記の権限は_Vaultサーバー_に付与するものである点に注意してください。Vaultに対して認証を行うIAMサービスアカウントまたはGCEインスタンスには、次のロールが必要です。
roles/iam.serviceAccountTokenCreatorソースコードもあわせてご確認ください。