etcdの中の平文を自分で確認して塞ぐ
目標
Secretが実際にどのように保存されるのかをetcdを直接開いて確認し、保存時の暗号化・エンベロープ暗号化・アクセス制御・外部ストアという4層の対策を、それぞれマニフェストとして書きます。
なぜ重要なのか
最も広まっている誤解が、「Secretはbase64だから安全だ」というものです。base64はキーのない表現方式なので、元に戻すのにコマンド1つで済みます。さらに重要なのは、デフォルト設定ではapiserverがSecretを平文のままetcdに書き込むという事実です。前のモジュールで学んだetcdスナップショットは、だからこそクラスターで最も機微なファイルでもあります。このラボでは、その事実を人から聞いた話ではなく、自分の手で確認します。対策は1層ではありません。EncryptionConfigurationはディスクを手に入れた人を防ぎ、KMSのエンベロープ暗号化はそのキーまでクラスターの外に出します。しかし、保存時に暗号化しても、APIで読み取れる人には意味がないのでRBACが必要であり、resourceNamesで名前単位まで絞れるものの、それがlist/watchには通用しないという限界も知っておく必要があります。最後に、値をそもそもクラスターの外に置く外部シークレット方式まで含めて、4つの層がそれぞれ何を防ぎ何を防げないのかを区別することが、このラボの核心です。
ステップ
- ネームスペース
secret-labを作成し、その中にSecretdb-passwordを作成してください。タイプはOpaqueで、キーpasswordの値はlabhub-Pr0d-2026です。あとで権限を比較するための対照群として、Secretother-secretも1つ作成してください。 db-passwordのdata.passwordの値をそのまま/root/ops/secrets/out/encoded.txtに、それをデコードした平文を/root/ops/secrets/out/decoded.txtに保存してください(デコード結果はlabhub-Pr0d-2026である必要があり、2つのファイルは互いに対応している必要があります)。そして/root/ops/secrets/out/base64-note.txtに、base64はエンコードにすぎず暗号化ではないという結論を1行で書いてください。/root/ops/secrets/encryption-config.yamlを書いてください。apiVersionはapiserver.config.k8s.io/v1、kindはEncryptionConfigurationで、resources[0].resourcesにsecretsが含まれている必要があります。providersの最初は実際の暗号化プロバイダー(aescbc、secretbox、aesgcmのいずれか)で、keys[0].nameが必要です。最後はidentityでなければなりません。/root/ops/secrets/kms-config.yamlを書いてください。kindはEncryptionConfigurationで、providers[0]はkmsである必要があります。kms.apiVersionはv2、kms.endpointはunix://で始まるソケットのパス、kms.nameも必要です。そして/root/ops/secrets/out/envelope-note.txtに、エンベロープ暗号化の2層構造(データキーとマスターキー)を説明して書いてください。/root/ops/secrets/external-secret.yamlに、2つのドキュメントを書いてください。1つはkind: SecretStore、もう1つはkind: ExternalSecretです。ExternalSecretには、spec.refreshInterval、spec.secretStoreRef.name、spec.target.name、spec.target.creationPolicy、spec.data[0].remoteRef.keyがすべて必要です。このファイルに、実際のパスワードlabhub-Pr0d-2026が含まれていてはいけません。secret-labにServiceAccountdb-clientを作成し、Roledb-secret-readerを作成してください。ルールは、secretsリソースに対して、resourceNamesはdb-passwordの1つ、verbsはgetの1つだけである必要があります。このロールをdb-clientにRoleBindingで紐づけてください。結果として、db-clientはdb-passwordは読み取れて、other-secretは読み取れず、Secretの一覧の取得(list)もできない状態になっている必要があります。そして/root/ops/secrets/out/resourcenames-note.txtに、resourceNamesがlist/watchには通用しないという点を書いてください。secret-labにSecretpinned-configを作成してください。immutable: trueで、キーbuildの値は2026-08-20です。そのあと、この値を変更しようとして、拒否メッセージを/root/ops/secrets/out/immutable-error.txtに保存してください。保存後も、buildの値は依然として2026-08-20である必要があります。- etcdから
/registry/secrets/secret-lab/db-passwordキーを取得した出力を、/root/ops/secrets/out/etcd-secret.txtに保存してください。このファイルの中に、平文labhub-Pr0d-2026が見えている必要があります。そして/root/ops/secrets/out/secrets-audit.jsonを作成してください。total_secretsはsecret-labの実際のSecretの数と一致している必要があり、encrypted_at_restはfalse、plaintext_visible_in_etcdはtrueです。mitigations配列には対応策を3つ以上書き、保存時の暗号化(EncryptionConfiguration、または韓国語で「暗号化」を意味する語)とアクセス権限の制御(RBAC、または韓国語で「権限」を意味する語)が必ず含まれている必要があります。
参考
- このラボのetcdは、クライアントTLSなしで
127.0.0.1:2379で待ち受けています。etcdctl --endpoints=127.0.0.1:2379 get /registry/secrets/secret-lab/db-passwordで取得できます。 - Kubernetesはオブジェクトを、JSONではなくprotobufでetcdに保存します。そのため、出力が壊れたバイナリのように見えるのは正常で、その中に人が読める文字列が混ざっています。パスワードはその中にbase64ではなく平文のまま入っています。それがこのステップの要点です。読みやすく見たいなら、
| stringsや| hexdump -Cで流し見して、ファイルには取得した出力をそのまま保存してください(ヌルバイトが気になる場合は、| tr -d '\0'で取り除けばよいです)。 - ステップ3の暗号化キーは、32バイトのランダムな値をbase64でエンコードして使います。
head -c 32 /dev/urandom | base64で作成できます。 - ステップ7の変更の試行は、
kubectl patchでもkubectl applyでもかまいません。エラーメッセージは標準エラー出力に出るので、2>&1で受け取らないとファイルに残りません。 - ステップ8のSecretの数は、
kubectl get secrets -n secret-lab -o json | jq '.items | length'で数えるのが安全です。手で数えるとずれやすいです。 - よくある間違い1: ステップ3で
identityをリストの先頭に置いてしまうことです。書き込みに使われるのは最初のプロバイダーなので、その瞬間に平文での保存に戻ります。 - よくある間違い2: ステップ6で
verbsにlistを一緒に入れてしまうことです。resourceNamesは一覧の取得を絞ってくれないので、すべてのSecretが露出します。 - よくある間違い3: ステップ5のマニフェストに実際の値を書いておくことです。値がないという点が、外部シークレット方式の存在理由です。
- ラボのPodはラボごとに新しく起動するので、前のラボで作ったクラスターの状態は残っていません。ネームスペースとSecretは、このラボの中で自分で作成してください。運用手順を記憶ではなくランブックとマニフェストとして残すべき理由が、まさにここにあります。
ラボ用のSecretを2つ作成する
ネームスペースsecret-labを作成し、その中にSecretdb-passwordを作成してください。タイプはOpaqueで、キーpasswordの値はlabhub-Pr0d-2026です。あとで権限を比較するための対照群として、Secretother-secretも1つ作成してください。
デフォルトのタイプが何かを確認してください。あとで権限の範囲を比較するための対照群が必要なので、2つ作成します。
base64を自分で元に戻してみる
db-passwordのdata.passwordの値をそのまま/root/ops/secrets/out/encoded.txtに、それをデコードした平文を/root/ops/secrets/out/decoded.txtに保存してください(デコード結果はlabhub-Pr0d-2026である必要があり、2つのファイルは互いに対応している必要があります)。そして/root/ops/secrets/out/base64-note.txtに、base64はエンコードにすぎず暗号化ではないという結論を1行で書いてください。
エンコードと暗号化の違いを、手で確かめるステップです。元に戻した値とエンコードした値が、互いに対応しているかも確認してください。
保存時の暗号化の設定を書く
/root/ops/secrets/encryption-config.yamlを書いてください。apiVersionはapiserver.config.k8s.io/v1、kindはEncryptionConfigurationで、resources[0].resourcesにsecretsが含まれている必要があります。providersの最初は実際の暗号化プロバイダー(aescbc、secretbox、aesgcmのいずれか)で、keys[0].nameが必要です。最後はidentityでなければなりません。
書き込みに使われるのは、リストの最初です。既存の平文を読み続けるには、最後に何が残っている必要があるのかを考えてみてください。キーには名前が必要です。
KMS v2のエンベロープ暗号化の設定を書く
/root/ops/secrets/kms-config.yamlを書いてください。kindはEncryptionConfigurationで、providers[0]はkmsである必要があります。kms.apiVersionはv2、kms.endpointはunix://で始まるソケットのパス、kms.nameも必要です。そして/root/ops/secrets/out/envelope-note.txtに、エンベロープ暗号化の2層構造(データキーとマスターキー)を説明して書いてください。
プラグインは、ネットワークではなくソケットで接続します。2層構造が何かを、メモに残してください。
外部シークレットストアと連携するマニフェストを書く
/root/ops/secrets/external-secret.yamlに、2つのドキュメントを書いてください。1つはkind: SecretStore、もう1つはkind: ExternalSecretです。ExternalSecretには、spec.refreshInterval、spec.secretStoreRef.name、spec.target.name、spec.target.creationPolicy、spec.data[0].remoteRef.keyがすべて必要です。このファイルに、実際のパスワードlabhub-Pr0d-2026が含まれていてはいけません。
ドキュメントが2つ必要です。1つはどこから取得するかを、もう1つは何をどの名前で作るかを定義します。マニフェストに実際のパスワードが含まれると、この方式を使う理由がなくなります。
Secret1つだけを開くロールを作成する
secret-labにServiceAccountdb-clientを作成し、Roledb-secret-readerを作成してください。ルールは、secretsリソースに対して、resourceNamesはdb-passwordの1つ、verbsはgetの1つだけである必要があります。このロールをdb-clientにRoleBindingで紐づけてください。結果として、db-clientはdb-passwordは読み取れて、other-secretは読み取れず、Secretの一覧の取得(list)もできない状態になっている必要があります。そして/root/ops/secrets/out/resourcenames-note.txtに、resourceNamesがlist/watchには通用しないという点を書いてください。
名前で範囲を絞るフィールドがあります。ただし、そのフィールドが通用しない動詞があるという点もあわせて整理してください。
不変のSecretを作成して変更の拒否を確認する
secret-labにSecretpinned-configを作成してください。immutable: trueで、キーbuildの値は2026-08-20です。そのあと、この値を変更しようとして、拒否メッセージを/root/ops/secrets/out/immutable-error.txtに保存してください。保存後も、buildの値は依然として2026-08-20である必要があります。
ロックしたあと、実際に変更しようとしてみて、拒否メッセージを残してください。エラーは標準エラー出力に出ます。
etcdから平文を取り出して証拠として残す
etcdから/registry/secrets/secret-lab/db-passwordキーを取得した出力を、/root/ops/secrets/out/etcd-secret.txtに保存してください。このファイルの中に、平文labhub-Pr0d-2026が見えている必要があります。そして/root/ops/secrets/out/secrets-audit.jsonを作成してください。total_secretsはsecret-labの実際のSecretの数と一致している必要があり、encrypted_at_restはfalse、plaintext_visible_in_etcdはtrueです。mitigations配列には対応策を3つ以上書き、保存時の暗号化(EncryptionConfiguration、または韓国語で「暗号化」を意味する語)とアクセス権限の制御(RBAC、または韓国語で「権限」を意味する語)が必ず含まれている必要があります。
Kubernetesのオブジェクトのキーパスのルールを思い出してください。値はprotobufなので壊れて見えますが、文字列はそのまま読めます。