コンテンツにスキップ

シークレットの扱い

Popcorn Web でのシークレットの規則はひとつです。プロセスには環境変数として届き、 コミットされるファイルやイメージにコピーされるファイルには決して書かれない。この ページの内容はすべてこの規則から導かれます。設定ファイルが「これはデプロイ側が 供給する」と言うための二つの書き方、手元のマシンが値を置くファイル、ホストごとの 注入のしかた、そして規則が破られたときに報告する検査です。

意図した例外がひとつあります。config.dev.toml には開発用データベースの DSN と、 pw init を実行したマシンのために生成された keyring シークレットが入っています。 このファイルはチームで共有するために書かれています。アプリケーションの隣で Devbox が動かすデータベースのパスワードは資格情報ではなく fixture であり、pw doctor--env=dev ではこれについて黙っています。同じ内容が config.prod.toml にあれば エラーです。

スキャフォールドされた config.prod.toml は、データベース接続をこう書きます。

[[middleware.rdb.connections]]
group = "default"
dsn = "${DATABASE_URL}"

${NAME} はファイルの読み込み時に、プロセスが起動したときの環境から展開されます。 未定義の名前は空の DSN ではなく読み込みエラーです。変数を渡し忘れたデプロイは、 どこにも繋がらないプールでリクエストを受け続けるかわりに、起動時に止まります。 参照形式はどのキーにも使えますし、テーブルの配列にはこれしかありません。繰り返される テーブルには、フラットな環境変数名がないからです。

スカラーのキーにはそれぞれ環境変数もあります。名前から導かれるか、env:"NAME" タグで指定されたものです。だから二つ目の書き方は、キーをファイルから外して変数を 設定することです。

Terminal window
SESSION_KEYRING_SECRET=$(openssl rand -base64 32)

どちらの書き方も行き着く先は同じです。ファイルに「この値が存在する」と記録したい ときは ${NAME} を選んでください。config.prod.toml を読む人が、そのファイルが 持たない入力も含めて、デプロイに必要な入力すべてを見渡せます。ファイルの中で誰も 探さないキー、たとえば OTLP のヘッダーなら、素の変数を選びます。各キーの変数は 設定リファレンスにあります。

自分の設定も同じ型に従います。secret:"mask" タグの付いたフィールドは起動時の サマリと pw doctor からマスクされ、env タグがデプロイの設定する変数名を決めます。

type MailerConfig struct {
APIKey string `secret:"mask" env:"MAILER_API_KEY" help:"transactional mail API key"`
}

タグがなくても、名前に secretpasswordtokenapi_keycredentialdsn といったよくある語を含むキーはマスクされます。smtp_passsigning_seed のようにその外にある名前はされないので、タグを付けてください。

起動時に作業ディレクトリから、プロセスの環境変数より前に、4 つの dotenv ファイルが この順で読まれます。.env.env.local.env.{APP_ENV}.env.{APP_ENV}.local。 線が引かれているのは、コミットされる 2 つとされない 2 つの間です。.env.env.{APP_ENV} にはどのチェックアウトでも同じ値、たとえば共有のステージング issuer の URL が入り、.local の 2 つは .gitignore が除外するものです。だから シークレットはそこに置きます。

Terminal window
cp .env.example .env.local
$EDITOR .env.local

.env.example には、pw init で選んだ機能が開発以外の環境で読む変数が、値を空に して並んでいます。ひとつの環境だけの値は .env.local ではなく .env.stg.local に 置いてください。同じチェックアウトをステージングとして動かすときと開発として 動かすときで、資格情報を共有しないためです。優先順位、APP_ENV 自体をどこに 書けるか、Cloudflare Workers のビルドが代わりに何を読むかといった仕組みの全体は 秘密情報と dotenv ファイルにあります。

ランタイムイメージが持つのはバイナリと config.prod.toml だけです。シェルも パッケージマネージャもシークレットもありません。.dockerignoreconfig.dev.toml.local の dotenv ファイルすべてをビルドコンテキストから外すので、シークレットが うっかりレイヤーにコピーされることはありません。イメージレイヤーは、後のレイヤーで ファイルを消しても、pull できる誰もが読めるままだからです。

変数はプラットフォームから来ます。Compose の environment、ECS タスク定義の valueFrom 付き secrets、Cloud Run の --set-secretsenv としてマウントした Kubernetes の Secret。どれもプロセスの起動前に変数を設定します。ファイル層が ${NAME} を展開できるのはその瞬間だけです。

ファイルとしてマウントされたシークレットも読まれます。/run/secrets の下の通常 ファイルはそれぞれひとつの変数です。Docker が Compose や Swarm のシークレットを置く 場所であり、Kubernetes の Secret ボリュームも慣習的にここにマウントされます。 ファイル名が変数名、中身が値です。だから Compose ファイルは、環境変数のエントリなしで データベース接続を渡せます。

services:
app:
secrets: [DATABASE_URL]
secrets:
DATABASE_URL:
file: ./deploy/database_url.txt

このディレクトリは dotenv ファイルの後、プロセスの環境変数の前に読まれるので、 両方で設定された変数はプロセスの値になります。ここから読んだ値は、キーの名前が 何であれ起動時のサマリと pw doctor でマスクされます。マウントされているという 事実は、フィールド名よりも確かに「これはシークレットだ」と語るからです。.local の dotenv ファイルから読んだ値もすべて同じ扱いです。スキャフォールドされた Dockerfile が何をコピーし、それがなぜかは コンテナイメージにあります。

Worker にはファイルシステムがないので、TOML も dotenv ファイルも読まれません。 pw build --target cloudflare-workersconfig.prod.toml のスカラーを、コンテナと 同じ変数名で wrangler.jsoncvars に展開し、${NAME} 参照はその名前の Wrangler シークレットから解決されます。

Terminal window
npx wrangler secret put DATABASE_URL

ターゲットの残りはサーバーレスにあります。

規則が守られたことを検査するもの

Section titled “規則が守られたことを検査するもの”

pw doctor --env=prod はデプロイが読むのと同じようにプロジェクトを読み、値では なくキーとファイルで報告します。

  • 設定ファイルにリテラルとして書かれたシークレット(PW0412)と、そのファイルが git に 追跡されていること(PW0415)、所有者以外にも読めること(PW0416)
  • スキャフォールドのプレースホルダのままの値(PW0413)。これはどの環境でも
  • --env=all で 2 つの環境ファイルに共有されているリテラル(PW0414)
  • コミットされる .env.example に代入された値(PW0438)

.env.prod.local から読んだシークレットは設定ファイルのリテラルではないので PW0412 は黙りますが、そのファイル自体は PW0415 と PW0416 の対象のままです。 起動時のサマリは実行時に同じことを語ります。マスクされたキーはすべて ***** と 出所のファイルか変数を示し、DSN はホストとデータベース名を残すので、このプロセスが どのデータベースに繋がっているかはサマリから分かります。.local ファイルや /run/secrets から来た値は出所だけを理由にマスクされるので、.env.local に書いた ポートも ***** と出ます。共有する値を .env に置くべき理由がもうひとつ増えます。

ポート、機能スイッチ、タイムアウト、ログレベルは TOML ファイルに置きます。デプロイを 読む人に見え、pw doctor が推論できる場所です。便利だからと設定をすべて .env.local に移すと、その両方から設定が隠れます。--generate-config env の ひな形を .env.example ではなく .env に書き出すのも、一手で同じことになります。 そこに並ぶ既定値はすべて、次の起動で誰かが設定した値として読まれます。