IPアドレスとSSHログインだけが手がかりの、用途不明なLinuxサーバー。構築者は退職済み、引き継ぎ資料なし。業務でこういうマシンと何台も向き合うことになり、調査手順を体系化した。誰も当時を知らないものの正体を、残された痕跡から推測する。この営みが遺跡の発掘に似ているので、個人的に「サーバー考古学」と呼んでいる。
この記事では、確立した調査手順(何をどんな順で見るか)と、それをAnsibleで自動化してAIに用途推測させるところまでを書く。
大原則: read-only で、軽く
調査対象は「用途不明」なだけで、本番稼働中かもしれない。だから鉄則が2つ。
- 読み取り専用 (設定変更・ファイル作成・サービス再起動は一切しない。調査自体が障害になったら本末転倒)
- 負荷をかけない (
findやduにはtimeoutと範囲制限を付ける。特にNFSやs3fsなどネットワークマウント越しの走査は、終わらない・ストールする・課金されるの三重苦なので除外する)
以降のコマンドはすべてこの原則に従っている。
調査は「5つの問い」を順に潰す
試行錯誤の結果、次の順番に落ち着いた。手前の問いほど少ないコマンドで大きな手がかりが得られる。実際、問い1と2でだいたい見当がつき、3以降は裏付けと影響範囲の確認になることが多かった。
- 何が動いているのか (ポート・プロセス・cron)
- 何が置いてあるのか (アプリディレクトリ・設定・証明書・パッケージ)
- 誰がいつ使っていたのか (ログイン履歴・コマンド履歴)
- 外部と何をやり取りしているのか (確立済み接続・マウント)
- いつ作られたのか (構築時期の痕跡)
問い1: 何が動いているのか
最初は LISTEN ポート。待ち受けポートは用途の最有力手がかりで、どのプロセスが開けているかまで分かる。
ss -tlnp # TCP
ss -ulnp # UDP
80/443ならWeb、3306ならMySQL系、10050ならZabbixエージェント、という具合。次にプロセスツリーと、「意図して常時動かす設定になっているもの」の確認。
ps auxf
systemctl list-unit-files --state=enabled
systemctl --failed # 「かつて動いていたが壊れている」機能が見つかる
--failed は地味に重要で、起動失敗したまま放置されたユニットから「本来あったはずの機能」が見えることがある。
そして cron。夜間バッチや月次集計専用のサーバーは日中に見るとほぼ無風で、LISTENポートにも現れない。
cat /etc/crontab
ls /etc/cron.d/ && cat /etc/cron.d/*
for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u "$u" 2>/dev/null; done
systemctl list-timers --all
atq # at ジョブも一応
ユーザー別crontabの全ユーザー走査を忘れがちなので注意。/var/spool/cron/ 直下を ls するだけでも「crontabを持っているユーザー」は分かる。
問い2: 何が置いてあるのか
次は「モノ」。アプリが置かれがちなディレクトリを更新日時付きで眺める。
for d in /opt /usr/local /srv /var/www /data /app /home; do
echo "=== $d ==="; ls -lat "$d" 2>/dev/null | head -50
done
timeout 60 du -sh -x /opt/* /srv/* /var/www/* 2>/dev/null # -x で別FSに降りない
ディレクトリ名と更新日時だけでも「いつ頃まで手が入っていた何か」が見えてくる。
Webサーバーが動いていたら設定を読む。設定内のドメイン名と proxy_pass の転送先は、そのまま用途の説明文になっていることが多い。
find /etc/nginx -maxdepth 4 -type f \( -name '*.conf' -o -path '*sites-enabled*' \) 2>/dev/null
find /etc/apache2 /etc/httpd -maxdepth 4 -type f 2>/dev/null | head -200
SSL証明書はCN/SANと有効期限を見る。有効期限が数年前に切れたままなら、その頃から使われていない可能性が高くなる。
for f in $(find /etc/letsencrypt/live /etc/ssl/certs /etc/pki/tls/certs -type f \
\( -name 'cert.pem' -o -name '*.crt' -o -name 'fullchain.pem' \) 2>/dev/null | head -50); do
echo "=== $f ==="
openssl x509 -in "$f" -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null
done
パッケージはRedHat系なら rpm -qa --last が優秀で、インストール日順に並ぶ。「構築直後に何を入れ、いつまで更新していたか」という経緯が読める。
rpm -qa --last | head -300
最後に先人のメモ探し。
timeout 60 find /root /home /opt /usr/local /srv /var/www /etc -xdev -maxdepth 6 -type f \
\( -iname 'README*' -o -iname 'NOTES*' -o -iname '*.md' \
-o -iname '手順*' -o -iname 'メモ*' -o -iname 'INSTALL*' \) 2>/dev/null | head -200
-xdev でファイルシステム境界を越えない(=ネットワークマウントに降りない)のがポイント。
問い3: 誰がいつ使っていたのか
last -F | head -100
lastlog | awk 'NR==1 || $0 !~ /Never logged in/'
最終ログインが数年前なら、少なくとも人手の運用は止まっている可能性が高い。そして bash_history。前任者の作業がそのまま残っている。
for d in /root /home/*; do
for h in .bash_history .zsh_history; do
[ -f "$d/$h" ] && { echo "=== $d/$h ==="; tail -n 300 "$d/$h"; }
done
done
あわせて /etc/passwd、/etc/sudoers(と /etc/sudoers.d/)、各ユーザーの authorized_keys で「誰がアクセスできる状態だったか」を押さえる。
問い4: 外部と何をやり取りしているのか
ここまでの仮説を、他システムとの関係で裏付ける。
ss -anp state established # 今つながっている相手
cat /etc/hosts # db-prod-01 みたいな手書きエントリは雄弁
cat /etc/fstab # NFSマウントなど
ip neigh # 同一セグメントで最近通信した相手
cat /etc/exports # 逆にこのサーバーがNFSで「提供している」もの
ここで得られる情報は用途推定だけでなく、「止めたらどこに影響が出るか」の見積もりに直結する。
問い5: いつ作られたのか
構築時期が分かると、「その頃に立ち上がったプロジェクトは何か」という組織側の記録と突き合わせられる。技術的な痕跡と人間の記憶の穴埋めができる。
stat /etc/ssh/ssh_host_*key # 初回セットアップ時に生成される
stat /
ls -la /var/log/installer/ # Debian/Ubuntu系
head -200 /root/anaconda-ks.cfg # RHEL系のキックスタート
Ansibleで自動化する
見る項目と順番が固定できたので、Ansibleのroleに落とした。構成はこんな感じ。
archaeology/
├── playbooks/audit.yml # 起動ファイル(Python検出などのpre_tasks入り)
├── roles/server_audit/
│ ├── tasks/main.yml # 17セクションの調査タスク
│ └── templates/report.md.j2 # Markdownレポートテンプレート
└── inventory/
全タスクに changed_when: false / failed_when: false を付けて、read-only かつ「個々のコマンドが失敗しても調査全体は止まらない」作りにしてある。タスクは全部この形の繰り返し。
- name: "02. TCP listening (ss -tlnp)"
ansible.builtin.shell: "ss -tlnp 2>/dev/null || ss -tln"
register: out_02_tcp
changed_when: false
failed_when: false
check_mode: false
tags: [listening_ports, section_02]
セクション単位でタグを付けてあるので、--tags listening_ports,cron のような絞り込み実行もできる。
レガシーサーバーならではの工夫も2つ入れた。
Python interpreterの自動検出。レガシー環境はPythonのパスがバラバラ(/usr/libexec/platform-python だったり python2 しかなかったり)なので、gather_facts: false にしておき、Python不要の raw モジュールで先に検出してから ansible_python_interpreter を動的に設定する。
- name: "Python interpreter を検出 (raw, Python不要)"
ansible.builtin.raw: |
for p in /usr/bin/python3 /usr/local/bin/python3 \
/usr/libexec/platform-python \
/usr/bin/python /usr/bin/python2 /usr/bin/python2.7; do
if [ -x "$p" ]; then echo "$p"; exit 0; fi
done
ネットワークマウントの除外。du や find を s3fs/NFS/CIFS に走らせると地獄なので、pre_tasks で /proc/self/mounts からネットワーク系マウントポイントを抽出し、後続タスクの走査対象から外す。
- name: "(safety) ネットワーク/FUSE系マウントを検出"
ansible.builtin.shell: |
awk '$3 ~ /^(fuse|fuse\.|s3fs|sshfs|nfs|cifs|smb|davfs|gcsfuse|fuseblk)/ {print $2}' \
/proc/self/mounts 2>/dev/null
register: archaeology_skip_mounts
実行すると、対象サーバー1台につき1つのMarkdownレポート(reports/<ホスト名>.md)が生成される。
レポートは「AIに読ませる前提」で設計する
ここが個人的に一番気に入っているところ。レポートはJinja2テンプレートでMarkdown化するのだが、冒頭のYAML frontmatterに分析指示そのものを埋め込んである。
---
report_kind: legacy_server_audit
goal: |
This is an audit of a Linux server whose original purpose is unknown.
All prior administrators have left and no documentation exists.
Please infer from the data below:
1. What role/purpose did this server most likely serve?
2. What business or technical function did it support?
3. What is its current operational state (active / dormant / decommissioned candidate)?
4. What downstream/upstream systems is it likely connected to?
5. What risks would removal of this server pose?
hostname: "srv-legacy-07"
distribution: "CentOS 7.9"
uptime_seconds: 39744217
...
---
つまり生成されたファイルをClaudeなどのAIにそのまま添付するだけで、プロンプトを書かなくても分析が始まる。返ってくるのはこんな根拠付きの推測。
ポート443で待ち受けており、証明書のドメイン名は社内ポータル。ただし最終ログインは3年前で証明書も期限切れ。社内ポータルの旧環境で、現在は休眠中と推測される。
人間はこの推測を元データと突き合わせてレビューし、停止・移行の判断に集中すればいい。
学び
用途推定は「複数の痕跡の突き合わせ」で精度が上がる。ポートだけ、履歴だけでは判断を誤る。独立した複数の手がかりが同じ方向を指して初めて確度の高い推測になる。これは人間でもAIでも同じで、だからこそ勘所を属人化させず、見る項目と順番を決めて漏れなく集めることに価値があった。AIの推測の質は、渡す材料の網羅性で決まる。
「無い」ことも手がかり。cronが空、ログイン履歴が途絶えている、証明書が期限切れのままなど、「無い」ことの積み重ねが休眠の証拠になる。
収集した情報の扱いは事前に決める。レポートには内部IP・bash_history・authorized_keys・証明書のCN/SANなど機密性の高い情報がまとまって入る。効率よく集められるということは、漏れたときの影響も大きいということ。保管場所と共有範囲(そしてAIに渡してよいか)は調査を始める前に決めておくべきだった。リポジトリでは reports/ を .gitignore に入れ、実IPを含むinventoryもテンプレートだけをGit管理対象にしている。
「触ってはいけない気がして誰も触れないサーバー」はどの組織にもあるはず。read-onlyの発掘調査で想像以上に多くのことが読み取れる。