3-3. セキュリティログと監視 - 起きていることを見る力

所要時間: 40-60分 ゴール: アクセスログから攻撃の兆候を読み解き、構造化ログ・集約・アラート設計まで一気通貫で考えられる コミット内容: nginx ログ解析の Go スクリプトと検知ルール例を ~/learn/security/day33/ にコミット


前章とのつながり

このレッスンの位置づけ

Level 3 の3本目。「攻撃を防ぐ → 攻撃に気づく」 へ視点を変える章。

「気づけないインシデントは存在しないのと同じ」。本章はインシデント対応の前提条件。

実害シーン: ログを見ていなかった代償

  • Target (2013): 監視システムは攻撃を 検知して警告を上げていた が誰も見ていなかった → 1.1億件流出
  • Sony Pictures (2014): 攻撃者が数ヶ月潜伏、気づかれず → 100TB 流出
  • Equifax (2017): 既知の Struts 脆弱性アラートが見過ごされ続けた → 1.47億件流出

検知能力ではなく「見る運用」の欠如が事故を生む。攻撃の50%以上は「ログを見ていれば気付けた」と言われる。


大前提: なぜログと監視を学ぶか

セキュリティは「防ぐ」だけでなく「起きていることに気付く」が同じくらい重要。完璧な防御は存在しない。だから「侵入されたかどうかを知る能力」がセキュリティ実力の半分を決める。

実例:

  • Target 大規模情報漏洩 (2013): ネットワーク監視システムは攻撃を検知して警告を上げていた。だが誰も見ていなかった。1.1億件の顧客情報が流出した後、調査で「警告は確かに出ていた」と判明。検知能力ではなく運用の欠如が原因。
  • Sony Pictures (2014): 攻撃者は数ヶ月間ネットワーク内に潜伏。誰も気付かなかった。最終的に映画公開予定作品を含む100TBが流出。
  • Uber (2016): 監視ログから攻撃を検知 → 公表せず賞金として攻撃者に金を払って隠蔽 → 2017年に発覚し当時のCSOが訴追。検知後の対応の失敗事例。

逆の例:

  • GitHub (2018, 1.35TbpsのDDoS): 10分以内に Akamai に切り替え、サービス維持。監視と即時対応の優等生
  • Cloudflare (2019, BGP リーク): 27分後に検知・対応開始。詳細なポストモーテムを公開。

ログと監視は「地味で大事」の代表格。攻撃の50%以上は「ログを見ていれば気付けた」と言われる。tail -f auth.log を1時間眺める経験で世界が変わる。


セッション①: ログの種類と「何を書く・書かない」(25-30分)

1. セキュリティ的に重要なログの分類

┌─────────────────────────────────────────────────────┐
│                    監視すべきログ                       │
├─────────────────────────────────────────────────────┤
│ 1. 認証ログ      /var/log/auth.log                   │
│ 2. アクセスログ  nginx access.log / lb access log     │
│ 3. エラーログ    nginx error.log / アプリ exception log │
│ 4. 監査ログ      /var/log/audit/audit.log (auditd)   │
│ 5. システムログ  journalctl, syslog                  │
│ 6. アプリ業務ログ ユーザー操作、決済、権限変更         │
└─────────────────────────────────────────────────────┘

それぞれのログから「読みたいこと」

ログ種類攻撃検知の観点
認証ログブルートフォース、不正アカウント作成、特権昇格
アクセスログパス探索、SQL injection 試行、異常なリクエスト
エラーログ攻撃の副作用(500エラー多発、未知エンドポイントへのアクセス)
監査ログファイル改ざん、権限変更、命令実行
アプリログ異常な業務操作(深夜の管理者操作、大量出金等)

どれか1つだけ見ていても不十分。全部の総合判断が攻撃検知。

2. ログに「書くこと」と「絶対に書かないこと」

機密情報をログに書かない

