Certified Kubernetes Security Specialist (CKS) #10 Secrets 管理: etcd 暗号化、External Secrets

#9 Pod Security Admission で危険な Pod を入り口で拒否するポリシーを固めたなら、この記事はその Pod が扱う 秘密データそのものをどう守るか です。Minimize Microservice Vulnerabilities ドメインの一つの軸である Secret セキュリティは、Kubernetes Secret が デフォルト状態では安全ではない という不都合な事実から始まります。この記事では etcd at rest 暗号化、既存 Secret の再暗号化、平文かどうかの確認、そして External Secrets と KMS の連携まで整理します。

Secret はデフォルトでは暗号化ではない #

まず最初に正すべき誤解があります。Kubernetes Secret は 暗号化されたストレージではありません。Secret オブジェクトのデータは etcd に base64 でエンコードされて 入るだけであり、base64 は誰でも 1 行のコマンドで戻せる単純なエンコーディングです。

この違いを直接確認してみます。Secret を 1 つ作ってその値をそのままデコードすると、平文がそのまま出てきます。

# Secret 作成
k create secret generic db-cred \
  --from-literal=password=SuperSecret123

# 保存された値を確認 (base64)
k get secret db-cred -o jsonpath='{.data.password}'
# U3VwZXJTZWNyZXQxMjM=

# base64 デコード: 平文がそのまま現れる
k get secret db-cred -o jsonpath='{.data.password}' | base64 -d
# SuperSecret123

base64 はセキュリティ装置ではなく、バイナリを安全に転送するためのエンコーディングです。つまり etcd データにアクセスできる人 は Secret を平文で読めます。etcd はディスクに保存されるので、etcd ファイルを奪取したり etcd バックアップ を手に入れた攻撃者も同じです。

そのため Secret セキュリティは 2 つの方向からアプローチします。1 つ目は 保存時の暗号化 (encryption at rest) で、etcd に入るデータそのものを暗号文にします。2 つ目は アクセス制御 で、Secret を読める主体を RBAC で最小化します。この記事は 1 つ目に重きを置き、2 つ目は #4 RBAC 最小権限 と合わせて締めくくります。

etcd at rest 暗号化の構造 #

Kubernetes は apiserver が etcd にデータを書き込む直前に暗号化し、読み出すときに復号する encryption at rest 機能を提供します。核心は apiserver に EncryptionConfiguration という設定ファイルを渡すことです。

流れは単純です。

  1. どのリソース (通常は secrets) をどの provider で暗号化するかを EncryptionConfiguration に記述する
  2. apiserver に --encryption-provider-config フラグでこのファイルのパスを渡す
  3. apiserver を再起動する
  4. 設定後に新しく書かれる Secret は暗号文で etcd に入る
  5. すでに入っていた Secret は一度書き直してやらないと暗号化されない (再暗号化)

ここでよく見落とすのが 5 番です。暗号化設定は それ以降の書き込みにのみ 適用されるので、既存 Secret を保存し直さなければ etcd には依然として平文が残ります。

provider の種類 #

EncryptionConfiguration の providers順序のあるリスト です。書き込みにはリストの 最初 の provider が使われ、読み込みには一致する provider を上から探して使います。試験で覚えておく provider は次のとおりです。

provider性格備考
identity暗号化しない (平文)デフォルト値。リストの最初なら実質的に無効化
aescbcAES-CBC 対称鍵32 バイト鍵。広く使われる基本の選択
secretboxXSalsa20+Poly130532 バイト鍵。aescbc の代替
aesgcmAES-GCM鍵のローテーションを頻繁にする必要がある点に注意
kms外部 KMS 連携推奨される方式。鍵をクラスターの外で管理

identity は「暗号化しない」を意味する特殊な provider です。リストの 最初identity が来ると新しい書き込みは平文になり、最後 に置くとまだ暗号化されていない既存データを読むためのフォールバックとして動作します。この順序の意味が試験の罠としてよく出ます。

EncryptionConfiguration の作成 #

実際のファイルを書いてみます。aescbcsecrets を暗号化する最も典型的な形です。

# /etc/kubernetes/enc/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64 でエンコードした 32 バイト鍵>
      - identity: {}

読む順序をもう一度押さえます。resources の下に暗号化対象のリソース (secrets) を書き、providers に書き込み用の provider (aescbc) を一番上に置きます。一番下の identity: {}暗号化設定の前に平文で保存された既存 Secret を読むためのフォールバック です。このフォールバックがないと、再暗号化前の既存 Secret を apiserver が読めずに障害が起きます。

鍵はランダムな 32 バイトを base64 でエンコードして作ります。

