開発側のセットアップは docs/getting-started.md にあります。この文書は本番側です。 サーバは さくらのクラウド VM(Linux)、DB は さくらインターネットのデータベース アプライアンス(MariaDB 10.11)(CLAUDE.md §3 / D-100)。
★出す直前に潰すチェックリストは「7. 本番へ出す前のセキュリティ確認」にあります。★
★コードが実際に読んでいる環境変数は5つだけです。★ (DATABASE_URL / LOG_LEVEL / NODE_ENV / USE_FIXTURES の4つに、 指示書I で AUTH_TOKEN が5つ目として加わりました)
| キー | 本番で入れる値 | 未設定だとどうなるか | 読んでいる場所 |
|---|---|---|---|
USE_FIXTURES |
★false★ |
★true になる★(!== "false" のため)。実DBに繋がらずサンプルデータのまま動く。例外もエラーも出ない |
lib/db/use-fixtures.ts |
DATABASE_URL |
参照用アカウントの接続URL | 画面が全部エラーになる | lib/db/prisma.ts |
AUTH_TOKEN |
★必ず変更した共有トークン★ | 誰もログインできない(=全機能が使えない) | lib/auth/token.ts |
LOG_LEVEL |
info |
NODE_ENV から判定(本番は info) |
lib/logger/index.ts |
NODE_ENV |
production(npm run start が設定) |
ログレベルと禁止フィールドの扱いが開発モードになる | lib/logger/index.ts |
★ALLOW_DIRECT_ACCESS★ |
★設定しない★ | 未設定 = false = 拒否(★これが正しい状態★) |
lib/config/allowed-ips.ts |
DATABASE_URL のパスワードに % や @ が含まれる場合は ★URLエンコードが必須★です (% はパーセントエンコードの開始記号、@ はホスト区切りとして解釈されるため)。
.env.local だけではありません★アプリ(Next.js)も npm run check:env も prisma7.config.ts も、同じ順で読みます (Next.js の @next/env。先に見つかった値が勝つ):
★実際の環境変数(process.env)★ > .env.$(NODE_ENV).local > .env.local > .env.$(NODE_ENV) > .env
env_file(例 app.env)/ environment で渡してください★。process.env なので、どのファイルよりも優先されます。エラー文の「環境変数(Docker の env_file・.env・.env.local など)」はこのことです.env.local を入れないこと★。コンテナ内にあると、env_file で渡していないキーについてはその値が読まれてしまいます(.dockerignore で除外する).env(COMPOSE_PROJECT_NAME など)はアプリには渡りません★。compose が ${...} の展開に使うだけで、env_file / environment で参照したキーだけがコンテナに入ります.env.production.example をコピーして .env.local を作る (★Docker で動かす場合は .env.local ではなく env_file(例 app.env)に同じ値を書く★。§1-1)
cp .env.production.example .env.local
各値を埋める。★AUTH_TOKEN は必ず変更する★ (changeme のままだと手順3で落ちます)
★npm run check:env を実行して通ることを確認する★
npm run check:env
★エラーが出たら先に進まないでください。★ 終了コードが非0になります。 警告だけなら 0 なので、内容を読んで意図どおりか判断してください。 (検査の中身は lib/config/env-check.ts。値そのものは出力しません — DATABASE_URL にパスワードが入るため)
npm run build
npm run start
ALLOW_DIRECT_ACCESS は本番で設定しないこと★★設定すると、nginx を通らない直接アクセスが素通りします。★
false = 拒否が正しい状態ですprocess.env.ALLOW_DIRECT_ACCESS === "true" なので、 ★未設定・空文字・タイポ・1・TRUE はすべて false(拒否)★になります。 設定を忘れたら閉じる方向に倒れますUSE_FIXTURES は !== "false"(未設定なら true)ですが、向きが逆なのは意図的です。★ セキュリティに関わる設定は「未設定を閉じる側に倒す」のが規則です。 ★「既存に合わせて統一しよう」として戻さないでください★(D-101)npm run check:env が、本番判定でこれをエラーとして検出します(検査2)X-Forwarded-For に「追記」すること★(D-077 項5)$proxy_add_x_forwarded_for 相当にしてください。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
★上書き($remote_addr を単に代入)にすると TRUSTED_PROXY_HOPS の前提が崩れます。★
アプリは X-Forwarded-For の ★末尾から TRUSTED_PROXY_HOPS 番目★ を接続元とみなします (TRUSTED_PROXY_HOPS = 1 = 末尾。LB と nginx は同一なのでプロキシは1段。D-079 項1)。 ★先頭は使いません。★ 先頭はクライアントが自由に詐称できるためです。
★これはネットワーク側の設定であり、アプリでは防げません。★
ALLOW_DIRECT_ACCESS はあくまで「XFF が取れないとき」の保険であって、 ★偽装 XFF は防げません★。直接アクセスを本当に塞ぐのはネットワーク側です。
★2026-09-10 時点で「アプリのポートが nginx からのみ到達可能か」は未確認のままです。★ デプロイ時に必ず確認してください(docs/decisions.md D-101 の申し送り)。
next start は XFF を自分で埋める★★★npm run start(Next.js の Node サーバ)は、X-Forwarded-For が無いリクエストに対して 自分で socket の接続元を1個入れます★(追記はしません。既にあれば触りません)。 段階5-2 の実測結果:
送った X-Forwarded-For |
アプリが見る要素数 | clientIp(hops=1) |
|---|---|---|
| (送らない) | 1 | ★::ffff:127.0.0.1(socket の接続元)★ |
203.0.113.10 |
1 | 203.0.113.10 |
1.2.3.4, 203.0.113.10 |
2 | 203.0.113.10 |
| ヘッダ行を2本 | 2 | 203.0.113.10 |
★帰結(重要)★:
clientIp() は実サーバ上で null を返しません。★ したがって ★ALLOW_DIRECT_ACCESS は実質的に発火しません★ALLOWED_IPS + ALLOW_ALL_WHEN_EMPTY=false です★。 ALLOW_DIRECT_ACCESS ではありませんALLOW_ALL_WHEN_EMPTY を必ず false にし、 ALLOWED_IPS を埋めてください。★★ 空のままだと (ALLOW_ALL_WHEN_EMPTY の既定が true なので)誰でも通ります::ffff: 付き)で来ることが実測で確認できました。 ★ALLOWED_IPS には正規化前の生の表記をそのまま書いてください★(★5-1 参照★)★アプリ側では付けていません。★ next.config.ts に headers() の設定はなく、X-Content-Type-Options / Referrer-Policy / X-Frame-Options / CSP のいずれも出していません (★意図的にそうしてあります。指示書AE 段階2-7 / D-126★)。
★アプリに入れなかった理由★:
ALLOWED_IPS)が前段にあります★★★nginx で付けるときの推奨(P1)★★:
add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy same-origin always; add_header X-Frame-Options DENY always;
add_header は「同じ階層に1つでも add_header があると親階層のものを継承しない」★という nginx の性質があります。★location ブロック側にも書くなら、3行まとめて書いてください★| 設定値 | 置き場所 |
|---|---|
ALLOWED_IPS |
lib/config/allowed-ips.ts |
TRUSTED_PROXY_HOPS |
lib/config/allowed-ips.ts |
ALLOW_ALL_WHEN_EMPTY |
lib/config/allowed-ips.ts |
★この3つは環境変数ではありません。.env.local を直しても変わりません。★
コードで管理しているのは、★3つとも「本番で意味のある値」だから★です。 社内IPの列挙はレビュー対象にすべき情報なので、コードに置いて履歴を残します (ALLOW_DIRECT_ACCESS だけが「本番では常に false、true にするのは開発中だけ」という 性質なので、そちらだけ環境変数にしてあります)。
ALLOWED_IPS の書き方★192.0.2.0/24 と書いても、その文字列と完全一致するIPは無いので常に拒否になります)lib/config/allowed-ips.ts と isAllowedIp() を作り直すことになります::ffff: の展開 / ポート除去はしない)。 ★nginx が実際に付ける表記をそのままログで確認してから登録してください★ (auth.ip_denied のログに client_ip が出ます)10.x / 172.16-31.x / 192.168.x)を書かないこと。★ 万一プロキシが2段だった場合、末尾は LB の内部IPになります。 そこに一致してしまうと「全員素通り」という逆方向の事故になります (1段のままなら全員403で即座に気づけます)| 症状 | 疑うところ |
|---|---|
| ★全員が 403★ | ① ALLOWED_IPS(lib/config/allowed-ips.ts)に接続元が入っていない ② TRUSTED_PROXY_HOPS の段数違い(★2段構成なら末尾は LB の内部IPになる★) ③ ALLOW_ALL_WHEN_EMPTY=false かつ allowlist が空 |
| ★特定の人だけ 403★ | ALLOWED_IPS の表記ゆれ(★IPv6短縮形 / IPv4射影 ::ffff: / ポート付き は正規化しないので、書き方が違うだけで拒否されます★) |
| ★ローカルで 403★ | .env.local に ALLOW_DIRECT_ACCESS=true が無い(★これが正しい既定の挙動です★) |
| ★ログインできるが誰でも入れてしまう★ | ★ALLOW_DIRECT_ACCESS=true が本番に残っている★(npm run check:env の検査2 が検出)/ ALLOWED_IPS にプライベートIPを書いてしまっている / ★ALLOW_ALL_WHEN_EMPTY=true のまま ALLOWED_IPS が空★(4-3 参照。これが最も起こりやすい) |
★接続元の生の表記は auth.ip_denied のログで確認できます★(client_ip フィールド)。 拒否されたときだけ出ます。ログインの成否ログには接続元IPを載せていません(D-101)。
| 実測した要素数 | 末尾の値 | 解釈 | TRUSTED_PROXY_HOPS |
|---|---|---|---|
| 1 | 社内IP(実際の接続元) | 想定どおり — クライアントが XFF を送っておらず nginx が1個足した | 1 |
| 2以上 | 社内IP(実際の接続元) | 想定どおり — クライアントが自称 XFF を送り、nginx がその後ろに足した | 1 |
| 2以上 | ★社内IPでない値(10.x / 172.16-31.x / 192.168.x)★ |
★2段構成を疑う★ — 末尾が LB の内部IPになっている | ★2★ |
★見るのは要素数ではなく「末尾の値が社内IPかどうか」です。★ 要素数だけでは「nginx 経由の正常系」と「直接アクセス」を区別できません。
★2 に変えるのは設定値1行だけです。コード変更は不要★ (clientIp() は最初から「末尾から N 番目」で一般化してあります)。
★この節は「出す直前に上から順に潰すチェックリスト」です。★ (指示書AE 段階2-5。★1〜6節に散っていた確認事項をここに集約しました★)
★「実装済みだから確認不要」にしないでください。★ 認証・IP制限のコードはすべて実装済みです(lib/auth/token.ts の定数時間比較、 lib/auth/client-ip.ts の XFF 末尾参照、lib/auth/cookie.ts の httpOnly/secure/sameSite、 lib/logger/redact.ts の秘匿化)。★足りないのはコードではなく「出す直前の確認」です。★
| # | 確認すること | どう確かめるか |
|---|---|---|
| 1 | ★AUTH_TOKEN が changeme のままでない★ |
npm run check:env(検査1)。★error で終了コードが非0になります★ |
| 2 | ★ALLOW_DIRECT_ACCESS が本番で未設定★ |
npm run check:env(検査2)。★NODE_ENV=production のとき error★(開発中は warn) |
| 3 | ★USE_FIXTURES=false★ |
npm run check:env(検査3)。★これは warn 止まりです★ — ★終了コードは0のまま通るので、出力を目で読んでください★。未設定だと true 扱いで、★実DBに繋がらずサンプルデータのまま動きます(例外もエラーも出ません)★ |
| 4 | ★ALLOW_ALL_WHEN_EMPTY が false、かつ ALLOWED_IPS が埋まっている★ |
★lib/config/allowed-ips.ts を目視★。★★2026-09-13 時点の既定は ALLOW_ALL_WHEN_EMPTY = true / ALLOWED_IPS = [](空)★★ — ★このまま出すと誰でも通ります★(4-3 参照) |
| 5 | ★TRUSTED_PROXY_HOPS が実構成と合っている★ |
★lib/config/allowed-ips.ts を目視★+★6-1 の判定表★。★LB と nginx が別段になったら 2 にする★ |
| 6 | ★nginx が X-Forwarded-For を「追記」している(上書きでない)★ |
★4-1 の手順★($proxy_add_x_forwarded_for)。★上書きにすると項目5の前提が崩れます★ |
| 7 | ★リポジトリに認証情報が入っていない★ | git grep で接続URL・パスワード相当の記述を探す。★2026-09-13 時点の実測では実在の値は0件★(pw / s3cret / example.com 等のダミーのみ)。★ただし 7-3 も読むこと★ |
| 8 | ★npm audit の結果を読んだ★ |
★7-4 の記録★。★直すかどうかは別判断です(勝手に major を上げないこと)★ |
.env.local を直しても変わりません★★★ALLOWED_IPS / ALLOW_ALL_WHEN_EMPTY / TRUSTED_PROXY_HOPS の3つは 環境変数ではなくコードです★(5節 / lib/config/allowed-ips.ts)。 ★★変更にはデプロイが要ります★★。
★★ここを見落とすと ALLOW_ALL_WHEN_EMPTY = true のまま本番へ出ます★★。 ★そしてその状態は「動く」ので、画面を見ても気づけません★。 ★チェックリストに項目4・5が載っている意味はここにあります★。
c832891(fix: テストから実在のDBユーザー名を除去する / 2026-09-10)で★除去済み★git grep して出ない」=「漏れていない」ではありません★★。 ★項目7 が確かめているのは「これ以上増えていないこと」だけです★docs/handoff.md の申し送り)npm audit の記録★★この節には、実行した日付と結果をそのまま残します★。 ★★直すかどうかは別判断です★★ — ★npm audit fix --force は使わないこと★ (prisma のダウングレードを含む破壊的変更が走ります)。
# npm audit report
deepmerge-ts <8.0.0
Severity: high
DeepmergeTS has stack exhaustion when merging recursive object graphs - https://github.com/advisories/GHSA-ggr8-5vv4-36mx
fix available via `npm audit fix --force`
Will install prisma@6.19.3, which is a breaking change
node_modules/deepmerge-ts
@prisma/config 6.13.0-dev.1 - 8.1.0-dev.4
Depends on vulnerable versions of deepmerge-ts
node_modules/@prisma/config
prisma >=6.13.0-dev.1
Depends on vulnerable versions of @prisma/config
Depends on vulnerable versions of mysql2
node_modules/prisma
js-yaml 4.0.0 - 4.3.1
Severity: high
js-yaml: maxTotalMergeKeys does not limit CPU use for empty merge sources - https://github.com/advisories/GHSA-2883-xcg3-v3hh
fix available via `npm audit fix`
node_modules/js-yaml
mariadb 3.4.0 - 3.4.5
Severity: high
MariaDB's connector leaks the cleartext password to an MitM despite `ssl: true` - https://github.com/advisories/GHSA-cqhc-2h57-wpxf
MariaDB has Cleartext Transmission of Sensitive Information and Insufficiently Protected Credentials - https://github.com/advisories/GHSA-42r5-vhpq-m858
MariaDB has possible SQL injection in Buffer parameter escaping under big5/gbk/sjis/cp932/gb18030 client charsets - https://github.com/advisories/GHSA-g5xc-5w98-jfvm
No fix available
node_modules/mariadb
@prisma/adapter-mariadb <=8.1.0-dev.4
Depends on vulnerable versions of mariadb
node_modules/@prisma/adapter-mariadb
mysql2 <=3.23.0
Severity: high
MySQL2: Auth Plugin Downgrade to mysql_clear_password Leaks Plaintext Credentials - https://github.com/advisories/GHSA-3f6p-5ww8-9rcr
MySQL2: Unbounded zlib inflate in compressed MySQL protocol handler allows decompression-bomb DoS - https://github.com/advisories/GHSA-rgwj-5xj2-c3m3
fix available via `npm audit fix --force`
Will install prisma@6.19.3, which is a breaking change
node_modules/mysql2
sharp <0.35.4
Severity: high
sharp: Vulnerabilities in libheif: GHSA-g89c-p67h-r497 and GHSA-2jg2-4ch7-h545 - https://github.com/advisories/GHSA-rgj7-g3m4-5g8c
fix available via `npm audit fix`
node_modules/sharp
8 vulnerabilities (1 moderate, 7 high)
★この実行では npm audit fix を一切かけていません★(--force なしも含む)。 ★package-lock.json に差分が無いことを git diff --stat で確認済み★。
mariadb コネクタの SQL インジェクション勧告(GHSA-g5xc-5w98-jfvm)について★★★これは「アプリコードに生SQLが無い」では防げない層の話です★★ — ★コネクタがパラメータをエスケープする処理そのものの問題だからです★ (アプリ側の生SQL禁止は D-122 で ESLint ルール化済みですが、それとは別の層)。
★★ただし、影響を受けるのはクライアント文字セットが big5 / gbk / sjis / cp932 / gb18030 のときだけです★★。
★2026-09-13 の実測: 該当しません。★
grep -n "charset\|collation" .env.example .env.production.example lib/db/prisma.ts # → ヒット0件
DATABASE_URL のひな型(.env.example / .env.production.example)は mysql://USER:PASSWORD@HOST:3306/DATABASE で、★クエリ文字列そのものがありません★lib/db/prisma.ts は new PrismaMariaDb(databaseUrl) で、★オプションを渡していません★★★本番の DATABASE_URL に charset= / collation= を足す場合は、この勧告を先に読んでください★★。 ★上記5つの文字セットを指定すると、この層の対策が無い状態になります★。
No fix available の扱い(★申し送り済み★)★★mariadb の3件は★上げても直りません★(No fix available)★。 ★★これは DFA のアプリコードでは閉じられないので、docs/handoff.md に インフラ/TeamWorks 向けの申し送りとして出してあります★★。