以下は絶対にログに出力してはいけない:

  1. パスワード(平文・ハッシュ問わず)
  2. APIトークン、JWT、セッションID
  3. クレジットカード番号、CVV、有効期限
  4. 個人情報(PII): 氏名、メール、電話番号、住所、マイナンバー
  5. 暗号鍵、秘密鍵
  6. クエリ文字列全体(パスワードリセットトークンが乗ってることがある)

ログは「侵害された時に最初に盗まれる」。アプリ本体より守りが薄い。Splunk のような集約サーバーに集めるとさらに広く流通する。「ログにあるもの = 漏れた時に漏れるもの」

アンチパターン: 機密情報のログ出力

// NG: リクエスト全文をログに
log.Printf("Request: %+v", req)
 
// req.Header に Authorization: Bearer eyJ... が含まれる
// req.Body に password が含まれる
// → ログから即座に認証情報が盗める
 
// NG: エラー時にユーザー入力を丸ごとログに
log.Printf("Login failed for %s with password %s", username, password)

なぜNGか:

  • ログを盗まれた瞬間に全ユーザー認証情報漏洩
  • 監視ツール(Datadog, Sentry)には画面で平文表示される
  • 多くのコンプライアンス(PCI DSS, GDPR)違反
  • 開発者本人もうっかりログを Slack に貼って二次漏洩

正しいログ出力

// OK: 必要な情報だけ、機密はマスク/IDで参照
logger.Info("login_attempt",
    "user_id", userID,         // ID は OK
    "ip", clientIP,            // IP も OK
    "user_agent", req.UserAgent(),
    "result", "failed",
    // password は絶対に書かない
)
 
// 機密が混ざるリクエストはマスク
log.Printf("Authorization: %s", maskToken(token))  // "Bearer eyJ...xxxx"

PII マスキングのパターン

  • 完全マスク: password***
  • 部分マスク: email: t****@example.comcard: ****1234
  • ハッシュ化: user_id: hash(email) で同一性は保ちつつ平文を出さない
  • トークン化: ID で参照、生データは別 DB(厳格に管理)

GDPR や個人情報保護法でも「マスキング・仮名化」は推奨手法として明記。

3. 構造化ログのセキュリティ利点

# 非構造化(grep しかできない)
2026-05-14 03:14:22 INFO User logged in: alice from 192.168.1.5
 
# 構造化 (JSON Lines)
{"ts":"2026-05-14T03:14:22Z","level":"info","event":"login","user_id":"alice","ip":"192.168.1.5","ua":"Mozilla/5.0..."}

構造化ログがセキュリティ的に強い理由

  • 集約・解析が機械的: SIEM (Splunk/ELK/Loki) で event:login AND result:failed のような検索が即座
  • アラート条件が書きやすい: 「特定ユーザーの失敗ログイン10件/分超」が SQL ライクに書ける
  • 時系列・相関分析: イベント ID で複数ログを結合 → 攻撃の流れを再構成
  • 異常検知の自動化: 機械学習で「通常パターンと違う」を検出

Go の例: 標準ライブラリの log/slog(Go 1.21+)、zap、zerolog。新規プロジェクトは slog 推奨

// Go の slog でセキュリティイベントを構造化
slog.Info("auth_event",
    slog.String("event", "login_failed"),
    slog.String("user_id", userID),
    slog.String("ip", clientIP),
    slog.Int("attempt_count", attemptCount),
    slog.String("reason", "wrong_password"),
)

4. ログ集約 - 分散したログを1箇所に

[サーバーA] ─┐
[サーバーB] ─┼→ [ログエージェント] → [ログサーバー] → [SIEM/可視化]
[サーバーC] ─┘   (Fluentd/Vector)    (Loki/Elastic)   (Grafana/Kibana)
[サーバーN] ─┘

主要な選択肢:

ツール特徴
Fluentd / Fluent BitOSS、軽量、CNCF プロジェクト、設定が豊富
VectorRust製、高速、新進気鋭。Datadog 社製
Loki + GrafanaPrometheus 思想、ラベルベース、安価
Elasticsearch (ELK)高機能、検索強い、運用重い
Splunk商用最強、超高機能、超高額
CloudWatch LogsAWS、雑にやれる、検索遅い・高い
Datadog LogsSaaS 最強、UI 良、高い
Sentryエラー特化、エラー集約と通知

ログ集約が必要な理由

  • サーバー単体のログは消える: サーバー停止・再起動・スケールイン
  • 横串で見たい: 「LB → API → DB の流れ」を1リクエストで追う
  • 分析・可視化: 数百台のログを目で見るのは無理
  • 改ざん耐性: 集約サーバー側に独立保存 = 侵入されたサーバーのログを消されても元が残る

侵入痕跡を消すのは攻撃者の定石。ログを別サーバーに即時転送することで対抗できる。

実務ユースケース

  • マイクロサービス全体の追跡: 1リクエストが10サービスを通る環境では集約必須
  • コンプライアンス対応: PCI DSS は1年、GDPR の事案調査では数年保存が必要なケース
  • インシデント調査: 「先月の3週目に何があった」を後から追える
  • 異常検知の機械学習基盤: 集約データに対してモデルを動かす

セッション②: 不正アクセス検知パターンとアラート設計(25-30分)

5. 検知すべき攻撃パターン

パターン1: ブルートフォース(短時間に多数の認証失敗)

# auth.log から失敗ログインを集計
sudo grep "Failed password" /var/log/auth.log | \
    awk '{print $11}' | sort | uniq -c | sort -rn | head -10
 
# 出力例:
#   1247 116.10.x.x
#    832 45.227.x.x
#    415 198.51.x.x

ブルートフォースの典型パターン

  • 横方向: 同IPから多くのユーザー名で試行(辞書攻撃)
  • 縦方向: 多数IPから同じユーザー名で試行(パスワードスプレー、低速で分散)
  • 時間帯: 業務時間外、深夜帯に集中することが多い

アラート設計の例:

  • 単一IPから5分間に50回以上の失敗 → 警告
  • 全システムで5分間に500回以上の失敗 → 警告(パスワードスプレーの兆候)
  • 過去30日未使用のアカウントへのログイン成功 → 警告(既存アカウント乗っ取りの兆候)

パターン2: 通常と違う場所からのログイン

# 通常: 日本のIPからログイン
2026-05-14 09:00:00 user=alice ip=121.x.x.x (Tokyo, JP) login=success
 
# 異常: 同ユーザーが3分後に別大陸から
2026-05-14 09:03:00 user=alice ip=185.x.x.x (Moscow, RU) login=success

「Impossible Travel」の検出

物理的に移動不可能な距離・時間でのログイン成功 → 認証情報漏洩の兆候。

検出ロジック:

  1. ユーザー毎に直近のログインIP・時刻を記録
  2. 新ログインのIPの地理座標を取得(MaxMind GeoIP)
  3. 直線距離 / 経過時間 = 移動速度
  4. 1000km/h 超 = 飛行機より速い = 不可能 → 警告

Office 365、Google Workspace などが標準実装。AWS GuardDuty も UnauthorizedAccess:IAMUser/ImpossibleTraveler で検知。

パターン3: SQL injection 試行

# nginx access.log でこんなのが流れる
127.0.0.1 - - [14/May/2026:03:14:22] "GET /api/users?id=1+UNION+SELECT+1,2,3 HTTP/1.1" 200
192.168.1.10 - - [14/May/2026:03:14:25] "GET /api/products?q=%27%20OR%201=1-- HTTP/1.1" 200
198.51.100.5 - - [14/May/2026:03:14:30] "POST /login HTTP/1.1" 401

検知パターン(正規表現):

