2026年4月19日にVercelが公式にセキュリティインシデントを発表した。Context.ai経由のサプライチェーン攻撃で、Sensitive指定されていない環境変数が平文で読み取られた可能性がある。VercelのダッシュボードでNeed To Rotateのオレンジ色バッジを見たら、数日ではなく数時間以内 にローテートすべきだ。この記事は発表から2日後に32blog.comで実際に走らせた手順をそのまま書く。途中で踏みかけた罠も2つ含めて。
僕もしばらくVercel Proで運用していて、環境変数のセキュリティ姿勢を真面目に考えたことはなかった。ログインしたらRESEND_API_KEYとUPSTASH_REDIS_REST_URL、UPSTASH_REDIS_REST_TOKENの3つにオレンジのバッジが付いていて、反射的に各行のローテートボタンを押したくなった。でも、それはVercelが実際に推奨している手順ではない し、勢いでやると余計な重複レコードが残ってしまう。
何が起きたのか
Vercelの公式告知によれば、Vercel社員のGoogle Workspaceアカウントが侵害され、内部システムへの不正アクセスが発生した。攻撃経路は以下:
- Context.ai OAuthアプリが侵害された(社員がWorkspaceに連携させていたサードパーティツールへのサプライチェーン攻撃)
- OAuthの権限経由で社員のセッショントークンが流出
- 攻撃者がVercelの内部システムにアクセス。期間は遡って2024年6月頃からとの報道
- 影響範囲: 「Vercel上に保存された非Sensitiveの環境変数(平文に復号可能なもの)が侵害された限定的な顧客」
ShinyHunters が犯行声明を出したとBleepingComputer が報じている。GitGuardian の分析 は、Upstash・Resend・Supabase 等のMarketplace連携がデフォルトで平文の環境変数を作る仕様のため、主な攻撃面になったと指摘している。
Vercel公式の対応指示は明快だ: 「Sensitive指定されていない環境変数をレビューしてローテートせよ」。
バッジが付いた変数・付かなかった変数
Vercelダッシュボードの Settings → Environment Variables を開くと、該当行にオレンジのNeed To Rotateバッジが表示される。32blog.comでは3つに付いていた:
RESEND_API_KEY— お問い合わせフォームがResend APIで使うUPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN
バッジが付いていなかったのは:
GA_SERVICE_KEY_BASE64— GA4用のGoogleサービスアカウント秘密鍵(バッジは無いが実質シークレット)GA_PROPERTY_ID— 数値のプロパティID、公開前提VERCEL_DEEP_CLONE— gitクローン深度を制御するboolフラグ
バッジが無い = 安全ではない。Vercel側の内部記録で今回のインシデントに紐付いていないという意味にすぎない。平文で保存されている秘密情報があるなら、メンテ窓でSensitiveへ移行すべきだ。
ローテート vs 削除の判断
何かを触る前に、まず 「これまだ使ってる?」 を確認する。
| 状況 | アクション |
|---|---|
| 現役で使っている | ローテート。元のサービスで新キー発行 → Vercelで値を差し替え → Redeploy |
| もう使ってない(コード参照削除済み、サービス解約済み) | 削除。Vercelから消す + Marketplace連携自体も解除 |
| Marketplace連携が自動生成した変数 | 連携ごと解除。Integrationを外すと紐付いていた環境変数が一括で消える |
使ってない変数をローテートすると時間の無駄だし、使っている変数を削除すると本番が壊れる。grepで実コードの参照を確認してから判断する:
grep -rn "UPSTASH_REDIS" --include="*.ts" --include="*.tsx" --include="*.mjs" .
32blog.comでgrepしたら、Upstashが参照されているのはチュートリアル記事のMDX2本だけ。実装としてのいいねボタンは数ヶ月前に削除済みだった。つまり 「3件ローテート」に見えて実は「2件ローテート + 1件撤収」 が正解だった。この判断を省略していたら、もう使っていないサービスに対して新しいトークンを発行してまた貼り直すという無駄な作業をしていた。
僕が実際に走らせた手順
ログインしてバッジを見てから2時間以内に、以下の順番で対応した。テスト含めて30分くらい。
ステップ1: まずResend(ドメインレピュテーションが一番脆い)
RESEND_API_KEYが漏れると、攻撃者が@32blog.com名義で正当なSPF/DKIM署名付き のメールを送れる。これが今回のインシデントで一番怖いシナリオだ。なぜなら ドメインレピュテーションは一度落ちると回復に数週間かかる。あなたのDKIM署名でフィッシングが送られたら、Gmail側のbulk-sender penaltyに乗って、今後の正規メールが迷惑フォルダ行きになる。
- Resendダッシュボード → API Keys → 旧キーをRevoke
- 新しいAPIキーを作成。Resendは発行時にしか値を表示しない ので、その場でコピーして一時的に安全な場所に控える
- Vercel → Settings → Environment Variables → 既存の
RESEND_API_KEYの行をDelete - Add New をクリック → 同じ名前
RESEND_API_KEYで値を貼り付け - Sensitiveのチェックボックスを必ずオン — ローテートする本当の目的はこれ
- Save → Deployments → 最新行の
⋯→ Redeploy
ステップ2: Upstashは連携ごと撤収した
Upstashは「使っていないサービス」のケースなので、ローテートではなく撤収にした:
- Upstashダッシュボード にログイン → Redis DBを削除 → アカウントごと削除(カード未登録なので請求の心配なし)
- Vercel → Integrations → Upstash → Remove Integration(連携作成時に自動登録された環境変数も一緒に消える想定)
- Environment Variables画面で
UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENが消えていることを確認
これは環境変数を手で2つ消すより綺麗。Integration自体が残っていると、将来また別の用途でUpstashを入れ直したとき同じ名前の変数が生える可能性があるからだ。
ステップ3: 「RESEND_API_KEY_」という踏みかけた罠
Resendを入れ直した後、環境変数リストを見たら2行あった:
RESEND_API_KEY(Sensitive, Production and Preview, "Updated just now")RESEND_API_KEY_(末尾にアンダースコア, All Environments, "Updated 50s ago")
末尾アンダースコア版は、旧キーを削除するときに「念のためリネームして残しておこう」と一瞬考えた残骸だった。これは本当に悪い癖だ:
- 末尾
_付きのレコードには、侵害された可能性のある旧キーがそのまま入っている - どこからも参照されていないから無害に見える
- でも漏洩情報を別の名前で保管し続けている 状態と同じ
気づいてすぐ削除した。
ステップ4: バッジが無いGA秘密鍵も先回りでSensitive化
GA_SERVICE_KEY_BASE64はバッジが付いていなかったが、中身はGoogleサービスアカウントの秘密鍵JSON。Resendと同じパターンで先回り対応:
- Google Cloud Console → IAM → サービスアカウント → 「鍵を追加」で新JSONキーを作成
base64 -w 0 new-key.jsonでbase64エンコード(macOSならbase64 new-key.json | tr -d '\n')- Vercel → 旧
GA_SERVICE_KEY_BASE64を削除 → Sensitive付きで 新しい値を貼って再登録 - Redeploy → 本番サイトの人気記事セクションが表示されることを確認
- Google Cloud Consoleで古いサービスアカウントキーを削除
GA_PROPERTY_IDは数値のプロパティIDなのでそのまま。クライアント側のGA4タグにも見えているので秘密ではない。
Sensitiveフラグで何が変わるのか
Vercelの告知によれば、Sensitive指定された変数は「読み取り不可能な方式で保存される」。運用上の挙動:
- 暗号化保存 され、ビルド時/実行時だけ復号される
- 保存後UIで値を閲覧できない(
●●●●●●だけ表示) - その場編集不可、丸ごと置き換えるしかない
- Development環境には作れない — Sensitiveにチェックを入れるとDevelopmentのチェックボックスがグレーアウトする
最後の制約は仕様として正しい。Development環境の変数はvercel devコマンドでローカルに降ろされる前提 = Sensitiveの意図と真逆。だから Production + Preview の組み合わせだけが Sensitive と共存できる。
トレードオフ
一度Sensitiveにすると、値を忘れた場合(パスワードマネージャに控えてなかった等)の救済策は 発行元で新キーを再発行するしかない。Vercelは本当に値を出してくれない。これはセキュリティの仕様として正しい挙動だし、APIキーの場合は再発行できるから致命的ではない。APIキーは失くしたら再発行すればいい 前提で設計されているので、Sensitiveを恐れる理由は実はほとんどない。
VERCEL_DEEP_CLONE の罠 — 「使ってない」と判定して削除するとビルドが壊れる
セキュリティとは無関係だが、今回の整理中に一度判定を誤ったので共有しておく。
VERCEL_DEEP_CLONE=true は Vercel公式ドキュメントに載っていない が、Vercelのビルドインフラが読み取る非公式の環境変数。これを設定するとgitクローンが shallow(デフォルト深度10)から full clone に切り替わる。
なぜ必要かというと、32blog.comのビルドスクリプト scripts/generate-updated-dates.mjs が、全MDX記事に対して git log -1 --format=%aI -- <file> を実行して最終更新日を取得しているからだ。shallow cloneだと大半の記事でgit logが空を返すので、updated-dates.jsonがほぼ空で上書きされる 事故が起きる(実際コメントにも shallow clone on Vercel と書いてある)。
Nextraの最終更新日機能、モノレポのNx affected-graph、changelog生成ツールなど、ビルド時にgit履歴を読むツールを使っている場合はVERCEL_DEEP_CLONE=trueが必須。GitHub Discussionのスレッド にコミュニティの参考情報がある。シークレットではないので平文のままでOK。
教訓: 「grepしてコード内に変数名が無い → 未使用 → 削除」という判定は危険。プラットフォーム側が読む変数はアプリのコードには出てこない。削除前にビルドログやビルドスクリプトを読んで、誰がその変数を消費するのかを確かめること。
Vercelから移行するべきか?
プラットフォームの侵害を見ると「セルフホストすべき」という反射が出る。冷静に比較する:
- 移行するメリット: Coolify + VPSでNext.jsをセルフホストすれば、このクラスのサプライチェーンリスクはほぼゼロになる。シークレットは自分のサーバーにしかない
- 移行するデメリット: OSパッチ、TLS更新、DDoS対策、Fluid Compute相当の運用を全部自分で背負う。攻撃面は消えるのではなく移動するだけだ
僕のスタンス: 32blog.comはVercelに残す。1人運営のブログにとって、セルフホストの限界セキュリティ利益は限界運用コストより小さい。代わりにやるのが、この記事で書いている「新しく作る環境変数は全部Sensitiveをデフォルトにする」ことと、定期的に環境変数リストを監査することだ。
チーム運営で本気の機密(決済情報、個人情報アクセスキー等)を扱うなら計算は変わる。月700円のVPSにCoolifyという選択肢は現実的だし、シークレット保存を自分の手に戻せる価値はある。痛みはあるが、耐えられないレベルではない。
コスト面でVercelの請求を気にしている人には、このインシデントは直接関係しない。ただしSpend Managementの設定 はセルフホスト検討の前に絶対やっておくべき最優先タスクだ。
FAQ
自分のVercel環境変数が侵害されたかどうかはどこで確認できる?
Settings → Environment Variables を見る。オレンジ色のNeed To Rotateバッジが付いている行が、今回のインシデントでVercel側が影響範囲として特定したもの。バッジが無い = その変数が今回の件に紐付いていないという意味だが、Sensitive指定されていない秘密情報があるなら、次のメンテ窓でSensitiveに移行しておいたほうが安全。
目に見える悪用が無ければ旧キーのまま放置してもいい?
技術的には可能だが危険。このクラスの漏洩によるキー悪用は 数週間〜数ヶ月後に突然表面化する ことが多い。Resendのレピュテーション急落、ドメイン宛のスパム苦情、原因不明の従量課金スパイクといった形で。今ローテートする固定コストは小さいが、放置した場合の変動コストは大きくて読めない。
Sensitiveがデフォルトじゃないの?
デフォルトじゃない。僕がVercel UI経由で入れた変数は全部「非Sensitive」がデフォルトだったし、Marketplace連携(Upstash、Resend、Supabase等)もSensitive非対応のまま平文で変数を作る仕様だった。Vercelはインシデント告知 でデフォルトの見直しに言及しているが、それが変わるまではオプトインなので、チェックボックスを自分で付けるしかない。
Vercelでローテートしたら発行元の旧キーも無効化される?
されない。Vercelは値を保存しているだけで、実際のキーは発行元(Resend、Upstash、Google Cloud)が持っている。発行元のダッシュボードで旧キーを明示的にrevokeする必要がある。これを忘れるのが一番よくあるローテートミスだ。Vercel側は新しい値になったけど、発行元ではまだ漏洩した旧キーが有効なまま、という状態になる。
ローテートしたのに新キーが動かない。何を忘れてる?
ほぼ確実に Redeployのし忘れ。Vercelの環境変数の変更は既存のデプロイには反映されない。新しいビルドからしか適用されない仕様だ。Deployments → 最新行 → ⋯ → Redeploy。Use existing Build Cache にチェックを入れてOK(ビルド内容は変わらないので)。
VERCEL_DEEP_CLONE は今回のインシデントの対象?
対象外。bool値(true/false)で秘密情報ではない、ビルド時のgitクローン深度を制御するフラグだ。ビルドツールがgit履歴を読むなら残す、何も読まないなら削除しても問題ない。
Vercelアカウントの2FAは有効にしておくべき?
すぐ有効にしたほうがいい。今回のインシデントはVercel社員のWorkspace経由で、顧客アカウントが直接狙われたわけではない。でも一般論として、Vercel → Settings → Security で 2FA を有効化 + 見覚えのないセッションをRevokeするのは2分で終わる基本対策。SaaSへの攻撃で最も多い経路の1つを潰せる。
まとめ
バッジを見たかどうかに関係なく、今すぐやる価値があること3つ:
- Vercel環境変数を監査して平文シークレットを洗い出す。APIキー、DB認証情報、サービスアカウントキーでSensitiveになっていないものは全部移行対象。Resend / Upstash / Supabase 等の連携が一番の被疑者
- 使っていないものを削除する。昔の実験で入れたMarketplace連携は、休眠シークレットの長い尾を引きがち。連携ごと外せば一括で消える
- これから作る新変数は全部Sensitiveをデフォルトに。値を後から見られないというトレードオフはあるが、今回のインシデントで顧客を守ったのは実質これだけだ
プラットフォームへのサプライチェーン攻撃は珍しいが、発生頻度ゼロではない。自分でコントロールできる防御姿勢 — Sensitiveフラグ、最小権限、ローテート規律 — は一度セットアップすれば安く維持できる。何もしていないときにこそ静かに守ってくれる類の投資だ。