Skip to main content

第22章:ログ・エラー・デバッグで漏らさない(ここで漏れる)🫣🧯

この章は「秘密をちゃんと隠してるつもりなのに、ログとエラーで全部バラす」事故を止める回です😂 “漏らさない作法”をコードに固定して、以後ずっと楽できる形にします💪✨


1) まず結論:漏れる場所トップ3 🥇🥈🥉

  1. console.log / logger に “うっかり” 出す(env全部、headers全部、例外オブジェクト全部…)🫠
  2. エラー応答にスタックトレースや内部情報を返す(開発のノリのまま本番へ)💥
  3. デバッグ用に出したログが、永遠に残って共有される(チケット、チャット、AI相談)📎🤖

ログは「自分だけが見るメモ」じゃなくて、現実には “配布物” になりがちです📦 (コンテナのログは docker logs / docker compose logs で誰でも見れたり、収集基盤に送られたりします)


2) 「秘密っぽいもの」一覧:これが1文字でもログに出たら負け😇🔒

最低限これらは 絶対にログに出さない ルールにします👇

  • パスワード / APIキー / トークン / JWT / Cookie / セッションID 🍪🔑

  • Authorization ヘッダ(特に Bearer ...)🪪

  • .env の中身、process.env の丸ごとダンプ 🌋

  • /run/secrets/* の中身(Compose secrets の実体)📄

    • secrets はコンテナ内に /run/secrets/<secret_name> としてマウントされます (Docker Documentation)
  • 個人情報(メール、住所、電話、IP、カード情報など)🧑‍🦰📞💳

ログ設計の考え方としても「必要な情報だけ」「機密は記録しない」が強く推奨されます (cheatsheetseries.owasp.org)


3) 今日からの作戦:3段ロックで守る🔐🔐🔐

ロックA:“出すログ”を最小化(そもそも入れない)✂️

  • リクエストボディは基本ログに入れない(特にログイン、決済、設定系)🙅‍♂️
  • headers を丸ごと出さない(必要なら許可リストで数個だけ)👀
  • エラーも「全部 err を丸ごと出す」ではなく、必要情報だけ構造化する🧩

ロックB:ログライブラリで自動マスク(うっかり保険)🧤

pino みたいに **redact(自動マスク)**を持つロガーを使うと、事故が激減します。 pino の redaction はパス指定で値をマスクできます (GitHub) ※重要:redact のパス文字列はユーザー入力から作らない(安全上の注意) (GitHub)

ロックC:収集基盤(OpenTelemetry等)側でも削る(最後の砦)🏰

テレメトリは一度流れると外部に出ていく可能性があるので、そもそも機密を載せない責任は実装側にある、という前提です (OpenTelemetry) さらに Collector 側で 属性削除・マスクもできます(redaction processor / transform など) (GitHub)


4) 実装:TypeScriptで「漏らさない logger」を固定する🧱✨

ここからは「テンプレ化」して、以後ずっと使い回すやつです😄

4-1. logger.ts:pino + redact で “危険キー” を自動マスク🧤🪓

// src/lib/logger.ts
import pino from "pino";

const isProd = process.env.NODE_ENV === "production";

/**
* ここで “絶対に出しちゃダメなキー” を固定
* ※ユーザー入力から paths を作らないこと(pinoの注意)
*/
const REDACT_PATHS = [
"req.headers.authorization",
"req.headers.cookie",
"req.headers.set-cookie",
"req.body.password",
"req.body.pass",
"req.body.token",
"req.body.apiKey",
"req.query.token",
"user.password",
"user.token",
"secrets",
"process.env", // “丸ごと出し”事故の保険(ただし process.env をそのまま渡さない運用が前提)
] as const;

export const logger = pino({
level: process.env.LOG_LEVEL ?? (isProd ? "info" : "debug"),
redact: {
paths: [...REDACT_PATHS],
censor: "[REDACTED]",
},
// 本番は人間より機械向け(JSON)でOK。開発はprettyでも良いが、まずは統一で。
base: undefined,
timestamp: pino.stdTimeFunctions.isoTime,
});

ポイント💡

  • “危険キーの辞書”をコードに固定して、毎回悩まない📌
  • LOG_LEVEL=debug にしても redact は効く(大事)🧯
  • process.env を直接 logger に渡さないのが基本(保険としてパスは置いてるだけ)

4-2. リクエストログ:必要最小だけ書く(bodyは捨てる)🧾🚫

// src/middleware/requestLog.ts
import type { Request, Response, NextFunction } from "express";
import { randomUUID } from "crypto";
import { logger } from "../lib/logger.js";

export function requestLog(req: Request, res: Response, next: NextFunction) {
const requestId = req.headers["x-request-id"]?.toString() ?? randomUUID();
res.setHeader("x-request-id", requestId);

const start = Date.now();

res.on("finish", () => {
const ms = Date.now() - start;

// ✅ “必要情報だけ”に絞る(headers/bodyは基本出さない)
logger.info(
{
requestId,
method: req.method,
path: req.originalUrl,
status: res.statusCode,
ms,
},
"http_request"
);
});

next();
}

