Symfony でアプリ開発をしていると .env.local にクレデンシャルをハードコードすることがありますが、センシティブな情報をプレーンテキストで扱うことに若干の怖さを感じています。かんたんにリネームもファイルコピーもできますし GitHub リポジトリに間違えてプッシュしちゃったら怖いですよね。

そんなうっかり操作のヒヤリハットを何らかの仕組みで防止できないか調べてみました。

はじめに

この記事の「クレデンシャル」とは Symfony アプリ内でクローズドに利用する接続情報やトークンのことを示しています。パラメータを経由してサービスクラスに注入しサードパーティベンダーの認証などに使用するイメージです。

# .env.local
APY_KEY=1234abcd
SLACK_TOKEN=xoxb-000000000000-EXAMPLE-TOKEN
# config/services.yaml
parameters:
    apiKey: '%env(APY_KEY)%'
    slackToken: '%env(SLACK_TOKEN)%'

App\Command\SomeService:
    arguments:
        $apiKey: '%apiKey%'
        $slackToken: '%slackToken%'

クレデンシャルは API キー / OAuth 認証トークン / デベロッパートークンなどさまざまな種類があり、ハードコード先も .env.* だけでなく .key .yaml などバリエーションがあります。広範囲に散らばっているクレデンシャルを守るには、いくつかの方法を組み合わせて多層防御するのがベターと言えそうです。

.env.local を git で追跡しない:.gitignore

gitignore は基本的でシンプルな方法です。.gitignore に書いたパターンにマッチすると git で追跡されず、ステージもコミットもされません。Symfony CLI でプロジェクトを作成するとプロジェクトに最適な .gitignore が作成されます。

# .gitignore

###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
...

けれどもプロジェクトは常に Symfony CLI で構築されるとは限りません。別プロジェクトからソースコードを流用したり、ゼロベースからディレクトリを構成することもあるでしょう。

https://github.com/github/gitignore のようなテンプレートを使って .gitignore を作成した場合 .env.* はその対象に含まれていないかもしれません。記事執筆時点の Symfony 最新バージョンは v8.0.6 ですがテンプレートは v4 以降のリリースに追随していないように見えます。

gitignore /Symfony.gitignore

グローバル設定の ~/.config/git/ignore.env.* と書いてあるから安全!という思い込みもうっかりミスを誘発します。新しいデバイスをセットアップした時、このディレクトリの同期作業をうっかり忘れてしまうかもしれないからです。

.gitignore による追跡防止は 開発の基本であって過信しないこと というのが私の考えです。

.env.local をコミットしない:1Password Local .env files

1Password は有償のパスワードマネージャーです。
2025/10 に「Local .env files」というベータ版機能がリリースされました。この機能は .env ファイルを安全な場所に退避し、プロセスからの読み取りに開発者の許可を必要とします。

Introducing new .env file support in 1Password environments | 1Password

1Password アプリで「環境」を追加しプロジェクトを判別する任意の名前を付与します。

追加した環境に .env.local をインポートします。

プロジェクトの既存の .env.local を削除し、替わりに 1Password のファイルを保存(マウント)します。

マウントした .env.local はファイルの実体ではなく UNIX の 名前付きパイプ です。その内容は 1Password によって保護されているため、プロセスが読み取ろうとすると認証ダイアログが表示されます。許可すると 1Password アプリのロックがかかるまでアクセスが可能になります。

この仕組みは「うっかりプッシュ」の観点で良い副作用をもたらします。ファイルの実体ではないため git でコミットできない のです。.gitignore にうっかり書き忘れても、.env.locall とファイル名をタイポしても、コミット時点でブロックされます。

git add .env.local
> error: .env.local: can only add regular files, symbolic links or git-directories
> fatal: updating files failed

そして .env.local を一元管理できるメリットもあります。自宅とオフィスで PC を使い分けているような場合、どのデバイスでも 1Password からマウントしておけば「自宅の PC で変更した内容をオフィスの PC に反映し忘れた」なんてことがなくなります。

ただし落とし穴もあります。一時的なバックアップのために cp .env.local .env.local.bak を実行すると .env.local.bak はファイルの実体として作成され、うっかりコミットが可能になります。

.env.local を作らない:Symfony’s secrets management system

そもそもクレデンシャルを .env.local にハードコードしない手段についても検討の余地があります。Symfony 公式で紹介されている「Symfony’s secrets management system」はそのひとつです。

How to Keep Sensitive Information Secret | Symfony docs

