Kubernetes etcd 機密データ暗号化チュートリアル — Secretを平文保存から保護する

KubernetesでSecretを作成すると、値がBase64で表示されます。そのため、初めて触れる人はSecretがすでに暗号化されていると誤解しがちです。

しかし、Base64は暗号化ではなくエンコーディングです。誰でも次のコマンドで元の値を確認できます。

echo 'bXlkYXRh' | base64 --decode

さらに重要な問題は、デフォルト設定のkube-apiserverがSecretをetcdに保存する際に、別途保存暗号化、つまりencryption at restを適用しないことです。etcdのバックアップファイルやデータディレクトリ、ディスクスナップショットが流出すると、RBACを経由せずに機密データが露出する可能性があります。

この記事では、kubeadmで構成したKubernetesクラスターを基準に、次のプロセスを直接確認します。

  • 暗号化適用前のetcdに保存されたSecretの確認
  • 32バイト暗号化キーの生成
  • EncryptionConfigurationの作成
  • kube-apiserver静的Podへの設定接続
  • 新しいSecretがetcdで暗号化されたか検証
  • 既存のSecretを書き換えてすべて暗号化
  • キーの交換と運用環境での注意事項

このチュートリアルは、Kubernetes公式ドキュメントのEncrypting Confidential Data at Restの手順に基づいて作成しました。

APIサーバーがSecretを暗号化してetcdに保存する流れ


まず理解すべきこと:何を暗号化するのか

Kubernetesの保存暗号化は、Podにマウントされたファイルやアプリケーションデータベースを暗号化する機能ではありません。

暗号化対象は、Kubernetes APIを通じて生成され、etcdに永続的に保存されるAPIリソースです。代表的に次のリソースを指定できます。

  • secrets
  • configmaps
  • ユーザー定義リソースであるCRDオブジェクト
  • その他etcdに保存されるKubernetes APIリソース

データフローは次のとおりです。

kubectl 또는 애플리케이션
        ↓
kube-apiserver
        ↓  EncryptionConfiguration에 따라 암호화
      etcd

クライアントがKubernetes APIでSecretを照会すると、kube-apiserverがetcdの暗号文を復号化して正常なAPI応答を返します。したがって、ユーザーは従来どおりkubectl get secretを使用できます。

保存暗号化は、etcdディスクやバックアップの流出に備える防御層です。次のセキュリティ制御を代替するものではありません。

セキュリティ制御 保護する領域
Kubernetes RBAC APIを通じて誰がSecretを読み取れるかを制御
TLS APIサーバーとetcd間の転送区間を保護
etcd 保存暗号化 etcdデータとバックアップに保存された値を保護
ディスク暗号化 ノードディスクやボリューム全体を保護
KMS/HSM 暗号化キーをクラスター外部で保護し、ライフサイクルを管理

一つだけ適用しても、他の層が不要になるわけではありません。

実習環境と事前注意事項

この記事は次の環境を想定しています。

  • kubeadmで構成されたKubernetesクラスター
  • etcd 3.x
  • 各コントロールプレーンでkube-apiserverが静的Podとして実行
  • コントロールプレーンノードにsudoでアクセス可能
  • kubectlとetcdctlが使用可能

クラスターバージョンは次のコマンドで確認します。

kubectl version

この作業はコントロールプレーンのコア設定を変更します。運用環境では、必ずetcdスナップショットと現在の静的Podマニフェストをバックアップし、復旧手順を確認してから進める必要があります。

sudo cp -a /etc/kubernetes/manifests/kube-apiserver.yaml 
  /root/kube-apiserver.yaml.before-encryption

高可用性クラスターの場合、すべてのコントロールプレーンノードが同じ暗号化設定とキーを持つ必要があります。ノードごとに設定が異なると、あるAPIサーバーはデータを書き込み、別のAPIサーバーはそのデータを復号化できない状況が発生する可能性があります。

1段階:暗号化がすでに適用されているか確認する

まず、kube-apiserverに--encryption-provider-config引数があるか確認します。

sudo grep -- '--encryption-provider-config' 
  /etc/kubernetes/manifests/kube-apiserver.yaml

何も結果が出ない場合、APIサーバーレベルの保存暗号化は有効になっていません。

引数があっても安心できません。EncryptionConfigurationの最初のプロバイダーがidentityの場合、新しいデータは平文で保存されます。プロバイダーの順序が読み書き動作を決定するためです。

  • 書き込み時:リストの最初のプロバイダーを使用
  • 読み込み時:上から順に試行し、復号化可能なプロバイダーを使用

マイグレーション中は、暗号化プロバイダーを最初に置き、過去の平文データを読み込むためのidentityを後ろに置きます。

