トークンの寿命とオーディエンスを扱う
目標
Podのアイデンティティがどのように発行され、どのような有効期間と対象を持つのかを自分で確認し、不要なトークンはそもそも渡さない設定を適用します。
なぜ重要なのか
コンテナが突破されたとき、攻撃者が最初に探すのが/var/run/secrets/kubernetes.io/serviceaccountのトークンです。そのため、このモジュールの核心となる問いは2つあります。このPodは本当にトークンが必要なのか、そしてそのトークンはいつ期限切れになるのか。1.24より前の方式は、どちらの問いにも悪い答えを持っていました。アカウントを作成すると有効期限のないトークンが自動的に作られ、Podは必要かどうかにかかわらず、それをマウントされていました。現在は、Podが受け取るトークンが有効期間を持ち、kubeletが更新し、automountServiceAccountToken: falseでそもそもオフにすることもできます。そこにaudienceが加わると、トークンが「どこで使うのか」まで含まれ、Vaultに渡すために発行したトークンがほかの場所で再利用されるのを防ぎます。この仕組みがそのままクラウドのワークロードアイデンティティにつながり、静的なキーそのものをなくす設計になります。最後に忘れないでください。アイデンティティを作っても、権限が生じるわけではありません。権限は依然としてRBACが決めます。
ステップ
- ネームスペース
sa-labを作成し、その中にServiceAccountpaymentsを作成してください。このアカウントには、自動生成されたSecretが付いていてはいけません。そして/root/ops/sa/out/sa-note.txtに、最近はなぜSecretが自動的に作られないのかを1行で書いてください(1.24以降に短期トークンへ変わった背景や、期限切れ・有効期間のような表現が含まれている必要があります)。 sa-labにPodpayments-apiを作成し、spec.serviceAccountNameをpaymentsに指定してください。デフォルト設定なら、サービスアカウントのトークンを含むprojectedボリュームが自動的に付きます。- ServiceAccount
lockedをautomountServiceAccountToken: falseで作成してください。そしてPodhardenedを作成し、spec.serviceAccountNameはlocked、spec.automountServiceAccountTokenはfalseにしてください。このPodには、トークンボリュームが1つも付いていてはいけません。 paymentsアカウントのトークンを、有効期間1時間(3600秒)、対象vaultで発行し、そのJWTのペイロードをデコードしたJSONを/root/ops/sa/out/token.jsonに保存してください。このJSONでは、subがsystem:serviceaccount:sa-lab:paymentsであり、audが空でなく、expとiatの差が約3600秒で、kubernetes.ioクレームが含まれている必要があります。sa-labにPodprojected-demoを作成してください。spec.serviceAccountNameはpaymentsにし、projectedボリュームにserviceAccountTokenソースを置いて、audience: vault、expirationSeconds: 3600、path: vault-tokenを指定してください。1つ目のコンテナは、このボリュームを/var/run/secrets/vaultのパスにマウントする必要があります。sa-labにSecretpayments-legacy-tokenを作成してください。typeはkubernetes.io/service-account-token、アノテーションkubernetes.io/service-account.nameはpaymentsです。そして/root/ops/sa/out/legacy-note.txtに、この方式がなぜ推奨されないのかを、60バイト以上、2文程度で書いてください(有効期限がないという点と、取り消し・ローテーションの負担が含まれている必要があります)。paymentsアカウントには、最小権限だけを与えてください。sa-labでConfigMapをgetとlistできて、Secretを読み取ったりConfigMapを削除したりはできない状態にする必要があります。権限は、paymentsをサブジェクトとするRoleBindingで紐づけてください。/root/ops/sa/out/sa-audit.jsonを作成してください。podsは配列で、sa-labの実際のPodの数と正確に一致している必要があります。各要素は、name、service_account、automountの3つのフィールドを持ちます。payments-apiのservice_accountはpaymentsで、hardenedのautomountはfalseで、service_accountがdefaultであるPodは1つもあってはいけません。最後に、recommendations配列に改善の提案を2つ以上書いてください。
参考
- トークンの発行は、
kubectl create token <어카운트> -n sa-lab --duration=3600s --audience=vaultです。JWTは헤더.페이로드.서명の構造(プレースホルダーはヘッダー、ペイロード、署名です)なので、2つ目の要素だけを取り出してデコードすればよいです。base64urlは長さを4の倍数にパディングする必要があるので、python3のbase64.urlsafe_b64decodeを使うと便利です。 - Podのボリューム構造は、
kubectl get pod <이름> -n sa-lab -o jsonpath='{.spec.volumes}'で確認できます。自動的に注入されたトークンボリュームは、名前がkube-api-access-で始まります。 - ステップ8のPod一覧は、
kubectl get pods -n sa-lab -o jsonからjqで作成するのが安全です。automountフィールドが明示されていないPodは、デフォルト値(true)で書いてください。 - よくある間違い1: ステップ3で、Podにだけオフにしてアカウントには設定しないことです。このラボは、両方を確認します。
- よくある間違い2: ステップ5とステップ3のPodが
defaultアカウントを使ってしまうことです。ステップ8の監査で引っかかります。Podごとに専用のアカウントを指定してください。 - よくある間違い3: ステップ4で、トークンの文字列そのものをファイルに保存してしまうことです。保存するのは、デコードしたペイロードのJSONです。
- ラボのPodはラボごとに新しく起動するので、前のラボで作ったクラスターの状態は残っていません。ネームスペースとアカウントは、このラボの中で自分で作成してください。運用手順を記憶ではなくランブックとマニフェストとして残すべき理由が、まさにここにあります。
専用のサービスアカウントを作成する
ネームスペースsa-labを作成し、その中にServiceAccountpaymentsを作成してください。このアカウントには、自動生成されたSecretが付いていてはいけません。そして/root/ops/sa/out/sa-note.txtに、最近はなぜSecretが自動的に作られないのかを1行で書いてください(1.24以降に短期トークンへ変わった背景や、期限切れ・有効期間のような表現が含まれている必要があります)。
最近のバージョンでは、アカウントを作成してもSecretはついてきません。なぜそのように変わったのかを、1行でまとめておいてください。
Podに専用のアカウントを指定する
sa-labにPodpayments-apiを作成し、spec.serviceAccountNameをpaymentsに指定してください。デフォルト設定なら、サービスアカウントのトークンを含むprojectedボリュームが自動的に付きます。
Podの仕様でアカウントを指定するフィールドの名前を確認してください。指定すると、トークンボリュームが自動的に付きます。
トークンの自動マウントをオフにする
ServiceAccountlockedをautomountServiceAccountToken: falseで作成してください。そしてPodhardenedを作成し、spec.serviceAccountNameはlocked、spec.automountServiceAccountTokenはfalseにしてください。このPodには、トークンボリュームが1つも付いていてはいけません。
同じフィールドを、Podにもアカウントにも書けます。オフにしたなら、ボリューム一覧にトークンが残っていてはいけません。
短期トークンを発行してペイロードを読み解く
paymentsアカウントのトークンを、有効期間1時間(3600秒)、対象vaultで発行し、そのJWTのペイロードをデコードしたJSONを/root/ops/sa/out/token.jsonに保存してください。このJSONでは、subがsystem:serviceaccount:sa-lab:paymentsであり、audが空でなく、expとiatの差が約3600秒で、kubernetes.ioクレームが含まれている必要があります。
JWTはドットで区切られた3つの要素で、真ん中がペイロードです。base64urlなので、パディングを合わせないとデコードできません。有効期間は、2つの時刻の差で確認してください。
対象が指定されたprojectedトークンを使う
sa-labにPodprojected-demoを作成してください。spec.serviceAccountNameはpaymentsにし、projectedボリュームにserviceAccountTokenソースを置いて、audience: vault、expirationSeconds: 3600、path: vault-tokenを指定してください。1つ目のコンテナは、このボリュームを/var/run/secrets/vaultのパスにマウントする必要があります。
トークンに対象を埋め込むと、その対象でのみ有効です。ファイル名とマウントパスを正確に合わせてください。
レガシーな長期トークンのSecretを作成してリスクを整理する
sa-labにSecretpayments-legacy-tokenを作成してください。typeはkubernetes.io/service-account-token、アノテーションkubernetes.io/service-account.nameはpaymentsです。そして/root/ops/sa/out/legacy-note.txtに、この方式がなぜ推奨されないのかを、60バイト以上、2文程度で書いてください(有効期限がないという点と、取り消し・ローテーションの負担が含まれている必要があります)。
特定のタイプとアノテーションがあって初めて、コントローラーがトークンを埋めてくれます。この方式の本当の問題は、便利さではなく有効期限がないという点です。
アカウントに最小権限だけを与える
paymentsアカウントには、最小権限だけを与えてください。sa-labでConfigMapをgetとlistできて、Secretを読み取ったりConfigMapを削除したりはできない状態にする必要があります。権限は、paymentsをサブジェクトとするRoleBindingで紐づけてください。
アカウントを作成することと、権限を与えることは別です。必要なものだけを開き、残りは確認して塞がれているか検証してください。
アカウントの使用状況を監査する
/root/ops/sa/out/sa-audit.jsonを作成してください。podsは配列で、sa-labの実際のPodの数と正確に一致している必要があります。各要素は、name、service_account、automountの3つのフィールドを持ちます。payments-apiのservice_accountはpaymentsで、hardenedのautomountはfalseで、service_accountがdefaultであるPodは1つもあってはいけません。最後に、recommendations配列に改善の提案を2つ以上書いてください。
Podの数は、実際のクラスターと正確に一致している必要があります。defaultアカウントを使うPodが1つでもあってはいけません。