環境変数とシークレット管理 — APIキーをGitに入れない実務
GitHubにAPIキーをコミットしてから数分以内にAWSの請求が跳ね上がる——そういった事例は実際に繰り返されている。git push の直後に自動スキャンが動き、秘密情報を見つけた攻撃者が即座に使い始めるためだ。
このドキュメントでは、秘密情報を「そもそもGitに入れない」ための基礎と、チームで安全に共有するための手段を整理する。
TL;DR
| やること | 最低限 | チーム・本番 |
|---|---|---|
| .env を Git に入れない | .gitignore に追加 | 必須 |
| シェルで渡す | export VAR=val | 必須 |
| 誤コミットしたら | キーを即ローテーション | 必須 |
| チーム共有 | SOPS + age | Vault / Secret Manager |
| CI/CD | リポジトリの Secrets 機能 | 必須 |
| 定期ローテーション | 90日以内 | 必須 |
なぜ秘密情報の管理が重要か
APIキーや接続文字列がGitに入った場合の被害は即時かつ具体的だ。
典型的な被害パターン:
- AWSアクセスキーが公開→攻撃者が数分で大量のEC2を起動→数万ドルの請求
- Stripe秘密鍵が漏洩→顧客への不正課金・返金費用
- DB接続文字列が流出→顧客データの全件取得
GitHubはコードをホストするサービスだが、パブリックリポジトリを常時スキャンしているボットが存在する。pushしてから悪用されるまでの時間は数十秒〜数分単位だ。
やってはいけないこと
コードに直接書く
# NG: ソースコードに直書き
client = openai.OpenAI(api_key="sk-proj-xxxxxxxxxxxxxxxx")
// NG: フロントエンドコードに直書き(ブラウザから丸見え)
const apiKey = "AIzaSyXXXXXXXXXXXXXX";
.env をコミットする
# .gitignore に入れないまま
git add .env
git commit -m "add config"
git push
# この瞬間、公開リポジトリならスキャナーが即座に収集する
ログに出力する
# NG: ログファイルがS3等に集約されると漏洩経路になる
print(f"Connecting with key: {api_key}")
logger.debug(f"AWS_SECRET_ACCESS_KEY={os.environ['AWS_SECRET_ACCESS_KEY']}")
.env と .gitignore の基礎
.env ファイルの書き方
.env はプロジェクトルートに置くテキストファイルで、環境変数をキー=値で記述する。
# .env
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxx
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
.env.example(値を空にしたテンプレート)はコミットしてよい。チームメンバーが必要な変数の一覧を把握できる。
# .env.example(コミット可)
DATABASE_URL=
OPENAI_API_KEY=
STRIPE_SECRET_KEY=
.gitignore の設定
プロジェクト作成時に .gitignore に秘密ファイルのパターンを追加する。後から追加することもできるが、一度でもコミットしたファイルは .gitignore を追加しても追跡が止まらない(後述)。
# .gitignore
.env
.env.local
.env.*.local
.env.production
*.pem
*.key
secrets/
config/secrets.yml
すでに追跡中のファイルを除外するには git rm --cached が必要だ。
# 追跡を止める(ファイル自体は残す)
git rm --cached .env
git commit -m "remove .env from tracking"
環境変数の渡し方
シェルから渡す
# 単一コマンドの前に渡す
DATABASE_URL="postgresql://..." python app.py
# シェルセッションにエクスポートする
export OPENAI_API_KEY="sk-proj-xxxx"
python app.py
export した変数はそのシェルセッション中は有効だ。ターミナルを閉じると消える。永続化したい場合は ~/.zshrc や ~/.bashrc に書く方法もあるが、シェルの設定ファイルをGitで管理していると漏洩リスクになる。
dotenv ライブラリで読み込む
各言語のライブラリが .env を自動で読み込む。
# Python: python-dotenv
from dotenv import load_dotenv
import os
load_dotenv() # .env を読み込む
api_key = os.environ["OPENAI_API_KEY"]
// Node.js: dotenv
require("dotenv").config();
const apiKey = process.env.OPENAI_API_KEY;
systemd サービスで渡す
デーモンとして動かすサービスには EnvironmentFile を使う。
# /etc/systemd/system/myapp.service
[Service]
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myapp
# /etc/myapp/env(rootのみ読める権限に設定する)
DATABASE_URL=postgresql://...
API_KEY=...
chmod 600 /etc/myapp/env
chown root:root /etc/myapp/env
コンテナ(Docker)で渡す
# 実行時に渡す
docker run -e "API_KEY=xxxx" myapp
# .env ファイルを指定する
docker run --env-file .env myapp
docker-compose.yml では env_file か environment を使う。
# docker-compose.yml
services:
app:
env_file:
- .env # .env をコンテナに渡す(ファイル自体はコミットしない)
誤コミットの履歴は残る
.env を一度コミットして git rm で削除しても、履歴にはコミット時点の内容が残り続ける。
# .env が過去のコミットに含まれていないか確認
git log --all --full-history -- ".env"
# 特定の文字列が履歴に含まれていないか確認
git log -S "AKIA" --all # AWSアクセスキーの接頭辞を検索
漏洩が発覚したときの対処順序
- まず該当APIキーや認証情報を無効化・ローテーションする(これが最優先)
- 被害の範囲を確認する(不正アクセスログ、課金状況)
- その後、履歴からの削除を検討する
履歴の削除には git filter-repo(git filter-branch の後継)を使う。
# git filter-repo のインストール
pip install git-filter-repo
# 特定ファイルを履歴から完全削除
git filter-repo --path .env --invert-paths
# 特定の文字列を別の文字列に置換
git filter-repo --replace-text <(echo "sk-proj-xxxx==>REMOVED")
履歴の書き換えは破壊的な操作で、共有リポジトリでは全員が git pull し直す必要がある。チームがいる場合は事前に周知する。また、GitHubはGit履歴書き換え後に git push --force が必要になる。
チーム共有の方法
個人開発なら .env をローカルに置くだけで済む。チームになると「どうやって .env を安全に共有するか」が問題になる。
SOPS + age(推奨)
SOPS(Secrets OPerationS)はシークレットファイルを暗号化してGitで管理できるツールだ。値だけを暗号化し、キー名は平文のまま残るため差分が見やすい。
age は暗号化アルゴリズムで、公開鍵/秘密鍵ペアで管理する。
# age のインストール(macOS)
brew install age sops
# 鍵ペアの生成
age-keygen -o ~/.age/key.txt
# 出力例:
# Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# .sops.yaml で暗号化設定を定義(コミットする)
cat .sops.yaml
# creation_rules:
# - path_regex: \.env$
# age: age1xxxx,age1yyyy # チームメンバーの公開鍵を並べる
# secrets.env を暗号化してコミット可能な状態にする
sops --encrypt secrets.env > secrets.env.enc
# 復号して使う
sops --decrypt secrets.env.enc > .env
.sops.yaml と secrets.env.enc はGitにコミットする。復号には ~/.age/key.txt(秘密鍵)が手元に必要だ。
HashiCorp Vault
本番環境でシークレットを動的に管理するためのツールだ。アプリがVaultに認証してその場でシークレットを取得するため、.env ファイル自体が不要になる。
小規模な開発チームにはオーバースペックになる場合が多い。AWSやGCPを使っているなら、クラウド側のSecret Managerを先に検討するほうが運用コストを抑えやすい。
クラウドのSecret Manager
| サービス | Secret Manager |
|---|---|
| AWS | AWS Secrets Manager / SSM Parameter Store |
| GCP | Secret Manager |
| Azure | Key Vault |
アプリのIAMロール(サービスアカウント)にシークレットへのアクセス権を付与し、コード内でAPIを呼び出してシークレットを取得する。
# AWS Secrets Manager から取得する例
import boto3
import json
client = boto3.client("secretsmanager", region_name="ap-northeast-1")
secret = client.get_secret_value(SecretId="myapp/production/api-key")
api_key = json.loads(secret["SecretString"])["api_key"]
CI/CD のシークレット
CI/CDパイプラインで .env ファイルをリポジトリに含めるのは危険だ。各サービスが提供するシークレット管理機能を使う。
GitHub Actions
リポジトリの Settings → Secrets and variables → Actions から登録する。
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh
登録したシークレットはログに出力されてもマスクされる(*** に置換される)。ただし、base64 エンコードなど変換をかけると検出を回避してしまう場合があるため、ログへの出力は避けるのが無難だ。
環境別シークレット
ステージング・本番で異なるシークレットを使う場合は、GitHub Environments を使って環境ごとにシークレットを分ける。
定期ローテーション
シークレットは定期的に更新(ローテーション)する必要がある。理由は2つだ。
- 過去に漏洩した可能性があるシークレットを無効化する
- 万が一漏洩しても被害期間を限定する
ローテーションのサイクル:
| シークレットの種類 | 推奨サイクル |
|---|---|
| APIキー(外部サービス) | 90日以内 |
| DB接続パスワード | 90日以内 |
| 長期的なアクセストークン | 30〜90日 |
| CI/CDシークレット | 90日以内 |
AWS Secrets Manager や GCP Secret Manager は自動ローテーションの仕組みを持っている。手動管理の場合は、ローテーション期限をカレンダーに入れておくか、ドキュメントに最終更新日を記録しておく。
ローテーション手順のポイント:
- 新しいキーを発行する
- アプリの設定を新しいキーに切り替える(デプロイして動作確認)
- 古いキーを無効化する
古いキーを先に削除するとダウンタイムが発生するため、必ず「発行→切り替え→削除」の順で行う。
まとめ
| フェーズ | 実施事項 |
|---|---|
| 開発開始時 | .gitignore に .env を追加・.env.example を用意 |
| コーディング中 | 秘密情報は環境変数経由で参照・コードへの直書き禁止 |
| CI/CD設定時 | リポジトリの Secrets 機能を使う |
| チーム展開時 | SOPS + age または クラウドSecret Manager |
| 漏洩発覚時 | 即座にローテーション(削除より先に無効化) |
| 運用中 | 90日以内のローテーションを定期実施 |
秘密情報の管理は「入れない」「共有する」「更新する」の3段階で考える。.gitignore の設定は開発開始時の1分の作業で、後から取り返しのつかない事態を防ぐ。