3-3. セキュリティログと監視 - 起きていることを見る力
所要時間: 40-60分 ゴール: アクセスログから攻撃の兆候を読み解き、構造化ログ・集約・アラート設計まで一気通貫で考えられる コミット内容: nginx ログ解析の Go スクリプトと検知ルール例を
~/learn/security/day33/にコミット
前章とのつながり
このレッスンの位置づけ
Level 3 の3本目。「攻撃を防ぐ → 攻撃に気づく」 へ視点を変える章。
- 3-1 サーバーhardening: 攻撃を 防ぐ
- 3-2 依存スキャン: 脆弱性を 検出する
- 本章 (3-3): 攻撃を 観察する = 起きていることに気づく力
- 次章 3-4 インシデント対応: 気づいた後 どう動くか
「気づけないインシデントは存在しないのと同じ」。本章はインシデント対応の前提条件。
実害シーン: ログを見ていなかった代償
- 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. ログに「書くこと」と「絶対に書かないこと」
機密情報をログに書かない
以下は絶対にログに出力してはいけない:
- パスワード(平文・ハッシュ問わず)
- APIトークン、JWT、セッションID
- クレジットカード番号、CVV、有効期限
- 個人情報(PII): 氏名、メール、電話番号、住所、マイナンバー
- 暗号鍵、秘密鍵
- クエリ文字列全体(パスワードリセットトークンが乗ってることがある)
ログは「侵害された時に最初に盗まれる」。アプリ本体より守りが薄い。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.com、card: ****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 Bit | OSS、軽量、CNCF プロジェクト、設定が豊富 |
| Vector | Rust製、高速、新進気鋭。Datadog 社製 |
| Loki + Grafana | Prometheus 思想、ラベルベース、安価 |
| Elasticsearch (ELK) | 高機能、検索強い、運用重い |
| Splunk | 商用最強、超高機能、超高額 |
| CloudWatch Logs | AWS、雑にやれる、検索遅い・高い |
| Datadog Logs | SaaS 最強、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」の検出
物理的に移動不可能な距離・時間でのログイン成功 → 認証情報漏洩の兆候。
検出ロジック:
- ユーザー毎に直近のログインIP・時刻を記録
- 新ログインのIPの地理座標を取得(MaxMind GeoIP)
- 直線距離 / 経過時間 = 移動速度
- 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 -20User-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/passwd404 連発は重要シグナル
同一IPから10秒間に20件以上の 404 → スキャナー濃厚。
対策:
- fail2ban の
nginx-noscriptjail を有効化- 自前のレート制限(nginx の
limit_req_zone)- 404 を返すパスを集計 → 攻撃トレンドの把握
6. アラート設計の原則
False Positive を減らす設計
アラートが多すぎると全部無視される(「狼少年効果」)。Target 漏洩がまさにこれ。
ベストプラクティス:
- Severity を3-4段階に: P0(即対応)/ P1(1時間以内)/ P2(営業時間内)/ P3(情報)
- アラート総量上限: 1日10件以内、超えたらルール見直し
- 抑制ルール: 既知の bot、CI からのアクセスを除外
- 相関分析: 単発じゃなく「ログイン失敗 → 別IPからの成功 → 重要データへのアクセス」のような連鎖を検知
- コンテキスト付き通知: 「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 Security | OSS ベース、自前運用可能 |
| Microsoft Sentinel | クラウドネイティブ、Azure 統合 |
| Wazuh | OSS、軽量、ホスト型 IDS と統合 |
| Datadog Security | SaaS、ログ製品と統合 |
SIEM を導入するタイミング
- 小規模: なくてもよい。Grafana + Loki で十分
- 中規模(50台超 + 顧客データ): 検討開始
- 大規模 or 規制業界(金融・医療): 必須
製品選定より「何を検知したいか」を先に決める。導入しても運用できる人がいないと「箱だけ買って動かない」。
8. ログ保持期間と改ざん防止
規制 / 業界 推奨保持期間
─────────────────────────────────
一般的なアプリ 3-6ヶ月
PCI DSS(決済) 1年(うち3ヶ月即時アクセス可能)
HIPAA(医療) 6年
SOX(米国上場) 7年
GDPR(事案調査) 必要な期間(明示なし、合理的期間)改ざん防止の方法
攻撃者の最初の行動は「ログを消す」。これを防ぐ:
- 追記専用(append-only):
chattr +a /var/log/secureで削除不可に- 外部送信: rsyslog で別サーバーへ即時転送(攻撃者が侵入したサーバー外)
- 不変ストレージ: S3 Object Lock、Glacier Vault Lock で「消せないバケット」へ
- 暗号学的ハッシュチェーン: 各ログにハッシュ + 前ログのハッシュ。改ざんすると検知(ブロックチェーン的)
- 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年保持などコンプライアンス要件を踏まえた設計ができる
さらに深掘りするなら
- 公式: OWASP Logging Cheat Sheet
- 公式: NIST SP 800-92 (Guide to Computer Security Log Management)
- ツール: Loki + Grafana, Wazuh, Falco
- 書籍: 『The Practice of Network Security Monitoring』Richard Bejtlich - NSM のバイブル
- 事例: Verizon DBIR (Data Breach Investigations Report) - 毎年公開される漏洩事例の統計
メタ認知: 自分の理解度を診断する
30秒セルフテスト
- ログに絶対書いてはいけない ものを3つ
- 構造化ログにする利点を1文で
- False Positive と False Negative、どちらが運用上 致命的 で理由は?
- PCI DSS のログ保持期間(目安)
- SIEM の役割を1文で
詰まったら → セッション①の分類表、セッション②の検知ルール設計を再読。
次のレッスン
3-4 インシデント対応 で「インシデント対応」を扱う。検知した後、最初の30分で何をすべきか。証跡確保、影響範囲、関係者通知、法的義務、ポストモーテム。「平時の準備」が「有事の差」を生む。
間隔反復ポイント
ログ運用は 設定が腐りやすい。3ヶ月に1度「自分が出したいアラートが実際に発火するか」をテストする習慣で、いざという時に動かない事故を防ぐ。