TT Lab
はじめる
学ぶ 学習パス コース

FDE総合演習:倉庫に同じ注文が3回届いた

外に出せないログ — 顧客が自分で実行するサポートバンドル

TT Labで続きを見る

一言でいうと

ログを外に送れない顧客に必要なのは、「ログを送ってください」という依頼ではなく、顧客が自分のサーバーで実行し、自分の目で確認してから承認できる収集ツールです。許可リストで集め、マスクした件数を書き、同じ入力なら同じバイト列になるようにまとめてはじめて、セキュリティ担当者が承認できます。

なぜ必要なのか

障害の3日目に、顧客の運用チームが言いました。「ログは社内規定で外に出せません。必要なものを整理してくだされば、こちらで取り出します」。FDEがファイル名をいくつか書いて送ったところ、返ってきた圧縮ファイルには、設定ファイルの原本が入っていました。DBのパスワードと決済APIのキーが、そのままでした。顧客のセキュリティチームは、そのファイルが外に出た事実を知ってから、次のバンドルからはすべて事前に確認すると言い、確認に2日ずつかかるようになりました。

問題は人ではなく、手順でした。何を入れるかを毎回人が選ぶと、毎回違う選び方をしますし、何をマスクしたかを書いておかなければ、確認する人は、ファイルを最初から最後まで読むしかありません。確認する人がすばやく承認するには、3つが見える必要があります。何が入ったのか(許可リスト)、何を何件マスクしたのか(manifest)、いま受け取ったファイルが、確認したそのファイルなのか(ハッシュ)。この3つを、人の代わりに、スクリプトが毎回同じように作るのが、サポートバンドルの収集ツールです。

どう動くのか

1. 許可リストで集める。「秘密が入ったファイルを除いて全部」は、拒否リストであり、拒否リストは、知らないファイルを通してしまいます。バージョン(VERSION)、設定(etcの下)、現在のログ(logsの直下の*.log)、サービスの環境のように、入れるものだけを名前で決めます。ローテーションされたapp.log.1、顧客データのディレクトリ、シンボリックリンクは、ルールにないので、自然に外れます。リンクをたどると、サーバーの外のファイル(たとえば、ホストの認証ファイル)が、一緒に入り込むことがあります。

2. 環境は、シェルではなくサービスのものを読む。収集ツールを動かしたシェルの環境変数は、障害と関係がありません。Linuxでは、proc_pid_environ(5)が説明する/proc/<pid>/environに、プロセスが開始されたときの環境が、NULバイトで区切られて入っています。同じドキュメントが指摘するとおり、開始後にプロセスが自分で変えた値は反映されません。順序が実行ごとに変わることがあるので、行に変えたあとで、名前順に並べ替えておきます。

3. マスクは狭く正確に。ルールが広いと、password_min_length: 12やtoken_ttl_sec: 3600のような設定まで消えて、バンドルが役に立たなくなり、狭いと、秘密が漏れます。そこで、「キー名がpassword・secret・token・api_keyで終わる値」、「Bearerのあとの値」、「ドメインのあるメールアドレス」、「BEGINからENDまでの秘密鍵のブロック」のように、形で決めます。順序も重要です。秘密鍵は複数行なので、行単位のルールより先に、ブロックごと置き換える必要があります。マスクした場所には、[REDACTED:token]のように、種類を残します。確認する人は、マーカーを数えて、manifestの数字と照らし合わせられますし、FDEは、「ここにトークンがあった」という事実だけでも、原因を絞り込めます。

4. マスクしてから切る。ログは大きいので、ファイルごとに上限を設け、最近の行だけを残します。先に切って、あとでマスクすると、切れた境界にかかった秘密鍵のブロックは、BEGINの行が消えて、ルールに引っかからず、本文だけが残ります。行の途中で切らないのも、同じ理由です。

5. manifestには、決定的な値だけ。ファイルごとに、パス、サイズ、sha256、マスクの件数、切り詰めの有無を書きます。生成時刻を入れたくなりますが、入れた瞬間に、同じ入力で作ったバンドルが毎回変わってしまいます。

6. 同じ入力なら同じバイト列。reproducible-builds.orgのアーカイブのドキュメントは、tarが、更新時刻、ファイルの順序、所有者の名前と番号、権限を記録するので、結果が揺らぐとまとめ、GNU tarには、--sort=name(1.28から)、--mtime、--owner=0 --group=0 --numeric-ownerを勧めています。Pythonでまとめるなら、tarfileのドキュメントのTarInfoで、mtime・uid・gid・uname・gname・modeを直接決められますが、もう1段あります。gzipのドキュメントは、GzipFileのmtimeを渡さなければ、現在の時刻がヘッダーに入り、時間に依存しない結果が必要なら、mtime=0を渡すように書いています。

以下は、このラボのイメージで、1秒待ちながら、元データのmtimeだけを変えてから、もう一度まとめて、sha256を比べた結果です(実測: GNU tar 1.35、gzip 1.12、Python 3.12.3)。

まとめ方 2回まとめたsha256
tar -czfをオプションなしで 異なる
tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner -czf 同じ
Pythonでtarfile.open(..., "w:gz")にディレクトリをadd 異なる
TarInfoの時刻は固定したが、GzipFileにmtimeを渡さない 異なる
TarInfoの時刻・所有者・権限を固定+GzipFile(mtime=0) 同じ

GNU tarの-zは、実測で、gzipのヘッダーの時刻が0で入りました。一方、Pythonのw:gzは、ヘッダーに現在の時刻が入り、ファイルの内容が同じでも、ハッシュが変わりました。

support-bundle/
  VERSION
  env.txt            ← run/environ 을 줄로 바꿔 정렬
  etc/...            ← 가린 설정
  logs/app.log       ← 가린 뒤 최근 줄만
  manifest.json      ← path·size·sha256·redactions·truncated

現場での姿

顧客のセキュリティ担当者との会話が変わります。以前は、「このファイルに機密情報がないことを、どう保証しますか」という問いに、答える言葉がありませんでした。収集ツールがあれば、「許可リストはこの8行で、マスキングのルールはこの4つで、今回のバンドルでは、トークン33件とメールアドレス45件をマスクしました。受け取ったファイルのsha256は、この値です」と言えます。確認する人は、マーカーの数を数え、ハッシュを比べて、承認します。

顧客ごとに、追加でマスクすべきものも出てきます。社員番号、社内のホスト名、内部のチケット番号のようなものです。これを収集ツールのコードに埋め込むと、顧客ごとに別のバージョンを管理することになり、顧客がルールを自分で直すこともできません。ルールファイルとして受け取って適用し、その種類もmanifestの件数にまとめて見せるほうが、長続きします。

実務で本当に大切なこと

次のラボですること

用意するものとして渡される、顧客サーバーのコピーで、収集の範囲を決め、マスクのフィルターと収集ツールを、順に育てます。manifest、ログのサイズ上限、決定的なtar.gz、顧客のルールファイルを、1段ずつ加え、最後に、用意するものから、受け渡し用のバンドルと、ハッシュ・マスクの報告書を作ります。採点ツールは、毎回秘密を新しく仕込んだサーバーのツリーで、あなたの収集ツールを直接動かしてみます。