環境変数とシークレット管理 — APIキーをGitに入れない実務

セキュリティ中級
セキュリティGit環境変数シークレット管理CI/CDSOPS

GitHubにAPIキーをコミットしてから数分以内にAWSの請求が跳ね上がる——そういった事例は実際に繰り返されている。git push の直後に自動スキャンが動き、秘密情報を見つけた攻撃者が即座に使い始めるためだ。

このドキュメントでは、秘密情報を「そもそもGitに入れない」ための基礎と、チームで安全に共有するための手段を整理する。

関連: GitHubで情報漏洩が起きる5つの経路


TL;DR

やること最低限チーム・本番
.env を Git に入れない.gitignore に追加必須
シェルで渡すexport VAR=val必須
誤コミットしたらキーを即ローテーション必須
チーム共有SOPS + ageVault / 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_fileenvironment を使う。

# 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アクセスキーの接頭辞を検索

漏洩が発覚したときの対処順序

  1. まず該当APIキーや認証情報を無効化・ローテーションする(これが最優先)
  2. 被害の範囲を確認する(不正アクセスログ、課金状況)
  3. その後、履歴からの削除を検討する

履歴の削除には git filter-repogit 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.yamlsecrets.env.enc はGitにコミットする。復号には ~/.age/key.txt(秘密鍵)が手元に必要だ。

HashiCorp Vault

本番環境でシークレットを動的に管理するためのツールだ。アプリがVaultに認証してその場でシークレットを取得するため、.env ファイル自体が不要になる。

小規模な開発チームにはオーバースペックになる場合が多い。AWSやGCPを使っているなら、クラウド側のSecret Managerを先に検討するほうが運用コストを抑えやすい。

クラウドのSecret Manager

サービスSecret Manager
AWSAWS Secrets Manager / SSM Parameter Store
GCPSecret Manager
AzureKey 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 で自動デプロイ

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つだ。

  1. 過去に漏洩した可能性があるシークレットを無効化する
  2. 万が一漏洩しても被害期間を限定する

ローテーションのサイクル:

シークレットの種類推奨サイクル
APIキー(外部サービス)90日以内
DB接続パスワード90日以内
長期的なアクセストークン30〜90日
CI/CDシークレット90日以内

AWS Secrets Manager や GCP Secret Manager は自動ローテーションの仕組みを持っている。手動管理の場合は、ローテーション期限をカレンダーに入れておくか、ドキュメントに最終更新日を記録しておく。

ローテーション手順のポイント:

  1. 新しいキーを発行する
  2. アプリの設定を新しいキーに切り替える(デプロイして動作確認)
  3. 古いキーを無効化する

古いキーを先に削除するとダウンタイムが発生するため、必ず「発行→切り替え→削除」の順で行う。


まとめ

フェーズ実施事項
開発開始時.gitignore.env を追加・.env.example を用意
コーディング中秘密情報は環境変数経由で参照・コードへの直書き禁止
CI/CD設定時リポジトリの Secrets 機能を使う
チーム展開時SOPS + age または クラウドSecret Manager
漏洩発覚時即座にローテーション(削除より先に無効化)
運用中90日以内のローテーションを定期実施

秘密情報の管理は「入れない」「共有する」「更新する」の3段階で考える。.gitignore の設定は開発開始時の1分の作業で、後から取り返しのつかない事態を防ぐ。

関連記事

セキュリティセキュリティ、どこまでやれば十分? 会社の規模で決める「合格ライン」の引き方セキュリティGitHubに秘密情報を誤プッシュした:3時間以内の対処フローチャートセキュリティSSH鍵管理の崩壊パターンと修復手順:散乱した鍵を棚卸しして統制する
記事一覧に戻る