0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MD5で保存したパスワードは一瞬で平文に戻る ── 暗号・シークレット管理の急所

0
Posted at

はじめに ── その"隠したつもり"が、命取りになる

数年前の私は、パスワードを自作の「MD5 + 固定ソルト」で保存して、「平文じゃないから安全」と本気で思っていました。しかも API キーや DB パスワードは .env にベタ書きで、その .env をうっかり Git にコミットまでしていました。

当時の私にとって、md5($salt . $password) は立派な「暗号化」でした。.env は「設定ファイルだから隠れている」つもりでした。どちらも「隠したつもり」だっただけで、実際には何も守れていなかったのです。

この記事は、そんなちょっと前の私に向けて書きます。

想定読者:

  • パスワードを自作のハッシュ(MD5 / SHA-1 など)で保存している(していた)Web アプリ開発者
  • 「ハッシュ」と「暗号」の違いを、なんとなくでしか説明できない人
  • API キーや DB パスワードを .env やコードに平文で置いている人
  • AWS のシークレット管理サービス(Secrets Manager / Parameter Store)を使ったことがない人

この記事は、社内セキュリティ勉強会(全6回)の第4回の内容をまとめたものです。シリーズのロードマップは次のとおりで、本記事は暗号・シークレット管理を扱う第4回にあたります。

  • 第1回 = 認証・認可
  • 第2回 = インジェクション・SSRF
  • 第3回 = XSS・CSRF・セッション管理
  • 第4回 = 暗号・データ保護・シークレット管理(本記事 = 現在地)
  • 第5回 = ネットワーク・WAF
  • 第6回 = ログ・インシデント追跡

第1回では「パスワードの安全な保存は第4回で」、第3回では「Cookie・セッションの暗号化と APP_KEY の保護は第4回で」と送りました。その伏線を、この記事で回収します。当日の発表スライドも公開しているので、あわせてご覧ください。

先に結論(この記事で伝えたい3点)

細かい説明に入る前に、いちばん伝えたいことを3つ先に出します。

  1. パスワードは Hash::make に委ね、MD5 で保存しない。自作ハッシュではなく、フレームワークや言語標準の専用アルゴリズムに任せます。
  2. 可逆データは Crypt で暗号化し、APP_KEY を守る。戻す必要があるデータは自前 AES ではなくフレームワークの暗号化に任せ、その鍵を最重要の秘密として扱います。
  3. 秘密はマネージドに置き、最小権限 + ローテーションで守る。鍵・API キー・DB パスワードは .env やコードに平文で置かず、マネージドサービスへ追い出します。

この記事全体を貫く核心は1つです。秘密は「守り方」も「置き場所」も、"自前実装"と"平文"をやめて、検証済みのフレームワーク/マネージドに委ねる。以降の本文は、この一言をほどいていくだけです。

この記事で扱うこと・扱わないこと

対象環境とバージョンは次のとおりです。

  • Laravel 13.x(2026年時点の現行安定版)
  • FuelPHP 1.x(レガシー)
  • PHP 標準: password_hash / password_verify / password_needs_rehash
  • AWS: Secrets Manager / Systems Manager Parameter Store / IAM / AWS SDK for PHP
  • OWASP / CWE: A02:2021 Cryptographic Failures、CWE-327 / CWE-328 / CWE-259 / CWE-331 / CWE-798 / CWE-312
  • 言語は PHP

範囲を絞るため、次の項目は本記事では扱いません。いずれも重要ですが、1記事に詰め込むと焦点がぼやけます。

  • 暗号アルゴリズムそのものの理論(AES / RSA、利用モード、ハッシュの危殆化の詳細)は扱いません(暗号学の入門は別記事にゆずります)。
  • ネットワーク境界・WAF での緩和は扱いません(第5回で扱う予定です)。
  • 鍵アクセスの検知や、異常なシークレットアクセスのログ追跡は扱いません(第6回で扱う予定です)。

ここでは「秘密を、守り方も置き場所も委ねる」の一点に絞ります。

秘密には"守り方"と"置き場所"の両方が要る

まず土台を1つ立てます。秘密を守るには、性質の違う2つの軸があります。

  • 守り方: どう保管するか。パスワードならハッシュ、戻したいデータなら暗号です。
  • 置き場所: どこに置くか。鍵・API キー・DB パスワードの保管先です。

