サーバー運用者の危機対応

鍵やトークンが公開されたとき

鍵が公開の場所に出たとき、無効にする順番、いつから・誰が取得できたかの調べ方、履歴の扱い、公開の前に止める手順を練習します。

  • レベル: 基礎
  • 約40分
  • 2026-11-12

#状況

公開中のサイトの JavaScript に、AWS の鍵が入っていました

同僚から連絡が来ました。「サイトの app.js に、AWS のアクセスキーがそのまま書いてある。」 調べると、リポジトリの src/upload.js に鍵が直接書かれ、それがビルドされて公開フォルダーに置かれていました。ソースマップ(app.js.map)も一緒に公開されています。

鍵やトークンが公開の場所に出たとき、運用者が決めることは多くありません。ただ、順番を間違えると、その間ずっと鍵が使えるままになります。消す前に無効にする。履歴を直す前に無効にする。この講座は、その順番と、調べ方を手順にした playbook です。

鍵の形式や検査の仕組みそのものは、認証情報の盗難と、秘密情報の漏えい で扱いました。ここでは、見つかったあとの判断と手順を、使い捨ての VM の中で実際に動かして確かめます。

順番すること使うもの
1. 見るリポジトリと公開フォルダーの鍵を見つけるAPI キー・秘密鍵の検出、公開されるフロントエンドの秘密の検査
2. 調べるいつから・どこに・誰が取得できたかを確かめるgit log -S、アクセスログ、アクセスログの診断(AccessScope)
3. 対応する発行元で無効にし、履歴を扱い、差し替える発行元の管理画面、git filter-repo
4. 防ぐ鍵が入ったまま公開しようとしたら止めるroamswitch scan-frontend の終了コード
5. 元に戻す練習の場を片付ける
練習は、使い捨ての環境で、無効な鍵で

この講座の鍵は、AWS の公式文書に載っている無効なサンプル(AKIAIOSFODNN7EXAMPLE と、対になるシークレットアクセスキー)です。本物の鍵は使いません。使う検査はどれも手元のファイルを読むだけで、外部のサービスには何も送りません。サイトへのアクセスも、VM の中からだけ行います。

#練習の場を用意する

VM に nginx と git-filter-repo を入れ、ダミーのリポジトリを作ります。2 つ目のコミットで鍵が入り、ビルドした app.js とソースマップを公開フォルダーへ置きます。コミットの日時は、場面に合わせて指定しています。

ダミーのリポジトリと、鍵の入った公開ファイルを作る

パッケージを入れ、ファイルを置く操作です(変更を伴う操作です)。片付けは、最後の演習の後にあります。

sudo apt-get install -y nginx git-filter-repo
mkdir -p ~/shop/src && cd ~/shop
git init -q -b main
git config user.name "Dev Sample"; git config user.email dev@example.com
echo '# shop (dummy for practice)' > README.md
printf 'import { upload } from "./upload.js";\nupload();\n' > src/main.js
git add -A
GIT_AUTHOR_DATE="2026-10-20T10:00:00+09:00" GIT_COMMITTER_DATE="2026-10-20T10:00:00+09:00" \
  git commit -qm "Initial commit"
cat > src/upload.js <<'J'
// Practice only: the invalid sample key from the AWS documentation
const cfg = {
  accessKeyId: "AKIAIOSFODNN7EXAMPLE",
  secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
};
export function upload() { return cfg.accessKeyId.length; }
J
git add -A
GIT_AUTHOR_DATE="2026-10-22T15:30:00+09:00" GIT_COMMITTER_DATE="2026-10-22T15:30:00+09:00" \
  git commit -qm "Add image upload"
# Build and publish (the way the key reached the public folder)
mkdir -p dist
{ cat src/upload.js src/main.js; echo '//# sourceMappingURL=app.js.map'; } > dist/app.js
echo '{"version":3,"sources":["src/upload.js","src/main.js"],"mappings":""}' > dist/app.js.map
sudo cp dist/app.js dist/app.js.map /var/www/html/
git log --oneline
実行結果
96653d2 Add image upload
c5e1f37 Initial commit

#検知を読む