これで、**追跡に必要な情報(いつ/どこ/何/どれくらい)**は残るのに、 秘密が混ざりやすい部分(headers/body)を避けられます👍


4-3. エラーハンドリング:返すメッセージは “控えめ”、ログは “十分” 🧯📦

Express は 本番環境だとスタックトレースをレスポンスに含めない挙動が明記されています (expressjs.com) (ただし、自分の実装次第で簡単に漏れるので、ここで固定します)

// src/middleware/errorHandler.ts
import type { Request, Response, NextFunction } from "express";
import { logger } from "../lib/logger.js";

export function errorHandler(err: unknown, req: Request, res: Response, _next: NextFunction) {
const isProd = process.env.NODE_ENV === "production";

// “ログには十分” ただし req 全部や env 全部を渡さない
logger.error(
{
method: req.method,
path: req.originalUrl,
// headersは丸ごと渡さない。必要なら許可リストで。
err: normalizeError(err),
},
"unhandled_error"
);

// “返すのは控えめ”
res.status(500).json({
error: "Internal Server Error",
...(isProd ? {} : { detail: normalizeError(err) }), // 開発だけ詳細を返す
});
}

function normalizeError(err: unknown) {
if (err instanceof Error) {
return {
name: err.name,
message: err.message,
// stack は開発だけ使う運用にしてもOK(ここは返却側で制御)
stack: err.stack,
};
}
return { message: String(err) };
}

✅ これで

  • クライアントには余計な内部情報を返さない
  • サーバ側ログには原因調査に必要な情報を残す が両立できます😄

5) Compose secrets とログ:“読み方”はOK、”出力”はNG📄🙅‍♂️

Compose secrets はコンテナ内で /run/secrets/<name> のファイルになります (Docker Documentation) 読み込むのは普通にOK。でも 値をログに出した瞬間に終わりです😂

// src/lib/secrets.ts
import { readFileSync } from "node:fs";

export function readSecret(name: string): string {
const path = `/run/secrets/${name}`;
// ✅ 読むだけ。ログに出さない。
return readFileSync(path, "utf-8").trim();
}

さらに注意⚠️

  • 例外メッセージに secret を混ぜない(throw new Error("token="+token) とか)🫣
  • 「デバッグだから一回だけ…」が一番危ない(ログは残る)🪦

6) ありがちな “漏れ方” デモ(演習)🧪🎯

演習1:process.env を出してしまう事故を潰す🌋🧯

  1. わざとこう書く(悪い例)
console.log(process.env);
  1. docker compose logs で見て「うわぁ…」ってなる😇
  2. 直す:env を丸ごと出さない。必要な設定値だけ、しかもマスクして出す。

演習2:headers丸ごとログで Authorization が漏れる🪪💥

悪い例👇

logger.info({ headers: req.headers }, "debug_headers");

直し方(許可リスト方式)👇

logger.info(
{
requestId,
userAgent: req.headers["user-agent"],
// authorization/cookie は入れない!
},
"debug_request_meta"
);

演習3:本番でスタックトレースを返してしまう事故📉🧨

  1. エラー時に err.stack をそのまま返す実装を入れてしまう
  2. 本番で内部構造が見える(ライブラリ名、パス、SQL、etc…)
  3. 直す:この章の errorHandler を採用。 Express の公式ガイドでも、本番ではスタックをレスポンスに含めない方針が示されています (expressjs.com)

7) AI拡張にログを貼る前の「3秒ルール」🤖⏱️🧼

AI相談は便利だけど、ログは貼りがち!📎 貼る前にこれだけ確認👇

  • Authorization / Cookie / token= / apiKey= / password の文字が見えたら 即マスク🧤
  • .env や secrets の値が混ざってそうなら まず削る✂️
  • 「長いログ」を丸ごと貼らず、必要な数十行だけにする📏

さらに強くしたいなら:

  • AIに貼る用の “sanitize スクリプト” を用意して、コピペ前に必ず通す(おすすめ)✨

8) OpenTelemetry/ログ収集の落とし穴:一度送ると戻らない📡🫠

OpenTelemetryは「何が機密か」を自動判定できないので、実装者が守る責任があると明記されています (OpenTelemetry) だから順番はこう👇

  1. アプリ側で機密を入れない(最重要)
  2. それでも混ざる前提で、Collector 側で redaction / transform で削る (GitHub)

9) 仕上げ:この章の “合格ライン” ✅🎉

最後にチェック!これが全部YESなら勝ちです😄✨

  • headers / body を丸ごとログしてない
  • Authorization / Cookie / token 系は 自動マスクされる
  • 本番のエラー応答に スタックトレースを出してない(Expressの方針とも整合) (expressjs.com)
  • secrets を読んでも、値をログに出してない(/run/secrets の扱いOK) (Docker Documentation)
  • 収集基盤に機密が流れない前提を作ってる(OpenTelemetryの注意点を踏んでる) (OpenTelemetry)

次の章(第23章)は「ビルド時の秘密:BuildKit secretsでレイヤに残さない🏗️🤫」に入るはずなので、 この第22章で作った “ログで漏らさない土台” が効いてきますよ〜😄🔑