ネットワークエンジニア

FortiGateのログをGoogle Cloudで完全可視化!BigQuery×データポータル(Looker Studio)で作る「月次セキュリティ健康診断レポート」構築ガイド

FortiGateのログをGoogle Cloudで完全可視化!BigQuery×データポータル(Looker Studio)で作る「月次セキュリティ健康診断レポート」構築ガイド

「オフィスや自宅に FortiGate を導入したものの、管理画面をほとんど開いていない」
「内蔵メモリだけでは数日分のログしか残らず、過去の通信履歴を追跡できない」
「専用ログ分析製品(FortiAnalyzer 等)は高額で、中小規模の現場には導入しづらい」

ネットワークやセキュリティの現場で、こうした悩みを抱えるインフラ担当者や経営者は少なくありません。UTM(統合脅威管理)は設置しただけでは真価を発揮せず、「どんな攻撃を防ぎ、社内にどんなリスク端末が潜んでいるか」を可視化して初めて投資対効果が生まれます。

そこで今回は、実機の FortiGate 60E からインターネット越し(拠点間VPN不要)に Google Cloud へ Syslog を転送し、Vector ➔ Cloud Storage ➔ BigQuery ➔ データポータル(Looker Studio) を組み合わせて「月次セキュリティ健康診断レポート」を自動生成する分析基盤をゼロから構築・検証しました。

本記事では、コピペで再現できる設定ファイル(vector.toml や BigQuery の SQL ビュー)に加え、実機検証で遭遇した Ubuntu 26.04 LTS の sudo-rs バグや BigQuery のスキーマ自動検出エラー、さらには FortiOS の TLS 自己署名証明書検証の壁といったリアルな突破法、そして稼働後に判明した**「1日30円」のリアルなクラウド費用内訳**まで、一次情報を余すことなく公開します。

💡 この記事でわかること(検証サマリー)

  • VPN不要・クラウド直集約:拠点側に追加機器を置かず、FortiGate 標準機能だけで安全にクラウドへログを集約する設計(将来の TLS 6514 暗号化へのロードマップ含む)
  • 生ログの自動JSON化:Rust製軽量コレクター「Vector」を使い、難解な key=value 形式を BigQuery 用 JSON へ自動変換する手順
  • 実機トラブル解決録:Ubuntu 26.04 の sudo-rs 拒否現象、外からの攻撃遮断(local-in-deny)が出ない罠、BigQuery 型エラーの回避策
  • 手作業ゼロの全自動パイプライン:BigQuery 外部テーブルを使い、18.6万件のログが Cloud Storage から即座に自動反映される仕組み
  • データポータル(Looker Studio)での健康診断レポート構築:SQL ビュー1枚で「外からの攻撃(1,428件遮断)」「古いOS端末(Android 8.0等)」「特権ID監査」を可視化する技術
  • AIによる月次診断サマリーの自動生成:情報処理安全確保支援士・PMP視点を取り入れ、経営層に刺さる「総合判定・推奨アクション」を自動執筆させる方法
  • リアルな運用コスト:1日約30円(月約1,000円)の課金正体と、複数拠点を束ねて1社あたり月20円で運用するマルチテナント設計

目次

  1. 全体アーキテクチャ設計:なぜ Vector × GCS × BigQuery なのか?
  2. 【ハンズオン前半】Google Cloud 受信基盤(GCS・GCE・Vector)の構築
  3. 【実機トラブル解決録】現場でハマった5つの罠と突破法
  4. 【ハンズオン後半】BigQuery ビューとデータポータル(Looker Studio)で作る「月次セキュリティ健康診断レポート」
  5. 【リアルな費用検証】「1日30円」の正体と、マルチテナント運用の損益分岐点
  6. まとめ:ログの「死蔵」を終わらせ、攻めのセキュリティ診断へ

第1章:全体アーキテクチャ設計:なぜ Vector × GCS × BigQuery なのか?

FortiGate の内蔵メモリ(Memory Log)は容量が小さく、通信量が多い環境では数時間〜数日で古いログが上書きされて消えてしまいます。かといって、拠点ごとにローカルの Syslog サーバーを立てると、そのサーバー自体の保守やディスク容量監視という新たな運用負荷が発生します。

この課題を解決するため、今回は Google Cloud 上に以下のサーバーレス中心のパイプラインを設計しました。

flowchart LR
    FG["FortiGate 60E<br/>(拠点ルーター)"] -- "Syslog (UDP 514)<br/>※将来TLS 6514対応" --> VM["GCE (e2-micro)<br/>Vector コレクター"]
    VM -- "JSON変換 & Gzip圧縮<br/>devid別・日付別仕分け" --> GCS[("Cloud Storage<br/>長期アーカイブ保管")]
    GCS -- "自動同期 (外部テーブル)" --> BQ[("BigQuery<br/>SQL分析・ビュー変換")]
    BQ -- "リアルタイム集計" --> LS["データポータル (Looker Studio)<br/>月次セキュリティ診断レポート"]

この構成を採用した3つの技術的理由

  1. 拠点間 VPN なしでクラウドへ直集約(将来の TLS 6514 暗号化への拡張性)
    FortiOS は標準で Syslog 送信機能を備えており、拠点側に専用 VPN ルーターや集約 PC を追加することなく、Google Cloud の受信基盤へログを直接届けることができます。まずは安定したデータ収集を実証しつつ、将来的に RFC 5425 準拠の TLS 暗号化 Syslog(TCP 6514)へシームレスに拡張可能な構成としています。
  2. Rust 製の超軽量コレクター「Vector」によるリアルタイム整形
    FortiGate が吐き出す Syslog は date=2026-10-18 time=09:33:19 devid="FGT60E..." srcip=192.168.1.10 ... という独自の key=value 形式です。これをそのままデータベースへ入れると分析が困難になります。Datadog が開発を主導するオープンソースの Rust 製データパイプライン「Vector」を GCE(e2-micro)上で動かすことで、メモリ消費量わずか 1.5MB で key=value を美しい JSON へリアルタイム変換し、機器シリアル番号(devid)ごとのフォルダへ自動仕分けできます。
  3. Cloud Storage × BigQuery × データポータル(Looker Studio)の圧倒的な分析力
    変換後のログは json.gz(圧縮された改行区切りJSON)として Cloud Storage に格安で長期保管しつつ、BigQuery へ読み込ませることで、数百万行のログでも数秒で SQL 集計が可能になります。さらに Google の無料 BI ツールである データポータル(Looker Studio) を繋げば、経営層やクライアントへそのまま提出できる PDF レポートへと昇華できます。