鍵が置かれる場所は、2 つに分かれます。リポジトリの中のソースと、公開フォルダーに置かれたビルドの成果物です。調べるコマンドも 2 つあります。

リポジトリと公開フォルダーを検査する

どちらも読むだけで、ファイルを変えません。リポジトリは audit-secrets、公開フォルダーは scan-frontend で調べます。

cd ~
roamswitch audit-secrets ./shop
sudo -E roamswitch scan-frontend /var/www/html
echo "exit code: $?"
実行結果
=====================================================
  🔑 機密情報・APIキー漏洩監査(ディレクトリ)
=====================================================

  3件のファイルを走査しました。

  ./shop/src/upload.js
    ⚠️  AWS Access Key ID (AWS) — 行番号 3
        マスク済み: AKIAIO...MPLE
        ↳ 推奨対応: AWS IAMコンソールからアクセスキーを非アクティブ化してください。

  合計 1 件の露出(1 ファイル)。コミット・共有前に対処してください。
=====================================================
  フロントエンドの公開ファイルの確認
=====================================================

調べた場所: .(ファイル 3 件)
- 機密情報(APIキー・秘密鍵など): app.js:3 AWS AKIAIO...MPLE
- ソースマップ(元のソースコードが読めてしまう): app.js.map
2 件(機密情報 1、トークン 0、ソースマップ 1)
見つかったキーやトークンは、ファイルをすでに公開していた場合、流出したものとして扱ってください。発行元で無効にして、新しく作り直します。秘密情報はビルドに含めず、サーバー側に置いてください。ソースマップは、本番に置かないか、取得できる人を限ってください。
exit code: 1

読むところは次のとおりです。

  • audit-secrets が調べたのは 3 件です。.git と dist は、この検査では数えません。公開されたビルドは、scan-frontend が受け持ちます
  • upload.js の 4 行目にあるシークレットアクセスキーは、出ていません。決まった形がないためです。対になる ID(3 行目)が見つかったら、同じ組の鍵がすべて漏れたと考えます
  • scan-frontend はソースマップも挙げます。ソースマップがあると、元のソースが読めます
  • scan-frontend は、見つけると終了コードが 1 になります。audit-secrets も同じです。この性質は、最後の対策で使います

#いつから・どこに・誰が取得できたか

無効にする前に、長く調べる必要はありません。ただ、発行元のログで何を探すかを決めるために、いつから公開されていたかは押さえます。ここでは、外の訪問者に見立てて、VM の中から公開フォルダーへアクセスしてから調べます。

コミットの日時、公開の時刻、取得した相手を確かめる

1 つ目のコマンドは、ふつうの訪問者と、.env などを探して回る相手のアクセスを作ります。VM 自身のアドレスを、外の相手に見立てます。

IP=$(hostname -I | awk '{print $1}')
curl -s -o /dev/null -A "Mozilla/5.0" http://127.0.0.1/app.js
for p in /.env /.git/config /app.js /app.js.map /.aws/credentials; do
  curl -s -o /dev/null -A "python-requests/2.31" "http://$IP$p"
done
cd ~/shop
git log -S AKIAIOSFODNN7EXAMPLE --date=iso --format='%h %ad %an <%ae> %s'
stat -c '%n %U:%G %a %y' /var/www/html/app.js src/upload.js
sudo grep -E '"GET /app\.js' /var/log/nginx/access.log
sudo -E accessscope scan /var/log/nginx/access.log
出力の抜粋
96653d2 2026-10-22 15:30:00 +0900 Dev Sample <dev@example.com> Add image upload
/var/www/html/app.js root:root 644 2026-11-12 01:37:30.967044820 +0000
src/upload.js admin:admin 644 2026-11-12 01:37:30.855203262 +0000
127.0.0.1 - - [12/Nov/2026:01:37:31 +0000] "GET /app.js HTTP/1.1" 200 327 "-" "Mozilla/5.0"
192.168.64.12 - - [12/Nov/2026:01:37:31 +0000] "GET /app.js HTTP/1.1" 200 327 "-" "python-requests/2.31"
192.168.64.12 - - [12/Nov/2026:01:37:31 +0000] "GET /app.js.map HTTP/1.1" 200 70 "-" "python-requests/2.31"