2段階:適用前にSecretを作成し、etcdの値を確認する

比較のためにテストSecretを一つ作成します。

kubectl create secret generic secret-before 
  --namespace default 
  --from-literal=mykey=mydata

Kubernetes APIで照会すると、値はBase64で表示されます。

kubectl get secret secret-before -n default -o yaml

予想されるデータは次のとおりです。

data:
  mykey: bXlkYXRh

次に、コントロールプレーンノードでetcdの元の値を確認します。

sudo ETCDCTL_API=3 etcdctl 
  --cacert=/etc/kubernetes/pki/etcd/ca.crt 
  --cert=/etc/kubernetes/pki/etcd/server.crt 
  --key=/etc/kubernetes/pki/etcd/server.key 
  get /registry/secrets/default/secret-before | hexdump -C

暗号化を適用していない状態であれば、出力内でSecretの構造とmydataに該当するデータを識別できます。これがBase64表示だけではetcd保存データを保護できない理由です。

etcdctl接続証明書とエンドポイントは、クラスター構成によって異なる場合があります。外部etcdを使用する場合は、実際のエンドポイントとクライアント証明書を指定する必要があります。

3段階:32バイト暗号化キーを生成する

公式ドキュメントは、ローカル暗号化プロバイダーを使用する際に32バイトの乱数キーを作成し、Base64でエンコードするように案内しています。

KEY=$(head -c 32 /dev/urandom | base64)

画面にキーを出力したり、シェル履歴、メッセンジャー、一般文書にコピーしたりしないことをお勧めします。このキーを持つ人は、etcdの暗号化されたデータを復号化できます。

暗号化設定を保存するディレクトリを作成します。

sudo install -d -m 700 /etc/kubernetes/enc

4段階:EncryptionConfigurationを作成する

今回の実習では、Kubernetes公式チュートリアルの保存形式と検証フローをそのまま理解するために、SecretとConfigMapをaescbcプロバイダーで暗号化します。

注意:現在のKubernetes公式ドキュメントは、aescbcをCBCパディングオラクル攻撃の可能性から弱い方式と分類し、運用環境には推奨していません。以下の構成は、保存暗号化の動作原理とマイグレーション手順を習得するための実習用です。新しい運用環境であればKMS v2を優先的に検討すべきです。

sudo tee /etc/kubernetes/enc/enc.yaml >/dev/null <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${KEY}
      - identity: {}
EOF

unset KEY
sudo chmod 600 /etc/kubernetes/enc/enc.yaml
sudo chown root:root /etc/kubernetes/enc/enc.yaml

プロバイダーの順序が重要です。

providers:
  - aescbc: ...
  - identity: {}

この構成では、新しく作成または更新されるデータはaescbcで暗号化されます。既存の平文で保存されたデータは、後ろのidentityプロバイダーのおかげで引き続き読み取ることができます。

逆にidentityを最初に置くと、APIサーバーは新しいデータを引き続き平文で保存します。

aescbc、aesgcm、secretbox、KMSの中から何を選択するか

Kubernetesは複数の暗号化プロバイダーをサポートしていますが、運用特性は異なります。

プロバイダー 特徴 運用時の考慮事項
aescbc 設定が単純なAES-CBCローカル対称鍵方式 公式ドキュメントが弱い方式と分類し、運用環境には非推奨
aesgcm 認証暗号化を提供 同じキーで約20万件を超えて書き込まないようにキーローテーションが必要
secretbox XSalsa20とPoly1305ベース ローカル設定ファイルにキーが存在
kms v2 外部KMSがキー暗号化キーを管理するエンベロープ暗号化 強力なキー隔離を提供、別途プラグインと可用性設計が必要

ローカルキーをEncryptionConfigurationに直接保存する方式は、暗号化を使用しないよりは優れていますが、コントロールプレーンファイルとetcdデータが同時に奪取された場合、防御力が制限されます。重要な運用環境であれば、クラウドKMSやHSMと連携する方式を検討すべきです。

5段階:kube-apiserverに暗号化設定を接続する

静的Podマニフェストを編集します。

sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml

kube-apiserverのcommandに次の引数を追加します。

- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml

同じコンテナのvolumeMountsに読み取り専用マウントを追加します。

volumeMounts:
  - name: enc
    mountPath: /etc/kubernetes/enc
    readOnly: true

Pod spec.volumesにはhostPathボリュームを追加します。

volumes:
  - name: enc
    hostPath:
      path: /etc/kubernetes/enc
      type: DirectoryOrCreate

ファイルを保存すると、kubeletが静的Podの変更を検知してkube-apiserverを再生成します。

kubectl -n kube-system get pod 
  -l component=kube-apiserver -w

APIサーバーが正常状態に戻るか確認します。

