Skip to main content

Javaのログ記録:何を記録し、何を記録しないべきか?

java loggging feature

2020年11月11日

0 分で読めます

ログは、ミスの発見やコードのデバッグに役立つ便利なツールです。エンジニアにとって、特にDevOps環境では、ログは非常に価値のあるツールです。

ログ記録は機能面だけでなく、Javaのセキュリティの観点からも欠かせません。セキュリティ侵害が発生したとき、何が起きたのかを探る最初の手がかりとなるのがログファイルです。しかし、Javaアプリケーションのログ記録に使えるフレームワークが数多くある中で、どれを選べばよいのでしょうか。さらに重要なのは、Javaで適切にログを記録するにはどうすればよいか、ということです。

ログの品質は非常に重要です。記録する情報が多すぎても少なすぎても、Javaのセキュリティに深刻な影響を及ぼす可能性があります。実際、「不十分なログ記録と監視(A10:2017)」はOWASP Top 10に含まれています。

この記事では、実践的なJavaのログ記録方法をご紹介します。何を記録し、何を記録すべきでないのか、そしてJavaで適切にログを実装する方法を解説します。

何を記録すべきか?

軍にいた頃、何かを報告する際には「5W-H」方式を使う必要がありました。誰が、何を、どこで、いつ、なぜ、どのようにというものです。この方法は、ログの記録にも非常に役立つ指針です。

誰が操作したのか?

リクエストを行ったユーザーまたはシステムを特定します。可能であれば、ユーザーIDとIPアドレスも記録しましょう。

何が起きているのか?

どのような種類のログなのかを記録します。多くの開発者は、メッセージの重要度を示すログレベル(ERROR、WARN、INFO、DEBUG、TRACE)をすでに使い分けています。機能ログ、セキュリティログ、必要に応じて監査ログを区別するとよいでしょう。

どこで発生したのか?

発生場所を特定します。通常、クラスごとにロガーを定義するため、それがよい出発点になります。ただし、具体的なメソッドも把握する必要があります。また、たとえばマイクロサービスアーキテクチャを使っている場合は、該当するノードも記録しましょう。

いつ発生したのか?

時刻は不可欠なので、必ずタイムスタンプを記録しましょう。タイムゾーンの違いに注意してください。特に、年に2回タイムゾーンが切り替わる地域もあります。誰にとっても分かりやすいよう、常にUTCで出力することをおすすめします。

なぜ発生したのか?

通常、知りたいのは「なぜ」起きたのかです。これを1行のログだけで捉えられるとは限りません。原因が明らかな場合は、メッセージに含めましょう。多くの場合、ログ同士の関連性を確認して初めて「なぜ」が分かります。そのため、ログメッセージは明確に記述してください。

どのように発生したのか?

「なぜ」と同様に、「どのように」起きたのかも、通常は1つのメッセージだけでは捉えられません。そのため、特にサービスベースのアーキテクチャでは、メッセージ同士を関連付けることが非常に重要です。そのために、アウトバウンドインターフェースを持つ各サービスで、受信リクエストごとに一意の相関IDを作成します。このIDを作成したら、すべてのログメッセージに記録してください。別のサービスにIDを渡す必要がある場合は、専用のHTTPヘッダーX-Correlation-IDを使うことで、ステートレスなまま関連付けられます。

セキュリティイベントのログ記録

Javaは主にバックエンド処理で使われ、データと直接やり取りします。そのため、セキュリティ関連イベントを追跡する必要があります。セキュリティイベントには優先順位を付け、特権の高い操作(管理者の操作)に関するイベントは、特に重要なものとして扱いましょう。

一般的には、次のようなイベントが該当します。

  • ログインの成功と失敗

  • ログアウト

  • パスワードの変更とリセット

  • ユーザーの作成と削除

  • 認可の失敗

  • アクセスレベルの変更