# 32 バイトのランダムな鍵を base64 に
head -c 32 /dev/urandom | base64
# この出力値を上の YAML の secret の位置に埋める

apiserver への接続 #

apiserver は static Pod なので、マニフェストを直接編集してフラグとボリュームを追加します。kubeadm 基準のパスは /etc/kubernetes/manifests/kube-apiserver.yaml です。

# /etc/kubernetes/manifests/kube-apiserver.yaml (抜粋)
spec:
  containers:
    - command:
        - kube-apiserver
        - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
        # ...既存のフラグ...
      volumeMounts:
        - name: enc
          mountPath: /etc/kubernetes/enc
          readOnly: true
  volumes:
    - name: enc
      hostPath:
        path: /etc/kubernetes/enc
        type: DirectoryOrCreate

3 つを一緒に手直しする必要があります。1 つ目は --encryption-provider-config フラグで設定ファイルのパスを指定し、2 つ目はそのファイルが入ったディレクトリを volumes でノードから引き込み、3 つ目はコンテナの中に volumeMounts でマウントします。フラグだけ追加してボリュームを抜かすと、apiserver が設定ファイルを見つけられずに起動に失敗します。static Pod なので、マニフェストを保存すると kubelet が apiserver を自動的に再起動します。

apiserver が再び立ち上がったか確認します。

# apiserver Pod の再起動を確認
k -n kube-system get pod -l component=kube-apiserver

# 起動に失敗したら kubelet ログで原因を追跡
journalctl -u kubelet -f

既存 Secret の再暗号化 #

先ほど強調した 5 番のステップです。暗号化設定は それ以降の書き込みにのみ 適用されるので、すでに etcd に入っていた Secret は一度保存し直さないと暗号文に変わりません。すべてのネームスペースの Secret を読んでそのまま書き直す 1 行が定石です。

# 全ネームスペースの Secret を読んでそのまま replace: 再暗号化
k get secrets -A -o json | k replace -f -

get ... -o json で現在の Secret 全体を引き込み、replace でそのまま上書きすると、apiserver が書き込み時点で新しい provider で暗号化して etcd に保存します。データの内容は変わらず、etcd に保存される表現だけが平文から暗号文に変わります。

規模の大きいクラスターなら、特定のネームスペースだけ選んで回したりリソースの種類を絞って負荷を減らすこともできます。ただし試験では上の 1 行で十分です。

平文かどうかの確認 #

暗号化が実際にかかったかは etcd を直接読んで 確認します。apiserver を経由すると復号された値が出るので意味がなく、etcd に保存された raw バイトを見る必要があります。etcdctl で Secret のキーを直接照会します。

# etcd に直接接続して Secret の raw 値を hexdump
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/db-cred | hexdump -C | head

判別の基準は単純です。

  • 出力の冒頭に k8s:enc:aescbc:v1:key1 のような prefix が見えれば暗号化成功 です。後に続くバイトは人が読めない暗号文です
  • 逆に SuperSecret123 のような 平文がそのまま見えれば暗号化されていない状態 です。再暗号化を抜かしたか、provider の順序が間違っています

この prefix の確認が、試験で「暗号化が適用されたか検証せよ」という作業の正答の根拠になります。

External Secrets と KMS #

etcd 暗号化はクラスターの中でデータを保護しますが、鍵と秘密のライフサイクルそのものをクラスターの外で管理 したいときがあります。ここで登場するのが KMS provider と External Secrets Operator です。

KMS provider #

EncryptionConfiguration の provider として kms を使うと、暗号化鍵を etcd やノードのディスクではなく 外部 KMS (例: クラウド KMS、HashiCorp Vault の transit) で管理します。apiserver はデータ暗号化鍵 (DEK) で Secret を暗号化し、その DEK をさらに KMS の鍵暗号化鍵 (KEK) で包みます。KEK は KMS の外に出ないので、etcd やノードだけ奪取しても復号できません。

# KMS provider 例 (抜粋)
resources:
  - resources:
      - secrets
    providers:
      - kms:
          apiVersion: v2
          name: myKmsPlugin
          endpoint: unix:///var/run/kmsplugin/socket.sock
      - identity: {}

KMS provider は別の KMS プラグインプロセスをノードでソケットで接続して動作します。運用環境で推奨される方式であり、aescbc のように鍵を平文で設定ファイルに書いておかない点が核心の利点です。

External Secrets Operator #

方向の違うアプローチもあります。External Secrets Operator (ESO) は、秘密を Kubernetes ではなく外部の秘密ストア (AWS Secrets Manager、GCP Secret Manager、Azure Key Vault、HashiCorp Vault など) に置き、その値を同期してクラスターの中の Secret オブジェクトとして 作ってくれる コントローラーです。

