Secret の仕組みと安全な扱い方
v1.37 時点(2026年10月25日 確認)
目次
この記事ではパスワードやトークン、鍵のような少量の機密のデータを持ち、Pod に渡すためのリソースである Secret の仕組みを、図で説明します。第17回の ConfigMap と何が同じで何が違うのかと、Secret の値は base64 でエンコードされているだけで、暗号化されてはいないことがわかります。Secret を安全に扱うために公式ドキュメントが勧める対策(etcd での保存時の暗号化、RBAC による読み取りの制限など)もわかります。後半では kind のクラスタで Secret を作り、その値を base64 でデコードすると元の値に戻ることを確かめる手順を示します。
設定のデータを Pod に渡す ConfigMap と、環境変数とボリュームの 2 つの渡し方を扱った第17回の内容を前提にしています。まだの方は先にこちらをどうぞ。
環境変数とボリュームで渡す ConfigMap の違い設定のデータをコンテナイメージから切り離して Pod に渡す ConfigMap の仕組みを図で説明します。環境変数とボリュームの 2 つの渡し方と、ConfigMap を書き換えたときにボリュームのファイルは後から更新され、環境変数は自動では更新されない理由を押さえ、kind のクラスタで確かめる手順を紹介します。以下の図は mysecret という名前の Secret と、その値を受け取る Pod、Secret を読み取る利用者の関係を表しています。後半の「手元で試す」で作る構成です。
Secret の data の値は base64 でエンコードした文字列で書きます。図の MWYyZDFlMmU2N2Rm は公式ドキュメントの例で 1f2d1e2e67df をエンコードした値です。公式ドキュメントの例では、Secret を受け取った Pod のコンテナの中には、エンコードする前の元の値が届いています。Secret を読み取れる利用者も、値を base64 でデコードするだけで元の値に戻せます。公式ドキュメントが注意しているとおり、base64 でのエンコードは暗号化ではありません。図ではボリュームとコンテナを結ぶ線を省き、ボリュームの中のファイルをコンテナの箱の中に書いています(第17回の図と同じ描き方です)。
参考:Managing Secrets using Configuration File | Kubernetes、Distribute Credentials Securely Using Secrets | Kubernetes、Good practices for Kubernetes Secrets | Kubernetes
Secret とは
Secret は パスワード、トークン、鍵のような少量の機密のデータを入れておくオブジェクト です。公式ドキュメントによると、Secret を使えば、機密のデータをアプリケーションのコードに含めなくて済みます。Secret は ConfigMap に似ていますが、特に機密のデータを入れるためのもの(specifically intended to hold confidential data)だと説明されています。
公式ドキュメントが挙げる Secret の使い道は、次の 3 つです。
- コンテナの環境変数にする
- SSH の鍵やパスワードなどの認証情報を、Pod に渡す
- kubelet が非公開のレジストリ(イメージの置き場所)からコンテナイメージを取得できるようにする
SSH は一般に、ほかのコンピューターにネットワーク越しに安全にログインするための仕組みです。公式ドキュメントによると、Kubernetes のコントロールプレーン自身もノードを登録する作業を自動にするためなどに Secret を使います。
Secret はそれを使う Pod とは別のオブジェクトとして作ります。公式ドキュメントによると、そのため Pod を作ったり、見たり、書き換えたりする作業の中で、Secret(とそのデータ)がさらされるおそれが少なくなります。Kubernetes と、クラスタで動くアプリケーションは、Secret に追加の対策をとることもできます。たとえば、機密のデータを不揮発性の記憶装置に書かないようにすることです。不揮発性の記憶装置は一般に、ディスクのように電源を切っても中身が残る記憶装置のことです。
ただし、公式ドキュメントは次のように注意しています。Secret は既定では、kube-apiserver の裏にあるデータストア(etcd)に、暗号化されないまま保存されます。kube-apiserver と etcd は第4回で扱った、コントロールプレーンのコンポーネントです。同じ注意書きには次のことも書かれています。
- API にアクセスできる人は誰でも、Secret を読み出したり書き換えたりできる。etcd にアクセスできる人も同じ
- ある Namespace(第37回)で Pod を作る権限を持つ人は、その権限を使って、その Namespace のどの Secret も読める。Deployment を作れる権限のような、間接的な権限も含む
1 つ目の「API にアクセスできる人」について、公式ドキュメントはこのすぐあとの対策に RBAC で Secret へのアクセスを最小限の権限にすることを挙げています。そのため、Secret を扱う権限を RBAC などで絞っていない場合の話だと読めます。2 つ目について、Secret の扱い方の推奨のページは、Secret を直接読むことを許されていない利用者でも、Secret をさらす Pod を動かせることがある、と書いています。Pod の spec には同じ Namespace の Secret を名前で指定でき、作った Pod の中で値を表示させられるからだと考えられます。Deployment は Pod を作るので(第12回)、Deployment を作れる人も同じです。
参考:Secrets | Kubernetes、Good practices for Kubernetes Secrets | Kubernetes、Pod | Kubernetes API Reference
そのうえで公式ドキュメントは Secret を安全に使うために、少なくとも次の 4 つを行うよう求めています(take at least the following steps)。どれも後の「Secret を安全に扱うための対策」で説明します。
- Secret の保存時の暗号化(Encryption at Rest)を有効にする
- Secret へのアクセスを最小限の権限にする RBAC のルールを、有効にするか設定する
- Secret にアクセスできるコンテナを、特定のコンテナに絞る
- 外部の Secret の保管サービス(external Secret store providers)を使うことを考える
なぜ Secret が必要なのか
第17回で見たとおり、公式ドキュメントは ConfigMap について、秘密を守ったり暗号化したりするものではないと注意しています。保存したいデータが機密なら、ConfigMap ではなく Secret か、データを守るための別のツールを使うよう書かれています。
公式ドキュメントによると、パスワードやトークンのような機密の情報は、Secret が無ければ Pod の spec かコンテナイメージに入れられることになりかねません。そうすると、Pod の spec やイメージを見られる人には、パスワードも見えてしまうはずです。Secret に値を置けば、Pod の spec には「どの Secret のどのキーを使うか」だけを書けば済みます。公式ドキュメントが Secret を使えばアプリケーションのコードに機密のデータを含めなくて済み、Pod を扱う作業の中でデータがさらされるおそれが少なくなる、と説明しているのはこのためだと読めます。これは、Pod の spec を見ても値そのものは出てこない、という意味だと考えられます。前の節で見たとおり、Pod を作れる人(Pod を作らせるワークロードを書き換えられる人を含む)は、Pod の中で Secret の値を読めるからです。
参考:Secrets | Kubernetes、Distribute Credentials Securely Using Secrets | Kubernetes
公式ドキュメントによると、ConfigMap と Secret は似た働きをしますが、Kubernetes は Secret のオブジェクトにいくつか追加の保護をかけています。たとえば、Secret はそれを必要とする Pod があるノードにだけ送られます。kubectl の側でも、kubectl get と kubectl describe は、既定では Secret の中身を表示しません。どちらも後で詳しく見ます。
参考:Secrets | Kubernetes、Managing Secrets using kubectl | Kubernetes
一方で、Secret を使うだけでは安全とは言えません。前の節で見たとおり、Secret は既定では暗号化されずに保存され、値も base64 でエンコードされているだけだからです。公式ドキュメントが Secret を安全に使うための対策を挙げているのは、このためです。
Secret の仕組み
Secret のマニフェスト
以下は公式ドキュメントにある Secret のマニフェストの例です。username と password の 2 つのキーを持ちます。後半の「手元で試す」でも、このマニフェストを使います。
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
username: YWRtaW4=
password: MWYyZDFlMmU2N2Rm公式ドキュメントによると、Secret の data のすべてのキーの値は、base64 でエンコードした文字列でなければなりません。例の値は次の 2 つのコマンドで作ったものです。echo は一般に、文字列をそのまま書き出すコマンドで、| でその出力を base64 のコマンドに渡しています。-n を付けると、書き出す文字列の最後に改行が付きません。
$ echo -n 'admin' | base64
$ echo -n '1f2d1e2e67df' | base64
公式ドキュメントの例では、出力は次のとおりです。
参考:Managing Secrets using Configuration File | Kubernetes、Secrets | Kubernetes、Managing Secrets using kubectl | Kubernetes
つまり、username の値は admin、password の値は 1f2d1e2e67df です。公式ドキュメントによると、data の値の文字列に改行を入れてはいけません(must be omitted)。macOS の base64 では長い行を分ける -b のオプションを使わないように、Linux では base64 に -w 0 を付けるか、-w が無ければ base64 | tr -d '\n' とつなぐように(should)書かれています。一般に、改行が入るのはエンコードした結果が長いときなので、この注意が当てはまるのは長い値のときで、この回の短い値ではオプションは要らないと考えられます。
Secret の主なフィールドを、v1.37 時点の API リファレンスと公式ドキュメントに沿って次の表にまとめます。
| フィールド | 内容 |
|---|---|
data | 機密のデータ。値は base64 でエンコードした文字列で書く。文字列ではないデータも入れられる |
stringData | base64 にせず、文字列のまま書ける、書き込み専用のフィールド。書き込むときに data にまとめられ、API から読み出すときには出力されない |
type | Secret の種類。プログラムが Secret のデータを扱いやすくするためのもの(「Secret の種類」で説明) |
immutable | true にすると、data を書き換えられなくなる(変えられるのはメタデータだけ) |
stringData を使うと、自分で base64 にしなくても Secret を作れます。公式ドキュメントによると、文字列は Secret を作るときか書き換えるときに、エンコードされます。Secret を読み出すと、stringData に書いた元の値ではなくエンコードされた data の値が返ります。同じキーを data と stringData の両方に書くと、stringData の値が使われます。以下は公式ドキュメントにある、その例です。
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
username: YWRtaW4=
stringData:
username: administrator参考:Managing Secrets using Configuration File | Kubernetes、Secret | Kubernetes API Reference
公式ドキュメントによると、この Secret の data の username は YWRtaW5pc3RyYXRvcg== になり、デコードすると administrator です。なお公式ドキュメントは、Secret の stringData は Server-Side Apply(kube-apiserver の側で、どのフィールドを誰が管理するかを記録しながら適用する方式。第7回)とうまく合わないと注意しています。この回の手順では stringData を使いません。
参考:Managing Secrets using Configuration File | Kubernetes、Server-Side Apply | Kubernetes
名前とデータには、次の決まりがあります。
- Secret の名前は DNS のサブドメイン名(第17回)として正しい形にする
dataとstringDataのキーには英数字、-、_、.だけを使う- 1 つの Secret の大きさは 1 MiB まで
大きさの上限は kube-apiserver と kubelet のメモリーを使い果たしかねない、とても大きな Secret を作らせないためです。公式ドキュメントによると、Namespace の中の Secret(やほかのリソース)の数は、ResourceQuota(第30回)で制限できます。
公式ドキュメントは Secret を作る方法として、kubectl のコマンド、マニフェスト(公式ドキュメントの言い方では設定ファイル)、Kustomize(第43回)の 3 つを挙げています。この回ではマニフェストを使います。
base64 はエンコードで、暗号化ではない
base64 は一般に、どんなバイトの並びでも、英大文字、英小文字、数字、+、/ の 64 種類の文字(と、長さを揃える =)だけで表す方式です。API リファレンスも data の値の形式として base64 の規格(RFC 4648)を挙げています。このように決まった手順で表し方を変えることを、一般にエンコード、元に戻すことをデコードと呼びます。手順は公開されているので、誰でも鍵なしでデコードできます。一方、暗号化は一般に、鍵を知らなければ元に戻せないようにデータを変えることです。
では、コンテナには base64 の文字列が届くのでしょうか。API リファレンスによると、base64 の文字列は Secret のデータを書き表すときの形(serialized form)で、その文字列が表しているのは任意の(文字列とは限らない)データの値です。つまり base64 はマニフェストや API でデータを書き表すときの形です。公式ドキュメントの例では、Pod のコンテナに届くのは base64 の文字列ではなく、エンコードする前の元の値です。どのコンポーネントがどの時点で元の値に戻すのかは、この回で確かめた公式ドキュメントには書かれていませんでした。
参考:Secret | Kubernetes API Reference、Distribute Credentials Securely Using Secrets | Kubernetes、Managing Secrets using Configuration File | Kubernetes
base64 でのエンコードが暗号化ではないことは、公式ドキュメントもはっきり書いています。Secret の扱い方の推奨(Good practices for Kubernetes Secrets)のページには、base64 でのエンコードは暗号化の方式ではなく(not an encryption method)、平文(暗号化していないそのままのデータ)と比べて機密性を少しも高めない、と書かれています。Secret のページの TLS の Secret の例にも、値は base64 でエンコードされていて見えにくくなっているが、意味のある機密性は何も与えない(does NOT provide any useful level of confidentiality)、という注記があります。
参考:Good practices for Kubernetes Secrets | Kubernetes、Secrets | Kubernetes
そのため、Secret のマニフェストの扱いにも気を付ける必要があります。公式ドキュメントによると、base64 でエンコードしたデータを書いたマニフェストを人と共有したり、ソースコードのリポジトリ(ファイルの変更の履歴をためておく場所)に入れたりすると、そのファイルを読めるすべての人が秘密を知れる状態になります。
kubectl にはうっかり値を見せないための工夫があります。公式ドキュメントによると、kubectl get と kubectl describe は、既定では Secret の中身を表示しません。Secret がうっかりさらされたり、端末のログに残ったりするのを防ぐためです。ただし、これは表示のしかたの工夫です。-o jsonpath などで data を表示すれば、base64 の値が見え、デコードすれば元の値に戻ります(後半の「手元で試す」で確かめます)。
参考:Managing Secrets using kubectl | Kubernetes、Managing Secrets using kubectl | Kubernetes
ConfigMap との違いと Pod への渡し方
ConfigMap と Secret の違いを、v1.37 時点の公式ドキュメントと API リファレンスに沿って次の表にまとめます。
| 項目 | ConfigMap(第17回) | Secret |
|---|---|---|
| 入れるデータ | 機密ではないデータ | パスワード、トークン、鍵のような少量の機密のデータ |
| 値を書くフィールド | data(UTF-8 の文字列)、binaryData(base64) | data(base64)、stringData(文字列。書き込み専用) |
種類(type) | フィールドが無い | ある。書かなければ Opaque |
kubectl describe の表示 | 公式の例では、値がそのまま表示される(第17回では試していない) | 中身を表示せず、キーごとの大きさ(バイト数)を表示する |
参考:ConfigMap | Kubernetes API Reference、Secret | Kubernetes API Reference、Configure a Pod to Use a ConfigMap | Kubernetes、Managing Secrets using kubectl | Kubernetes、Secrets | Kubernetes
Pod への渡し方は ConfigMap とほぼ同じです。第17回で見た決まりのうち、Secret のページにも同じことが書かれているものを、次の表にまとめます。
| 決まり | 内容 |
|---|---|
| 渡し方 | 環境変数か、ボリュームのファイルとして渡せる |
| 先に作っておく | Pod が参照する Secret は、Pod より先に作る。optional を付けた場合を除き、Secret がそろうまで Pod のどのコンテナも起動しない |
| ボリュームの更新 | Secret を書き換えると、ボリュームのファイルも最終的に更新される。subPath でマウントすると更新されない |
| static Pod | static Pod(第4回)からは使えない、と公式ドキュメントに書かれている |
| 書き換えられない Secret | immutable: true にできる。元には戻せず、変えるには消して作り直す。v1.21 から stable |
参考:Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes
公式ドキュメントによると、参照した Secret が取得できないと、kubelet はその Pod の実行を定期的にやり直し、そのことを Pod のイベントとして記録します。ボリュームのファイルが更新されるまでの遅れは、最大で kubelet の同期の間隔とキャッシュに伝わるまでの遅れを足した長さになり得ます。この遅れと、immutable の利点は、第17回の ConfigMap の説明と同じです。
参考:Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes
環境変数で渡した値が Secret を書き換えたときにどうなるかは、Secret のページには書かれていません。ConfigMap のページによると、kubelet はコンテナを起動するときに ConfigMap のデータを使い、環境変数で使う ConfigMap は自動では更新されず、Pod の再起動が必要です。Secret も同じ valueFrom の仕組みで参照するので、同じく自動では更新されないと考えられます。
参考:Secrets | Kubernetes、ConfigMaps | Kubernetes、ConfigMaps | Kubernetes
Pod の spec に書くフィールドの名前は、ConfigMap と Secret で違います。対応を次の表にまとめます。
| 渡し方 | ConfigMap のとき | Secret のとき |
|---|---|---|
| キーを 1 つ選んで環境変数にする | env[*].valueFrom.configMapKeyRef | env[*].valueFrom.secretKeyRef |
| すべてのキーを環境変数にする | envFrom[*].configMapRef | envFrom[*].secretRef |
| ボリュームにする | volumes[*].configMap.name | volumes[*].secret.secretName |
参考:Distribute Credentials Securely Using Secrets | Kubernetes、Pod | Kubernetes API Reference、Pod | Kubernetes API Reference
ボリュームのところだけ、ConfigMap の name に対して、Secret では secretName と書く点に注意します。API リファレンスによると、secretName は Pod と同じ Namespace の Secret の名前です。items、defaultMode、optional は ConfigMap と同じ意味で、defaultMode の既定は 0644 です。
参考:Pod | Kubernetes API Reference、Distribute Credentials Securely Using Secrets | Kubernetes
以下は後半の「手元で試す」で使う Pod のマニフェストです。Secret mysecret のキー username を環境変数 SECRET_USERNAME に、すべてのキーをボリューム secret-volume のファイルにして渡します。公式ドキュメントの、ボリュームで渡す例(secret-pod.yaml)と、環境変数で渡す例(pod-single-secret-env-variable.yaml)を 1 つにまとめました。
apiVersion: v1
kind: Pod
metadata:
name: secret-demo
labels:
app: secret-demo
spec:
containers:
- name: busybox
image: busybox:1.28
command: ['sh', '-c', 'sleep 3600']
env:
- name: SECRET_USERNAME
valueFrom:
secretKeyRef:
name: mysecret
key: username
volumeMounts:
- name: secret-volume
mountPath: /etc/secret-volume
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: mysecret参考:Distribute Credentials Securely Using Secrets | Kubernetes、Distribute Credentials Securely Using Secrets | Kubernetes、Pod | Kubernetes API Reference
公式ドキュメントの例のイメージは nginx ですが、この回では第9回や第16回と同じ busybox:1.28 を使います。sleep 3600 は 3600 秒(1 時間)待つだけのコマンドで、手順を終えるまでコンテナを動かしておくためのものです。公式ドキュメントによると、data の各キーが mountPath の下のファイルになるので、コンテナの中には /etc/secret-volume/username と /etc/secret-volume/password ができるはずです。
参考:Distribute Credentials Securely Using Secrets | Kubernetes
第17回の手順では値をコンテナのログに書き出して確かめました。この回ではログに書き出さず、kubectl exec で値を読みます。公式ドキュメントは、アプリケーションは機密のデータを平文のままログに書いてはいけない(must avoid)としているからです。ただし、kubectl exec で読んだ値は端末に表示されます。公式ドキュメントは、kubectl get と kubectl describe が Secret の中身を見せない理由の 1 つに、端末のログに残るのを防ぐことを挙げています。本物の値を端末に表示するときも、同じ注意が要ると考えられます。この回では例の値なので表示します。
参考:Good practices for Kubernetes Secrets | Kubernetes、Managing Secrets using kubectl | Kubernetes
公式ドキュメントによると、Secret のボリュームは tmpfs を使います。tmpfs は RAM(メモリー)の上に置くファイルシステムで、ボリュームの中身が不揮発性の記憶装置に書かれることはありません。Secret はいつも読み取り専用(readOnly)でマウントされます。
Secret の種類
Secret には種類があり、type のフィールドで指定します。公式ドキュメントによると、種類はプログラムが Secret のデータを扱いやすくするためのものです。組み込みの種類ごとに、行われる検証や、Kubernetes が課す制約が違います。v1.37 時点で公式ドキュメントが挙げる組み込みの種類を、次の表にまとめます。
種類(type) | 使い道 |
|---|---|
Opaque | 利用者が決める任意のデータ。type を書かないときの既定 |
kubernetes.io/service-account-token | ServiceAccount のトークン(古い仕組み) |
kubernetes.io/dockercfg | ~/.dockercfg のファイル(Docker の古い形式の設定のファイル)の中身 |
kubernetes.io/dockerconfigjson | ~/.docker/config.json のファイル(Docker の設定のファイル)の中身 |
kubernetes.io/basic-auth | Basic 認証の認証情報 |
kubernetes.io/ssh-auth | SSH の認証の認証情報 |
kubernetes.io/tls | TLS のクライアントやサーバーのためのデータ |
bootstrap.kubernetes.io/token | ブートストラップトークン(ノードを登録するときに使うトークン)のデータ |
この回のマニフェストは type: Opaque です。公式ドキュメントによると、type を書かなければ Opaque になり、空の文字列も Opaque として扱われます。kubectl のコマンドで Opaque の Secret を作るときは、kubectl create secret generic のように generic のサブコマンドを使います。
組み込みの種類を使うときは、その種類の決まりをすべて満たす必要があります。公式ドキュメントが挙げる主な決まりを、次の表にまとめます。
| 種類 | 決まり |
|---|---|
kubernetes.io/dockerconfigjson | data に .dockerconfigjson のキーが要る。kube-apiserver はキーがあるかと、値が JSON として読めるかを確かめるが、中身が Docker の設定のファイルかどうかは確かめない |
kubernetes.io/basic-auth | data に username か password のキーが要る。便宜のための種類で、Opaque で作ってもよい |
kubernetes.io/ssh-auth | data(か stringData)に ssh-privatekey のキーが要る。Kubernetes の API が、キーがあるかを確かめる |
kubernetes.io/tls | data(か stringData)に tls.crt と tls.key のキーが要る。kube-apiserver はキーがあるかを確かめるが、値は検証しない |
参考:Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes
表のことばを補います。Basic 認証は一般に、利用者名とパスワードで認証する簡単な方式です。TLS は一般に、通信を暗号化し、相手が本物かを証明書で確かめる仕組みです。公式ドキュメントの TLS の Secret の例では、tls.crt と tls.key に、base64 でエンコードした証明書と鍵を入れるよう書かれています。TLS の Secret のよくある使い方は、Ingress(第24回)の通信の暗号化を設定することですが、ほかのリソースや、ワークロードで直接使うこともできます。Docker の設定のファイル(config.json)は、Docker の公式ドキュメントによると、docker login でレジストリにログインしたときに、認証情報の保管場所を設定していなければ認証情報が base64 の形式で保存されるファイルです。この種類の Secret は、kubelet がイメージを取得するときの認証情報に使います。Pod の spec の imagePullSecrets というフィールドに、同じ Namespace の Secret を並べて指定します。
参考:Secrets | Kubernetes、Secrets | Kubernetes、Secrets | Kubernetes、docker login | Docker Docs
Docker の設定のファイルの Secret について、公式ドキュメントは、中の auth の値は base64 でエンコードされていて見えにくいが、秘密ではない(obscured but not secret)と注意しています。その Secret を読める人は誰でも、レジストリにアクセスするためのトークンを知れます。
kubernetes.io/service-account-token は、ServiceAccount(Pod の中のプログラムが API を使うときの身元。第38回)の、期限の無いトークンを Pod に渡す古い仕組みです。公式ドキュメントによると、v1.22 以降は、TokenRequest の API で、期限が短く自動で入れ替わるトークンを取得するのが勧められる方法です。この種類の Secret は、TokenRequest の API を使えず、期限の無いトークンを読み取れるオブジェクトに置いておく危険を受け入れられる場合にだけ作るべき(should only create)だ、と書かれています。
組み込みではない種類も作れます。公式ドキュメントによると、Kubernetes は種類の名前に制約を課しません。空でない文字列を type に書けば、自分で決めた種類として使えます。公開して使う種類なら、cloud-hosting.example.net/cloud-api-credentials のように、ドメイン名を頭に付けた名前にするよう書かれています。
Secret を安全に扱うための対策
ここからは、冒頭で見た 4 つの対策を、Secret の扱い方の推奨のページの説明と合わせて見ます。このページはクラスタの管理者向けの推奨と、アプリケーションの開発者向けの推奨に分かれています。
以下の図は、Secret のデータが通る場所と、それぞれの場所で公式ドキュメントが挙げる対策を表しています。
①は kube-apiserver が etcd にデータを保存するところ、②は kube-apiserver が利用者の操作を受け付けるところ、③はノードの kubelet が Pod にデータを渡すところです。4 つ目の対策の外部の保管サービスは、データをクラスタの外に置く方法なので、図には描いていません。
図の決まりで、入れ物の外からの線は入れ物の中のアイコンではなく入れ物の枠に向けて引いています。②の線はコントロールプレーンの枠を指していますが、利用者の操作を受け付けるのは枠の中の kube-apiserver です。ノードからの紫の点線は、kubelet が kube-apiserver を監視していること(第4回)を表します。このシリーズの図では監視の線を監視する側から引くので、矢印はノードからコントロールプレーンへ向いています。Secret のデータが届く向きはその逆で、コントロールプレーンからノードへ、そのノードの Pod が必要とする Secret だけが届きます。
③では、Secret のボリュームをマウントするのはコンテナ app だけで、コンテナ sidecar には環境変数もボリュームのマウントも書いていません。そのため sidecar からは、この Secret は見えません(例外は「Secret を渡すコンテナを絞る」で説明します)。
etcd での保存時の暗号化
この節で扱うのは、クラスタの管理者が kube-apiserver に対して行う設定です。初めて読むときは、次の 3 点がわかれば十分です。細かい方式の違いは必要になったときに読み返してください。
- 既定では、Secret は etcd に暗号化されずに保存される
- 暗号化を有効にするには、kube-apiserver の設定が要る
- 公式ドキュメントの方式の表で、いちばん強い(Strongest)とされているのは、外部の KMS を使う
kmsの方式
公式ドキュメントによると、既定では、kube-apiserver はリソースを平文のまま etcd に保存し、保存時の暗号化(at-rest encryption)は行いません。etcd の中身を読める人には、Secret の値も読めることになります。Secret の扱い方の推奨のページは、クラスタの管理者向けの推奨として、etcd の中の Secret のデータの暗号化を設定すべき(should)だとしています。
参考:Encrypting Confidential Data at Rest | Kubernetes、Good practices for Kubernetes Secrets | Kubernetes
暗号化は kube-apiserver の設定で有効にします。公式ドキュメントによると、kube-apiserver は --encryption-provider-config という引数で、設定のファイルのパスを受け取ります。ファイルには EncryptionConfiguration という形式で、どのリソース(Secret や ConfigMap など)を、どの方式(プロバイダー)で暗号化するかを書きます。以下は公式ドキュメントにある例の、先頭から aesgcm の鍵までの部分です。この後ろには aescbc や secretbox などが続きますが、省きました。冒頭のコメントのとおり、公式ドキュメントはこの例を自分のクラスタに使わないよう注意しています。
---
#
# CAUTION: this is an example configuration.
# Do not use this for your own cluster!
#
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
- pandas.awesome.bears.example # a custom resource API
providers:
# This configuration does not provide data confidentiality. The first
# configured provider is specifying the "identity" mechanism, which
# stores resources as plain text.
#
- identity: {} # plain text, in other words NO encryption
- aesgcm:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==resources に暗号化の対象のリソースを、providers に方式を並べます。pandas.awesome.bears.example は、利用者が足した独自のリソース(カスタムリソース。第44回)の例です。keys の secret が暗号化に使う鍵です。この形式では、鍵がファイルの中にそのまま書かれます。このことがあとで見る鍵の置き場所の違いにつながります。
参考:Encrypting Confidential Data at Rest | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes
providers は暗号化の方式を並べた、順番に意味のある一覧です。公式ドキュメントの説明を次の表にまとめます。
| 場面 | 使われる方式 |
|---|---|
| ストレージに書き込むとき | 一覧の最初の方式で暗号化する |
| ストレージから読み出すとき | 保存されたデータに合う方式が、一覧の順に復号(元に戻すこと)を試みる。どの方式でも読めなければエラーになり、そのリソースにはアクセスできない |
2 番目以降の方式は読み出すときに使われます。前の設定で保存されたデータも読めるようにするためだと考えられます。identity は暗号化をしない方式で、ほかに指定しなければ identity が既定です。この例のように identity が最初にあると、新しく書き込むデータは暗号化されません。公式ドキュメントによると、最初の方式が identity 以外なら、保存時の暗号化は有効です。つまり暗号化を有効にするには、identity 以外の方式を一覧の最初に置きます。
参考:Encrypting Confidential Data at Rest | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes
v1.37 時点で公式ドキュメントが挙げる方式を、次の表にまとめます。
| 方式 | 公式ドキュメントの説明 |
|---|---|
identity | 暗号化しない。既定 |
aescbc | 強さは弱い(Weak)。CBC の弱点(パディングオラクル攻撃)があるため、勧められない |
aesgcm | 20 万回書き込むごとに鍵を入れ替える必要がある。鍵を自動で入れ替える仕組みが無いなら、勧められない |
secretbox | 強い(Strong)。高い水準の審査を求める環境では、受け入れられないことがある |
kms v1 | v1.28 から非推奨 |
kms v2 | いちばん強い(Strongest)。鍵の管理に外部のツールを使うなら、よい選択(A good choice)。v1.29 から stable |
パディングオラクル攻撃は一般に、暗号文を少しずつ変えて送り、復号したときにエラーになるかどうかの違いから、元のデータを推し量る攻撃です。
KMS は、kms の方式が使う、鍵を管理する外部のサービスのことです。公式ドキュメントによると、kms の実装は、ハードウェアのセキュリティモジュールや、クラウドの事業者が管理する暗号化のサービスと連携できます。ハードウェアのセキュリティモジュールは一般に、鍵を外に取り出せないように守る専用の装置です。鍵をどこに置くかで、守れる範囲が違います。公式ドキュメントの説明を次の表にまとめます。
| 鍵の置き場所 | 守れる範囲 |
|---|---|
コントロールプレーンのホストの設定のファイル(aescbc など) | etcd が破られても守れるが、ホストが破られると守れない。鍵が設定のファイルにあるので、攻撃者に取り出されるおそれがある |
KMS(kms) | エンベロープ暗号化を使う。平文を得るには、攻撃者は etcd と KMS の両方を破る必要がある |
参考:Encrypting Confidential Data at Rest | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes
エンベロープ暗号化は、公式ドキュメントによると、Kubernetes がリソースをデータ鍵で暗号化し、そのデータ鍵をさらに KMS で暗号化する方式です。暗号化したデータ鍵は、暗号化したデータと一緒に etcd に保存されます。データ鍵を元に戻すための鍵(key encryption key)は Kubernetes の外の KMS にあります。データ鍵の作り方は、方式の表では kms の版ごとに書き分けられています。v1 はリソースごとにデータ鍵を作り、v2 は kube-apiserver ごとのデータ鍵を使い、暗号化のたびに秘密の種から新しいデータ鍵を作ります。
参考:Encrypting Confidential Data at Rest | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes
暗号化を有効にしているかどうかは、kube-apiserver の引数でわかります。公式ドキュメントの説明を次の表にまとめます。
| kube-apiserver の状態 | 保存時の暗号化 |
|---|---|
--encryption-provider-config が無い | 有効ではない |
引数があり、ファイルの最初の方式が identity | 有効ではない |
引数があり、ファイルの最初の方式が identity 以外 | 有効。ただし、前からあるデータの暗号化が済んだかどうかは、これではわからない |
表の最後の行のとおり、暗号化を有効にしても、すでに保存されているデータがすぐに暗号化されるわけではありません。公式ドキュメントは前からあるオブジェクトにも暗号化を当てはめるため、Secret を 1 つずつ置き換える(replace)操作をする方法を示しています。次のコマンドはすべての Namespace の Secret を読み出し、そのまま置き換えます。公式ドキュメントによると、オブジェクトの中身は変わらず、保存時に暗号化されます。
$ kubectl get secrets --all-namespaces -o json | kubectl replace -f -
--all-namespaces(短く書くと -A)はすべての Namespace を対象にする指定です。kubectl replace の -f - はファイルの代わりに、| で受け取った内容を読む指定です。前半のコマンドが JSON で書き出したすべての Secret を、後半のコマンドがそのまま置き換えます。
このコマンドはクラスタの管理者が暗号化の設定を入れたあとに実行するものです。この回の手順では実行しません。公式ドキュメントのこの手順は、各コントロールプレーンのノードで kube-apiserver が static Pod として動いていることを前提にしています。暗号化の設定は kube-apiserver を動かすコントロールプレーンのホストで行う作業だと読めます。
Secret の扱い方の推奨のページは、etcd そのものの管理についても次の 2 つを挙げています。
- etcd が使っていた記憶装置を使わなくなったら、消去するか破棄することを考える
- etcd が複数あるなら、etcd どうしの通信を SSL/TLS で暗号化し、通信中の Secret のデータを守る
RBAC で読み取りを絞る
RBAC(Role-Based Access Control)は、利用者やプログラムごとに、どのリソースにどの操作を許すかを決める仕組みです。詳しくは第38回で扱います。この回では Secret の読み取りにかかわる操作だけを見ます。kube-apiserver は読み取りのリクエストを次の 3 つの操作(verb)に分けて扱います。公式ドキュメントの説明を次の表にまとめます。
| 操作(verb) | 対象 |
|---|---|
get | 1 つのリソース |
list | リソースの一覧(オブジェクトの中身もすべて含む) |
watch | 1 つのリソースか、リソースの一覧の変化を見続ける |
参考:Authorization | Kubernetes、Using RBAC Authorization | Kubernetes
kubectl get secret mysecret のように名前を指定して 1 つを読むのが get、kubectl get secrets のように一覧するのが list にあたると考えられます。リファレンスによると、kubectl get の --watch は、求めたオブジェクトを一覧(または取得)したあとで、変化を見続ける指定です。そのため --watch を付けると、list(または get)に加えて watch にあたると考えられます。
ここで気を付けたいのが list です。公式ドキュメントは Secret の list を許すと、返ってくるすべての Secret の data が見えてしまう と注意しています。RBAC の推奨のページ(Role Based Access Control Good Practices)によると、kubectl get secrets -A -o yaml のような一覧(List)の応答には、すべての Secret の中身が含まれます。-A はすべての Namespace を対象にする指定です。同じページは watch も同じく、Secret の中身を実質的に見せてしまうと書いています。
参考:Authorization | Kubernetes、Role Based Access Control Good Practices | Kubernetes
Secret の扱い方の推奨のページは、RBAC などで Secret へのアクセスを決めるときの指針を挙げています。次の表にまとめます。
| 対象 | 指針 |
|---|---|
| コンポーネント(クラスタで動くプログラム) | watch と list は、いちばん強い権限を持つ、システムのコンポーネントだけに許す。get は、そのコンポーネントがふだんの動作で必要とする場合だけ許す |
| 人 | Secret の get、watch、list を制限する。etcd へのアクセスはクラスタの管理者だけに許す(読み取りだけのアクセスも含む) |
もう 1 つ気を付けたいのが Pod を作る権限です。公式ドキュメントによると、Secret を使う Pod を作れる利用者は、その Secret の値も見られます。冒頭で見たとおり、Deployment を作れるような間接的な権限も含みます。推奨のページは、Secret を直接読むことをクラスタの方針で許されていない利用者でも、Secret をさらす Pod を動かせることがある、と書いています。Pod の中で Secret を環境変数やファイルとして受け取り、それを表示させればよいからだと考えられます。後半の「手元で試す」でも、Pod を作ってその中で Secret の値を読みます。
参考:Good practices for Kubernetes Secrets | Kubernetes、Secrets | Kubernetes、Role Based Access Control Good Practices | Kubernetes
公式ドキュメントは、マウントする Secret へのアクセスを分けるために、Namespace を分けるよう勧めています。こうした権限を持つ利用者によって、わざと、またはうっかり Secret のデータがさらされたときに、それに気付いたり、影響を小さくしたりする方法として、次の 2 つも挙げています。
- 期限の短い Secret を使う
- 1 人の利用者が複数の Secret を同時に読むといった、特定の出来事を知らせる監査(だれが何をしたかの記録を調べること)のルールを用意する
参考:Good practices for Kubernetes Secrets | Kubernetes、Good practices for Kubernetes Secrets | Kubernetes、Secrets | Kubernetes
Secret を渡すコンテナを絞る
ノードの側にも、Secret を必要な場所にだけ置く仕組みがあります。公式ドキュメントの説明は次のとおりです。
- Secret は、そのノードの Pod が必要とするときだけ、ノードに送られる
- kubelet は Secret を Pod にマウントするために、データの写しを tmpfs に置き、長く残る記憶装置には書かない
- Secret を使う Pod が消されると、kubelet はデータの写しを消す
- 1 つのノードに Pod がいくつあっても、Pod のコンテナから見えうるのは、その Pod が求めた Secret だけで、ほかの Pod の Secret にはアクセスできない
Pod の中でも Secret を見られるコンテナを絞れます。公式ドキュメントによると、既定では、コンテナがアクセスできるのは default の ServiceAccount と、それに関連する Secret だけです。ほかの Secret へのアクセスを与えるには、環境変数かボリュームのマウントをはっきり書く必要があります。そのため推奨のページは、Pod に複数のコンテナ(第10回)があり、Secret が要るのが 1 つだけなら、ほかのコンテナがアクセスできないように、環境変数やボリュームのマウントを書くよう勧めています。図の「コンテナ sidecar(指定なし)」は、mysecret のような Secret を指定していないコンテナという意味です。
参考:Secrets | Kubernetes、Good practices for Kubernetes Secrets | Kubernetes
公式ドキュメントはその使い方の例も挙げています。HTTP のリクエストを受けて複雑な処理をし、最後に秘密鍵でメッセージに署名するプログラムがあるとします。処理が複雑なので、サーバーには離れた場所からファイルを読み出せる、気付かれていない弱点があるかもしれず、それによって秘密鍵が攻撃者に知られるおそれがあります。そこで、利用者とのやり取りと処理を受け持ち、秘密鍵を見られないフロントエンドのコンテナと、秘密鍵を見られて、フロントエンドからの簡単な署名の依頼に応える署名のコンテナに分けます。こうすると、攻撃者はアプリケーションのサーバーをだまして、かなり自由な操作をさせる必要があり、それはファイルを読ませるより難しいことがある、と説明されています。
ただし例外があります。公式ドキュメントによると、privileged: true で動くコンテナは、そのノードで使われているすべての Secret にアクセスできます。privileged はコンテナに強い権限を与える設定で、第39回で扱う SecurityContext の項目です。
読み取ったあとの扱いと外部の保管サービス
Secret から値を読み取ったあとも、注意が要ります。公式ドキュメントによると、アプリケーションは機密の値を読み取ったあとも、それを守る必要があります。たとえば、Secret のデータを平文のままログに書いたり、信頼できない相手に送ったりしてはいけません(must avoid)。
機密のデータをクラスタの外に置く方法もあります。推奨のページによると、外部の Secret の保管サービスにデータを置き、Pod がそれを読めるように設定できます。例として挙げられているのは Kubernetes Secrets Store CSI Driver です。CSI は一般に、外部のストレージを Kubernetes につなぐための共通の仕組みで、ドライバーはその仕組みに沿って作られたプログラムです。このドライバーは DaemonSet(第15回)として動き、kubelet が外部の保管場所から Secret を取得し、アクセスを許した特定の Pod にボリュームとしてマウントできるようにします。
Secret のページは Secret の代わりになる方法も挙げています。同じクラスタの中のアプリケーションどうしの認証に ServiceAccount のトークンを使う方法や、機密のデータを管理し、認証した相手にだけ見せるサードパーティのツール(クラスタの中でも外でも動かせる)を使う方法などです。
手元で試す
ここまでの内容を手元の kind のクラスタで試す手順を示します。値を base64 でエンコードして Secret を作り、kubectl describe では値が表示されないことを見ます。続けて data の値を取り出し、base64 でデコードすると元の値に戻ることを確かめます。Secret を受け取る Pod を作り、コンテナの中に元の値が届くことも確かめます。最後に、クラスタの kube-apiserver に保存時の暗号化の引数があるかを見ます。コマンドは macOS や Linux のシェル(bash や zsh)で実行する前提で書いています。Windows では WSL(Windows の中で Linux を動かす仕組み)などを使ってください。試す環境は次のとおりです。
| 項目 | 内容 |
|---|---|
| kind | v0.33.0 |
| Kubernetes | v1.37.0(kind v0.33.0 の既定のノードイメージ) |
| ノード構成 | シングルノード(クラスタ名 kind):コントロールプレーン 1 |
| 必要なもの | Docker、kubectl(v1.36〜v1.38)、テキストエディタ、端末(ターミナル)2 つ |
参考:Release v0.33.0 · kubernetes-sigs/kind、Version Skew Policy | Kubernetes
kind でクラスタを作る手順は、以下の記事で解説しています。この回ではシングルノードのクラスタを使います。この回で見る Secret の値の表し方と渡り方は、ノードが 1 つでも確かめられるからです。
kind で検証用のクラスタを作るDocker のコンテナをノードにして、手元のマシンに Kubernetes のクラスタを作るツール kind の仕組みを図で説明します。kind create cluster で何が起きるかを押さえ、シングルノードとマルチノードのクラスタを作って、ノードの正体が Docker のコンテナであることを確かめる手順を紹介します。この手順の admin と 1f2d1e2e67df は公式ドキュメントの例の値です。本物のパスワードは使わないでください。
シングルノードのクラスタを用意する
まず、前の回のクラスタが残っていないかを確かめます。
$ kind get clusters
kind が表示されたら、作り直さずにそのまま使えるので、クラスタを作る次のコマンドは飛ばします。表示されなければ、クラスタを作ってから、もう一度一覧します。前の回の multi-node が表示されても、この回では使いません。手元のマシンの資源(メモリなど)に余裕がなければ、kind delete cluster --name multi-node で消してから進めてください。
$ kind create cluster
$ kind get clusters
kind get clusters に kind が表示されれば、クラスタができています。第3回で決めたとおり、ここからのコマンドには --context kind-kind を付けて、操作するクラスタをはっきりさせます。
続けて、ノードを一覧します。
$ kubectl get nodes --context kind-kind
ノードが 1 つ表示され、STATUS 列が Ready になっていれば準備ができています。Ready になっていなければ、少し待ってからもう一度実行します。
値を base64 でエンコードする
「Secret のマニフェスト」と同じく、2 つの値を base64 でエンコードします。
$ echo -n 'admin' | base64
$ echo -n '1f2d1e2e67df' | base64
公式ドキュメントの例と同じコマンドなので、1 行目は YWRtaW4=、2 行目は MWYyZDFlMmU2N2Rm と表示されるはずです。これがマニフェストの data に書いた値です。base64 のコマンドは手元の OS に入っているものを使います。公式ドキュメントも外部のツールを使う危険を減らすため、OS が信頼する手元のツールを使うよう勧めています。
参考:Distribute Credentials Securely Using Secrets | Kubernetes
Secret を作って表示を確かめる
「Secret のマニフェスト」の最初のマニフェスト(stringData を使わないほう)を、mysecret.yaml という名前で、コマンドを実行するディレクトリに保存します。マニフェストを渡します。
$ kubectl apply -f mysecret.yaml --context kind-kind
公式ドキュメントの例では、出力は次のとおりです。
手元でも同じ 1 行が出るはずです。続けて、Secret を表示します。
$ kubectl get secret mysecret --context kind-kind
公式ドキュメントの例(kubectl get secrets で db-user-pass という Secret を一覧した例)では、出力は次のとおりです。
手元では名前を指定して 1 つだけ表示するので、見出しの行と、mysecret の行だけが出るはずです。NAME 列は mysecret、AGE 列は作ってからの時間、TYPE 列はマニフェストに書いた Opaque になるはずです。公式ドキュメントによると、DATA 列は Secret に入っているデータの項目の数なので、キーが 2 つの mysecret では 2 になります。値はどこにも表示されません。
続けて、kubectl describe(第4回)で詳しく表示します。
$ kubectl describe secret mysecret --context kind-kind
公式ドキュメントの例(db-user-pass)では、出力は次のとおりです。
手元では Name: が mysecret になるはずです。Data の下には値ではなくキーごとの大きさが表示されます。admin は 5 文字、1f2d1e2e67df は 12 文字で、英数字は一般に 1 文字が 1 バイトなので、手元でも password は 12 bytes、username は 5 bytes になるはずです。第17回では試していませんが、公式ドキュメントの ConfigMap の例では、kubectl describe で値がそのまま表示されていました。Secret では、公式ドキュメントの説明のとおり、中身がうっかり見えないようになっています。
参考:Managing Secrets using kubectl | Kubernetes、Configure a Pod to Use a ConfigMap | Kubernetes
base64 でデコードして元の値に戻す
kubectl get と kubectl describe が値を見せないのは、表示のしかたの工夫にすぎません。第5回で見た -o jsonpath で data を取り出します。
$ kubectl get secret mysecret -o jsonpath='{.data}' --context kind-kind
公式ドキュメントの例(db-user-pass)では、出力は次のとおりです。
参考:Managing Secrets using kubectl | Kubernetes、JSONPath Support | Kubernetes
手元では、password の値が MWYyZDFlMmU2N2Rm、username の値が YWRtaW4= になるはずです。例の username も admin をエンコードした値なので、同じ YWRtaW4= です。空白の入り方は例と違って見えることがあると考えられます。マニフェストに書いた base64 の値がそのまま返ってきています。
次に、password の値だけを取り出し、base64 --decode に渡してデコードします。公式ドキュメントが勧める、取り出しとデコードを 1 つにつないだコマンドです。
$ kubectl get secret mysecret -o jsonpath='{.data.password}' --context kind-kind | base64 --decode
1f2d1e2e67df と表示されるはずです。鍵もパスワードも使わずに、元の値に戻りました。これが、base64 はエンコードであって暗号化ではない、ということです。値の最後に改行が無いので、次のプロンプトが同じ行に続けて表示されることがあると考えられます。{.data.password} を {.data.username} に変えれば、admin が表示されるはずです。
Pod に Secret を渡す
「ConfigMap との違いと Pod への渡し方」の Pod のマニフェストを、secret-demo.yaml という名前で、mysecret.yaml と同じディレクトリに保存します。
ここからは端末を 2 つ使います。第11回・第13回〜第17回と同じく、1 つ目の端末で変化を追い続け、2 つ目の端末でマニフェストを渡したり、値を確かめたりします。この回で 1 つ目の端末が追うのは Pod です。2 つ目の端末を開いたら、cd でマニフェストを保存したディレクトリに移動しておきます。
1 つ目の端末で、app=secret-demo のラベルを持つ Pod を監視します。
$ kubectl get pods --watch -l app=secret-demo --context kind-kind
Pod がまだ無いので、何も表示されないか、見つからないことを知らせる 1 行が出たあと、止まったように見えると考えられます。
2 つ目の端末で、Pod のマニフェストを渡します。
$ kubectl apply -f secret-demo.yaml --context kind-kind
pod/secret-demo が作られたことを知らせる 1 行が出るはずです。1 つ目の端末には secret-demo の行が書き足され、STATUS 列が Pending、ContainerCreating、Running と移っていくと考えられます。クラスタを作ったばかりなら、busybox:1.28 のイメージの取得に時間がかかるので、ContainerCreating のまましばらく止まって見えることがあります(第9回)。参照する mysecret は先に作ってあるので、Secret が無いために起動が止まることはないはずです。
Running になったら、2 つ目の端末で、コンテナの中の環境変数 SECRET_USERNAME を表示します。第10回で見たとおり、kubectl exec はコンテナの中でコマンドを動かすコマンドで、-- の後ろがコンテナの中で動かすコマンドです。公式ドキュメントの例と同じく、sh -c でシェルを通して echo を動かします。一般に、手元のシェルは二重引用符の中の $SECRET_USERNAME を先に置き換えてしまいますが、単引用符(')で囲んだ部分はそのまま渡します。kubectl exec は -- の後ろをコンテナの中のコマンドとして動かすので、環境変数を置き換えるシェルとして、コンテナの中の sh を -c で動かすと考えられます。こうすると、$SECRET_USERNAME はコンテナの中の環境変数の値に置き換わるはずです。
$ kubectl exec secret-demo --context kind-kind -- sh -c 'echo "$SECRET_USERNAME"'
参考:kubectl exec | Kubernetes、Distribute Credentials Securely Using Secrets | Kubernetes
admin と表示されるはずです。公式ドキュメントの、環境変数で受け取る例でも、元の値が表示されています。コンテナには base64 の YWRtaW4= ではなく元の値が届いています。
続けて、ボリュームのファイルを一覧します。
$ kubectl exec secret-demo --context kind-kind -- ls /etc/secret-volume
公式ドキュメントの例(test-secret という Secret をマウントした例)では、出力は次のとおりです。
参考:Distribute Credentials Securely Using Secrets | Kubernetes
例の Secret も username と password の 2 つのキーを持つので、手元でも password と username の 2 つが表示されるはずです。例は対話のシェルの中で ls を動かしています。この回は端末を割り当てずに動かすので、1 行に 1 つずつ表示されることがあると考えられます。Secret の各キーがファイルの名前になっています。ファイルの中身を表示します。cat は一般に、ファイルの中身を表示するコマンドです。
$ kubectl exec secret-demo --context kind-kind -- cat /etc/secret-volume/password
1f2d1e2e67df と表示されるはずです。ファイルの中身もエンコードする前の元の値です。公式ドキュメントの例でも、cat でファイルを表示すると元の値が表示されています。この Pod を作れた人はこうして Secret の値を読めます。「RBAC で読み取りを絞る」で見た、Pod を作れる利用者は Secret の値も見られる、という注意のとおりです。
参考:Distribute Credentials Securely Using Secrets | Kubernetes、Good practices for Kubernetes Secrets | Kubernetes
最後に、Pod の spec に何が書かれているかを見ます。コンテナの env を取り出します。
$ kubectl get pod secret-demo -o jsonpath='{.spec.containers[0].env}{"\n"}' --context kind-kind
参考:JSONPath Support | Kubernetes、Pod | Kubernetes API Reference
環境変数の名前 SECRET_USERNAME と、valueFrom の secretKeyRef(Secret の名前 mysecret とキー username)が表示されるはずです。admin という値はどこにも無いはずです。Pod の spec には、どの Secret のどのキーを使うかだけが書かれていて、値そのものは Secret の側にあります。
保存時の暗号化が有効かを確かめる
最後に、このクラスタの kube-apiserver に --encryption-provider-config の引数があるかを見ます。第4回で見たとおり、kind のクラスタでは kube-apiserver が static Pod として動き、mirror Pod として API から見えます。kubeadm は static Pod に component のラベルを付けるので、component=kube-apiserver で選べます。2 つ目の端末で、その Pod の中身を YAML で出力し、grep に渡します。grep は一般に、指定した文字列を含む行だけを表示するコマンドです。-e は一般に、続く文字列が - で始まっていても、オプションではなく探す文字列として扱わせる指定です。
$ kubectl get pods -n kube-system -l component=kube-apiserver -o yaml --context kind-kind | grep -e '--encryption-provider-config'
参考:Implementation details | Kubernetes、Static Pods | Kubernetes
公式ドキュメントによると、--encryption-provider-config の引数を付けずに kube-apiserver を動かしているなら、保存時の暗号化は有効ではありません。kubeadm の実装の詳細のページが挙げる、kubeadm が kube-apiserver に付ける引数には、--encryption-provider-config は含まれていません。kind はノードを kubeadm で初期化するので(第2回)、kind が別に付けていなければ何も表示されず、このクラスタでは Secret が暗号化されずに etcd に保存されていると考えられます。kind が別に付けるかどうかは、kind の公式ドキュメントでは確かめられませんでした。
何も表示されなかったとき、引数が YAML に出ていないのか、コマンドの打ち間違いなのかを見分けるため、必ずある引数でも同じように確かめます。kubeadm の実装の詳細のページによると、kubeadm は kube-apiserver に --client-ca-file を付けます。
$ kubectl get pods -n kube-system -l component=kube-apiserver -o yaml --context kind-kind | grep -e '--client-ca-file'
--client-ca-file を含む行が表示されるはずです。こちらが表示され、--encryption-provider-config は表示されないなら、kube-apiserver の引数は YAML に出ていて、その中に保存時の暗号化の引数は無いと読めます。--encryption-provider-config を含む行が表示された場合は、「etcd での保存時の暗号化」の表のとおり、設定のファイルの最初の方式で、有効かどうかが決まります。
参考:Encrypting Confidential Data at Rest | Kubernetes、Implementation details | Kubernetes
片付け
1 つ目の端末で Ctrl+C を押して、Pod の監視を止めます。
2 つ目の端末で、作った Pod と Secret を、マニフェストを指定して削除します。
$ kubectl delete -f secret-demo.yaml -f mysecret.yaml --context kind-kind
少し待ってから、残っていないかを確かめます。
$ kubectl get pod secret-demo --context kind-kind
$ kubectl get secret mysecret --context kind-kind
Pod が Terminating で表示されていたら、少し待ってからもう一度実行します。どちらのコマンドでも、見つからないことを知らせるエラーの 1 行が出れば、削除できています。公式ドキュメントによると、Pod が消されると、kubelet はノードに置いた Secret のデータの写しも消します。
最後に、クラスタを削除し、消えたことを確かめます。
$ kind delete cluster
$ kind get clusters
kind delete cluster は存在しないクラスタを指定してもエラーを返しません。kind get clusters に kind が表示されなくなったことを確かめます。kubeconfig に kind-kind のコンテキストが残っていたら、第3回の片付けと同じく kubectl config delete-context kind-kind で消します。
注意点
Secret でつまずきやすい点と、既定のままでは守られない点を、次の表にまとめます。
| 項目 | 内容 |
|---|---|
| base64 は暗号化ではない | data の値は base64 でエンコードしただけ。誰でも鍵なしでデコードできる |
| 既定では etcd に暗号化されずに保存される | 保存時の暗号化は、kube-apiserver の --encryption-provider-config で設定する。既定の方式は暗号化しない identity |
| 暗号化を有効にしても前からのデータはそのまま | 前からある Secret を暗号化するには、置き換える(replace)操作が要る |
list と watch でも中身が見える | Secret の list を許すと、返るすべての Secret の data が見える。watch も同じ |
| Pod を作れる人は Secret の値を見られる | Deployment を作れるような間接的な権限も含む。Namespace を分けてアクセスを分ける |
privileged: true のコンテナ | そのノードで使われているすべての Secret にアクセスできる |
| マニフェストを共有しない | base64 の値を書いたマニフェストを共有したり、リポジトリに入れたりすると、読めるすべての人が秘密を知れる |
| 読み取った値の扱い | アプリケーションは、Secret のデータを平文のままログに書いたり、信頼できない相手に送ったりしてはいけない |
ボリュームの指定は secretName | ConfigMap の name と違い、Secret のボリュームでは secretName と書く |
| 大きさは 1 MiB まで | 1 つの Secret の大きさは 1 MiB まで |
参考:Secrets | Kubernetes、Good practices for Kubernetes Secrets | Kubernetes、Encrypting Confidential Data at Rest | Kubernetes、Authorization | Kubernetes、Pod | Kubernetes API Reference
Secret を使えば、機密のデータを Pod の spec やイメージから切り離せます。ただ、既定のままでは、値は base64 でエンコードされただけで、etcd にも暗号化されずに保存されます。そのため、公式ドキュメントが挙げる保存時の暗号化、RBAC での読み取りの制限、Secret を渡すコンテナを絞ることを、Secret と組み合わせて使う必要があると考えられます。
関連記事
コンテナに渡すコマンドの引数と環境変数、Pod 自身の情報をコンテナに渡す Downward API については、以下の記事で解説しています。
Downward API で Pod の情報を読むPod の名前や IP アドレス、コンテナの CPU とメモリの値を、コンテナの中のプログラムに渡す Downward API の仕組みを図で説明します。コンテナに渡すコマンドと引数、環境変数の書き方を押さえ、環境変数と downwardAPI ボリュームで渡せる情報の違いを、kind のクラスタで確かめる手順を紹介します。Secret の get、list、watch を誰に許すかを決める ServiceAccount と RBAC については、以下の記事で解説しています。
Secret へのアクセスを分けるために公式ドキュメントが勧める Namespace と、Namespace で分けられる範囲については、以下の記事で解説しています。
Namespace で分けられるもの、分けられないもの1 つのクラスタの中でリソースをグループに分ける Namespace の仕組みを図で説明します。名前の範囲になること、Namespace に属さないクラスタ全体のリソース、既定では Namespace をまたいで届く通信、Namespace を消すと中身も消える流れを押さえ、kind のクラスタで確かめる手順を紹介します。