さらに、特定の攻撃を示す可能性がある兆候も記録しましょう。

  • 入力検証エラー:XSS攻撃につながるスクリプトタグや、SQLインジェクションを示すUNIONなどのパラメーターを検知する

  • HTTP 4xxおよび5xxレスポンス

  • 特定のIPアドレスから異常な数のリクエストが届く

  • IPアドレスの範囲を確認する。たとえば、内部サービスに外部IPアドレスからアクセスがあった場合など

特定のセキュリティログイベントにしきい値を設定するのは効果的な方法です。しきい値を超えたときにアラートを送信できます。これにより、適切な対応を行い、被害の拡大を防げます。

何を記録すべきでないか?

情報を記録しすぎることは、記録が少なすぎるのと同じくらい有害です。本番環境で役に立たない情報がログを埋め尽くすのは避けたいものです。そのため、本番システムではWARNやERRORなど、適切なログレベルを設定しましょう。

開発時には、アプリケーションのデバッグのために、個人データを含む可能性のある機密情報をエンジニアがログに記録することがあります。これを防ぐよう努めるべきですが、内部向けのデバッグメッセージにはDEBUGやTRACEなど適切なログレベルを設定し、本番環境には出力されないようにしてください。

一般に、アプリケーション固有またはユーザー固有のデータをアプリケーションから露出させてはいけません。攻撃者に悪用される可能性があるためです。また、個人情報を公開すると、プライバシー関連の規制に準拠できなくなる可能性が高くなります。ログへのアクセスを社内関係者のみに制限していても、ログは単一障害点となります。つまり、こうした情報が悪意のある第三者の手に渡る事態は避けなければなりません。

開発時には、何をログに記録する必要があるのかを考えましょう。内部ID、暗号化されていないパスワード、誰かのクレジットカード情報を記録する必要はあるでしょうか。Javaでデータオブジェクト全体を出力しないよう注意してください。出力内容は、そのオブジェクトのtoString()メソッドに依存します。

デバッグのために個人情報をログに記録すると、自ら定めた多くのセキュリティポリシーを回避することになります。こうした情報は、そもそもログに記録しないのが賢明です。特定のデータを記録する必要がある場合は、DEBUGなど適切なログレベルを設定してください。また、本番システムではWARNなど、より高いログレベルを設定しましょう。

要するに、個人情報や機密情報を本番環境のログに含めてはいけません。

Javaでのセキュリティログ記録

何よりもまず、Javaのロギングフレームワークを使いましょう。当たり前のことのようですが、多くのアプリケーションでは、今でも古いSystem.out.println()を使ってコンソールに直接出力しています。これは絶対に避けるべきです。Spotbugs、PMD、SonarLintなどの優れたリンターが役立ちます。

Javaにはさまざまなロギングフレームワークがあります。この記事では、現在最も広く使われているLogbackとLog4j2を取り上げます。どのフレームワークを使う場合でも、セキュリティログと一般的なアプリケーションログを分けるのが賢明です。LogbackとLog4j2では、ロガーにマーカーを追加するだけで簡単に分離できます。SECURITYやAUDITなどの専用マーカーを使えば、ログから該当する情報を簡単に絞り込めます。また、ロガーにアペンダーを追加し、特定のマーカーでフィルタリングして、ログを別々に保存または表示することもできます。

Logback

Logbackを実装するには、以下のようにlogback-classic依存関係を追加するだけです。これにより、logback-coreとslf4j-apiの両パッケージも取り込まれます。

<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <version>1.2.3</version>
</dependency>

マーカー

以下のように、任意の文字列をマーカーとして設定し、ログレベルに関係なくロガーに追加できます。

private static final Logger logger = LoggerFactory.getLogger(LogbackExample.class);
private static final Marker security = MarkerFactory.getMarker("SECURITY");

public boolean  login(String userName, String encryptedPassword) {
   logger.warn(security, "login attempt by user: {}", userName);
  ...
}

設定

次のLogback設定例では、logback.xmlファイルに2つのアペンダーを作成しています。

最初のアペンダーは、追加したマーカーも含め、すべてのログ行をコンソールに表示します。2つ目のアペンダーは、マーカーSECURITYを基準にセキュリティ関連のログだけを抽出し、日ごとに別のセキュリティログファイルへ保存します。

