Vercel に Next.js を載せると、デプロイも TLS も DDoS 緩和も「何もしなくても動く」状態から始まります。そのせいで、利用者側で決めるべき設定が既定値のまま残りがちです。Preview に本番の鍵が入っている、本番ドメインが誰にでも開ける、個人ごとのレスポンスが CDN に載る。どれもダッシュボードとリポジトリを見比べれば見つかります。
この記事は、スタートアップで Next.js を Vercel に載せているテックリード向けの設定監査チェックリストです。各項目について「何を確認するか」「どこで設定するか」「既定値」「なぜ重要か」を並べます。機能名・既定値・プランの差は、2026年10月時点の Vercel と Next.js の公式ドキュメントで確かめたものです。Vercel は 2026年8〜9月にいくつかの機能の名前と料金体系を変えているので、古い記事の情報とは食い違う箇所があります。
環境変数と Firewall にはそれぞれ詳しい記事があるので、この記事では要点だけ書いてリンクします。
まず責任分界:Vercel が守るもの、あなたが決めるもの
結論:Vercel が責任を持つのは基盤(インフラ・ネットワーク・保存時の暗号化)です。環境変数の扱い、アプリの認証、ログの長期保管、どの機能を使うかの選択は利用者の責任です。
Vercel の Shared Responsibility Model は、利用者側の責任を具体的に挙げています。
| 領域 | Vercel | 利用者 |
|---|---|---|
| インフラ・ネットワーク・コンピュートの分離 | 責任を持つ | — |
| 通信中と保存時の暗号化(Vercel が管理するサービス) | 責任を持つ | 連携するサードパーティ側の安全確保 |
| コードと環境変数 | — | 環境変数の露出を含むコード管理、APIキーなど秘密情報の適切な取り扱い |
| 認証 | プラットフォームの認証と Deployment Protection を提供 | アプリの認証 |
| ログ | 短期のランタイムログを提供 | 長期保管・監査のための Drains の設定 |
| インシデント対応 | Vercel が管理する基盤 | 自分のコード・設定・連携 |
つまり「Vercel を使っているから安全」ではなく、Vercel が用意した仕組みをどう設定したかが利用者側の安全性を決めます。以下のチェックリストはその設定の棚卸しです。自分たちで見る時間が取れない場合は、Vercel 設定と Next.js のデプロイ境界のレビューで同じ観点を確認しています。
1. 環境変数と環境の分離
結論:鍵・トークン・パスワードは Secret 型で保存し、Preview と Production で値を分けます。NEXT_PUBLIC_ を付けた値はブラウザに配られるので、秘密を入れません。
| 確認すること | 場所 | 既定値 | なぜ重要か |
|---|---|---|---|
| 秘密値が Secret 型か | Project Settings → Environment Variables(各変数の Type) | 種類を選んで保存する | Secret は保存後に読み出せない(書き込み専用)。Config は権限のあるメンバーが後から読める |
| Separate Production Secret Values | Team Settings → Security(owner が有効化) | 任意のポリシー | 有効にすると、Production の Secret は Preview・Development・カスタム環境と同じ値にできない |
| Preview に本番の値がないか | 各変数の対象環境 | 変数ごとに選ぶ | Preview には未レビューのブランチも載る。本番の鍵が入っていれば、そこが本番への入口になる |
| ブランチ別の Preview 変数 | 変数の対象に特定ブランチを指定 | 全非本番ブランチ | ブランチ指定の値は、同名の Preview 変数を上書きする |
NEXT_PUBLIC_ の中身 | コードと変数一覧 | — | ビルド時に JS バンドルへインライン化され、ブラウザに届く |
| 長期の鍵の代わりに OIDC | Project Settings → Security / 接続先クラウド | — | AWS・GCP・Azure と短命トークンで接続でき、長期の資格情報を環境変数に置かずに済む |
押さえておく変更点があります。2026年8月24日に、環境変数の「Sensitive」の切り替えは Config / Secret の2種類に置き換わりました。既存の Sensitive 変数は自動で Secret として扱われます。チームポリシーの「Enforce Sensitive Environment Variables」は非推奨になり、代わりに上の Separate Production Secret Values が用意されました。以前は Sensitive を Development に使えませんでしたが、Secret は Development でも使えます。
CLI では vercel env add に --type secret を付けます(--visibility は非推奨の別名)。
# 本番用の Secret を追加(値は対話入力)
vercel env add STRIPE_SECRET_KEY production --type secret
ビルドログには保護があり、32文字以上の Secret の値がビルドログに出ると [REDACTED] に置き換わります。ただし対象はビルドログだけです。ランタイムログでの置き換えはドキュメントに書かれていません(9章)。
カスタム環境(staging など)は Pro と Enterprise で作れます。NEXT_PUBLIC_ の落とし穴と型安全な env の境界は Vercel 環境変数・シークレット管理ガイド、秘密がクライアントに混ざる経路と検出方法は Next.js の環境変数から秘密が漏れる経路と防ぎ方で詳しく扱っています。
2. Deployment Protection:既定では本番ドメインは守られない
結論:新規プロジェクトの既定は Standard Protection で、Preview は守られますが本番ドメインは対象外です。本番も隠したいなら All Deployments を選びます。2026年9月9日以降、Vercel Authentication で All Deployments を守る設定は全プランで追加費用なしです。
| 確認すること | 場所 | 既定値 | なぜ重要か |
|---|---|---|---|
| 保護の範囲 | Project → Security → Deployment Protection | Standard Protection(本番ドメイン以外を保護) | 本番カスタムドメインは Standard では守られない |
| All Deployments が要るか | 同上 | — | 社内ツールや公開前のサイトは、本番 URL も含めて閉じる |
| 保護の方式 | 同上 | — | Vercel Authentication が推奨の方式。無効にすると既存のデプロイがすべて保護されない状態になる |
| Password Protection | 同上 | — | Pro では有料のオプション(2026年9月9日からプロジェクト単位で購入)、Enterprise は込み。Hobby では使えない |
| Trusted IPs | 同上 | — | Enterprise のみ。IPv4 アドレスと IPv4 CIDR |
| チームの既定値 | Team Settings | — | 新規プロジェクトに適用する既定の保護レベルと方式を決められる |
Standard Protection の範囲に注意してください。 公式の説明は「本番ドメイン以外のすべてのドメインを保護する」です。*.vercel.app のデプロイ固有 URL は保護されますが、example.com のような本番ドメインは誰でも開けます。
Vercel Authentication で保護されたデプロイを見られるのは、プロジェクトにアクセスできるチームメンバー、アクセスを許可された Vercel ユーザー、Shareable Link を受け取った人、後述のバイパス用ヘッダーを送るツールです。
例外の穴を点検する
保護を有効にしたら、次は例外として開けた穴を確認します。
- Protection Bypass for Automation(全プラン):
x-vercel-protection-bypassヘッダー(またはクエリパラメータ)に秘密を付けると保護を通過できます。秘密はプロジェクトのすべてのデプロイに効き、取り消すまで有効です。CI・E2E テストなど用途ごとに複数作れるので、用途別に発行して不要になったものは消します。ヘッダーを付けられない Webhook 送信元にはクエリパラメータで渡せますが、その場合は秘密が相手側の設定画面に残ります。 - Shareable Links(全プラン):特定ブランチのデプロイに、クエリパラメータ付きの URL で外部アクセスを許可します。配った先と取り消しを管理します。
- OPTIONS Allowlist(全プラン):CORS のプリフライト用に、登録したパスで始まる OPTIONS リクエストの保護を外します。
/apiを登録すれば/api/v1/usersも対象です。前方一致なので、必要最小限のパスにとどめます。
自動化ツールから保護されたデプロイに入る方法としては、ほかに Trusted Sources(短命の OIDC トークンを x-vercel-trusted-oidc-idp-token ヘッダーで送る)もあります。
3. Git 連携:誰のコミットがデプロイされるか
結論:フォークからの PR は承認なしにデプロイさせず、ビルドログとソースの公開設定を既定のまま閉じておきます。
| 確認すること | 場所 | 既定値 | なぜ重要か |
|---|---|---|---|
| Git Fork Protection | Project Settings → Security | 有効(無効化もできる) | フォークからの PR のデプロイに、あなたかチームメンバーの承認を求める。環境変数と OIDC トークンの漏えいを防ぐ |
| コミット作者の確認 | 設定項目なし | 常に適用 | 非公開リポジトリでは、コミット作者が Vercel のプロジェクトにアクセスできる場合だけデプロイされる(GitHub の organization など組織配下のリポジトリが対象) |
| Require Verified Commits | Project Settings → Git | 無効 | GitHub で検証済みのコミットだけをデプロイし、それ以外は自動でキャンセルする(GitHub のみ) |
| Build Logs and Source Protection | Project Settings → Security | 有効 | /_src(ソースとビルド出力)と /_logs(ビルドログ)をチームメンバーだけに限る |
Git Fork Protection を無効にすると、外部の人が出した PR のコードがあなたの Preview 環境変数を使ってビルドされることになります。ドキュメントも「環境変数を確認するまで無効にしないこと」と注意しています。
Build Logs and Source Protection は、CLI の --public オプションや vercel.json の public プロパティで上書きされます。リポジトリに "public": true が残っていないか確認してください。また、この設定を切り替えても既存のデプロイには反映されません。
4. Next.js の境界:Server Actions は公開エンドポイント
結論:Server Actions と Route Handlers は、UI を経由せず直接呼べる公開 API として扱います。認証と認可は Proxy(旧 middleware)ではなく、各関数の中で確かめます。
Next.js の Data Security ガイドによると、作成して export した Server Action は、アプリの UI からだけでなく直接の POST リクエストで呼べます。コードのどこからも import されていなくても外部から呼べる、と明記されています。暗号化されたアクション ID などの保護もありますが、ガイドは「それでも各アクションの中で認証と認可を確かめる」よう求めています。
// app/invoices/actions.ts
"use server";
import { z } from "zod";
import { verifySession } from "@/lib/dal"; // server-only のデータアクセス層
import { db } from "@/lib/db";
const DeleteInvoiceInput = z.object({ invoiceId: z.uuid() });
export async function deleteInvoice(formData: FormData): Promise<void> {
// 1. 認証:Proxy を通っていても、ここで必ず確かめる
const session = await verifySession();
if (!session) throw new Error("Unauthorized");
// 2. 入力検証:直接 POST されうるので、引数の型を信用しない
const { invoiceId } = DeleteInvoiceInput.parse({
invoiceId: formData.get("invoiceId"),
});
// 3. 認可:対象が本人のものであることを削除条件に含める
await db.invoice.deleteMany({
where: { id: invoiceId, ownerId: session.userId },
});
}
確認するポイントは次のとおりです。
- Proxy だけで認可していないか。 Next.js 16 で
middlewareは非推奨になり、proxyに名前が変わりました。Proxy のドキュメントは、matcher の変更やリファクタで Server Function が別のルートに移ると Proxy の保護が黙って外れうるとして、「Proxy だけに頼らず各 Server Function の中で確かめる」よう書いています。認証ガイドも、Proxy では Cookie を読むだけの楽観的なチェックにとどめるよう勧めています。 - CVE-2025-29927 の教訓。 2025年3月に公開されたこの脆弱性(深刻度 critical)は、認可チェックを middleware で行っている場合にそれを迂回できるというものでした。修正版は 15.2.3 / 14.2.25 / 13.5.9 / 12.3.5 です。アドバイザリには「Vercel でホストされた Next.js のデプロイは自動的に保護されている」と書かれており、Vercel 上のアプリが影響を受けたわけではありません。教訓は「middleware という1か所に認可を集めると、そこが外れたときに全部が外れる」という設計上の話です。
- クロージャの暗号化に頼っていないか。 コンポーネント内で定義した Server Action が外側の変数を参照すると、その値はクライアントを往復します。Next.js は自動で暗号化しますが、ドキュメントは「暗号化だけに頼ることは勧めない」としています。秘密の値をクロージャに入れないようにします。
serverActions.allowedOriginsを広げすぎていないか。 Server Actions は POST でしか呼べず、Next.js はOriginヘッダーとHost(またはX-Forwarded-Host)を比べて CSRF を防ぎます。リバースプロキシの構成で必要なときだけ、allowedOriginsに許可するホストを足します。process.envを読む場所を絞っているか。 Data Security ガイドは、import 'server-only'を付けたデータアクセス層だけがprocess.envを読む構成を勧めています。
5. 個人ごとのレスポンスをキャッシュさせない
結論:ユーザーごとに中身が変わるレスポンスには Cache-Control: private を付けます。Cookie で認証しているリクエストに s-maxage を返すと、CDN にキャッシュされる条件がそろうことがあります。
Vercel の CDN は、次のどれかに当てはまるレスポンスをキャッシュしません。
- リクエストに
Authorizationヘッダーがある - レスポンスに
set-cookieヘッダーがある Cache-Controlにprivate、no-cache、no-storeのどれかがあるVary: *、またはCookieのような値の種類が多いヘッダーをVaryに含む
Cache-Control の既定値は public, max-age=0, must-revalidate で、CDN にもブラウザにもキャッシュさせません。危ないのは、自分で s-maxage を付けたときです。Cookie ベースのセッションは Authorization ヘッダーを使わないので、上の除外条件に当たりません。ユーザー固有の JSON を返すルートに s-maxage を付け、set-cookie も返していなければ、そのレスポンスはキャッシュ対象になりえます。
// app/api/me/route.ts
import { verifySession } from "@/lib/dal";
import { getProfile } from "@/lib/profile";
export async function GET() {
const session = await verifySession();
if (!session) return new Response("Unauthorized", { status: 401 });
const profile = await getProfile(session.userId);
// 個人ごとのレスポンス:共有の CDN キャッシュに載せない
return Response.json(profile, {
headers: { "Cache-Control": "private, max-age=0" },
});
}
2026年9月30日からは、Vary に Cookie を含むレスポンスを Vercel の CDN がキャッシュしなくなりました。これは安全側への変更ですが、Vercel は「Cookie に依存するレスポンスは Cache-Control: private を使う」よう案内しています。Vary: Cookie に頼らず、private を明示しておくのが確実です。キャッシュされなかったレスポンスは x-vercel-cache: MISS になり、ランタイムログに vary_key_denied:cookie が記録されます。
6. セキュリティヘッダー:既定は HSTS だけ
結論:Vercel のドキュメントで既定と明記されているセキュリティヘッダーは HSTS だけです。CSP などは自分で設定し、CSP は Report-Only から始めます。
| ヘッダー | 既定 | 対応 |
|---|---|---|
Strict-Transport-Security | あり(カスタムドメインは max-age=63072000、*.vercel.app は includeSubDomains; preload 付き) | 必要なら値を上書きする |
Content-Security-Policy | なし | Report-Only で観察してから強制する |
X-Frame-Options / X-Content-Type-Options / Referrer-Policy | ドキュメントに既定の記載なし | 自分で設定する |
設定場所は next.config.ts の headers()、vercel.json の headers、または vercel.ts です。vercel.ts と vercel.json はどちらか一方だけを使います。
// next.config.ts
import type { NextConfig } from "next";
// まずは Report-Only で違反を観察し、壊れないと確認してから
// キー名を Content-Security-Policy に切り替える
const csp = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join("; ");
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Content-Security-Policy-Report-Only", value: csp },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "X-Frame-Options", value: "DENY" },
],
},
];
},
};
export default nextConfig;
この設定のまま script-src 'self' を強制すると、Next.js の App Router がページに入れるインラインスクリプトは止まります。だからこの例は Report-Only で始め、違反レポートで確かめます。強制して動かせる方式は、Next.js の CSP ガイドが示す2つです。1つは nonce で、リクエストごとに nonce を作るので動的レンダリングが必須になります。もう1つはハッシュで、Subresource Integrity(SRI)によるハッシュベースの CSP は静的生成を保てますが、App Router 限定の実験的機能です。静的に配信しているページが多いアプリでは、この代償も含めて方式を選びます。
7. Firewall とレート制限
結論:カスタムルール、レート制限、Attack Mode、BotID の Basic はすべてのプランで使えます。ログイン・サインアップ・高コストな API には、少なくともレート制限を入れます。
| 機能 | プラン(2026年10月時点) | 確認すること |
|---|---|---|
| DDoS 緩和(L3・L4・L7) | 全プラン・自動 | 設定不要 |
| カスタムルール | 全プラン(Hobby 3 / Pro 40 / Enterprise 1000 まで) | 管理画面や管理 API へのアクセスを絞っているか |
| レート制限 | 全プラン(Hobby はルール1つ) | ログインや問い合わせフォームにかけているか。カウンタはリージョンごと |
| Attack Mode | 全プラン・無料 | 攻撃時に誰が有効にするか決まっているか。自分の Functions と Cron は通過する |
| Bot Protection マネージドルールセット | 全プラン | 既定は無効 |
| OWASP コアルールセット | Enterprise のみ | Pro では使えない前提で設計する |
| BotID(Basic) | 全プラン・無料 | サインアップや決済のような悪用されやすい操作に使うか |
以前の「Attack Challenge Mode」は、現在のドキュメントでは Attack Mode という名前です。ルールの書き方や BotID の組み込み方は Vercel Firewall × WAF × BotID 実装ガイドにまとめています。入口の Firewall は多層防御の一層で、4章の認可の誤りは防げない点も忘れないでください。
8. Cron と Webhook:呼び出し元を確かめる
結論:Cron のエンドポイントは CRON_SECRET で認証し、未設定のときも拒否します。Webhook は送信元の署名を検証します。どちらも2回届いても壊れない処理にします。
Cron のエンドポイントは普通の HTTP ルートなので、そのままでは誰でも呼べます。プロジェクトに CRON_SECRET を設定すると、Vercel は Cron の実行時にその値を Authorization: Bearer <値> として送ります。Vercel は16文字以上のランダムな文字列を勧めています。
// app/api/cron/cleanup/route.ts
import { timingSafeEqual } from "node:crypto";
import { purgeExpiredSessions } from "@/lib/jobs";
function isAuthorizedCron(header: string | null, secret: string | undefined): boolean {
// CRON_SECRET が未設定なら必ず拒否する(fail closed)
if (!secret || !header) return false;
const expected = Buffer.from(`Bearer ${secret}`);
const actual = Buffer.from(header);
return actual.length === expected.length && timingSafeEqual(actual, expected);
}
export async function GET(request: Request) {
if (!isAuthorizedCron(request.headers.get("authorization"), process.env.CRON_SECRET)) {
return new Response("Unauthorized", { status: 401 });
}
// 同じ実行が2回届いても結果が変わらない(冪等な)処理にする
await purgeExpiredSessions();
return Response.json({ ok: true });
}
公式のサンプルも !cronSecret || で始まる fail closed の形です。もし逆に if (process.env.CRON_SECRET && ...) と書くと、変数を入れ忘れた環境では誰でもジョブを実行できる状態になるので注意してください。
Cron の運用で押さえておく仕様は次のとおりです。
- Vercel が呼ぶのは本番デプロイの URL です。
- 同じスケジュールの実行が2回以上届くことがあるので、処理は冪等にします。
- リダイレクトは追いません。3xx を返すとそこで終わります。
- Hobby は1日1回まで(実行時刻のずれは ±59分)。Pro と Enterprise は1分ごとまで設定できます。
Webhook のエンドポイントでは、Stripe のように送信元が署名を付けるサービスなら、処理の前に必ず署名を検証します。Preview で Webhook を受けるために前述の Protection Bypass の秘密を URL に入れた場合は、その秘密の扱いも棚卸しの対象です。
9. ログとチームのアクセス権
結論:ランタイムログの保持期間はプランで決まり、長期保管は Drains で利用者が用意します。秘密値をログに書かないのはアプリ側の責任です。
| 項目 | プラン(2026年10月時点) |
|---|---|
| ランタイムログの保持 | Hobby 1時間 / Pro 1日 / Enterprise 3日(Observability Plus を付けると Pro・Enterprise とも30日) |
| Drains | Pro・Enterprise |
| Audit Log Drains | Enterprise のみ |
| Audit Logs(CSV は最大90日分) | Enterprise のみ |
| Developer ロール | Pro・Enterprise |
| Access Groups・プロジェクト単位のロール | Enterprise のみ |
| SAML SSO | Enterprise、Pro は有料アドオン |
| Directory Sync | Enterprise のみ |
ビルドログについては 1章で触れた置き換えがありますが、ランタイムログで秘密値が置き換えられるとはドキュメントに書かれていません。ここからは私たちの推論ですが、アプリが console.log でリクエストヘッダーやトークンを出力すれば、その値はランタイムログに残り、Drains を設定していれば転送先にも届くと考えるのが安全です。責任共有モデルでも、秘密情報の適切な取り扱いは利用者の責任とされています。
チームの権限では、Pro でも使える Developer ロールが役立ちます。Developer は Preview と Development の環境変数は扱えますが、本番の環境変数とチーム設定は変更できません。全員を Member にしている場合は、本番の鍵を触る必要がある人を絞れないか見直します。
一覧表:Vercel セキュリティ設定チェックリスト
| 項目 | 既定値 | 推奨 | プラン |
|---|---|---|---|
| 秘密値の環境変数の型 | 変数ごとに選ぶ | Secret | 全プラン |
| Separate Production Secret Values | 任意 | 有効 | チームの Security 設定(owner) |
| Preview と Production の値 | 変数ごとに選ぶ | 本番の鍵を Preview に入れない | 全プラン |
NEXT_PUBLIC_ | — | 公開してよい値だけ | — |
| 外部クラウドの認証 | — | OIDC Federation | 全プラン |
| Deployment Protection の範囲 | Standard(本番ドメインは対象外) | 本番を隠すなら All Deployments | 全プラン |
| 保護の方式 | — | Vercel Authentication | 全プラン |
| Password Protection | — | 必要な場合だけ | Pro は有料オプション、Enterprise は込み、Hobby 不可 |
| Trusted IPs | — | 社内ネットワーク限定にしたい場合 | Enterprise のみ |
| Protection Bypass の秘密 | — | 用途別に発行し、不要なら取り消す | 全プラン |
| OPTIONS Allowlist | 空 | 最小限のパス(前方一致) | 全プラン |
| Git Fork Protection | 有効 | 有効のまま | 全プラン |
| Require Verified Commits | 無効 | 必要に応じて有効 | GitHub 連携のみ |
| Build Logs and Source Protection | 有効 | 有効のまま(public の上書きに注意) | 全プラン |
| Server Actions の認可 | — | 各関数の中で認証・認可 | — |
| 個人ごとのレスポンス | public, max-age=0, must-revalidate | private, max-age=0 | — |
| セキュリティヘッダー | HSTS のみ | CSP(Report-Only から)ほか | — |
| WAF カスタムルール・レート制限 | なし | ログインや高コスト API に設定 | 全プラン(ルール数に差) |
| Attack Mode | 無効 | 有効化の手順を決めておく | 全プラン |
| Cron の認証 | なし | CRON_SECRET、fail closed | 全プラン |
| ランタイムログの保持 | 1時間〜3日 | 必要なら Drains | Drains は Pro・Enterprise |
| 監査ログ | — | 必要なら検討 | Enterprise のみ |
まとめ
Vercel の既定値は「すぐ動く」ことを優先しています。本番ドメインは公開、環境変数の型は自分で選ぶ、セキュリティヘッダーは HSTS だけ、という状態から、チームの事情に合わせて締めていくのは利用者の仕事です。
優先順位を付けるなら、まず Preview に本番の鍵が入っていないかと Server Actions の中で認可しているかの2つです。どちらも、設定画面やコードを一度通して見るだけで確かめられます。設定とコードの両方をまとめて第三者に見てほしい場合は、Vercel 設定のセキュリティレビューで、このチェックリストと同じ観点を実際のプロジェクトに当てて確認します。