TT Lab
はじめる
学ぶ 学習パス コース

コンテナセキュリティ

イメージに入ったトークンは取り消せない

TT Labで続きを見る

一言でいうと

環境変数は、保管場所ではなく受け渡しの方式です。そして、イメージのビルド中に入れた シークレットは、削除しても消えません。レジストリにプッシュしたなら、すでに漏えいしたものと みなして、取り消す必要があります。

なぜ必要なのか

.envファイルを読み込んだあとに削除すれば安全だと、考えがちです。ところが、 /proc/<pid>/environは、プロセスが生きている間は読み取れ続けます。同じホストにある サイドカー、デバッグ用のシェル、クラッシュダンプ、モニタリングエージェントが、すべて読める 場所です。環境変数は、値をプロセスに渡すための通り道にすぎず、値を隠してくれる仕組みでは ありません。

イメージの側は、もっと悪い状況です。ENVで入れたトークンは、イメージ設定に永久に残ります。

docker inspect -f '{{json .Config.Env}}' bad:v1
["NPM_TOKEN=npm_9fA3...", ...]

ARGで受け取って使い、rm ~/.npmrcで削除する方式も同じです。ファイルは 下位のレイヤーに残り、ビルドの履歴にも残ります。

docker history --no-trunc bad:v2 | grep -o 'NPM_TOKEN=[^ ]*'

ここから導かれる結論は1つです。レジストリにプッシュしたなら、そのトークンはすでに 漏えいしたものとみなして取り消す必要があります。イメージを削除しても、元には 戻せません。

どう動くのか

ビルド中にシークレットが本当に必要なら、BuildKitのsecret mountを使います。

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

このマウントはtmpfsなので、該当するRUNが終わると消え、レイヤーに残りません。 ビルド引数で渡す方式とは、根本的に違います。

KubernetesのSecretのbase64は、暗号化ではありません。エンコードにすぎず、etcdの 暗号化や外部のシークレットストアを別に有効にして初めて、実際の保護が始まります。

すでにコミットしたシークレットへの措置の順序は、決まっています。取り消し → 影響の調査 → 履歴の 整理です。順序を変えてはいけません。そして、履歴の書き換えは、漏えいを元に戻す措置ではなく、 再発を減らす衛生作業です。すでにフォーク、ローカルのクローン、PR参照、CIキャッシュに 複製されているからです。

現場での姿

ローテーションは運用手順ではなく、設計上の属性です。コードが「有効なキーはちょうど 1つ」と仮定していると、ローテーションは必ず失敗します。新しいキーをデプロイする瞬間と、古いキーを 使うクライアントが残っている瞬間が重なるからです。そのため、署名は現在のキー 1つで、検証は有効なキーの集合全体で行うように設計する必要があります。この原則がなければ、ローテーションの 計画書は実行されません。

検知ツールにも、3つの限界があります。1つ目は、pre-commitフックは--no-verifyを1回付ければ 回避されることです。そのため、フックは利便のための仕組みであり、統制の仕組みはサーバー側の検査です。2つ目は、 ルールがAKIA、sk_live_のように形がはっきりした値にしかうまく効かないことです。社内システムの 独自形式のトークンは、うまく捕捉されません。3つ目は、スキャナーがコードリポジトリしか見ないことです。Wiki、 Issueの添付ファイル、チャットログは見られません。

最後に、チームに投げかけるべき質問が1つあります。いまこの認証情報が公開されたと 仮定したとき、取り消して交換し、サービスを正常化するのに何分かかりますか。この質問に 答えられなければ、シークレット管理の体制はまだないということです。

ファイルのマウントも、そのまま放置してはいけません

環境変数からファイルに移すことは改善ですが、ファイルだからといってひとりでに安全になるわけではありません。移したあとに確認すべきことが、いくつか残ります。

権限と所有者: ファイルが、ほかのユーザーも読める状態で置かれていれば、移した意味がありません。コンテナの中にユーザーが1人しかいないので大丈夫に見えても、サイドカーが同じボリュームをマウントしていれば、そちらからも読めます。マウントするときに権限を明示し、サイドカーには、そのボリュームをマウントしません。

ディスクに残るか: シークレットを入れたボリュームがメモリベースなら、ノードのディスクには書き込まれませんが、通常のボリュームなら、そのまま残ります。ノードが廃棄されるときにディスクが消去されるかどうかまでが、この判断の範囲です。

ログとダンプ: 前に見たように、値を読み込んだコードがそれをエラーメッセージに載せれば、ファイルでも環境変数でも結果は同じです。そして、プロセスが落ちるときに残るメモリダンプには、読み込んだ値がそのまま入っています。ダンプを収集する設定が有効になっているか、それがどこに蓄積され、誰が読めるのかもあわせて見る必要があります。

更新されるとき何が起きるか: ファイルのマウントの価値の1つは、再起動なしで更新されることですが、アプリケーションがそのファイルを読み直さなければ、何の意味もありません。前の設定の注入の話と同じで、シークレットではさらに重要です。古いキーが取り消されたのに、プロセスがいまだに古い値を持っていれば、その時点からリクエストがすべて失敗します。

まとめると、シークレット管理は、保管場所を選ぶ問題ではなく、ライフサイクルを設計する問題です。どこに置くかよりも、誰が読め、いつ変わり、変わったことをどう察知するかが、実際の安全を決めます。

次のラボですること

環境変数で渡したシークレットが、docker inspectにそのまま見えることを確認し、同じ 値をファイルのマウントに移して、inspectから消えることを確認します。そして、コンテナを 再起動しないまま値を変更して、2つの方式のローテーションの特性の違いを自分で測ったあと、 簡単なシークレット検知のスクリプトを書きます。