<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
       <encoder>
           <pattern>%d{HH:mm:ss.SSS} [%thread] %-8marker %-5level %logger{36} - %msg%n</pattern>
       </encoder>
   </appender>

   <timestamp key="byDay" datePattern="yyyyMMdd"/>
   <appender name="SECURITY-FILE" class="ch.qos.logback.core.FileAppender">
       <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
           <evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
               <marker>SECURITY</marker>
           </evaluator>
           <OnMatch>ACCEPT</OnMatch>
           <OnMismatch>DENY</OnMismatch>
       </filter>
       <file> security-${byDay}.log</file>
       <append>true</append>
       <encoder>
           <pattern>%d{HH:mm:ss.SSS} [%thread] %-8marker %-5level %logger{36} - %msg%n</pattern>
       </encoder>
   </appender>

   <root level="warn">
       <appender-ref ref="STDOUT" />
       <appender-ref ref="SECURITY-FILE" />
   </root>
</configuration>

Log4j2

以下のように、使用するにはlog4j-apiとlog4j-coreのパッケージを含める必要があります。

<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-api</artifactId>
   <version>2.13.3</version>
</dependency>

<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-core</artifactId>
   <version>2.13.3</version>
</dependency>

マーカー

マーカーの作成と追加はLogbackの例と似ていますが、今回は専用のLog4jマネージャーを使ってロガーとマーカーを作成します。

private static final Logger logger = LogManager.getLogger(Log4j2Example.class);
private static final Marker security = MarkerManager.getMarker("SECURITY");

public boolean login(String userName, String encryptedPassword) {
   logger.warn(security, "login attempt by user: {}", userName);
  ...
}

設定

Logbackの設定と同様に、Log4j2でも2つのアペンダーを設定し、マーカーを基準にセキュリティログを別ファイルへ保存します。この設定は、クラスパス上のlog4j2.xmlに記述する必要があります。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
   <Appenders>
       <Console name="Console" target="SYSTEM_OUT">
           <PatternLayout pattern="%d{HH:mm:ss.SSS} [%thread] %-8marker %-5level %logger{36} - %msg%n"/>
       </Console>
       <File name="SECURITY-FILE" fileName="securityFile.log">
           <Filters>
               <MarkerFilter marker="SECURITY" onMatch="ACCEPT" onMismatch="DENY"/>
           </Filters>
           <PatternLayout pattern="%d{HH:mm:ss.SSS} [%thread] %-8marker %-5level %logger{36} - %msg%n"/>
       </File>
   </Appenders>

   <Loggers>
       <Root level="WARN">
           <AppenderRef ref="Console"/>
           <AppenderRef ref="SECURITY-FILE"/>
       </Root>
   </Loggers>
</Configuration>

分散システムのログ記録

分散システムでは、サービスごとに単一のファイルへログを記録するのは避けたほうがよいでしょう。代わりに、中央の保存先を使うことをおすすめします。ログを一元的に保存すれば、ログ行を関連付けて問題の可能性を見つけやすくなります。

プロのヒント:ログを一元管理する際は、ログ行をJSON形式に設定すると便利な場合がよくあります。インデックス作成や検索が簡単かつ高速になります。

ログの一元管理に加えて、ログ行を積極的に監視し、アラートを設定することも検討しましょう。ログをもとにリアルタイムのグラフを作成し、あらかじめ設定したしきい値を超えると通知する製品は数多くあります。

まとめ

ログ記録は、ただ行えばよいというものではありません。何をいつ記録するのか、そしてログをどう活用するのかを深く考えることは、迅速な調査や対処に欠かせません。予防に勝る治療はありませんが、システムが侵害される可能性は常にあります。被害の拡大を防ぐために備えておきましょう。

いざという苦い事態が起きたとき、ログが役立つようにしておきましょう!

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpは、実際の依存関係アップグレード307件を対象にAIエージェントをテストします。脆弱性が解消され、ビルドとテストが成功した場合にのみ、修正としてカウントされます。

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。