第2章:【ハンズオン前半】Google Cloud 受信基盤(GCS・GCE・Vector)の構築

ここからは、実際に Google Cloud プロジェクト(fortigate-log-lab)を作成し、ログ受信基盤を構築した手順をステップバイステップで解説します。

Step 1:ログ保管庫(Cloud Storage バケット)の作成

まずは、Vector が整形した JSON ログを保存するバケットを作成します。

  • バケット名:任意の一意な名前(今回は fortigate-logs-20261016)
  • ロケーションタイプ:Region(asia-northeast1 東京)
  • ストレージクラス:Standard
  • 公開アクセスの防止:オン(必須)

Cloud Storage バケットの作成完了画面

Step 2:Syslog 受信サーバー(GCE e2-micro)の作成

続いて、FortiGate からの Syslog を24時間待ち受ける最小構成の仮想マシン(Compute Engine)を作成します。

  • 名前:forti-syslog-receiver
  • リージョン:asia-northeast1(東京)
  • マシンタイプ:e2-micro(2 vCPU、1 GB メモリ)
  • ブートディスク:Ubuntu 26.04 LTS Minimal(x86/64)、10 GB 標準永続ディスク
  • アクセススコープ:「すべての Cloud API に完全アクセス権を許可」を選択(※これを有効にしておくことで、サービスアカウントキーの JSON ファイルを手動配置することなく、VM から Cloud Storage へ安全にファイルを書き込めます)

GCE インスタンスの作成設定画面

Step 3:VPC ファイアウォールルールの作成

初期状態では外部からの Syslog 通信が遮断されているため、拠点(FortiGate 側のグローバルIP)からの通信のみを許可するファイアウォールルールを追加します。

  • 名前:allow-fortigate-syslog
  • トラフィックの方向:上り(内向き)
  • ソース IPv4 範囲:<FortiGate拠点のグローバルIP>/32(※セキュリティ担保のため、0.0.0.0/0 全開放ではなく拠点IPに絞ることを強く推奨します)
  • プロトコルとポート:
    • UDP:514(今回の実証パイプラインで使用)
    • TCP:514, 6514(将来の TLS 暗号化転送拡張用)

VPC ファイアウォールルールの作成画面

Step 4:Vector のインストールと自己署名 TLS 証明書の発行

VM が起動したら、Google Cloud コンソールの「ブラウザでの SSH」からログインし、Vector 本体のインストールと、TLS 待ち受け用のサーバー証明書(有効期限10年)を作成します。

# 1. Vector 公式インストールスクリプトの実行
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev | bash -s -- -y
sudo cp ~/.vector/bin/vector /usr/local/bin/

# 2. TLS証明書格納ディレクトリの作成と自己署名証明書の発行
sudo mkdir -p /etc/vector/certs
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/vector/certs/server.key \
  -out /etc/vector/certs/server.crt \
  -subj "/CN=forti-syslog-receiver"
sudo chmod 644 /etc/vector/certs/server.key /etc/vector/certs/server.crt

Vector のインストールと TLS 証明書作成画面

Step 5:vector.toml の作成とサービス起動

ここがパイプラインの心臓部です。UDP 514 番と将来用の TLS TCP 6514 番の両方でログを待ち受けられるよう定義し、先頭の Syslog プライオリティ(<189> 等)を除去したうえで、parse_key_value 関数によって全フィールドを JSON キーへ自動展開します。さらに Cloud Storage へ保存する際、devid={{ devid }}/dt={{ date }}/ というプレフィックスで自動的にフォルダ分けを行います。 (※今回の実機検証では、まず安定した UDP 514 番でパイプラインを稼働させ、18.6万件のログ収集・分析基盤を確立しています)

# /etc/vector/vector.toml

# ① 入力1:テスト用 UDP 514番ポート待ち受け
[sources.fortigate_udp]
type = "syslog"
address = "0.0.0.0:514"
mode = "udp"

# ② 入力2:本番用 TLS暗号化 TCP 6514番ポート待ち受け
[sources.fortigate_tls]
type = "socket"
address = "0.0.0.0:6514"
mode = "tcp"
tls.enabled = true
tls.crt_file = "/etc/vector/certs/server.crt"
tls.key_file = "/etc/vector/certs/server.key"

# ③ 変換処理:FortiGateの key=value 形式を綺麗なJSONに自動変換
[transforms.parse_fortigate]
type = "remap"
inputs = ["fortigate_udp", "fortigate_tls"]
source = '''
raw_msg = string!(.message)
clean_msg = replace(raw_msg, r'^<\d+>\s*', "")

parsed, err = parse_key_value(clean_msg)
if err == null {
  . = parsed
}

if !exists(.devid) {
  .devid = "unknown_device"
}
if !exists(.date) {
  .date = format_timestamp!(now(), format: "%Y-%m-%d")
}
'''

# ④ 出力先1:画面(journalctl)にもリアルタイム表示(動作確認用)
[sinks.console_debug]
type = "console"
inputs = ["parse_fortigate"]
encoding.codec = "json"

# ⑤ 出力先2:Google Cloud Storage バケットへ自動保存(1分ごと)
[sinks.gcs_archive]
type = "gcp_cloud_storage"
inputs = ["parse_fortigate"]
bucket = "fortigate-logs-20261016"
key_prefix = "devid={{ devid }}/dt={{ date }}/"
filename_time_format = "%H%M%S"
filename_append_uuid = true
filename_extension = "json.gz"
compression = "gzip"
encoding.codec = "json"
framing.method = "newline_delimited"
batch.max_secs = 60

設定ファイルを作成したら、Systemd サービスとして登録して起動します。起動後のステータスを確認すると、メモリ使用量はわずか 1.5M(1.5MB) で軽快に稼働していることがわかります。