| # | 深刻度 | 確度 | 検知 | 件数 | 主な送信元 |
|---|---|---|---|---|---|
| 1 | 低 | 中 | 秘密・設定ファイル・管理画面の探索 | 3 (1 IP) | 192.168.64.12(3) |
  • いつから: git log -S は、その文字列が入ったコミットを出します。鍵が入ったのは 10 月 22 日 15:30(日本時間)です。発行元のログは、この日時から後を調べます
  • どこに: stat の時刻は、公開フォルダーに置かれた時刻です。+0000 は UTC で、01:37 は日本時間の 10:37 です。実際の運用では、公開されていた期間は、これより前の配布も含めて考えます
  • 誰が取得できたか: アクセスログに、app.js を 200 で受け取った相手が 2 つあります。app.js はふつうのページの一部なので、訪れた人は全員、鍵を受け取っています
  • AccessScope が挙げた 3 件は、.env・.git/config・.aws/credentials を探したアクセスです。--report FILE で根拠のログ行が出ます。ふつうのファイルの取得は、探索として数えません
アクセスログで分かるのは「取得した」ことまで

アクセスログは、誰がファイルを受け取ったかを示します。その鍵が使われたかは、示しません。使われたかどうかは、発行元(AWS なら CloudTrail など)の操作ログでしか分かりません。

#無効にする順番

鍵が公開の場所に出たら、ファイルを消すより先に、発行元で無効にします。ここだけは RoamSwitch の外で行います。検査の推奨対応にも「AWS IAMコンソールからアクセスキーを非アクティブ化してください」とあります。

  1. 発行元で無効にする。AWS なら、IAM でそのアクセスキーを無効にします
  2. 使われた形跡を調べる。発行元の操作ログを、鍵が入った日時から後について見ます。無効にしたあとでも、ログは残ります
  3. 新しい鍵を作り、置き場所を変える。ブラウザに渡るファイルには入れません
  4. ソースと履歴から取り除く。次の章で扱います
  5. 公開の前に止める仕組みを入れる。最後の章で扱います
すぐ無効にするか、差し替えてから無効にするか

鍵を無効にすると、その鍵で動いていた機能は止まります。公開の場所(公開サイト、公開リポジトリ)に出た鍵は、すぐ無効にします。 機能が止まることは受け入れます。誰が持っているか分からないからです。社内のリポジトリだけに入り、読めた人が限られるときは、新しい鍵を先に作って差し替え、その直後に古い鍵を無効にしても構いません。どちらでも、古い鍵は必ず無効にします。

やってはいけないこと
  • ファイルから消しただけで、無効にしないまま終える
  • 履歴の書き換えを、無効にするより先にする(その間も鍵は使えます)
  • 「本当に使えるか」を確かめるために、漏れた鍵を自分で使ってみる
  • 状況を共有するために、鍵の全体をチャットや Issue に貼る(マスクされた表示を使います)

#履歴の扱い

ソースから鍵を消してコミットしても、鍵は過去のコミットに残ります。audit-secrets はフォルダーを調べるとき .git に入らないので、履歴は別に調べます。

消したあとも、履歴に鍵が残ることを確かめる

鍵を使う処理をサーバー側に移したことにして、upload.js を書き換えてコミットします。そのあと、今のファイルと、過去のすべての版を調べます。

cd ~/shop
cat > src/upload.js <<'J'
// The key now stays on the server. The page asks the server for a one-time upload URL.
export async function upload() { return (await fetch("/api/upload-url")).ok; }
J
GIT_AUTHOR_DATE="2026-11-04T10:50:00+09:00" GIT_COMMITTER_DATE="2026-11-04T10:50:00+09:00" \
  git commit -qam "Move upload signing to the server"
roamswitch audit-secrets ./src                   # the current files
roamswitch audit-secrets "$(git log -p --all)"   # every past version
実行結果
=====================================================
  🔑 機密情報・APIキー漏洩監査(ディレクトリ)
=====================================================

  2件のファイルを走査しました。

  機密情報・APIキーの露出は検出されませんでした。
=====================================================
  🔑 機密情報・APIキー漏洩監査