kubectl get --raw='/readyz?verbose'

エラーが発生した場合は、kubeletログとコンテナランタイムログを確認し、事前にバックアップしたマニフェストで復旧します。

sudo journalctl -u kubelet -n 200 --no-pager

6段階:新しいSecretが暗号化されて保存されるか検証する

暗号化設定を適用した後、新しいSecretを作成します。

kubectl create secret generic secret-after 
  --namespace default 
  --from-literal=mykey=mydata

etcdから値を直接読み取ります。

sudo ETCDCTL_API=3 etcdctl 
  --cacert=/etc/kubernetes/pki/etcd/ca.crt 
  --cert=/etc/kubernetes/pki/etcd/server.crt 
  --key=/etc/kubernetes/pki/etcd/server.key 
  get /registry/secrets/default/secret-after | hexdump -C

出力には次のプレフィックスが表示されるはずです。

k8s:enc:aescbc:v1:key1:

各項目の意味は次のとおりです。

値 意味
k8s:enc Kubernetes保存暗号化データ
aescbc 使用したプロバイダー
v1 保存形式バージョン
key1 暗号化に使用したキー名

次に、APIを介した復号化も正常か確認します。

kubectl get secret secret-after 
  --namespace default 
  -o jsonpath='{.data.mykey}' | base64 --decode
echo

次の値が出力されれば正常です。

mydata

つまり、etcdには暗号文が保存されますが、権限のあるユーザーはKubernetes APIを通じて正常にSecretを使用できます。

7段階:既存のSecretも書き換えて暗号化する

ここで最も見落としがちな部分があります。

暗号化設定を有効にしても、すでにetcdに保存されたデータが自動的に再暗号化されるわけではありません。設定適用後に新しく作成または更新されたリソースから暗号化されます。

既存のすべてのSecretを同じ内容で再度保存すると、APIサーバーが暗号化プロバイダーを適用してetcdに記録します。

kubectl get secrets --all-namespaces -o json 
  | kubectl replace -f -

書き込み衝突が発生しても、再度実行しても安全です。規模の大きいクラスターでは、一度にすべてのSecretを更新するよりも、namespace単位に分けて実行する方が良いでしょう。

ConfigMapも暗号化対象に指定した場合は、別途再度保存する必要があります。

kubectl get configmaps --all-namespaces -o json 
  | kubectl replace -f -

マイグレーション後、最初に作成したsecret-beforeも暗号化プレフィックスを持つか確認します。

sudo ETCDCTL_API=3 etcdctl 
  --cacert=/etc/kubernetes/pki/etcd/ca.crt 
  --cert=/etc/kubernetes/pki/etcd/server.crt 
  --key=/etc/kubernetes/pki/etcd/server.key 
  get /registry/secrets/default/secret-before | hexdump -C

8段階:平文フォールバックを削除するか

すべての対象データが十分に暗号化されたことを検証したら、identityプロバイダーを削除してAPIサーバーが平文保存データを読み取れないようにすることができます。

providers:
  - aescbc:
      keys:
        - name: key1
          secret: <BASE64_ENCODED_32_BYTE_KEY>

しかし、この段階は慎重に行う必要があります。暗号化されていないリソースが一つでも残っていると、APIサーバーはそのオブジェクトを読み取れません。

したがって、次の条件をすべて満たした後に進める必要があります。

  • すべての対象リソースを再度保存したか
  • すべてのコントロールプレーンが同じ設定を使用しているか
  • etcdで暗号化プレフィックスをサンプルまたは全体検査したか
  • 暗号化キーを安全にバックアップしたか
  • 復旧手順を実際に確認したか

運用初期にはidentityを読み取り用フォールバックとして維持し、マイグレーション検証が完了した後に別途変更作業で削除する方が安全です。

9段階:暗号化キーを無停止で交換する原理

キー交換は、既存のキーをすぐに削除する作業ではありません。

Kubernetes公式ドキュメントが案内する順序は次のとおりです。

  1. 新しいキーを既存のプロバイダーの2番目のキーとしてすべてのコントロールプレーンに追加する。
  2. すべてのAPIサーバーが新しいキーを読み取れるように設定を反映する。
  3. 新しいキーを安全にバックアップする。
  4. 新しいキーをキーリストの最初の位置に上げる。
  5. すべてのAPIサーバーが新しい書き込みに新しいキーを使用するように設定を反映する。
  6. 既存のSecretをkubectl replaceで再度保存する。
  7. すべてのデータが新しいキーで暗号化されたことを検証した後、以前のキーを削除する。

例えば、設定は中間段階で次のような形になります。

providers:
  - aescbc:
      keys:
        - name: key2
          secret: <NEW_BASE64_KEY>
        - name: key1
          secret: <OLD_BASE64_KEY>
  - identity: {}

