Secret — どこに保存され、誰が読めるのか
一言でいうと
KubernetesのSecretのbase64はエンコードであって暗号化ではなく、デフォルト設定ではetcdに平文で保存されます。etcdのディスクを手に入れた人は、すべてのSecretを手に入れます。
なぜ必要なのか
Secretのマニフェストを初めて見ると、値が読み取れない文字列になっているので、暗号化されているように見えます。しかし、元に戻すのにコマンド1つで済みます。
kubectl get secret app-db -o jsonpath='{.data.password}' | base64 -d
# pr0d-Db-Pass
キーもなければ、秘密もありません。base64は、バイナリをテキストとして安全に運ぶための表現方式にすぎません。さらに重要な事実は、その次にあります。デフォルト設定のクラスターでは、apiserverはSecretを平文のままetcdに書き込みます。etcdのスナップショットファイル、ディスクイメージ、バックアップアーカイブを手に入れた人は、その中のパスワード文字列をそのまま読み取れます。前のモジュールでスナップショットを取って移す方法を学んだので、そのファイルがどれほど機微なものなのかも、あわせて知っておく必要があります。
どう動くのか
対策は、層を積み重ねて行います。
1層目: 保存時の暗号化(EncryptionConfiguration)。apiserverに設定ファイルを渡すと、Secretをetcdに書き込む前に暗号化します。
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: [secrets]
providers:
- aescbc:
keys:
- name: key1
secret: <32바이트 base64 키>
- identity: {}
ここでは順序がすべてです。リストの最初のプロバイダーが書き込みに使われ、読み取りでは上から順に試します。identityは「暗号化しない」という意味なので、最後になければなりません。先頭に置くと、その瞬間から平文での保存に戻ります。また、設定を変えても、既存のオブジェクトは書き直すまで平文のまま残ります。全体を一度読み込んで書き直す作業が必要です。
2層目: エンベロープ暗号化(KMS v2)。キーをクラスターの設定ファイルに書いておくのは、結局もう1つのシークレットを作ることになります。KMSプロバイダーは、この問題を2つの層に分けます。データはデータキー(DEK)で暗号化し、そのデータキーをさらにKMSのマスターキー(KEK)で暗号化します。マスターキーはクラスター外のハードウェアセキュリティモジュールやクラウドのKMSにあり、apiserverはUnixソケットで接続されたプラグインを通してのみアクセスします。キーをローテーションするときに、データをすべて再暗号化する必要がないことも、大きな利点です。
3層目: アクセス制御(RBAC)。保存時に暗号化しても、APIで読み取れる人には何の意味もありません。あるネームスペースでget secrets権限を持つ人は、そのネームスペースのすべてのSecretを読み取れます。便宜上与えた編集権限が、事実上、本番の認証情報の閲覧権限になっているケースが非常によくあります。範囲を絞るには、resourceNamesで名前を固定します。
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-password"]
verbs: ["get"]
ただし、resourceNamesはlistとwatchには通用しません。一覧の取得は、対象の名前をあらかじめ特定できないリクエストだからです。そのため、名前単位の制限はgetを絞るための道具であり、listはそもそも与えないのが答えです。
4層目: 値をクラスターの外に置く。External Secrets Operatorのようなツールは、実際の値をシークレットマネージャーに置き、クラスターには参照だけをコミットします。SecretStoreが「どこから取得するか」を、ExternalSecretが「何をどの名前で作るか」を定義します。マニフェストに値がないので、Gitにそのままコミットしてもかまいません。ただし、Operatorが結局クラスターのSecretを作り出すので、前述のRBACと保存時の暗号化の問題はそのまま残ります。
最後に、immutable: trueがあります。値を変更できないようロックすると、誤操作による変更が防げ、kubeletが変更を監視しなくてよくなるので負荷も減ります。その代わり、変更するには新しい名前で作成して参照を移す必要があります。
現場での姿
1つ目は、対応時間が成熟度を表すことです。シークレット管理の水準を測る質問は「うまく隠せているか」ではなく、今この値が公開されたと仮定したとき、取り消して入れ替えるのに何分かかるかという問いです。その答えが時間単位であれば、ツールを買い足すよりも、ローテーションの経路を先に作るべきです。
2つ目は、漏えい対応の順序です。Gitの履歴でキーを見つけたときの最初の対処は、履歴の書き換えではなく取り消しです。公開された値は数秒で自動スキャナーに収集されるので、履歴をいくら消しても、すでにコピーされた値は元に戻せません。取り消し → 影響調査 → 履歴の整理が、正しい順序です。
3つ目は、環境変数は保存場所ではなく受け渡しの方式だということです。コンテナに注入された環境変数は、同じホストから/proc/<PID>/environで読み取れ、子プロセスに継承され、クラッシュレポートやログに漏れ出します。ファイルマウントのほうが安全な理由が、ここにあります。
次のラボですること
Secretを作成してbase64を自分で元に戻したあと、保存時の暗号化の設定とKMS v2のエンベロープ暗号化の設定を書きます。外部のシークレットストアと連携するマニフェストを値なしで作成し、resourceNamesでSecret1つだけを開くロールを作成して、その限界を確認します。不変のSecretを作成して変更が拒否されるのを見たあと、最後にetcdを直接参照して平文が見えることを証拠として残し、対応策を整理します。