=====================================================

  2件の機密情報・APIキー露出を検出しました。コミットや外部共有前に削除してください。

  ⚠️  AWS Access Key ID (AWS): 行番号 14
      マスク済み: AKIAIO...MPLE (エントロピー: 3.68)
      ↳ 推奨対応: AWS IAMコンソールからアクセスキーを非アクティブ化してください。

  ⚠️  AWS Access Key ID (AWS): 行番号 35
      マスク済み: AKIAIO...MPLE (エントロピー: 3.68)
      ↳ 推奨対応: AWS IAMコンソールからアクセスキーを非アクティブ化してください。

今のファイルからは見つかりません。履歴の差分(git log -p)には 2 か所あります。消した行(-)と、入れた行(+)です。行番号は、git log -p の出力の中の行です。リポジトリを手に入れた人は、git show で過去の版をそのまま読めます。

履歴を書き換えるか

履歴を書き換えても、すでに誰かが持っているクローンや、公開先の控えからは消えません。漏れた鍵への対処は、無効にすることです。書き換えると、コミットの ID が変わり、全員がクローンし直す必要があります。それでも書き換えるのは、無効にした鍵が検査に毎回出るのを止めたいときや、これから配るリポジトリに鍵を残したくないときです。無効にしたあとで行います。

履歴から鍵を置き換える

git filter-repo で、すべてのコミットの中の鍵を REMOVED に置き換えます。履歴を書き換える操作です(変更を伴う操作です)。戻せるように、先に丸ごと複製します。

cd ~
cp -a shop shop.bak                              # a backup (it still holds the key)
cd shop
printf '%s\n' 'AKIAIOSFODNN7EXAMPLE==>REMOVED' \
  'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY==>REMOVED' > /tmp/replace.txt
git log --oneline
git filter-repo --force --replace-text /tmp/replace.txt
git log --oneline
roamswitch audit-secrets "$(git log -p --all)"
実行結果
9aa9bba Move upload signing to the server
96653d2 Add image upload
c5e1f37 Initial commit
Parsed 3 commitsHEAD is now at 7e184d5 Move upload signing to the server

New history written in 0.02 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
Completely finished after 0.06 seconds.
7e184d5 Move upload signing to the server
ea1a703 Add image upload
c5e1f37 Initial commit
=====================================================
  🔑 機密情報・APIキー漏洩監査
=====================================================

  機密情報・APIキーの露出は検出されませんでした。
  • 鍵の入っていない最初のコミット(c5e1f37)は、ID が変わりません。鍵が入ったコミットから後は、ID がすべて変わります
  • 書き換えたあとの履歴を調べ直すと、見つかりません
  • 戻すときは、shop.bak を元の場所に戻します
変更を伴う操作です

git filter-repo は、手元のリポジトリの履歴を書き換えます。共有のリポジトリに反映するには、強制的な push が必要で、ほかの人は全員クローンし直します。バックアップの shop.bak には鍵が残っています。確かめが済んだら消します。

#差し替えと、公開の前の検査

新しい鍵は、ブラウザに渡るファイルに入れません。この場面では、アップロード用の URL をサーバーが発行する形に変えました(前の章のコミット)。次に、鍵が入ったまま公開しようとしたら止まるようにします。scan-frontend は、見つけると終了コードが 1 になります。これを、公開の手順の途中に入れます。

鍵が入った版は公開できない手順にする

ビルドして、検査して、問題がないときだけ公開フォルダーへ写す、短いスクリプトです。鍵が入っていた版(バックアップを 1 つ前のコミットに戻したもの)と、今の版で試します。最後に、公開フォルダーに残っているソースマップを消します(変更を伴う操作です)。

cd ~
cat > deploy.sh <<'D'
#!/bin/sh
# Build into dist, check it, then copy it to the public folder
set -e
cd "$1"
rm -rf dist && mkdir dist
cat src/upload.js src/main.js > dist/app.js
roamswitch scan-frontend dist || { echo "公開を止めました"; exit 1; }
sudo cp dist/app.js /var/www/html/
D
chmod +x deploy.sh
cd ~
git -C shop.bak checkout -q HEAD~1    # the version that held the key
./deploy.sh ~/shop.bak; echo "exit code: $?"
./deploy.sh ~/shop; echo "exit code: $?"
sudo rm /var/www/html/app.js.map
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/app.js.map
sudo -E roamswitch scan-frontend /var/www/html
実行結果
=====================================================
  フロントエンドの公開ファイルの確認