最初のキーは新しいデータを暗号化するために使用され、後ろにあるキーは過去のデータを復号化するために使用されます。すべてのデータを新しいキーで再度保存する前に以前のキーを削除すると、既存のSecretを復号化できません。

Kubernetesは--encryption-provider-config-automatic-reload=trueオプションで設定ファイルの自動再読み込みもサポートしています。有効にすると、APIサーバーがファイルの変更を定期的に確認するため、キー交換のたびにAPIサーバーを再起動する必要がありません。ただし、実際の適用時点は関連メトリクスで確認する必要があります。

高可用性コントロールプレーンで必ず守るべき順序

コントロールプレーンが複数台ある場合、すべてのノードを同時に変更せず、1台ずつ順次適用します。

段階 確認事項
1 すべてのノードに同じenc.yamlを安全にデプロイ
2 最初のAPIサーバー設定変更および正常復帰確認
3 残りのAPIサーバーを1台ずつ変更
4 ロードバランサーを通じてAPIリクエストが正常か確認
5 新しいデータの暗号化プレフィックス確認
6 既存データ再保存

ローリング変更中も、すべてのAPIサーバーが現在のetcdデータを読み取れる必要があります。新しいキーをすべてのノードにデプロイする前に、そのキーを最初の書き込みキーに昇格させてはなりません。

運用環境ではKMSを推奨する理由

これまで使用したaescbc方式は、公式手順に従って保存暗号化の動作を実習するには有用です。しかし、現在の公式ドキュメントはこの方式を弱い暗号化と分類しており、暗号化キーも/etc/kubernetes/enc/enc.yamlに含まれています。

攻撃者がetcdデータとコントロールプレーンの設定ファイルを同時に取得すると、暗号文と復号化キーを同時に確保できます。

KMSプロバイダーを使用すると、実際のデータ暗号化キーをクラスター外部のKMSやHSMで保護できます。また、次の運用機能を活用しやすくなります。

  • キーアクセス権限の分離
  • 集中型キーローテーション
  • キー使用監査ログ
  • HSMまたはクラウド管理型KMSの活用
  • Kubernetes管理者とキー管理者の役割分離

したがって、この記事のローカルaescbc構成は保存暗号化の動作原理を学ぶ出発点と捉え、高いセキュリティレベルが必要な運用クラスターではKMSプロバイダーを利用したデータ暗号化を検討することをお勧めします。

よくある間違い

1. SecretのBase64を暗号化だと考える

Base64は誰でも復元できる表現方式です。etcd保存暗号化とは別物です。

2. identityを最初のプロバイダーにする

読み込みは可能ですが、新しいデータは平文で保存されます。書き込みプロバイダーはリストの最初の項目です。

3. 設定だけ適用し、既存のSecretを再度保存しない

既存データは自動変換されません。kubectl replaceのような再書き込みプロセスが必要です。

4. ノードごとに異なるキーをデプロイする

HA環境でAPIサーバーごとに復号化可能かどうかが異なり、断続的な障害が発生する可能性があります。

5. キーバックアップなしで以前のキーを削除する

該当キーで暗号化されたデータは復旧できなくなります。etcdバックアップのみでキーバックアップがない場合、暗号化されたSecretを復元できません。

6. 暗号化設定ファイルの権限を緩くする

ローカルプロバイダーのキーは事実上復号化権限です。rootとAPIサーバー実行主体のみが読み取れるように制限する必要があります。

実習まとめ

今回の実習で確認した核心は次のとおりです。

  1. Kubernetes SecretのBase64は暗号化ではない。
  2. デフォルト設定では、APIリソースがetcdに保存暗号化なしで記録される可能性がある。
  3. EncryptionConfigurationの最初のプロバイダーが新しいデータを暗号化する。
  4. identityはマイグレーション中に既存の平文データを読み込むためのフォールバックである。
  5. 設定適用だけでは既存データは暗号化されない。
  6. etcdでk8s:enc:aescbc:v1:key1:プレフィックスを確認する必要がある。
  7. キーとetcdバックアップは一緒に管理するが、アクセス権限は分離する必要がある。
  8. 運用環境では外部KMS連携がより強力なキー保護を提供する。

保存暗号化は、攻撃者がKubernetes API権限を取得する状況を防ぐ機能ではありません。代わりに、etcdディスク、スナップショット、バックアップファイルが流出した際に機密データがすぐに露出するリスクを低減します。

Kubernetesセキュリティは、RBAC、ネットワーク保護、監査ログ、バックアップセキュリティ、保存暗号化が連携して機能するときに完成します。


参考にした公式ドキュメント


Comments

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です