Dev Study
← 出して、戻して、直す — 変更と復旧の実務
レッスン 5 / 12·約 7 分·第24期 出して、戻して、直す

秘密は、読める者の一覧でしか守られない

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

壊れたときに打てる手は、壊れる前に用意した分しかありません。秘密がどこに置かれ誰が読めるかを知っていることも、その用意の1つです。今回はアプリのプロセスに渡した秘密の行き先を追います。

まず結論

秘密をコードに直書きせず環境変数に移すのは正しい一歩です。ただし秘密は消えたのではなく、置き場所が移っただけです。

移った先ごとに「誰が読めるか」の一覧があります。コンテナの設定、プロセスの起動時の環境、Kubernetes の etcd。守りの強さはその一覧で決まり、値の置き方の名前では決まりません。

? 読む前に予測

コンテナを `-e API_TOKEN=...` で起動し、中の Python が起動直後に `os.environ.pop('API_TOKEN')` で値を消しました。その後 `docker exec` で `/proc/1/environ` を読むと、どうなるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

直書きをやめるのは正しい。問題はその先

API キーをソースに直接書かず、環境変数や管理サービスから読み込む。既存のクイズの多くがこれを正解にしており、この判断はそのまま正しいです。The Twelve-Factor App は理由をこう書きます。

Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code.

判定の試金石は whether the codebase could be made open source at any moment, without compromising any credentials.。コードを公開しても資格情報が漏れないか、です。同じページは置き場所として環境変数を勧めます(The twelve-factor app stores config in environment variables)。

ただし、この試金石が見ているのはコードの側だけです。環境変数に移した値を、次に誰が読めるかには、このページは触れていません。秘密はコードから消えましたが、世界から消えたわけではありません。値は起動の手順に沿って、いくつもの場所を通ります。

コンテナに渡した秘密が通る場所
コード直書きをやめる(試金石を満たす)
起動時に -e で渡す
コンテナの設定Config.Env。docker inspect で見える
execve でプロセスへ
PID1 の起動時の環境/proc/1/environ。後で消しても残る
後から起動した子消した後なら渡らない
  • ・値が通った場所ごとに、読める者の一覧が1つずつある
  • ・秘密の守りは、それらの一覧を合わせた範囲で決まる
具体実物

消したはずの値が残る場所

本教材の実演です。python:3.13-slim のコンテナを API_TOKEN 付きで起動し、PID1 の Python が起動直後に os.environ.pop('API_TOKEN') で値を消してから、子プロセス sleep 300 を起動します。値はダミーです。PID1 は root で動いています。

本教材の実演の出力(スクリプト非掲載。Docker 28.4.0 / macOS aarch64。2回とも同一)コマンド出力
== docker inspect の Env
["API_TOKEN=dummy-not-a-real-token","PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305","PYTHON_VERSION=3.13.15","PYTHON_SHA256=1e66a7945a48390ee4c2a4268a0e4185884059a13c4aab6d148aa208deea4a76"]
== PID1 (pop 済み) の /proc/1/environ
API_TOKEN=dummy-not-a-real-token
== pop 後に起動した子の environ
(子にはなし)
== 別ユーザ(nobody)から PID1 の environ
cat: /proc/1/environ: Permission denied
読めない

読めることは4つです。

  1. コンテナの設定に残る。 docker inspect の Config.Env に値があります。コンテナを操作できる者は、これで読めます。
  2. プロセスの起動時の環境に残る。 man は /proc/pid/environ を initial environment と呼び、後の変更について this file will not reflect those changes. と書きます。
  3. 消した後に起動した子には渡らない。 消すことに意味が無いわけではありません。
  4. 権限の無い別ユーザーからは読めない。 man は読めるかどうかを PTRACE_MODE_READ_FSCREDS の検査で決めると書きます(ptrace(2) によれば、uid が違っても対象の user namespace で CAP_SYS_PTRACE を持つ者は uid の照合を通る)。実演では root の PID1 に対し、nobody からは拒否されました。
✓ ここまでの確認

実演で、pop した後も `/proc/1/environ` に値が残った理由として、man の記述に合うのはどれですか。

抽象原理

「環境変数は安全か」ではなく「誰が読めるか」

ここから両向きの誤りが出てきます。