vector.toml の設定と systemctl status の起動確認画面


第3章:【実機トラブル解決録】現場でハマった4つの罠と突破法

机上の設計図どおりにすんなり動かないのがインフラ構築のリアルです。今回の検証でも、最新OSの仕様変更や FortiGate 特有のログ構造によって4つのトラブルに遭遇しました。同じ構成を試す方が迷わないよう、原因と解決コマンドをすべて共有します。

🔥 罠①:Ubuntu 26.04 LTS の sudo が映画『2001年宇宙の旅』のセリフで拒否してくる怪現象

Vector の設定ファイルを書き終え、sudo systemctl enable --now vector を実行しようとした瞬間、ターミナルに見慣れない不気味な英語メッセージが表示されました。

sudo: I'm sorry [username]. I'm afraid I can't do that

Ubuntu 26.04 LTS の sudo-rs エラーと突破画面

「えっ、ハッキングされた!?」と一瞬背筋が凍るようなメッセージですが、実はこれ、映画『2001年宇宙の旅』に登場する人工知能 HAL 9000 の有名なセリフ(イースターエッグ)です。

  • 原因:Ubuntu 25.10 / 26.04 LTS から、従来の C 言語製 sudo に代わって Rust 製の sudo-rs がデフォルト採用されました。Google Cloud のブラウザ SSH ログイン直後、google-guest-agent による google-sudoers グループの権限同期タイミングと sudo-rs の判定が噛み合わず、権限拒否(HAL 9000 メッセージ)が発生することがあります。
  • 突破法:システム内に残されている従来の C 言語版バイナリ(/usr/bin/sudo.ws)を直接呼び出して root シェルへ昇格することで、即座に回避できました。
# sudo-rs が権限エラーを出す場合の緊急回避コマンド
/usr/bin/sudo.ws -i

🔥 罠②:Syslog を有効化したのに1件も届かない!execute telnet と tcpdump による3分切り分け術

続いて、自宅の FortiGate 60E の管理画面(GUI:「ログ&レポート」>「ログ設定」)から「Syslogロギング」をオンにし、GCE の外部 IP アドレスを入力して適用しました。

FortiGate 60E の GUI ログ設定画面

ところが、GCE 側でログを待ち受けても 1行もログが流れてきません。こうした通信トラブル時は、闇雲に設定をいじるのではなく「① 経路(ファイアウォール)の問題か」「② プロセス(Vector)の問題か」をコマンドで切り分けます。

FortiGate 管理画面右上の >_ アイコンから CLI コンソールを開き、以下の2つのコマンドを実行しました。

# 切り分け1:GCPのSyslogポート(6514)へTCP接続できるか?
FTG60E # execute telnet <GCEの外部IP> 6514
Trying <GCEの外部IP>...
Connected to <GCEの外部IP>.  <-- ★ 接続成功!

# 切り分け2:VM側でパケット自体が着弾しているか?(GCE側で実行)
root@forti-syslog-receiver:~# tcpdump -i any port 514 or port 6514 -nn

実は当初、Google Cloud 側のファイアウォールに登録していた自宅グローバルIPと、実際の FortiGate WAN 側 IP が異なっていたためパケットがドロップしていました。送信元 IP を修正した瞬間、tcpdump に怒涛の勢いでパケットが着弾し、journalctl -u vector -f の画面に美しく JSON 化された FortiGate ログがリアルタイムで流れ始めました!

tcpdump でのパケット到達確認と Vector の JSON 変換ログ

【現場のリアルな教訓】TCP 6514(TLS暗号化)への切り替えと自己署名証明書の壁

UDP 514 でのログ受信が確認できたため、続いて FortiGate の CLI から以下のコマンドを投入し、Syslog over TLS(TCP 6514 / mode reliable) への切り替えを試みました。

config log syslogd setting
    set status enable
    set server "<GCEの外部IP>"
    set mode reliable
    set port 6514
    set enc-algorithm high
end

FortiGate CLI での TLS 設定画面

しかし、ここで現場ならではの大きな壁に直面しました。FortiOS で set mode reliable(RFC 5425 準拠の TLS 通信)を有効化すると、FortiGate 側が接続先サーバー(GCE 上の Vector)の SSL 証明書を厳格に検証します。今回 Vector 側で発行した証明書はオレオレ証明書(自己署名証明書)だったため、ハンドシェイク時に証明書エラーとなり、セッションが切断されてログ送信がストップしてしまったのです。

FortiGate に自己署名証明書の CA をインポートするか、Let's Encrypt などのパブリック証明書を導入すれば TLS 化は可能ですが、今回はまず「Google Cloud 上に全自動のログ蓄積・BigQuery 分析・Looker Studio レポート基盤を完成させること」を最優先としました。

そのため、今回は FortiGate 側の設定を一旦 UDP 514 に戻して本番ログの収集とパイプラインの検証を先行させました(※ポート 6514 による完全 TLS 暗号化は、証明書運用の整備とあわせて次回の強化フェーズで実施予定です)。