union\s+select
1\s*=\s*1
'\s*or\s+'?\d
'\s*--
information_schema
sleep\s*\(
benchmark\s*\(

WAF が前段にあっても自前検知は必要

CloudFlare、AWS WAF などが「明らかな攻撃」を弾くが:

  • WAF を経由してもログには記録される(攻撃者の存在自体は見える)
  • WAF が見逃すケース(未知のペイロード)に備えて自前検知も必要
  • 「攻撃を受けている」という事実自体が重要なシグナル

パターン4: 異常な User-Agent / スキャナー

# 怪しい User-Agent パターン
sqlmap/1.6
nikto/2.1.5
nmap scripting engine
curl/7.x  ← 一般ユーザーは普通使わない(APIアクセスは例外)
python-requests/2.x
go-http-client/1.1  ← 自前 bot
""(空)  ← 大多数は怪しい
# nginx access.log で User-Agent を集計
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

User-Agent 偽装は容易

攻撃ツールも --user-agent で偽装可能。これだけで判断せず、他のシグナル(リクエスト頻度、パスパターン、IPの評判)と組み合わせて判定。

パターン5: パス探索(ディレクトリトラバーサル試行)

# こういうのが大量に来るのは bot スキャン
GET /.env
GET /.git/config
GET /wp-admin/setup-config.php
GET /phpmyadmin/index.php
GET /actuator/env
GET /admin/login
GET /../../../etc/passwd

404 連発は重要シグナル

同一IPから10秒間に20件以上の 404 → スキャナー濃厚。

対策:

  • fail2ban の nginx-noscript jail を有効化
  • 自前のレート制限(nginx の limit_req_zone
  • 404 を返すパスを集計 → 攻撃トレンドの把握

6. アラート設計の原則

False Positive を減らす設計

アラートが多すぎると全部無視される(「狼少年効果」)。Target 漏洩がまさにこれ。

ベストプラクティス:

  1. Severity を3-4段階に: P0(即対応)/ P1(1時間以内)/ P2(営業時間内)/ P3(情報)
  2. アラート総量上限: 1日10件以内、超えたらルール見直し
  3. 抑制ルール: 既知の bot、CI からのアクセスを除外
  4. 相関分析: 単発じゃなく「ログイン失敗 → 別IPからの成功 → 重要データへのアクセス」のような連鎖を検知
  5. コンテキスト付き通知: 「user=alice の異常」より「user=alice (管理者) が深夜にDB全件取得」の方が動きやすい
# Grafana / Loki でのアラート例
groups:
  - name: security
    rules:
      - alert: HighFailedLoginRate
        expr: |
          sum(rate({app="auth"} |= "login_failed" [5m])) > 1
        for: 5m
        labels:
          severity: P1
        annotations:
          summary: "高頻度の認証失敗を検知"
          description: "{{ $value }} req/sec の認証失敗が継続中"
 
      - alert: PrivilegedAccountLoginUnusualHour
        expr: |
          sum by (user) (
            {app="auth"} |= "login_success" |= "admin"
          ) > 0
        # 業務時間外条件は別途

7. SIEM の入口

SIEM (Security Information and Event Management) = ログ集約 + リアルタイム分析 + アラート + 調査画面を統合したシステム。

製品特徴
Splunk業界標準、超高機能、ライセンス高額
ELK + Elastic SecurityOSS ベース、自前運用可能
Microsoft Sentinelクラウドネイティブ、Azure 統合
WazuhOSS、軽量、ホスト型 IDS と統合
Datadog SecuritySaaS、ログ製品と統合

SIEM を導入するタイミング

  • 小規模: なくてもよい。Grafana + Loki で十分
  • 中規模(50台超 + 顧客データ): 検討開始
  • 大規模 or 規制業界(金融・医療): 必須

製品選定より「何を検知したいか」を先に決める。導入しても運用できる人がいないと「箱だけ買って動かない」。

8. ログ保持期間と改ざん防止

規制 / 業界           推奨保持期間
─────────────────────────────────
一般的なアプリ       3-6ヶ月
PCI DSS(決済)      1年(うち3ヶ月即時アクセス可能)
HIPAA(医療)        6年
SOX(米国上場)      7年
GDPR(事案調査)     必要な期間(明示なし、合理的期間)

改ざん防止の方法

攻撃者の最初の行動は「ログを消す」。これを防ぐ:

  1. 追記専用(append-only): chattr +a /var/log/secure で削除不可に
  2. 外部送信: rsyslog で別サーバーへ即時転送(攻撃者が侵入したサーバー外)
  3. 不変ストレージ: S3 Object Lock、Glacier Vault Lock で「消せないバケット」へ
  4. 暗号学的ハッシュチェーン: 各ログにハッシュ + 前ログのハッシュ。改ざんすると検知(ブロックチェーン的)
  5. WORM ストレージ: Write Once Read Many、書き換え不可ハードウェア

アンチパターン: ログサーバー自体にログイン履歴がない

# NG: SIEM ログサーバーへの管理者ログインが記録されていない
# → ログサーバー自体が改ざんされても誰がいつ入ったか分からない

なぜNGか: 「監視する側を監視する」がない。最後の砦が破られた時に何も残らない。SIEM サーバーへのアクセスは別の SIEM へ送る(メタ監視)。


練習課題: nginx アクセスログから異常検知 Go スクリプト

// access_log_analyzer.go
// nginx access.log を解析して、SQL injection 試行・404連発・User-Agent 異常を検出
package main
 
import (
	"bufio"
	"fmt"
	"log/slog"
	"os"
	"regexp"
	"strings"
)
 
type LogEntry struct {
	IP        string
	Method    string
	Path      string
	Status    string
	UserAgent string
}
 
// 簡易パーサ(実務では nginx-go-parser など使う)
var logPattern = regexp.MustCompile(`^(\S+) - \S+ \[[^\]]+\] "(\S+) ([^"]+)" (\d+) \S+ "[^"]*" "([^"]*)"`)
 
var sqliPatterns = []*regexp.Regexp{
	regexp.MustCompile(`(?i)union\s+select`),
	regexp.MustCompile(`(?i)'\s*or\s+'?\d`),
	regexp.MustCompile(`(?i)1\s*=\s*1`),
	regexp.MustCompile(`(?i)'\s*--`),
	regexp.MustCompile(`(?i)information_schema`),
	regexp.MustCompile(`(?i)sleep\s*\(`),
}
 
var suspiciousUA = []string{
	"sqlmap", "nikto", "nmap", "masscan", "w3af",
}
 
func detectSQLi(path string) (bool, string) {
	for _, p := range sqliPatterns {
		if p.MatchString(path) {
			return true, p.String()
		}
	}
	return false, ""
}
 
func main() {
	file, err := os.Open("/var/log/nginx/access.log")
	if err != nil {
		slog.Error("ファイルが開けない", "err", err)
		os.Exit(1)
	}
	defer file.Close()
 
	notFoundByIP := make(map[string]int)
 
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		matches := logPattern.FindStringSubmatch(scanner.Text())
		if len(matches) < 6 {
			continue
		}
		entry := LogEntry{
			IP: matches[1], Method: matches[2], Path: matches[3],
			Status: matches[4], UserAgent: matches[5],
		}
 
		// SQLi 試行
		if hit, pat := detectSQLi(entry.Path); hit {
			slog.Warn("SQLi試行検知",
				"ip", entry.IP, "path", entry.Path, "pattern", pat,
			)
		}
 
		// 怪しい User-Agent
		uaLower := strings.ToLower(entry.UserAgent)
		for _, ua := range suspiciousUA {
			if strings.Contains(uaLower, ua) {
				slog.Warn("攻撃ツールUA検知",
					"ip", entry.IP, "ua", entry.UserAgent,
				)
				break
			}
		}
 
		// 404 連発
		if entry.Status == "404" {
			notFoundByIP[entry.IP]++
			if notFoundByIP[entry.IP] == 20 {
				slog.Warn("パススキャン疑い",
					"ip", entry.IP, "404_count", notFoundByIP[entry.IP],
				)
			}
		}
	}
 
	// サマリ
	fmt.Println("\n=== 404 多発IP TOP10 ===")
	// 本来はソートして上位10件
	for ip, count := range notFoundByIP {
		if count >= 10 {
			fmt.Printf("  %s: %d\n", ip, count)
		}
	}
}

実行:

go run access_log_analyzer.go

これを cron で毎時動かし、出力を Slack へ送れば「自前 IDS」の最小版になる。本気でやるなら Wazuh や Falco、osquery を使うが、仕組みを理解するために手で書いてみる経験は重要


業界事例: Capital One 1億人漏洩(2019)

何が起きたか: 元AWSエンジニアが Capital One の AWS WAF を悪用し、IAM ロールのクレデンシャルを SSRF で取得。S3 から1億件の個人情報をダウンロード。

重要な観点:

  • S3 のアクセスログにはダウンロード行為がしっかり記録されていた
  • だが、誰も気付かなかった(4ヶ月後に攻撃者自身がGitHubに自慢して発覚)
  • 異常な大量データアクセスをアラートしていれば即検知可能だった

教訓:

  • アクセスログを集めても、アラート設計しないと意味がない
  • 「1日に通常 10MB のアクセス → 突然 30GB」のような単純な閾値でも検知可能だった
  • データアクセスパターンの異常検知は SIEM の基本機能

締め: git で証跡を残す

cd ~/learn/security/day33
git add access_log_analyzer.go detection_rules.yml
git commit -m "feat(security): アクセスログ異常検知スクリプト"

チェックリスト

  • auth.log / access.log / error.log の役割を区別できる
  • ログに書いてはいけないもの6種類を即答できる
  • 構造化ログのメリットを3つ語れる
  • ブルートフォース、Impossible Travel、SQLi 試行、UA異常、パススキャンの5パターンを説明できる
  • False Positive を減らすアラート設計の原則を理解した
  • ログ保持期間の規制要件(PCI DSS = 1年)を覚えた
  • ログ改ざん防止の方法を3つ語れる

詰まった時のチートシート

やりたいことコマンド
認証失敗をIP別集計grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn
ログイン成功履歴grep "Accepted" /var/log/auth.log
直近の sudo 実行journalctl _COMM=sudo
nginx 404 集計awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn
User-Agent 集計awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn
同一IP リクエスト集計awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
追記専用属性付与sudo chattr +a /var/log/secure

「実務OK」基準

  • tail -f /var/log/auth.log を流して異常を即指摘できる
  • 構造化ログでアプリのセキュリティイベントを設計できる
  • 「ログ集約しよう」と言った時に Loki/ELK/Splunk の選び方を語れる
  • 検知ルールを False Positive 視点で評価できる
  • PCI DSS 1年保持などコンプライアンス要件を踏まえた設計ができる

さらに深掘りするなら


メタ認知: 自分の理解度を診断する

30秒セルフテスト

  1. ログに絶対書いてはいけない ものを3つ
  2. 構造化ログにする利点を1文で
  3. False Positive と False Negative、どちらが運用上 致命的 で理由は?
  4. PCI DSS のログ保持期間(目安)
  5. SIEM の役割を1文で

詰まったら → セッション①の分類表、セッション②の検知ルール設計を再読。

次のレッスン

3-4 インシデント対応 で「インシデント対応」を扱う。検知した後、最初の30分で何をすべきか。証跡確保、影響範囲、関係者通知、法的義務、ポストモーテム。「平時の準備」が「有事の差」を生む

間隔反復ポイント

ログ運用は 設定が腐りやすい。3ヶ月に1度「自分が出したいアラートが実際に発火するか」をテストする習慣で、いざという時に動かない事故を防ぐ。