この方法は PHP 7.2+ でバンドルされているモジュール sodium を使ってクレデンシャルを暗号化し config/secrets/* に格納します。

# モジュールが含まれているか調べる
php -m | grep sodium
> sodium

クレデンシャルの登録や削除はコマンドで行います。

# 暗号化・復号化のためのキーペアを作成(初回のみ)
php bin/console secrets:generate-keys

# クレデンシャルを登録
php bin/console secrets:set API_KEY

プロダクション環境用にはプレフィクス APP_RUNTIME_ENV=prod を付与してコマンドを実行します。

# 暗号化・復号化のためのキーペアを作成(初回のみ)
APP_RUNTIME_ENV=prod php bin/console secrets:generate-keys

# クレデンシャルを登録
APP_RUNTIME_ENV=prod php bin/console secrets:set API_KEY

一部のクレデンシャルをローカル開発用にカスタマイズする時は --local オプションを使用します。

bin/console secrets:set API_KEY --local

dev prod local それぞれの環境用に生成されたファイル群は以下のように格納されます。

config/secrets
├── dev
│   ├── dev.API_KEY.b58958.php # 個別のクレデンシャル
│   ├── dev.decrypt.private.php # 復号化の秘密鍵
│   ├── dev.encrypt.public.php # 暗号化の公開鍵
│   ├── dev.list.php # クレデンシャルのリスト
└── prod
    ├── prod.API_KEY.b58958.php # 個別のクレデンシャル
    ├── prod.decrypt.private.php # 復号化の秘密鍵
    ├── prod.encrypt.public.php # 暗号化の公開鍵
    └── prod.list.php # クレデンシャルのリスト

.env.dev.local # ローカル開発用にカスタマイズした値

これらのファイルのうち、プロダクション環境の秘密鍵 prod.decrypt.private.php だけはコミットしてはいけません。秘密鍵を直接サーバーに置くか、サーバーの環境変数 SYMFONY_DECRYPTION_SECRET に登録します。

# (A) サーバーに直接配置
config/secrets/prod/prod.decrypt.private.php

# (B) 環境変数に登録
export SYMFONY_DECRYPTION_SECRET={Base64 エンコードした鍵}

登録したクレデンシャルはコマンドでリストアップできます。

# --reveal オプションで値の表示ができる
php bin/console secrets:list --reveal

> ------------ ---------- ------------- 
>  Secret       Value      Local Value  
> ------------ ---------- ------------- 
>  API_KEY      "123abc"   "111aaa"    
>  DB_PASSWORD  "123def"   "222bbb"
> ------------ ---------- ------------- 

この仕組みは .env.* を使用せず 「うっかりプッシュ」対象を「プッシュしても良い情報」 に置き換えます。ただしローカル開発用にカスタマイズした .env.dev.local は依然としてうっかりコミットの可能性が残ります。

プッシュをブロック:GitHub Push protection

クレデンシャルの格納先は .env.* とは限りません。.key .yaml にハードコードした内容を無害なファイルパスとして .env.local に渡すことがあります。GitHub の Push protection はそのようなケースで有効です。

ユーザーのプッシュ保護 | GitHub ドキュメント

Organization に属さない個人アカウント配下でパブリックなリポジトリを作成したところ、デフォルトで Push protection が有効になっていました。

試しに AWS アカウントなどダミーのクレデンシャルを書いたファイルをプッシュすると Push protection によってデータ転送がブロックされました。

# config/keys/sample.key

DUMMY_GITHUB_TOKEN=ghp_dummy12341234nopqrstuvwxyz....
DUMMY_AWS_KEY=AKIAQA2IFJE3MPUDUMMY
DUMMY_AWS_SECRET=gsDummyyGtzLXigRMVcDHeuS7Neg6vd....
# 主要な部分のみ抜粋
git push origin main

Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: 
remote: - GITHUB PUSH PROTECTION
remote:   —————————————————————————————————————————
remote:     Resolve the following violations before pushing again
remote:     - Push cannot contain secrets
remote:     
remote:       —— Amazon AWS Access Key ID ——————————————————————————
remote:        locations:
remote:          - commit: 0a000111222...
remote:            path: config/keys/sample.key:4
remote:     
remote:       —— Amazon AWS Secret Access Key ——————————————————————
remote:        locations:
remote:          - commit: 0a000111222...
remote:            path: config/keys/sample.key:5
remote: 
 ! [remote rejected] main -> main (push declined due to repository rule violations)

# ブロックしてくれた ☺️

ダミーの RSA 秘密鍵も試したところ、こちらは Push protection が反応せずプッシュが完了しました。

# config/keys/sample.key(実在しないクレデンシャル)

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArandomrandomrandomrandomrandomrandomrandomrandom
....
-----END RSA PRIVATE KEY-----
git push origin main
> Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
> + 1234c7c...4567ccc main -> main

# プッシュ完了...🙄

GitHub ドキュメントの Supported secrets を読むと Push protection は単純なパターンマッチングのようなチェックではないことが分かります。

  • トークン提供元のプロバイダ(AWS / Google など)によるチェックと、非プロバイダ(Generic)に分かれる
  • プロバイダにクレデンシャルを送信してトークンの有効性をチェックすることがある
  • トークンやプロバイダによってチェック内容の網羅度合いが異なる
  • 個人アカウントのパブリックリポジトリに適用されるチェックと GitHub Team / Enterprise の GitHub Secret Protection で適用されるチェックに違いがある

偽物のクレデンシャルとほんの数回のプッシュ確認では、ブロック可能なパターンを帰納的に洗い出すことはできませんでした。

しかしながら、あらゆるクレデンシャルを対象に GitHub がスキャンしてくれる機能はとても強力です。ブロックが成功した AWS アクセスキーについてはファイルから削除するだけではダメで、コミットログも完全に消し去らないとプッシュができませんでした。

GitHub Push protection は .env.local外に漏れ出てしまった情報の監視役 として有効に機能してくれそうです。

まとめ

Symfony や GitHub など、それぞれがうっかりプッシュの防止策を提供してくれています。どれもすべてのクレデンシャルやユースケースを網羅できる仕組みではありませんが、組み合わせれば高い効果が期待できそうです。

これさえやっておけば大丈夫!と慢心せず、ローカルでもクラウドでも電子的に保存したデータは常に漏洩リスクを孕んでいることを念頭に開発していきたいと思っています。