こうして UDP 514 で安定したログ受信を確立したことで、1分ごとに Cloud Storage バケット内の devid=FGT60E.../dt=2026-10-18/*.json.gz へリアルタイムに自動保存されるようになりました。

🔥 罠③:BigQuery ロード時に Rows: 1044; errors: 1 で全件失敗するスキーマ自動検出の罠

Cloud Storage に溜まった .json.gz ファイルを BigQuery のテーブル(fortigate_logs.raw_logs)へ読み込ませようとした際、以下のエラーでジョブが失敗しました。

Error while reading data, error message: JSON parsing error in row starting at position 398972:
No such field: parsed.sentbyte. Rows: 1044; errors: 1.
  • 原因:FortiGate のログは、通信が許可された時(action="accept")と遮断された時(action="deny")、あるいはシステムイベントログで出力される項目(キーの種類)が1行ごとに変化します。BigQuery の「スキーマ自動検出」は先頭付近の行をサンプリングして列定義を決めるため、後半の行に「先頭には無かった新しい項目」が登場するとエラー扱いになり、デフォルト設定(許容エラー数=0)では1,044行すべてがロールバックされてしまいます。
  • 突破法:BigQuery のテーブル作成画面の下部にある「詳細オプション」を開き、以下の2項目を設定することで一発で解決しました。
    1. 許容されるエラー数(Max bad records):1000 を入力
    2. 不明な値(Ignore unknown values):チェックを ON にする(スキーマにない余分なキーが来てもエラーにせず無視して取り込む設定)

BigQuery テーブル作成画面の詳細オプション設定

この設定を入れてテーブルを作成したところ、わずか数秒で 2,530 件のログが BigQuery にロードされ、標準 SQL で国別通信ランキングや端末別通信量を瞬時に集計できるようになりました!

BigQuery での SQL クエリ集計結果

🔥 罠④:BigQuery の TIME 型カラムに SUBSTR を使うと型エラーになる問題

BigQuery 上で「時間帯別(00時〜23時)の通信件数」を集計しようと SUBSTR(time, 1, 2) を実行したところ、以下のエラーが発生しました。

No matching signature for function SUBSTR
Argument types: TIME, INT64, INT64
  • 原因:BigQuery のスキーマ自動検出が優秀すぎるあまり、"time": "09:33:19" という文字列を単なる STRING ではなく、時刻専用の TIME 型として自動変換していたため、文字列切り出し関数 SUBSTR が直接使えなくなっていました。
  • 突破法:SUBSTR(CAST(time AS STRING), 1, 2) のように一度 STRING 型へ明示的にキャストするか、EXTRACT(HOUR FROM time) を使うことで綺麗に解決できます。

🔥 罠⑤:FortiGate の初期設定では「外からの攻撃遮断(Ingress Deny)」が1行もログに出ない罠

ログ収集基盤を立ち上げて通信ログを可視化した際、社内から外部への通信(Egress)は大量に出るものの、「外部(インターネット)からの攻撃・スキャン遮断ログ」が1件も記録されていないことに気づきました。

  • 原因:FortiGate(FortiOS)の初期設定では、大量のポートスキャンによって本体メモリが枯渇するのを防ぐため、**FortiGate 自身の WAN IP 宛てに届く不正アクセスを遮断したログ(local-in-policy の破棄ログ)がデフォルトで無効(disable)**になっています。つまり、FortiGate は水際で攻撃をしっかり防御しているにもかかわらず、「ブロックした」という記録を Syslog へ1行も吐き出さない仕様になっているのです。
  • 突破法:FortiGate の CLI から以下のコマンドを投入し、着信拒否ログの出力を明示的に有効化します。
config log setting
    set local-in-deny-unicast enable
    set local-in-deny-broadcast enable
end

あわせて、GUI の「ポリシー&オブジェクト > ファイアウォールポリシー」の一番下にある「暗黙のDeny(Implicit Deny)」の設定を開き、「違反トラフィックのログ記録」を有効(ON)にします。この設定を入れた瞬間、海外からの無差別ポートスキャン遮断ログが怒涛の勢いで BigQuery へ流れ込み、1,400件超の外部攻撃ブロック実績がレポート上に浮かび上がりました!


第4章:【ハンズオン後半】BigQuery ビューとデータポータル(Looker Studio)で作る「月次セキュリティ健康診断レポート」

BigQuery にデータが入った段階で、すぐに Google の無料 BI ツール「データポータル(Looker Studio)」へ接続してグラフを作ってみたのが以下の画面(Before)です。

データポータル(Looker Studio)初期状態(Before:白背景で英語カラム名が並ぶ状態)

グラフ自体は描画できていますが、これでは**「月次セキュリティレポート」としてクライアントや経営層に提出するには不十分**です。なぜなら、srccountry や action、osname といった生の英語カラム名がそのまま露出しており、「結局うちのネットワークは安全なのか?」「どの端末を対処すべきなのか?」が一目で伝わらないからです。

解決策:BigQuery 側に「翻訳&自動リスク診断ビュー(v_security_report)」を噛ませる

データポータル(Looker Studio)側で複雑な計算フィールドを作るのではなく、前段の BigQuery 側で「① 英語カラムの日本語化」「② 内向き(Ingress)/外向き(Egress)の方向判定」「③ 古い危険なOS端末の自動フラグ付け」、さらに「④ VPN不正アクセス試行や管理者ログイン・設定変更履歴(イベントログ)の自動抽出」までをすべて済ませた仮想テーブル(ビュー)を作成します。

実は FortiGate のログ設定で「イベントロギング:すべて」を有効にしているため、raw_logs テーブルには通常の通信ログ(type = 'traffic')だけでなく、VPN のログイン失敗・接続履歴(subtype = 'vpn')や、管理画面へのログイン成否・設定変更履歴(subtype = 'system')といった重要な監査イベント(type = 'event')もすでに記録されています。

これらを一つのビューで統合し、既存のグラフを壊さずに「VPN・管理者アクセス監査」までカバーできる完全な SQL コードがこちらです。

CREATE OR REPLACE VIEW `fortigate_logs.v_security_report` AS
SELECT
  date,
  time,
  SUBSTR(CAST(time AS STRING), 1, 2) AS hour_of_day,
  devid,
  devname,
  -- ① アクションの日本語化
  CASE
    WHEN action = 'accept' THEN '🟢 許可 (Accept)'
    WHEN action = 'deny' THEN '🔴 遮断 (Deny)'
    WHEN action = 'close' THEN '⚪ 正常終了 (Close)'
    WHEN action IN ('timeout', 'client-rst', 'server-rst') THEN '🟡 切断/Timeout'
    ELSE CONCAT('⚪ その他 (', COALESCE(CAST(action AS STRING), 'unknown'), ')')
  END AS action_label,
  action,
  -- ② 通信の向き(Ingress / Egress / Local)の自動分類
  CASE
    WHEN srcintfrole = 'wan' THEN '📥 外部からの着信 (Ingress)'
    WHEN srcintfrole = 'lan' AND dstintfrole = 'wan' THEN '📤 社内から外部へ (Egress)'
    ELSE '🏠 社内/ローカル通信 (Local)'
  END AS traffic_direction,
  -- ③ 端末情報の整理(srchwvendor / srcswversion)
  srcip,
  srcmac,
  COALESCE(CAST(srcname AS STRING), CAST(srcip AS STRING), '不明な端末') AS device_name,
  COALESCE(CAST(srchwvendor AS STRING), '不明') AS vendor,
  COALESCE(CAST(osname AS STRING), '不明') AS os_name,
  COALESCE(CAST(srcswversion AS STRING), '-') AS os_version,
  -- ④ 端末のセキュリティリスク自動判定
  CASE
    WHEN osname = 'Android' AND SAFE_CAST(SPLIT(CAST(srcswversion AS STRING), '.')[OFFSET(0)] AS INT64) <= 10 THEN '⚠️ 要注意 (古いOSバージョン)'
    WHEN dstcountry NOT IN ('Japan', 'United States', 'Canada', 'Reserved', 'Germany', 'United Kingdom', 'Italy', 'Singapore') AND dstcountry IS NOT NULL THEN '⚠️ 要確認 (マイナー国への通信)'
    WHEN srcname IS NULL AND srcintfrole = 'lan' THEN '🟡 端末名未登録'
    ELSE '✅ 正常'
  END AS security_status,
  -- ⑤ 通信先・サービス・通信量(数値化)
  COALESCE(CAST(dstcountry AS STRING), 'ローカル/不明') AS dst_country,
  dstip,
  SAFE_CAST(dstport AS INT64) AS dst_port,
  COALESCE(CAST(service AS STRING), '不明') AS service,
  SAFE_CAST(sentbyte AS INT64) AS sent_bytes,
  SAFE_CAST(rcvdbyte AS INT64) AS rcvd_bytes,
  ROUND((COALESCE(SAFE_CAST(sentbyte AS INT64), 0) + COALESCE(SAFE_CAST(rcvdbyte AS INT64), 0)) / 1024.0 / 1024.0, 3) AS total_mb,
  -- ⑥ 【追加】VPN不正アクセス&管理者アクセス監査(未定義列エラーを100%防ぐJSON抽出)
  CAST(type AS STRING) AS log_type,
  CASE
    WHEN type = 'event' AND (
      JSON_VALUE(TO_JSON_STRING(t), '$.subtype') = 'vpn'
      OR LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) LIKE '%vpn%'
      OR CAST(action AS STRING) LIKE 'ssl-login%'
      OR CAST(action AS STRING) = 'negotiate'
    ) THEN
      CASE
        WHEN CAST(action AS STRING) IN ('ssl-login-fail', 'ipsec-login-fail')
          OR LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) LIKE '%fail%'
          OR LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) LIKE '%error%'
          OR JSON_VALUE(TO_JSON_STRING(t), '$.status') = 'failed'
          THEN '🚨 VPN不正ログイン・攻撃試行'
        ELSE '🔐 VPN正常接続・トンネル通信'
      END
    WHEN type = 'event' AND (
      LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) LIKE '%admin%'
      OR CAST(action AS STRING) IN ('login', 'logout', 'Edit', 'Add', 'Delete')
      OR JSON_VALUE(TO_JSON_STRING(t), '$.cfgpath') IS NOT NULL
    ) THEN
      CASE
        WHEN LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) LIKE '%fail%'
          OR JSON_VALUE(TO_JSON_STRING(t), '$.status') = 'failed'
          THEN '🚨 管理者ログイン失敗(不正試行)'
        ELSE '👤 管理者ログイン・設定変更履歴'
      END
    WHEN type = 'event' THEN 'ℹ️ システムイベント(定常)'
    ELSE '🌐 一般トラフィック'
  END AS audit_category,
  CASE
    WHEN CAST(action AS STRING) = 'ssl-login-fail' OR LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) LIKE '%ssl vpn login fail%' THEN 'SSL-VPN ログイン失敗(不正試行)'
    WHEN LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) LIKE '%ipsec phase 1 error%' THEN 'IPsec VPN フェーズ1認証エラー'
    WHEN LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) = 'admin login failed' THEN '管理画面(GUI/SSH)ログイン失敗'
    WHEN LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) = 'admin login successful' THEN '管理画面(GUI/SSH)ログイン成功'
    WHEN LOWER(COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), '')) = 'admin logout successful' THEN '管理画面ログアウト'
    WHEN CAST(action AS STRING) IN ('Edit', 'Add', 'Delete') OR JSON_VALUE(TO_JSON_STRING(t), '$.cfgpath') IS NOT NULL THEN CONCAT('設定変更: ', COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.cfgpath'), JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), 'コンフィグ編集'))
    ELSE COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.logdesc'), CAST(action AS STRING), '通常通信')
  END AS audit_event_label,
  COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.user'), '未特定') AS audit_user,
  COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.ui'), CAST(srcip AS STRING), '-') AS audit_src_ip,
  COALESCE(JSON_VALUE(TO_JSON_STRING(t), '$.msg'), JSON_VALUE(TO_JSON_STRING(t), '$.reason'), JSON_VALUE(TO_JSON_STRING(t), '$.cfgattr'), '-') AS audit_detail
FROM
  `fortigate_logs.raw_logs_auto` AS t;

TIP

BigQuery スキーマ自動検出の未定義エラーを100%防ぐ裏技
FortiGate のイベント専用項目(user や ui、cfgpath など)は、初回ロード時のサンプリング行に含まれていない場合、直接 SELECT cfgpath と書くと Unrecognized name: cfgpath エラーになります。上記 SQL のように JSON_VALUE(TO_JSON_STRING(t), '$.cfgpath') と記述しておけば、列が存在する場合は安全に値を取り出し、存在しない場合でもエラーにならず NULL を返してくれます。

手作業ゼロへ:BigQuery 外部テーブル(raw_logs_auto)による全自動リアルタイム更新

「毎月レポートを作るたびに bq load コマンドを手動で叩くのは面倒だ」と感じる方も多いはずです。そこで今回は、Cloud Storage のバケットに新しいログファイルが保存された瞬間、人間の手作業を一切介さずに BigQuery とデータポータル(Looker Studio)へ即時反映される「外部テーブル(External Table)」 を導入しました。

VM 上で以下のコマンドを実行するだけで、全自動更新用の外部テーブル raw_logs_auto が完成します。

# 1. 既存のログファイルから外部テーブル定義ファイルを自動生成
bq mkdef --autodetect --source_format=NEWLINE_DELIMITED_JSON "gs://fortigate-logs-20261016/devid=FGT60ETK20008408/*.json.gz" > /tmp/table_def.json

# 2. 将来の日付フォルダもすべて自動捕捉し、dstport: "N/A" などの警告を解消する設定を注入
python3 -c '
import json
with open("/tmp/table_def.json") as f:
    d = json.load(f)
d["sourceUris"] = ["gs://fortigate-logs-20261016/devid=FGT60ETK20008408/*.json.gz"]
d["ignoreUnknownValues"] = True
d["maxBadRecords"] = 1000
for field in d.get("schema", {}).get("fields", []):
    if field.get("name") in ["dstport", "srcport"]:
        field["type"] = "STRING"
with open("/tmp/table_def.json", "w") as f:
    json.dump(d, f, indent=2)
'

# 3. 外部テーブル「raw_logs_auto」を作成
bq rm -f -t fortigate_logs.raw_logs_auto 2>/dev/null || true
bq mk --external_table_definition=/tmp/table_def.json fortigate_logs.raw_logs_auto

この設定により、過去ログから最新ログまで 18万6,137件のログが一瞬でマージ され、手作業によるロード作業が完全になくなりました!


月別・日別のドロップダウンとカレンダー期間選択の実装

クライアントや経営層にダッシュボードを共有する際、「先月の状況はどうだったか?」「特定の日(インシデント発生日など)だけをドリルダウンしたい」という要望にスムーズに応えられるよう、レポート上部に2つのコントローラーを設置します。

  1. 期間設定コントロール(カレンダー/今月・先月選択):
    • 上部メニュー「コントロールを追加」>「期間設定」を配置するだけで、ワンクリックで「今月」「先月」「過去30日間」のデータへ画面全体のグラフ・表が瞬時に切り替わります。
  2. プルダウン リスト(日付・年月単位の絞り込み):
    • 上部メニュー「コントロールを追加」>「プルダウン リスト」を配置し、コントロールフィールドに date を設定します。プロパティの日付アイコンからデータタイプを「年、月(YYYY/MM)」に切り替えることで、月別選択ドロップダウンとしても活用できます。

18.6万件の実機データから「AI月次セキュリティ健康診断書」を自動生成する

ダッシュボードのグラフが揃った段階で、クライアントや経営層が本当に知りたいのは「で、今月うちは安全なのか?どんな脅威を防ぎ、何を改善すべきなのか?」というプロの診断結論です。

そこで、BigQuery のビューに対して以下の集計クエリを実行し、その集計数値を AI(生成AI・LLM)に入力してエグゼクティブ向けの「月次セキュリティ健康診断サマリー」 を生成させました。

SELECT
  MIN(date) AS start_date,
  MAX(date) AS end_date,
  COUNT(*) AS total_logs,
  COUNTIF(action_label LIKE '%遮断%') AS blocked_attacks,
  COUNTIF(action_label LIKE '%許可%') AS accepted_traffic,
  COUNTIF(traffic_direction LIKE '%外部から内部%' OR traffic_direction LIKE '%Ingress%') AS ingress_attacks,
  COUNTIF(security_status LIKE '%要注意%' OR security_status LIKE '%危険%') AS risky_devices_logs,
  COUNTIF(audit_category LIKE '%不正%' OR audit_category LIKE '%失敗%') AS security_alerts
FROM
  `fortigate_logs.v_security_report`;

実際に 186,137 件のログを集計した結果がこちらです:

[{
  "start_date": "2026-10-18",
  "end_date": "2026-10-20",
  "total_logs": "186137",
  "blocked_attacks": "31040",
  "accepted_traffic": "106482",
  "ingress_attacks": "1428",
  "risky_devices_logs": "16457",
  "security_alerts": "0"
}]

この実数値をベースに、情報処理安全確保支援士・PMP の視点を取り入れて AI が生成した診断コメントがこちらです。データポータル(Looker Studio)の最上段にあるテキスト枠にそのまま貼り付けて提出できます。

【月次セキュリティ健康診断結果】 総合判定:B+(境界防御は極めて強固 / 社内端末の分離を推奨)

■ 診断総評:
2026年10月18日〜10月20日の通信ログ(計186,137件)を精査した結果、外部からの不正侵入および特権ID侵害の兆候は確認されませんでした。境界防御において計31,040件の不要・不正通信を正常にブロックしています。

■ 重点監査項目と分析結果:
1. 【外部攻撃遮断(Ingress)】★防御成功:インターネット側からの無差別ポートスキャンおよび侵入試行が計1,428件着弾しましたが、FortiGate のローカル防御ポリシーにより 100% 水際で破棄(Deny)され、社内への到達はゼロでした。
2. 【社内端末リスク(古いOS)】⚠️ 改善推奨:サポート終了済みの古いOS(Android 8.0 等)に起因する通信が 16,457件 検知されました。既知の脆弱性を悪用されるリスクがあるため、業務ネットワークからゲストWi-Fiへの隔離を推奨します。
3. 【管理者・VPNアクセス監査】✅ 正常:管理画面およびVPNに対する不正ログイン試行は 0件 であり、特権アクセスの安全性は保たれています。

■ 今月の推奨アクション:
・古いAndroid端末(Sharp製等)のWi-Fi接続先を「ゲスト用ネットワーク」へ切り替え(優先度:中)

単に「今月も何事もありませんでした」と報告するのではなく、「1,428件の攻撃が実際に来ていたが、FortiGate がすべて止めたため安全だった」と数値で証明できるため、セキュリティ投資の必要性と保守契約の価値をクライアントへ強烈にアピールできます。


完成した「月次セキュリティ健康診断レポート」(After)

この v_security_report ビューをデータポータル(Looker Studio)に接続し、テーマをダークモード(「星座 / Constellation」テーマ)に変更してレイアウトした完成形がこちらです!

データポータル(Looker Studio)完成形(After:月次セキュリティ健康診断レポート)

白背景の素っ気ない画面から一変し、セキュリティ専門ベンダーの SOC(Security Operation Center)レポートのような重厚感あるダッシュボードへ生まれ変わりました。実際にこのレポート画面(スクリーンショット)から読み取れる3つの重要な発見と機能を解説します。

  1. 社内リスクの自動検知:⚠️ 要注意 (Sharp / Android 8.0) 端末(252件・3.87MB)の発見
    レポート下部の端末一覧テーブルの2行目に、黄色い警告アイコン付きで Android / Sharp / バージョン 8.0(252件・3.87MB) が浮かび上がりました。自宅の Wi-Fi に繋がっていた古い Sharp 製スマートフォン(または Android TV 系家電)が、サポート切れの Android 8.0 のまま外部と通信していたことが判明し、レポート上部の総評コメントにも「LAN内の1端末(Sharp / Android 8.0)がサポート終了OSのため、ゲストWi-Fiへの分離を推奨します」と具体的な改善提案を記載できています。人間の目で生ログを眺めていては気づけない「シャドーIT・放置端末」が、SQL ビューの条件分岐によって一発で可視化された瞬間です。
  2. 「クロスフィルタリング(連動絞り込み)」によるリスク端末の通信先追跡
    実は上記のスクリーンショットは、下部の表の2行目(Sharp / Android 8.0)をマウスで1回クリックして選択した状態を撮影しています。注目していただきたいのは、表の1行をクリックしただけで、上部のスコアカード(Record Count: 252、total_mb: 3.87)、左の円グラフ(通信先国:Canada 44%、United States 37.7%、Japan 12.3%、Germany 4% 等)、右の棒グラフ(🟢 許可 (Accept) / ⚪ 正常終了 (Close))がすべて**「その古い Android 8.0 端末だけの通信内訳」へリアルタイムに連動して切り替わっている**点です。「要注意の端末を見つけたら、その行をクリックするだけで、どの国のサーバーへ何MB通信していたかを即座にドリルダウン追跡できる」のは、データポータル(Looker Studio)ならではの強力な分析機能です。
  3. ブロック実績(deny)および VPN不正アクセス・管理者操作履歴(イベント監査)の可視化
    前章の BigQuery 集計画面でも、udp/5353 の遮断(deny:580件)や中国(China)等へのタイムアウト通信(13件)が数値で記録されていました。さらに、先述の SQL で追加した audit_category(監査種別)と audit_event_label(イベント内容)を使った「🔐 VPN不正アクセス&管理者操作履歴」の表をレポート下部に配置し、フィルタで audit_category = 🌐 一般トラフィック を除外することで、以下の重要イベントだけを一覧表示できます。
    • 🚨 VPN不正ログイン・攻撃試行:外部からの SSL-VPN パスワード総当たり攻撃(ssl-login-fail)や IPsec フェーズ1認証エラーの試行回数・攻撃元IP
    • 🚨 管理者ログイン失敗(不正試行):管理画面(GUI/SSH)への不正ログイン試行と試行されたアカウント名(audit_user)
    • 👤 管理者ログイン・設定変更履歴:いつ・誰が管理画面にログインし、どの設定(cfgpath = log.syslogd.setting 等)を変更したかの監査証跡

情報セキュリティ監査(ISMS / ISO27001 やプライバシーマーク、取引先からのセキュリティチェックシート)では「特権ID(管理者)のアクセスログおよび設定変更履歴を保存・点検しているか」「VPN等の外部接続に対する不正アクセスを監視しているか」が高頻度で問われます。このテーブルを月次レポートに1つ載せておくだけで、単なる通信集計を超えた「監査対応レベルのセキュリティレポート」としてそのまま提出できるようになります。


第5章:【リアルな費用検証】「1日30円」の正体と、マルチテナント運用の損益分岐点

ダッシュボードが完成して一晩稼働させた翌日、Google Cloud の課金画面をチェックしたところ、「1日あたり約30円」の費用が発生していることが判明しました。

「えっ、無料枠の範囲内で収まるはずでは?」と疑問に思う方のために、なぜ1日30円(月額換算で約960円〜1,120円)が発生したのか、その正確な内訳とコスト最適化の結論を共有します。

なぜ「1日30円(月約1,000円)」がかかったのか?

Google Cloud の無料枠(Always Free)の公式仕様(参考:Google Cloud 公式ドキュメント:Free Tier プログラム)と照らし合わせると、犯人は明確です。

利用サービス 今回の構成・設定 月額概算費用(24時間稼働時) 課金の理由・無料枠との違い
Compute Engine (VM) e2-micro(東京 asia-northeast1) 約 760円 / 月(約25円/日) e2-micro の無料枠は米国3リージョン(us-central1 等)のみ対象。東京リージョンは時間課金が発生する。
固定外部 IPv4 アドレス VM に付与されたグローバルIP 約 360円 / 月(約12円/日) 2024年以降の価格改定により、使用中の外部 IPv4 アドレスにも約 $0.005/時間 の課金が発生する。
Cloud Storage Standard(数MB〜数GB) 約 1円未満 / 月 1GBあたり約3円/月。テキスト圧縮ログ(.json.gz)は極めて軽量なためほぼ0円。
BigQuery ストレージ&SQLクエリ 0円 毎月 ストレージ 10GB・クエリ分析 1TB まで完全無料。数千〜数百万行の規模なら無料枠内で余裕で収まる。
データポータル(Looker Studio) レポート閲覧・PDF出力 0円 標準機能はすべて完全無料。
合計(24時間稼働時) - 約 1,000円〜1,120円 / 月 1日あたり約 30円〜37円

つまり、**ログの保管(Cloud Storage)や分析(BigQuery)、可視化(データポータル/Looker Studio)にかかっていた費用は「0円」**であり、1日30円の正体はすべて「東京リージョンで24時間電源を入れていた受信VM(e2-micro)と外部IPの基本料金」だったのです。

運用目的に応じた2つのコスト最適化戦略

この検証結果から、用途に応じたベストプラクティスは以下の2通りに整理できます。

① 自宅ラボ・個人検証で使う場合(目標コスト:月0円〜数十円)

  • 使わない時は VM を「停止(Stop)」しておく:Compute Engine のインスタンスを停止すれば、CPU・メモリ課金も外部IP課金(エフェメラルIPの場合)も即座に 0円 になります。10GB の標準永続ディスク代(月約60円=1日2円)しかかかりません。なお、すでに BigQuery へ取り込んだデータは VM を停止しても消えないため、データポータル(Looker Studio)のレポートは24時間いつでも無料で閲覧・編集できます。
  • 24時間稼働させたい場合は米国リージョン(us-central1 等)に VM を置く:Syslog の送信はリアルタイム通信(音声通話など)ではないため、日本から米国アイオワ州へ送信して 0.1 秒の遅延があっても実用上の影響はゼロです。これにより VM 本体代(約760円/月)が無料枠で 0 円になります。

② 法人向け「月次セキュリティ診断サービス(MSSP)」として事業化する場合

もしあなたが IT コンサルタントや情シス支援事業者として、クライアント企業へ「月額 10,000円〜30,000円のセキュリティ健康診断レポートサービス」を提供する場合、この「月1,000円(1日30円)」の見え方は180度変わります。

なぜなら、顧客が10社・50社に増えても、Google Cloud 側の受信 VM(forti-syslog-receiver)は「最初の1台(月1,000円)」を使い回せるからです。

第2章で設定した vector.toml の以下の1行を思い出してください。

key_prefix = "devid={{ devid }}/dt={{ date }}/"

FortiGate から送られてくるログには、機器固有のシリアル番号(devid)が含まれています。Vector はこの devid を読み取って、A社のログは devid=FGT_A社/、B社のログは devid=FGT_B社/ へと1台の VM で自動的にフォルダを仕分けます。e2-micro 上の Vector はメモリを 1.5MB しか使っていないため、50社〜100社分の FortiGate から同時に Syslog を受信しても余裕で処理できます。

  • 1社だけで使う場合:インフラ原価 約1,000円 / 社
  • 10社を収容する場合:インフラ原価 約100円 / 社
  • 50社を収容する場合:インフラ原価 約20円 / 社(粗利率 99% 超)

高価な専用アプライアンスを顧客ごとに購入・設置することなく、クラウド側のサーバーレス基盤をマルチテナント共有することで、圧倒的なコスト競争力を持ったセキュリティ診断サービスを構築できます。


第6章:まとめ:ログの「死蔵」を終わらせ、攻めのセキュリティ診断へ

今回の実機検証を通じて、FortiGate 60E から Google Cloud への安定した Syslog 転送から、BigQuery での自動リスク判定、データポータル(Looker Studio)による「月次セキュリティ健康診断レポート」の構築まで、すべてのパイプラインが実用レベルで稼働することを実証できました。

最後に、本記事の重要ポイントを振り返ります。

  1. 拠点に機器を足さないシンプル設計:FortiGate 標準の Syslog 送信と GCE 上の Vector を組み合わせることで、高価な中継サーバーや専用 VPN なしでクラウドへログを直集約できる(※将来の TCP 6514 TLS 暗号化への拡張性も確保)。
  2. 生ログは Vector と BigQuery ビューで「意味のある言葉」に翻訳する:parse_key_value で JSON 化し、BigQuery の SQL ビューで「外からの侵入試行(Ingress)」や「古いOS端末(⚠️ 要注意)」へ変換することで、誰でも直感的にリスクを把握できるレポートになる。
  3. トラブル時は切り分けコマンドと現場の仕様を把握する:Ubuntu 26.04 の sudo-rs 問題(/usr/bin/sudo.ws -i)、FortiGate からの疎通確認(execute telnet)、FortiOS の証明書検証仕様、BigQuery のスキーマ不一致回避(不明な値: ON)を押さえておけば、現場で詰まることはない。
  4. マルチテナント化で圧倒的なコストパフォーマンスを実現:VM 1台(月約1,000円)で数十拠点の FortiGate を devid 別に同時収容できるため、社内複数拠点の統合監視や、顧客向けセキュリティ診断事業の基盤として極めて優秀である。

「UTM を入れたけれど、毎月どんな攻撃を防いでいるのか分からない」「社内に古い端末が放置されていないか不安だ」という方は、ぜひ週末のハンズオンとして Google Cloud と FortiGate を繋ぎ、ご自身のネットワークの「健康診断」を試してみてください! (※次回は、正規証明書を配備した「Syslog over TLS(TCP 6514)完全暗号化編」の知見もお届けする予定です)


実践ノウハウ & 個人開発ツール公開中

インフラ設計・セキュリティ実装から自律AI組織の運用ログまで無料公開!

当ブログ『LEEの自宅サーバ運用記』では、インフラエンジニア・PMP・情報処理安全確保支援士の知見を活かし、実機検証に基づくクラウド・ネットワーク構築ガイドや、AIエージェント自律組織「Jet Studio」の実践記録を発信しています。

  • クラウド&セキュリティ実機検証: FortiGate × Google Cloud ログ分析基盤・ゼロトラスト設定の徹底解説
  • 自律AI組織の舞台裏: 人間1名×AIエージェントチームで回す開発・マーケティング運用のリアル
  • 難関資格&PM支援ツール開発: 記憶定着ドリル『忘却バスター』・シンプル進捗管理『Simple PM』

著者プロフィール

LEE
インフラエンジニア / PMP®(Project Management Professional) / 情報処理安全確保支援士 / ネットワークスペシャリスト。
製造業から30代未経験でIT業界へ転職。現場でのネットワーク更改やセキュリティ監査、大規模プロジェクトマネジメントに従事。個人ブログ『LEEの自宅サーバ運用記(https://jetree.work/)』にて、実機を用いたインフラ・セキュリティ検証や、人間1名×自律AIエージェントによる次世代スタジオ「Jet Studio」の運営ノウハウをオープンに発信中。

LEE
Author

LEE

現役インフラエンジニア / PMP / Jet Studio 代表

30代未経験からインフラの世界に入り、実務1.5年でネスペ一発合格。現在はSI管理職を務めつつ、AIエージェント自律組織の研究・開発や、無料Webドリル『忘却バスター』・プロジェクト管理シート『Simple PM』を自作公開しています。

PMP (2026年取得) 情報処理安全確保支援士 (2024年合格) ネットワークスペシャリスト (2023年合格) 自作Webドリル『忘却バスター』開発者 PMツール『Simple PM』開発者