「環境変数に入れたから安全」は誤りです。値は設定とプロセスに残り、Docker の文書は Environment variables are often available to all processes, and it can be difficult to track access. They can also be printed in logs when debugging errors without your knowledge. と書きます。ログを読める者も一覧に加わります。

「環境変数は危険だから使うな」も誤りです。The Twelve-Factor App は置き場所として勧めており、実演でも nobody からは読めませんでした。

問うべきは形式ではなく、値が通る場所ごとに、読める者の一覧を書き出せるかです。秘密の守りはその一覧の外には及びません。一覧が書けない秘密は、漏れたときに入れ替える範囲も決められません。

なお、ブラウザ向けのビルドで環境変数が文字列として埋め込まれる話は、第7期で扱います。そこでは読める者の一覧が「ページを開いた全員」になります。

具体実物

Kubernetes の Secret も、一覧で読む

専用のオブジェクトに入れれば一覧は狭くなるでしょうか。Kubernetes の公式文書は、Secret の説明に次の注意書きを置いています。

Kubernetes Secrets are, by default, stored unencrypted in the API server's underlying data store (etcd). Anyone with API access can retrieve or modify a Secret, and so can anyone with access to etcd. Additionally, anyone who is authorized to create a Pod in a namespace can use that access to read any Secret in that namespace; this includes indirect access such as the ability to create a Deployment.

条件を落とさずに読みます。既定では暗号化されずに etcd に置かれる。API を使える者と etcd に触れる者は読める。その namespace で Pod を作れる者も読め、Deployment を作れる権限でも同じです。「Secret に入れた=暗号化された」ではありません。

文書は続けて対策を4つ挙げます。Enable Encryption at Rest for Secrets.、最小権限の RBAC、Restrict Secret access to specific containers.、Consider using external Secret store providers.。どれも一覧を狭める手です。

✓ ここまでの確認

「DB パスワードを Kubernetes の Secret に入れたので、Pod を作れるだけの開発者には値が見えない」という理解は妥当ですか。

⇄ 2つを見比べる
具体例 A

コンテナの環境変数

直書きをやめた点は正しい。値はコンテナの設定と PID1 の起動時の環境に残り、デバッグ時のログに出ることもある。実演では nobody からは読めなかった。

置き場所と読める者(実演と文書から)表
置き場所 読める者
Config.Env コンテナを操作できる者(docker inspect)
/proc/1/environ ptrace の検査を通る者(nobody は拒否)
ログ ログを読める者(知らないうちに出ることがある)
具体例 B

Kubernetes の Secret

機密用のオブジェクトだが、既定では etcd に暗号化されずに置かれる。API と etcd に触れる者に加え、その namespace で Pod を作れる者も読める。

置き場所と読める者(公式文書の注意書きから)表
置き場所 読める者
etcd etcd に触れる者(既定は暗号化なし)
API API で Secret を取得できる者
namespace 内の Pod Pod や Deployment を作れる者

片方は汎用の環境変数、片方は機密専用のオブジェクトです。守りの強さを決めているのは何でしょうか。

表の右の列だけを見比べてください。

この章のまとめ

  • 直書きをやめて環境変数へ移すのは正しい。The Twelve-Factor App の試金石は「コードを公開しても資格情報が漏れないか」。
  • ただし秘密は置き場所が移っただけ。コンテナの設定と /proc/<pid>/environ に残り、後で消しても起動時の値は残る。
  • 読めるかは ptrace の検査で決まり、実演では権限の無い nobody から拒否された。「環境変数は危険」も言い過ぎ。
  • Kubernetes の Secret は既定では etcd に暗号化なしで置かれ、Pod を作れる者はその namespace の Secret を読める。
  • 問うのは器の名前ではなく、置き場所ごとの読める者の一覧。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 2正解: 0

The Twelve-Factor App が、設定がコードから正しく切り出せたかを判定する試金石として挙げるのはどれですか。

用語集(3語)
起動時の環境 (initial environment)
execve でプログラムが起動したときに渡された環境変数の組。`/proc/<pid>/environ` はこれを示し、その後の変更を反映しない。
PTRACE_MODE_READ_FSCREDS
`/proc/<pid>/environ` を読めるかを決める ptrace のアクセス検査の種類。man はこの検査に従うと書く。
etcd
Kubernetes の API サーバが背後に持つデータストア。公式文書は、Secret が既定では暗号化されずにここへ置かれると書く。