運用の流れは次のとおりです。

  1. 秘密の原本は外部ストアにのみ置く (クラスターには原本がない)
  2. SecretStore または ClusterSecretStore で外部ストアの接続情報を定義する
  3. ExternalSecret リソースで「外部のどのキーをどの Secret に取り込むか」をマッピングする
  4. ESO が周期的に外部の値を読んで Kubernetes Secret を作成・更新する

こうすると秘密の単一の出所が外部ストアになり、ローテーション・監査・アクセス制御をそのストアで一元化できます。試験で ESO のインストールを直接やらせることはまれですが、「秘密をクラスターの外で管理する」という概念と KMS との違い は知っておくほうがよいです。整理すると、KMS は etcd に入るデータを外部の鍵で暗号化する方式であり、ESO は秘密の原本そのものを外部に置いて同期する方式です。

Secret アクセスは RBAC で最小化 #

暗号化をどれだけ固くかけても、Secret を読める主体が広ければ意味が薄れます。etcd at rest 暗号化は 保存媒体を奪取されたとき を防ぎ、RBAC は 正常な経路で Secret を読む主体 を防ぎます。2 つの防御線は互いを置き換えられず、一緒に進める必要があります。

#4 RBAC 最小権限 で扱った原則を Secret にそのまま適用します。

  • Secret に対する getlistwatch 権限を、本当に必要な ServiceAccount とユーザーにのみ付与する
  • ワイルドカード (resources: ["*"]verbs: ["*"]) で Secret アクセスを漏らさない
  • 特定の Secret だけ必要な場合は resourceNames で対象をその Secret に絞る
# 特定の Secret だけ読ませる Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: app
  name: read-db-cred
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["db-cred"]
    verbs: ["get"]

Secret を Pod に渡す方法もセキュリティに影響します。#12 ConfigMap と Secret の深掘り で見たように、環境変数より ボリュームマウント がより安全な選択です。環境変数は子プロセスに継承され一部のツールで露出しやすい一方、ボリュームマウントはファイル権限で制御しやすく、ローテーションされた値が自動で反映される利点もあります。

試験ポイント #

CKS 試験で Secret セキュリティは etcd at rest 暗号化の有効化 が圧倒的な定番作業です。次の順序を身に付けておきます。

  • Secret は base64 なだけで暗号化ではありません。 etcd に平文レベルで保存されるという前提を覚えます
  • EncryptionConfiguration を作成します。resources: [secrets]providersaescbc (または secretbox/kms) を一番上、identity を一番下に置きます
  • apiserver static Pod に --encryption-provider-config フラグ + volume + volumeMount の 3 つを一緒に追加します。ボリュームを抜かして起動失敗するミスを避けます
  • 既存 Secret の再暗号化 を忘れません。k get secrets -A -o json | k replace -f - で一度書き直します
  • 検証は etcdctl で直接 やります。raw 出力に k8s:enc:aescbc:v1: prefix が見えれば成功、平文が見えれば失敗です
  • provider の 順序の意味 (最初 = 書き込み、最後の identity = 既存平文を読むフォールバック) を正確に答えられる必要があります
  • 概念問題に備えて KMS provider と External Secrets Operator の違い を 1 行で説明できるようにしておきます

まとめ #

この記事で押さえたこと:

  • Secret はデフォルトで etcd に base64 でだけ保存 されます。base64 はエンコーディングなだけで暗号化ではないので、etcd にアクセスすれば平文が現れます
  • EncryptionConfiguration + --encryption-provider-config フラグ で secrets を at rest 暗号化します。provider は aescbc・secretbox・kms の中から選び、identity をフォールバックとして一番下に置きます
  • 設定はそれ以降の書き込みにのみ適用されるので、既存 Secret を k get secrets -A -o json | k replace -f - で再暗号化 する必要があります
  • 検証は etcdctl で raw 値を読んで prefix の有無で判別します
  • KMS provider は鍵を外部で管理し、External Secrets Operator は秘密の原本を外部ストアに置いて同期します
  • 暗号化とは別に Secret アクセスを RBAC で最小化 してこそ、2 つの防御線が一緒に動きます

次へ — 隔離 (gVisor) #

秘密データは etcd レベルで守りました。これからワークロードそのものをホストカーネルから引き離す隔離に移ります。

#11 隔離: gVisor、Kata Containers、RuntimeClass では、コンテナがホストカーネルを直接呼び出す構造の危険、gVisor がシステムコールをユーザー空間で横取りして隔離する原理、Kata Containers が軽量 VM でカーネルを分離する方式、そして RuntimeClass で特定の Pod だけをサンドボックスランタイムに載せるパターンを直接作りながら整理します。

X