procfsで稼働中コンテナのプロセスをOSパッケージに紐づける方法
公開日:2026年9月13日
はじめに
イメージの脆弱性スキャンは、アプリケーションが一度も読み込まないパッケージも含め、インストール済みソフトウェアの脆弱性を報告します。
通常は/procにマウントされるLinuxのprocfsでは、稼働中プロセスの実行ファイルとマッピングされたファイルを観測し、コンテナのパッケージデータベースからOSパッケージに紐づけられます。1
この記事では、次のことを記載します。
- Dockerホストから、プロセスと読み込まれたファイルの情報を収集する方法を説明する
- dpkg・apkのメタデータからファイルの所有元を特定する方法を説明する
- パスだけではパッケージを確定できない場合の扱いと、権限・サンプリングの限界を説明する
- コマンド例はLinux上のDocker Engineと既存の
webコンテナを前提とし、Dockerデーモンが動くマシン上で実行する
procfsのパッケージ紐づけの流れを追う動く図解では、コンテナ内のnginxワーカー1つについて、読み込んだlibssl.so.3をprocfsとdpkgの記録からlibssl3に紐づけ、Trivyの結果と照合して読み込みを確認するまでを8ステップで確認できます。
読み込まれたファイルから分かること
実行可能なメモリマッピングにあるライブラリは、プロセスがそのファイルからコードを読み込んだ根拠になります。
| 根拠 | 確認できること |
|---|---|
| イメージ内でパッケージを検出 | スキャナーがインストール済みソフトウェアを特定したこと |
| procfsで実行ファイルやライブラリを観測 | サンプル取得時点で、プロセスがそのファイルを実行またはマッピングしていたこと |
| ファイルの所有元とバージョンが一致 | 観測したパスが、検出結果にあるパッケージのバージョンに対応すること |
| 脆弱な関数への到達 | この方法では確認できません |
- ここでいう「使用」は読み込まれたことを指し、特定の関数が実行されたことは意味しない
- 読み込みの観測は、対応の優先順位を判断する肯定的な根拠になる
- 未観測でも、別のリクエスト・ワーカー・短時間のプロセスが後から読み込む可能性があるため、未使用とは証明できない
コンテナのホスト側PIDを取得する
Dockerのプロセス一覧から、ホストの/proc/<pid>で使うPIDを取得します。
出力例:
- 1列目の
PIDがホスト側のPIDで、以降の/proc/<pid>で使う PPIDは親プロセスのPIDで、ワーカーの親がマスターであることを示す- この例では、
USERがrootの行はマスタープロセス、nginxの行はワーカープロセスである - 確認したLinux版Docker Engineは、ランタイムから取得したPIDを基にホストで
psを実行し、対象プロセスの出力を抽出する23 - 以下の例では一覧から1つを選ぶが、収集プログラムではワーカーや子プロセスも含め、すべてのPIDを調べる
以下の例で使うPIDとして、プロセス一覧の先頭のPIDを1つ選んで変数pidに入れます。
/proc/<pid>/cgroup内のコンテナIDは、所属を照合する補助情報として使う- cgroupのパス構造はバージョン・名前空間・ランタイム設定で異なるため、Docker IDが見つからないだけではPIDを除外しない
/procからプロセスの情報を読み取る
実行ファイルとマッピングされたライブラリ
対象プロセスの実行ファイルのパスを確認するため、exeのリンク先を読み取ります。
出力例:
/usr/sbin/nginxが対象プロセスの実行ファイルのパスで、パッケージの所有情報と照合する対象になる- 実行ファイルがアンリンクされると
exeの末尾に(deleted)が付くが、プロセスは動作を続けることがある4
対象プロセスがマッピングしている実行ファイルやライブラリを確認するため、mapsを読み取ります。
出力例:
<range> r-xp <offset> <dev> <inode> /usr/sbin/nginx
<range> r-xp <offset> <dev> <inode> /usr/lib/x86_64-linux-gnu/libc.so.6
<range> r-xp <offset> <dev> <inode> /usr/lib/x86_64-linux-gnu/libssl.so.3
- 各行の末尾のパスがマッピングされたファイルで、この例では
nginx・libc.so.6・libssl.so.3が確認できる - 2列目の
r-xpに含まれるxが、実行可能なマッピングであることを示す mapsの列は、アドレス範囲・権限・ファイルオフセット・デバイスのメジャー番号とマイナー番号・inode・任意のパス名である5- 計測用の収集プログラムは、権限に
xを含み、パス名が/で始まり、inodeが0でない行を保持する20 - 対象は実行可能なファイルのマッピングであり、プロセスが読み取ったすべてのファイルではない
- 匿名メモリや
[heap]・[stack]・[vdso]からは、パッケージが所有するファイルを特定できない - 空白を含むパスに対応するため、固定列の後に残る部分全体をパス名として解析する
(path, dev, inode)で重複を除き、各ファイルをマッピングしたプロセスの世代を保持する- マッピングにも
(deleted)が付くため、接尾辞を除いてパスを処理する場合も、削除の印は別に保持する5
実効UIDとケーパビリティ
対象プロセスの実効UIDと実効ケーパビリティを確認するため、statusからUidとCapEffの行を抽出します。
出力例:
- UIDの4列は、順に実UID・実効UID・保存UID・ファイルシステムUIDである
- 実効UIDは2番目の数値で、
Uid:を含めると3番目のフィールドであり、この例では101である CapEffは、実効ケーパビリティのビットマスクを16進数で表す6- この例の
CapEffはすべて0で、実効ケーパビリティがないことを示す - 権限を持つマスターとワーカーなどを区別する情報として、コンテナ内の全パッケージではなく個々のプロセスに紐づける
- ユーザー名前空間のリマッピング時は、ホストとコンテナ内のUIDが異なり得るため、
/proc/<pid>/uid_mapで解釈する
名前空間の識別情報とPIDの再利用
名前空間とプロセスの世代を記録し、収集中の入れ替わりを確認します。
対象プロセスが属するネットワーク・マウント・ユーザー名前空間の識別子を取得するため、それぞれの名前空間リンクを読み取ります。
出力例:
net・mnt・userの各行が、それぞれネットワーク・マウント・ユーザー名前空間を表す- 角かっこ内の値が各名前空間の識別子で、プロセス間や収集前後で同じ種類の名前空間を比較するために使う
- 識別子でプロセスを名前空間ごとにまとめるが、同じコンテナでもすべての名前空間を共有するとは限らない7
/proc/<pid>/statの22番目のフィールドstarttimeは、起動からの経過時間をクロックティック単位で表し、(PID, starttime)で世代を区別する8- 括弧内のプロセス名には空白や括弧が含まれ得るため、
statを単純な空白区切りで解析してはいけない - 収集前後で
starttimeと名前空間を確認し、変化した場合や再確認できない場合は、そのPID経由のファイルシステム・ネットワーク情報も含めてサンプルを採用しない - コンテナの再起動を検出するため、Dockerの
State.StartedAtも再確認する
待ち受けソケットと対応するプロセス
/proc/<pid>/net/tcpとtcp6は、そのプロセスのネットワーク名前空間全体のTCPテーブルを公開します。9
- 状態が
0AのLISTEN行を選ぶ
対象プロセスのネットワーク名前空間で待ち受けているTCPソケットを調べるため、LISTEN行からローカルアドレスとポート・状態・inodeを抽出します。
出力例:
- 1列目の
00000000:0050はローカルアドレスとポートで、このIPv4の例では0.0.0.0:80を表す - 2列目の
0AがLISTEN状態を示す -
3列目の
<socket-inode>を、各プロセスのファイルディスクリプターのリンク先と照合する -
コンテナ内の各プロセスの
fd/を列挙し、リンクのinodeをLISTEN行と照合する
変数pidで指定したプロセスが持つソケットのinodeを確認するため、ファイルディスクリプターのリンク先を一覧表示します。
出力例:
<fd-number>が対象プロセスのファイルディスクリプター番号であるsocket:[<socket-inode>]内の値がLISTEN行のinodeと一致すれば、そのプロセスが待ち受けソケットのファイルディスクリプターを持つことが分かる- アドレスのデコードでは、32ビットワードごとのホストのネイティブバイトオーダーを考慮する10
- マスターとワーカーがソケットを共有し得るため、一致するファイルディスクリプターを持つ全プロセスを保持する11
- TCPテーブルは名前空間ごとに1回読み取り、一致するプロセスのファイルディスクリプターがなければ紐づけを未確定にする
- 待ち受けだけでは、Dockerによるポート公開やインターネットからの到達可能性は確認できない
コンテナ内のパスを解決する
/usr/lib/x86_64-linux-gnu/libc.so.6などの観測パスは、ホストではなく対象プロセスのルートから調べます。
対象プロセスのルートを変数rootに入れ、そのルート経由でコンテナ内のdpkgデータベースのディレクトリを一覧表示します。
/proc/<pid>/rootでは、マウント名前空間やプロセスごとのマウントを含む、そのプロセスのファイルシステムにアクセスできる12- ホストのパッケージデータベースを検索すると、別の対象を調べることになる
- 絶対パスのシンボリックリンクも対象ルート内に収める必要があり、パスの先頭にルートを付けるだけでは制約を強制できない
- 変更され得るファイルシステムでは、開いたルートを基準に
openat2のRESOLVE_IN_ROOTを使うなど、カーネルが制約を強制する方法が必要である13 - 計測用実装はユーザー空間でリンクの各構成要素をたどるため、妨害的な変更のないテストには対応できるが、解決後にファイルを開くまでに競合が起こり得る20
- この実装は、カーネルによるルート内への制限と同等ではない
DebianとUbuntu:dpkg
/var/lib/dpkg/info/*.listから所有元の索引を作り、/var/lib/dpkg/statusからバージョンを取得します。
libc6:amd64が所有するファイルのパスを確認するため、コンテナ内の対応する.listファイルを読み取ります。
出力例:
- 各行が
libc6:amd64の所有ファイルとして記録されたパスである - 観測した実行ファイルやライブラリのパスを、この一覧と照合する
.listにあるのはパスだけなので、バージョンはstatus内の対応する段落から読み取る
続いて確認する/var/lib/dpkg/status内のlibc6の段落を、抜粋で示します。
出力例:
Packageのlibc6とArchitectureのamd64が、先ほどの.listに対応するパッケージ名とアーキテクチャであるStatus: install ok installedが、インストール済みの状態を示すVersionの値がインストール済みバージョンで、スキャナーの検出結果との照合に使うlibc6:amd64.listの基本パッケージ名はlibc6で、amd64は独立したアーキテクチャ情報として扱う- パッケージ名・バージョン・アーキテクチャ・ファイル一覧の利用可否を保持する
- ホストからの手動確認には
dpkg-query --admindirが便利だが、所有元の検索は記録済みパスを使うため、別名のパスに注意する14
Alpine:apk
/lib/apk/db/installedには、空行で区切られたパッケージレコードとファイルの所有情報があります。
コンテナ内のapkパッケージの名前・バージョン・所有ファイルを確認するため、インストール済みパッケージのデータベースを読み取ります。
出力例:
Pはパッケージ名、Vはバージョン、Aはアーキテクチャであり、この例はx86_64向けのmuslのレコードであるFは現在のディレクトリを設定し、後続の各Rはその中のファイル名を表す- この例では、
/lib/ld-musl-x86_64.so.1がmuslに属する15 - レコード境界でディレクトリの状態をリセットし、同じレコード内に先行する
FがないRは不正な形式として扱う - 別パッケージのディレクトリを流用して、有効なパスにしてはいけない
Distroless:status.d
Debianベースのdistrolessでは、次の配置からメタデータとファイル一覧を読み取ります。
出力例:
- 1行目は
libc6の名前・バージョンなどのメタデータを格納するファイルのパスである - 2行目はチェックサムと相対パスの組を格納する
.md5sumsファイルのパスである
libc6.md5sumsの内容を、1行の抜粋で示します。
出力例:
- 1列目の
<checksum>がチェックサム、2列目のlib/x86_64-linux-gnu/libc.so.6がファイルの相対パスである - 相対パスの先頭に
/を付けて索引を作れば、dpkgやシェルがなくてもファイル一覧を再構成できる16 .md5sumsがなければファイル一覧は利用できないが、パッケージのメタデータもないとは限らない- チェックサム情報はファイル名の取得に使うだけで、読み込まれたバイト列を検証しているわけではない
docker execを使わない理由
procfs経由でメタデータを読むと、収集コマンドをホスト上で実行できます。
docker execは対象コンテナに別プロセスを起動するため、シェルや照会コマンドがサンプルに現れ、追加のライブラリを読み込む可能性がある- distrolessには通常シェルやパッケージマネージャーのコマンドがないが、procfs経由ならそれらを追加せずに収集できる
実際のイメージで見つかったパスとデータベースの注意点
/usr統合によるパスの別名
/usr統合で/libが/usr/libを指す場合、同じファイルが異なるパスで記録されることがあります。
| 情報源 | パス |
|---|---|
dpkgの.list |
/lib/x86_64-linux-gnu/libc.so.6 |
プロセスのmaps |
/usr/lib/x86_64-linux-gnu/libc.so.6 |
- 計測したDebianでは、一覧にない別名で
dpkg-query -Sを実行すると失敗したが、libcの所有元がないことを意味しない20 - 照合する両側のパスについて、コンテナのルート内でディレクトリのシンボリックリンクを解決する
- 固定の接頭辞置換ではなく、
/bin・/sbin・/lib64などにも同じ解決規則を適用する
シンボリックリンクは別のパッケージに属することがある
計測したDebianでは、リンクとリンク先の所有元が異なりました。 次の例は、確認した所有関係を要約したものです。
出力例:
- 1行目は、シンボリックリンク
/usr/bin/ld.soの所有元がlibc-binであることを示す - 矢印の先はリンク先のローダーを表し、その所有元が
libc6であることを示す - 索引作成時に最後のリンクまでたどると、ローダーに存在しない2つ目の所有元が生じ、本来は有効な照合ができなくなった
- データベースの各パスはディレクトリ部分だけを解決し、最後の構成要素を保持する
- リンクの所有元がリンク先も所有するわけではなく、カーネルのマッピングは実体を指すため、リンクの所有情報を引き継がない
ファイルを持たないapkパッケージはデータベースの不備ではない
AlpineのRedisイメージにあった仮想パッケージ.redis-rundepsは、依存関係をまとめるだけでファイルをインストールしません。
F・Rのないこのレコードは、有効で完全な空のファイル一覧である- 一覧の欠落と扱うと、データベース全体が不完全とみなされ、無関係な所有元のないファイルまで未解決になった
- 空の一覧と読み取り・解析できない一覧を区別し、後者だけを所有情報の欠落として扱う
パッケージデータベースに含まれない実行ファイルもある
ある稼働中ホストでは、/usr/local/bin/node・同梱JDK・静的リンクのGo実行ファイルがOSパッケージの一覧にありませんでした。
- JVMが読み込んだほかのライブラリでは、所有元のOSパッケージを特定できた
- 読み取り可能で完全な一覧に所有元がなければ、
unownedと記録する - 実行ファイル名・配置ディレクトリ・似た名前のスキャナー検出結果から、所有元を推測してはいけない
- データベースやファイル一覧がない場合は所有元の不在を確認できないため、別の結果として扱う
OverlayFSではデバイス番号が異なることがある
動的リンクのテストでは、mapsとコンテナのルート経由のstatでデバイス番号が異なりました。
mapsは下位レイヤー、statはオーバーレイのデバイスを示し、inodeは一致してもdev + inodeは一致しなかった- OverlayFSの識別情報は設定により異なるため、この不一致だけでは置き換えと判断できない17
- libcなど変更されていないと見込まれるファイルで比較の成立を確認し、計測用実装では確認に失敗すると置き換え比較を未評価にした
- inodeだけの一致は観測結果にすぎず、一般的な置き換え検出方法として検証されたものではない
- 実行ファイルのアンリンク・ライブラリのアンリンク・renameによるライブラリ置き換えでは、いずれも削除済みマッピングを確認した
- 削除の印を独立して保持し、古いマッピングに同じパスの置き換え後のファイルの現行バージョンを割り当ててはいけない
権限:計測結果とLinuxによるチェック
必要な権限は、読み取る情報とホストの設定によって異なります。
| 操作 | 関連するチェック |
|---|---|
exeのリンクやmapsの読み取り |
PTRACE_MODE_READ_FSCREDSのアクセスチェック |
| 名前空間へのリンクの読み取り | ptraceの読み取りアクセスチェック |
rootをたどり、コンテナ内のファイルを読み取る |
ptraceの読み取りアクセスチェックと、ディレクトリおよびファイルの権限 |
fd/の列挙と、そのリンクの読み取り |
ディレクトリの権限と、リンクに対するptraceの読み取りアクセスチェック |
statusやTCPテーブルの読み取り |
procfsの可視性と通常の権限。mapsと同じ個別のptraceチェックではありません |
| Dockerへのプロセス一覧の要求 | Docker APIへのアクセス |
- WSL2では、非rootでも
CAP_SYS_PTRACEでexe・maps・status・名前空間リンク・TCPテーブル・root経由のパッケージメタデータを読めた20 fd/はroot所有のモード0500で、ptraceチェックに通っても列挙できず、CAP_DAC_READ_SEARCHが追加で必要だったCAP_DAC_READ_SEARCHは読み取り・検索の制限を回避し、コンテナ内の保護されたファイルにも必要になる場合がある19- ptraceチェックはデバッガーの接続を意味せず、資格情報・対象のdumpable状態・ケーパビリティ・セキュリティモジュールの制限を評価する18
CAP_SYS_PTRACEは、対象のユーザー名前空間で有効である必要がある- ケーパビリティのテスト環境は、WSL2 Linux 6.6.114、Docker Engine 29.1.3、Yamaの
ptrace_scope=1、hidepid制限なしだった - AppArmorのプロファイルが適用された条件での非rootと各ケーパビリティの組み合わせは引き続き未検証である
- 後の実運用環境での検証では、収集プロセスがAppArmorのunconfinedで動作する条件で、非rootに
CAP_SYS_PTRACEとCAP_DAC_READ_SEARCHを付与すると有効な読み取りが12/12となり、比較した全項目がrootと一致した - Yamaの
ptrace_scopeはattachモードとPTRACE_TRACEMEを対象とし、これらのreadモードを直接制限しないが、ほかのポリシーで拒否される場合がある18 - 計測結果は一般的な保証ではなく、procfsのマウントオプション・ユーザー名前空間・ファイルモード・AppArmorやSELinuxのポリシーで変わる1
- 権限による失敗は、プロセス終了やパッケージデータベース不在と分けて記録する
プロセスとパッケージの紐づけの限界
サンプリングでは短時間の動作を見落とす
短時間のプロセスは、定期的なサンプリングで捉えられないことがあります。
- curlを5秒ごとに約0.1秒実行し、300秒間にわたり10秒ごとに観測したところ、捉えたのは30回のサンプルのうち1回だった
- 60秒間隔で600秒間(開始のずれ0秒)と、30秒間隔で300秒間(開始のずれ7秒)の条件でも、curlをそれぞれ10サンプル中1回捉えたが、残る4条件では0回で、頻度だけでなく観測期間や開始タイミングも結果に関わる
- プロセスや一時的な
dlopenは、観測と観測の間に開始して終了する可能性がある
言語の依存パッケージには別の紐づけ方法が必要
OSパッケージの所有情報だけでは、言語側の依存パッケージを特定できません。
- JavaのJAR・npm・pip・Goモジュールの識別情報は、この方法ではファイル内容から復元できない
- JARやネイティブ拡張が見える場合や、言語関連ソフトウェアがディストリビューションによりパッケージ化されている場合も、特定できるのはOS側のパッケージである
- 内包されるどの言語依存パッケージがスキャナーの検出結果に対応するかは、確認できない
- PythonやNode.jsを観測しても、すべてのpip・npmパッケージの使用を確認したことにはならない
静的リンクでは個々のライブラリを識別できない
静的リンクされたコードには、この方法で解決できる独立したライブラリのパスがありません。
- テストした静的リンクのGoバイナリには共有ライブラリのマッピングがなく、実行ファイル自体は
exeとメモリマッピングの両方に現れた - 実行ファイルをパッケージに紐づけても、組み込まれた静的ライブラリやGoモジュールは再構成できない
- 所有関係はデータベース上のパス情報であり、メタデータを更新しない上書きやbindマウントで、インストール済みバージョンとの対応が崩れる場合がある
全体の手順
収集プログラムでは、次の処理を繰り返せます。
- コンテナを列挙し、コンテナID、イメージID、開始時刻を記録する
- Dockerからホスト側PIDを取得し、各プロセスの識別情報を確定する
- 実行ファイル、実行可能なマッピング、ステータス、名前空間、ソケットを読み取る
- 各プロセスのルートとパッケージデータベースを使って、パスを解決する
- 根拠を採用する前に、プロセスとコンテナの識別情報を再確認する
-
特定したパッケージ名とバージョンを、スキャナーの検出結果と照合する
-
所有元の索引はマウント名前空間とデータベースの世代ごとに作り、サンプルごとにキャッシュを再確認して、変更があれば再構築する
- 代表PIDが終了した場合は、データベース不在と判断する前に、同じマウント構成が見える別のPIDを試す
- Trivyでは、バイナリパッケージ名を
PkgName、バージョンをInstalledVersionと比較する - バージョンはエポック・リビジョンを含む完全な表記を保持し、タグだけに頼らず、観測したイメージがスキャン対象であることを確認する
| 分類 | 意味 |
|---|---|
confirmed |
有効なサンプルで、実行ファイルまたはマッピングを、検出結果のOSパッケージ名とバージョンに紐づけられた |
unknown |
読み取りの失敗、メタデータの欠落、所有元の曖昧さ、識別情報やバージョンの問題により、信頼できる結論を出せなかった |
unobserved |
有効な収集を行ったものの、確認につながる根拠が得られなかった。一致するマッピングがない、言語パッケージの紐づけに未対応などの理由を添える |
- 分類と併せて、タイムスタンプ・成功したサンプル数・読み取り失敗・未解決のパスを保持する
- ソケットの紐づけに失敗しても、有効なパッケージの紐づけを取り消す必要はない
- 実行時にパッケージの使用が確認できた場合は、KEV・EPSS・深刻度などで判断した緊急度は変えず、同じ緊急度の検出結果の中で先に対応する判断材料にし、使用を確認できなかったことを理由に優先度を下げない
- 確認できた場合は、この方法の限界の範囲内でパッケージの使用を観測したことを示す
unknown・unobservedは、脆弱性を報告対象から除外したり、安全と判断したりする根拠にはならない
参考資料
文書の確認日は2026年9月13日です。
- 計測は2026年9月12日に行い、特定のホストと実行時の結果を示している
- ほかのイメージやLinuxの設定で同じ結果を保証するものではない
KestreLynxは、稼働中のDocker・KubernetesコンテナのイメージをTrivyで定期的にスキャンし、前回からの変化と、深刻度・CISA KEV・EPSSに基づく優先度をSlackやWebhookへ通知するOSSです。Docker・Compose向けの修正計画と修正PR(実験的)で、更新候補の比較からGitHubへのPR作成まで修正を支援します(既定で無効)。マージ・再ビルド・デプロイは利用者が行います。