=====================================================

調べた場所: .(ファイル 1 件)
- 機密情報(APIキー・秘密鍵など): app.js:3 AWS AKIAIO...MPLE
1 件(機密情報 1、トークン 0、ソースマップ 0)
見つかったキーやトークンは、ファイルをすでに公開していた場合、流出したものとして扱ってください。発行元で無効にして、新しく作り直します。秘密情報はビルドに含めず、サーバー側に置いてください。ソースマップは、本番に置かないか、取得できる人を限ってください。
公開を止めました
exit code: 1
=====================================================
  フロントエンドの公開ファイルの確認
=====================================================

調べた場所: .(ファイル 1 件)
公開されるファイルの中に、見つかった問題はありません
exit code: 0
404
=====================================================
  フロントエンドの公開ファイルの確認
=====================================================

調べた場所: .(ファイル 2 件)
公開されるファイルの中に、見つかった問題はありません
  • 鍵が入っていた版は、検査で止まり、公開フォルダーへ写されません
  • 今の版は終了コード 0 で通り、app.js が置き換わります
  • ソースマップを消すと、app.js.map は 404 になります。公開フォルダーを調べ直すと、問題は見つかりません

練習の場を片付けます。バックアップには鍵が残っているので、一緒に消します。nginx と git-filter-repo は、この練習のために入れたものです。使い捨ての VM のときだけ消します。

cd ~
rm -rf shop shop.bak deploy.sh /tmp/replace.txt
sudo rm -f /var/www/html/app.js
sudo apt-get purge -y -qq nginx nginx-common git-filter-repo > /dev/null
sudo rm -rf /var/www/html /var/log/nginx
systemctl is-active nginx
実行結果
inactive

#見えない部分と確認

この講座で確かめたこと・確かめていないこと
  • 鍵は、公開されている無効なサンプルだけを使いました。発行元での無効化、操作ログの確認、新しい鍵の発行は、RoamSwitch の外で行う対応で、この講座では動かしていません
  • 検査が見つけるのは、知っている形式の鍵です。この講座でも、シークレットアクセスキーは出ませんでした。「見つからない」は「ない」ことの証明になりません
  • audit-secrets は、フォルダーを調べるとき .git・node_modules・dist・build などに入りません。履歴は git log -p を渡して調べます
  • scan-frontend が読むのは、js・mjs・cjs・css・map・html・json・txt のファイルと、.env・.env.*・*.env です(コードで確認)。ほかの名前のファイルに鍵がないかは、audit-secrets でも調べます
  • アクセスログで分かるのは、誰がファイルを取得したかまでです。CDN、検索エンジンの控え、誰かのクローンに残った写しは、このサーバーからは見えません
確認問題

公開中の app.js から鍵を取り除いて配り直し、scan-frontend も「問題はありません」になりました。鍵はどう扱いますか

解説を見る

訪れた人は全員、鍵の入った app.js を受け取っています。ファイルを直しても、受け取った写しは消えません。対処の中心は、発行元で無効にすることです。

確認問題

ソースから鍵を消してコミットしたあと、roamswitch audit-secrets ./shop で何も見つかりませんでした。正しい解釈はどれですか

解説を見る

フォルダーの検査は .git に入りません。過去の版は、git log -p の出力を audit-secrets に渡すと調べられます。

次は、認証情報の盗難と、秘密情報の漏えい で、検査が見つける鍵の形式と、ソースマップの扱いを学べます。公開フォルダーに Web シェルが置かれたときの手順は、Web シェルが見つかったとき にあります。

扱う機能

対応する攻撃手法 (ATT&CK): T1204.004

確認した内容

2026-11-12に確認しました。対象は Linux 1.17.39 です。使い捨ての Ubuntu 24.04 の VM(公開版 1.17.39)で、ダミーのリポジトリと nginx と AWS の無効なサンプルキーを使い、すべての演習を日本語と英語で実行しました。

コマンドの出力は、製品の版や環境によって変わります。違いが出たら、まず製品の版を確かめてください。