大事なのは、この2つは片方だけでは守れないことです。どれだけ丁寧にパスワードをハッシュ化しても、その鍵や DB パスワードが平文で Git に転がっていれば意味がありません。逆に置き場所を固めても、パスワードを MD5 で保存していれば漏れます。両方に共通する正解が、次の図の帰着点です。

この「守り方」と「置き場所」の2枚看板は、記事の最後の「脅威 → 対策マッピング」で、対策を載せてもう一度戻ってきます。

ハッシュ(一方向)と暗号(可逆)は別物

守り方の話に入る前に、言葉を1つ揃えます。ハッシュと暗号は別物です。ここを取り違えると、対策そのものを間違えます。

  • ハッシュは一方向の変換で、元に戻せません。平文を等価なハッシュ値へ変換します(出典: CCT eBook 書籍1764 / PDF1775 Module 14)。戻せないからこそ、パスワードの照合に向きます。
  • 暗号は可逆の変換で、鍵で元に戻せます。読み取り可能な平文を読み取り不可能な暗号文に変換し、鍵があれば復号できます(出典: CCT eBook 書籍1729 / PDF1740 Module 14)。戻せるからこそ、Cookie や機密データを守るのに使います。
ハッシュ(一方向) 暗号(可逆)
変換の向き 戻せない 鍵で戻せる
向く用途 パスワードの照合 Cookie / 機密データ
代表例 bcrypt / Argon2id AES など

だから「パスワードを暗号化する」という言い回しは危険ですし、「データをハッシュする」も用途違いです。パスワードはハッシュ、戻す必要のあるデータは暗号。これが出発点です。

共通の敗因は「自前実装」と「平文」

守り方と置き場所、この両方に共通する敗因も2つです。

  • 自前実装: MD5・固定ソルト・独自方式など、検証されていない自作の仕組みで守ろうとする。
  • 平文: .env・コード・ログに、秘密をそのまま(平文で)置いてしまう。

この2つが事故のもとです。裏返した正解が、先ほどの図の「検証済みのフレームワーク/マネージドに委ねる」です。ここから先の各論は、すべてこの一つの思想をほどいているだけだと思って読んでください。

【実演①】MD5 は"隠せていない" ── 一瞬で平文に戻る

理屈の前に、まず何が起きるかを見ます。ここからは説明用のフィクスチャ(架空の値)を使います。MD5 は決定的な関数なので、以下のハッシュ値は実際に計算した値ですが、あくまで説明目的の例です。実在する逆引きサービスの画面は捏造しません。

見てほしいのは1点だけです。「MD5 + 固定ソルト」で保存した想定のハッシュ値を逆引きに入れると、元のパスワードに戻ってしまうか

# 「MD5+固定ソルト」で保存された想定のハッシュ値を、オンライン逆引き/レインボーテーブルに入れる
md5("password")  = 5f4dcc3b5aa765d61d8327deb882cf99
  → 逆引き結果: password          ← 一瞬で平文に戻る(誰もが知る有名な MD5)

# 「複雑そう」なパスワードでも同じ
md5("P@ssw0rd") = 161ebd7d45089b3446ee4e0d86dbcf92
  → 逆引き結果: P@ssw0rd
# MD5/SHA1 は「暗号化」ではなく一方向ハッシュ。しかも高速+衝突既知で、保存には不適

一瞬で password に戻りました。「複雑そう」に見える P@ssw0rd も同じで、あっさり戻ります。MD5 は暗号化ではなく一方向ハッシュですが、保存用途では、レインボーテーブルや総当たりの前にまったく無力です。値はダミーですが、起きている構図は本物です。

なぜこうも簡単に戻るのか

理由は2つあります。

  • 速すぎる。MD5 や SHA のような汎用ハッシュは、1秒間に大量に計算できます。これは裏を返せば、総当たり(ブルートフォース)に向いているということです。
  • ソルトが無い、または固定。同じパスワードが常に同じハッシュ値になるため、あらかじめ計算した対応表(レインボーテーブル)で一括照合できます。固定ソルトをコードに埋め込んでも、全ユーザーで同じソルトなら効果は限定的です。

さらに、MD5 も SHA-1 も、すでに衝突(コリジョン)が見つかっています(出典: CCT eBook 書籍1760 / PDF1771 Module 14 表14.2)。SHA-1 については、2017年に CWI Amsterdam と Google が世界初の実際の衝突を示しました(SHAttered。出典: https://shattered.io/)。OWASP Top 10 の A02:2021 Cryptographic Failures も、MD5・SHA1 のような非推奨の暗号関数を避けるよう明記しています(出典: https://owasp.org/Top10/2021/A02_2021-Cryptographic_Failures/)。

なお、GPU による総当たりの「毎秒何回」といった定量値は、信頼できる一次情報がそろわないため、ここでは断定しません。「高速なので短時間に大量に試行できる」という定性的な事実だけを押さえておけば十分です。

パスワードの正しい守り方 ── 言語標準/フレームワークに委ねる

ここで発想を転換します。汎用ハッシュ(MD5 / SHA)は「速さが正義」ですが、パスワード保存は逆で、遅いほど安全です。総当たりのコストをわざと上げたいからです。OWASP も「SHA-256 のような高速ハッシュはパスワード保存に不適」と明記しています(出典: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)。A02:2021 でも、高速ハッシュはソルトを付けても GPU で解読されうる、とされています(出典: 前掲 A02:2021)。だから、保存には専用のアルゴリズムが要ります。

正しく守る3つの武器

パスワードを正しく守る道具は3つです。それぞれ、効く脅威が違います(出典: 前掲 Password Storage Cheat Sheet)。

武器 効果 効く脅威
ソルト パスワードごとに一意のランダム文字列を足す 使い回し / レインボーテーブル
ストレッチング ハッシュを繰り返して遅くする(work factor) 総当たり
専用アルゴリズム Argon2id / bcrypt GPU 総当たり
  • ソルトは、パスワードごとに一意のランダム文字列を付与します。同じパスワードでもハッシュ値が変わるので、使い回しやレインボーテーブルが効かなくなります。
  • ストレッチングは、ハッシュを何度も繰り返して意図的に遅くします。この反復の強度を work factor と呼びます。
  • 専用アルゴリズム(Argon2id / bcrypt)は、これらを前提に設計されています。

bcrypt / Argon2id が3つの武器を全部内蔵する

うれしいことに、bcrypt と Argon2id は、この3つの武器を全部内蔵しています。

  • ソルトを自動生成して、ハッシュ値に同梱します。
  • コストを work factor で調整でき、ハードウェアの進歩に合わせて遅さを引き上げられます。
  • Argon2id はメモリを大量に消費するため、GPU による総当たりに強くなります。

OWASP の第一推奨は Argon2id で、bcrypt はレガシー向けの位置づけです(出典: 前掲 Password Storage Cheat Sheet)。パラメータの推奨値と、PHP / Laravel の既定値は次のとおりです。

対象 推奨/既定値
OWASP 推奨順 Argon2id > scrypt > bcrypt > PBKDF2
Argon2id 最小 m=19456, t=2, p=1
bcrypt work factor 最小10
PHP PASSWORD_DEFAULT bcrypt(既定 cost は PHP 8.4 以降 12 / 8.3 以前 10)
Laravel bcrypt / argon ['rounds'=>12] / ['memory'=>1024,'time'=>2,'threads'=>2]
ペッパー ハッシュとは別に保管する共有秘密

(出典: Argon2id / bcrypt の推奨値は前掲 Password Storage Cheat Sheet、PHP の既定 cost は https://www.php.net/manual/en/function.password-hash.php、Laravel のパラメータは https://laravel.com/docs/13.x/hashing。)

Laravel: Hash::make が正しく全部やってくれる

Laravel を使っているなら、答えは Hash::make の1つです。Hash ファサードは既定ドライバに bcrypt を使い、ソルトも自動で生成します(argon / argon2id も選べます)(出典: https://laravel.com/docs/13.x/hashing)。

// app/Http/Controllers/Auth/RegisterController.php (Laravel 13.x)
use Illuminate\Support\Facades\Hash;

// 登録: 既定ドライバ bcrypt でハッシュ化。ソルトもコストも Hash に任せる
$user->password = Hash::make($request->password);      // ← 自分でソルト/アルゴリズムを書かない

// ログイン照合
if (Hash::check($request->password, $user->password)) {
    // パスワード一致
}

// work factor(コスト)を上げたときは、ログイン時に再ハッシュ
if (Hash::needsRehash($user->password)) {
    $user->password = Hash::make($request->password);
}

Hash::make でハッシュ化、Hash::check で照合、そして work factor を上げたら Hash::needsRehash が変更を検知して再ハッシュのタイミングを教えてくれます。自分でソルトもアルゴリズムも書きません。さきほど一瞬で平文に戻ったあの MD5 は、この1行で解決します。slow is good(遅いほど安全)、これがパスワード保存の合言葉です。

before → after ── この自前実装が、デモの正体だった

さきほどのデモの正体は、次のような自前実装のコードです。これがそのまま「MD5 + 固定ソルト」のアンチパターンになっています。

// レガシーの自社コード(自前実装。password_hash 普及以前によく書かれた)
// ※FuelPHP の SimpleAuth 自体は PBKDF2 を使う(F16)。これは Auth を使わず自前で書いた例
class LegacyPasswordHasher
{
    // 登録時: 自作の "MD5 + 固定ソルト" でハッシュ化
    public static function hash($password)
    {
        $salt = 'my_app_secret_salt';       // ← 固定ソルトをコードに埋め込み(全ユーザー同じ)
        return md5($salt . $password);       // ← MD5。高速=総当たり向き・衝突既知 → 逆引き可能
    }
}

正しくは、フレームワークが無くても言語標準に委ねられます。PHP の password_hashPASSWORD_DEFAULT を渡すだけです。既定は bcrypt で、ソルトは自動生成されハッシュ値に同梱されます(出典: https://www.php.net/manual/en/function.password-hash.php)。

// 正しくは言語標準に委ねる(FuelPHP/レガシーでも書ける)
class PasswordHasher
{
    // 登録時: ソルトもアルゴリズムも password_hash に任せる(既定 bcrypt。argon2id も可)
    public static function hash($password)
    {
        return password_hash($password, PASSWORD_DEFAULT);  // ← ソルト自動生成・ハッシュに同梱
    }
    // ログイン時
    public static function verify($password, $hash)
    {
        return password_verify($password, $hash);           // ← 照合
    }
    // 運用中に work factor を上げたくなったら
    public static function needsRehash($hash)
    {
        return password_needs_rehash($hash, PASSWORD_DEFAULT);
    }
}

password_hash は毎回ランダムなソルトを自動生成してハッシュに同梱するため、同じパスワードでも毎回異なるハッシュになり、逆引きやレインボーテーブルが効きません。危険な md5($salt . $password) と、この password_hash を見比べてみてください。書く量はほとんど変わらないのに、安全性はまったく違います。

補足: FuelPHP は MD5 ではなく PBKDF2

ひとつ誤解されやすい点を正確に断っておきます。上の「MD5 + 固定ソルト」は、FuelPHP の認証機構が使っている方式ではありません。FuelPHP 1.x の SimpleAuth は、パスワードのハッシュに PBKDF2 という安全な仕組みを使います(出典: https://fuelphp.com/docs/packages/auth/simpleauth/intro.html)。つまり上の自前実装は、Auth を使わずに開発者が独自に書いた(または password_hash が普及する前に書かれた)アンチパターンだと考えてください。「FuelPHP だから MD5 になる」わけではありません。

可逆データの守り方 ── 暗号と APP_KEY

パスワードは「戻せなくてよい」のでハッシュでした。一方で、あとで戻す必要があるデータ(Cookie の中身や、外部 API のトークンなど)は暗号で守ります。これは第3回で「Cookie・セッションの暗号化と APP_KEY の保護は第4回で」と送った続きです。

Laravel は APP_KEY を使った暗号化を最初から持っています。自前で AES を書く必要はありません(出典: https://laravel.com/docs/13.x/encryption)。

// .env: php artisan key:generate で生成(config/app.php の 'key' が参照する)
// APP_KEY="base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY="

use Illuminate\Support\Facades\Crypt;

// 可逆で守りたい機密データは Crypt で暗号化(OpenSSL / AES-256-CBC + MAC 署名)
$user->api_token = Crypt::encryptString($request->token);   // 保存時に暗号化
$plain           = Crypt::decryptString($user->api_token);  // 取り出し時に復号
// ※改ざんされた値は DecryptException(MAC で検知)
// ※Cookie・セッションも APP_KEY で自動的に暗号化・署名される(=APP_KEY が漏れると全て復号される)

Crypt::encryptString は OpenSSL と AES-256-CBC を使い、すべての暗号値に MAC(メッセージ認証コード)署名を付けます。だから暗号化後に改ざんされた値は、復号時に DecryptException として検知できます(出典: 同)。自前 AES より安全側です。そして重要なのは、Cookie とセッションも、この APP_KEY で自動的に暗号化・署名されていることです。ここが第3回の続きになります。

APP_KEY が漏れれば、暗号は全部無効になる

ただし、この暗号には急所があります。APP_KEY です。APP_KEY で暗号化しているということは、APP_KEY を持てば復号できるということです。だから APP_KEY が平文の .env や Git に漏れると、署名の偽造も、暗号化した全データの復号もされてしまいます(出典: 同)。

なお、鍵を変えると全認証セッションがログアウトされ、旧鍵で暗号化したデータは復号できなくなりますが、APP_PREVIOUS_KEYS(カンマ区切りで旧鍵を指定)を使えば、ローテーション時も旧鍵での復号を試みて緩和できます(出典: 同)。

整理すると、Crypt による暗号化は MAC 付きで改ざんも検知でき、守り方としては解けました。しかし、その鍵である APP_KEY、それに DB パスワードや API キーをどこに置くのか。守り方の要である鍵そのものが、次の「置き場所」問題になります。

【実演②】.env を Git に上げると、履歴に鍵が残る

置き場所の失敗を、もう1つのデモで見ます。ここでも説明用のフィクスチャを使います。鍵は AWS 公式の EXAMPLE 値(明らかに偽)と、Laravel ドキュメントに載っている APP_KEY の例値です。リポジトリやホストも説明用(example.com など)です。

見てほしいのは1点です。.env を一度コミットしてしまい、あとで削除しても、その秘密は履歴に残るのか

# .env を平文のままうっかりコミット
$ cat .env
APP_KEY=base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY=
DB_PASSWORD=super-secret-db-pass
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
$ git add .
$ git commit -m "add config"

# あとで気づいて .gitignore に追加し、追跡から外す
$ echo ".env" >> .gitignore
$ git rm --cached .env
$ git commit -m "remove .env from tracking"

# しかし過去コミットには平文が残っている(作業ツリーから消えても履歴は不変)
$ git log --oneline
9f2a1c3 remove .env from tracking
3b7d0e1 add config            ← ここに平文の鍵が残る
$ git show 3b7d0e1:.env
APP_KEY=base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY=
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY   ← 丸見え(GitHub ならコード検索でも即発見)

git rm --cached は作業ツリーとインデックスから .env を外すだけで、過去のコミットには平文がそのまま残りますgit show で過去コミットを開けば、APP_KEY も AWS の鍵も丸見えです。GitHub 上なら、コード検索や secret scanning で即座に見つかります。

第2回では、SSRF から IAM の認証情報を抜き出す攻撃を見ました。実は、あのとき抜いたのと同じ IAM 鍵が、この .env にも平文で漏れていた、という構図もよくあります。攻撃の入口は違っても、行き着く先は同じ「平文の鍵」です。

削除だけでは無意味 ── ローテーションが要る

いま起きたことを流れで押さえます。

.env に平文で鍵を置き、Git にコミットすると履歴に固定されます。削除しても、クローンやフォーク、キャッシュされたビュー、その commit を参照する PR に残りえます(出典: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository)。だから対策は、削除だけでは無意味で、鍵のローテーション(失効・再発行)が必須です。GitHub の公式ドキュメントも、削除対象が秘密(パスワード / トークン / 資格情報)なら、履歴を書き換える前に、まずその秘密を失効・ローテーションする必要があると明記しています(出典: 同)。

なお、履歴そのものを書き換える手段としては git-filter-repo が GitHub 公式に推奨されています(出典: 同)。ただし、繰り返しますが、履歴を書き換える前にまず鍵をローテーションするのが先です。漏れた鍵は、削除しただけでは「無かったこと」にはできません。

秘密の正しい置き場所 ── マネージドに委ねる

では、正しい置き場所はどこか。ここが今回いちばんの山場です。第2回で「SSRF で抜いた IAM 認証情報の悪用、最小権限、ローテーション、シークレット管理は第4回で」と送ったのが、ここです。

まずローカル ── .env はコミットしない

ローカル開発の正解はシンプルです。.gitignore.env を必ず入れてコミットせず、チームで共有するのは変数名だけで値は空の .env.example にします。

# .gitignore に .env を必ず入れる(コミットしない)
echo ".env" >> .gitignore

# 追跡してしまっていたら外す(※過去コミットには残るので鍵はローテーションする)
git rm --cached .env

# 共有するのは「変数名だけ」の .env.example(値は空)
#   .env.example:
#     APP_KEY=
#     DB_PASSWORD=
#     AWS_SECRET_ACCESS_KEY=

もし一度でも .env を上げてしまったら、削除では戻らないので即ローテーションします(出典: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)。

本番の秘密は、平文管理では破綻する

では本番の秘密はどこに置くか。.env や環境変数も、結局サーバー上は平文です。サーバーが増えれば、配布も更新も失効も破綻します。本番の秘密には、アクセス制御・監査・ローテーションが要ります。OWASP も、秘密をソースコードや設定ファイルに平文でハードコードしないよう明記しています(出典: 同 Secrets Management Cheat Sheet)。だから、専用のマネージドサービスへ移します。

AWS の正解 ── Secrets Manager / Parameter Store に委ねる

AWS の正解は、Secrets Manager か Systems Manager Parameter Store に秘密を置いて委ねることです(出典: 同 Secrets Management Cheat Sheet)。アプリは IAM ロール経由で、起動時に SDK で取得します。鍵をコードや .env に書きません。

// AWS SDK for PHP: 起動時に Secrets Manager から取得(.env に平文で置かない)
use Aws\SecretsManager\SecretsManagerClient;

$client = new SecretsManagerClient([
    'region'  => 'ap-northeast-1',
    'version' => 'latest',
    // 認証情報は EC2/ECS の IAM ロールから自動取得(鍵をコードに書かない)
]);
$result = $client->getSecretValue(['SecretId' => 'prod/app/db']);
$secret = json_decode($result['SecretString'], true);
$dbPassword = $secret['password'];
// ※ Parameter Store(SecureString)なら: $ssm->getParameter(['Name'=>'/prod/app/db', 'WithDecryption'=>true]);

こうすると、KMS による暗号化、アクセス制御、監査も付いてきます。これが置き場所版の「委ねる」です。

使い分け ── Secrets Manager vs Parameter Store

2つのサービスは、料金やローテーションの機構が違います。ざっくり、自動ローテーションが要る DB 資格情報などは Secrets Manager、静的な config 値は Parameter Store が目安です。

観点 Secrets Manager Parameter Store
料金 $0.40/月 + $0.05/1万API 標準 = 無料 / 拡張 = 有料
ローテーション 新バージョン作成(追加課金なし) 組み込みなし
暗号 KMS SecureString = KMS
最大サイズ 標準4KB / 拡張8KB
個数上限 標準1万 / 拡張10万
向く用途 回す DB 資格情報 静的 config

(出典: 料金・ローテーションは https://aws.amazon.com/secrets-manager/pricing/、標準/拡張パラメータの上限は https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-advanced-parameters.html。)

Secrets Manager は1シークレット月 $0.40 と1万 API あたり $0.05 の有料ですが、自動ローテーションがあり RDS などと統合できます。バージョン作成そのものには課金されません。Parameter Store は標準なら無料で、SecureString は KMS で暗号化されますが、ローテーションは組み込みではありません。

IAM 最小権限 ── アプリのロールは「必要な秘密だけ」読める

置き場所を委ねても、権限が広すぎたら台無しです。IAM は最小権限にします。GetSecretValue を特定の secret ARN だけに限定し、全シークレット読み取り("Resource": "*")にはしません(出典: 同 Secrets Management Cheat Sheet)。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:prod/app/db-*"
    }
  ]
}

ここが第2回とつながります。仮に SSRF で鍵を抜かれても、最小権限なら、読めるのはその1つだけです。被害を限定できます。

ローテーション ── 「漏れる前提」で短命化する

最後の武器がローテーションです。「漏れる前提」で、秘密は定期的に、あるいはイベント駆動で回します。漏れたら即失効させます。削除だけでは残るからです(出典: 同 Secrets Management Cheat Sheet)。Secrets Manager なら自動ローテーションがあり、バージョン作成に追加課金はありません(出典: 前掲 Secrets Manager pricing)。第2回で見た「一時的な鍵でも悪用できる」という指摘も、短命化で受け切れます。

レガシー / FuelPHP でも、今日から移せる

「うちは FuelPHP だから無理」で終わらせないでほしいのです。順序は次のとおりです。

  1. まず .env.gitignore に入れ、履歴からも除去し、必ずローテーションする。
  2. 取得は AWS SDK for PHP で、Secrets Manager や Parameter Store から起動時に行う。

フレームワークが古くても、段階的に必ず移せます。パスワードの password_hash と同じで、「委ねる先」はレガシーにもちゃんと用意されています。

脅威 → 対策マッピング

ここまでを1枚に整理します。冒頭で立てた「守り方」と「置き場所」の2枚看板に、今度は対策を載せて戻ってきます。

脅威 CWE(代表) 対策
守り方 弱いハッシュ / 平文保存 CWE-327 / CWE-328 Hash::make(委ねる)/ Crypt 暗号化 + APP_KEY 保護
置き場所 平文の .env / Git 漏洩 CWE-798 / CWE-312 マネージド + IAM 最小権限 + ローテーション

(CWE 名称: CWE-327 = Use of a Broken or Risky Cryptographic Algorithm、CWE-328 = Use of Weak Hash(2021-10-28 に旧称 Reversible One-Way Hash から改称)、CWE-798 = Use of Hard-coded Credentials、CWE-312 = Cleartext Storage of Sensitive Information。出典: https://cwe.mitre.org/data/definitions/327.html / https://cwe.mitre.org/data/definitions/328.html / https://cwe.mitre.org/data/definitions/798.html / https://cwe.mitre.org/data/definitions/312.html、および前掲 A02:2021。)

守り方の脅威にも、置き場所の脅威にも、共通の正解は1つです。自前と平文をやめて、フレームワーク/マネージドに委ねる。全部ここに収束します。

まとめ ── 秘密は、守り方も置き場所も委ねる

最後に、この記事の要点を振り返ります。先に出した結論と同じ3点です。

  1. パスワードは Hash::make に委ね、MD5 で保存しない。自作の「MD5 + 固定ソルト」は一瞬で平文に戻ります。bcrypt / Argon2id が「ソルト・ストレッチング・専用アルゴリズム」を全部内蔵しているので、それに任せます。
  2. 可逆データは Crypt で暗号化し、APP_KEY を守る。自前 AES は書かず、MAC 付きの Crypt に委ねます。その鍵 APP_KEY が漏れれば暗号は全部無効になるので、鍵こそ最重要の秘密です。
  3. 秘密はマネージドに置き、最小権限 + ローテーションで守る.env もコードも平文はダメで、git rm --cached しても履歴に残ります。漏れたら削除ではなくローテーションです。

ちょっと前の私が一番助かったであろう一言は、「"隠したつもり"はやめて、正解に委ねろ」でした。ここが腑に落ちると、md5() の1行も、平文の .env の1ファイルも、すべて「自前と平文」という同じ敗因の別の顔だと見えてきます。

Next action: 明日からできる3つ

理想論で終わらせないために、明日からできる具体を3つ挙げます。

  1. 棚卸し: md5 / sha1 / 自前ハッシュをコードから grep し、Hash::make または password_hash に置き換える。
  2. .env 確認: .env.gitignore に入っているか、過去にコミットしていないかを確認する。上げていたら即ローテーションする。
  3. マネージド: 本番の鍵を1つでいいので Secrets Manager か Parameter Store へ移し、最小権限ロールで取得する。

このうち1つでも赤信号があれば、そこが「隠したつもり」の穴です。まずは md5 の grep から始めてみてください。

あわせて読みたい

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?