インベントリ、SBOM、そしてゲート
このラボは本物のVM上で動きます
この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが
別に動き、systemdが実際にサービスを管理し、dockerは偽物ではなく
本物のDockerエンジンです。docker runで起動したコンテナは、実際にプロセスに
なり、docker execもdocker logsもそのまま動作します。
以前は、このラボはPodの中で動いていました。カーネルの権限をすべて下ろした箱だったため、 コンテナを起動するステップが塞がれており、そのためイメージのアーカイブを自分で展開して みるという回り道で学んでいました。もう回り道は必要ありません。
知っておくべきことが2つあります。
- 最初の起動に1分ほどかかります。VMが起動してDockerをインストールするためです。Podのラボ(通常は40秒)より遅くなります。
- ブラウザープレビューはありません。VMに入ってくる接続は、採点ポート
1つだけが開いています。Webサーバーを起動したなら、VMの中で
curlで確認してください。
目標
スキャナーが実際に行うこと(パッケージのインベントリ → DBとの照合)を、手で再現します。ベースと ランタイムのイメージのパッケージ一覧を取り出して、増えた攻撃対象領域を数字で数え、SBOMを自分で 作成し、ポリシーゲートのスクリプトを書いたあと、イメージに焼き付けられたシークレットを見つけて修正します。
なぜ重要なのか
スキャナーを魔法の箱のままにしておくと、2つの誤った結論に陥ります。「きれいだから安全だ」と、
「1,247件すべて直すべきだ」です。スキャナーはイメージを実行することもコードを分析することもせず、
インストールされたパッケージの名前・バージョンを脆弱性DBと照合するだけです。そのため、curl | tar xz
で入れたバイナリはまったく見えず、一度も呼び出されないパッケージの脆弱性は、
そのまま上がってきます。この原理を手で再現してみると、「何を直すか」よりも
「なぜこのパッケージがイメージにあるのか」が先に出てこなければならないことが、はっきりします。
アプリケーションがリンクする共有ライブラリが8個なのに、イメージにパッケージが432個
入っているなら、問題は脆弱性ではなく、イメージの構成です。
ステップ
/root/sec2ディレクトリを作成し、alpine:3.20にインストールされたパッケージを、이름-버전の1行形式(プレースホルダーは名前とバージョンです)で/root/sec2/alpine-pkgs.txtに保存します。10行以上である必要があり、muslが入っている必要があります。- 同じ形式で、
nginx:1.27-alpineのパッケージ一覧を/root/sec2/nginx-pkgs.txtに保存します。10行以上である必要があり、nginxが入っている必要があり、ステップ1より行数が多い必要があります。 - 2つの一覧を、バージョンを除いたパッケージ名を基準に比較して、nginx側にだけあるパッケージの個数を
/root/sec2/extra-count.txtに数字だけで保存します(誤差2以内は許容)。 /root/sec2/sbom.jsonを作成します。最上位のimageの値はnginx:1.27-alpine、packagesは配列で、項目数がステップ2のファイルの行数と同じである必要があります。各項目には、空ではないnameとversionがある必要があります。/root/sec2/deny.txtに、禁止パッケージの名前を1行に1つずつ書きます(必ずcurlを含めます)。そして、実行権限のある/root/sec2/gate.shを作成します。第1引数で受け取ったパッケージ一覧のファイルに禁止パッケージがなければ終了コード0、あれば0以外の終了コードで終了しつつ、該当したパッケージ名を出力する必要があります。ARGで受け取った値をENV APP_TOKENに渡すDockerfileでlabhub/leak:v1をビルドし、イメージ設定からそのまま読み取れるトークンの値を/root/sec2/leak.txtに保存します。- 同じアプリをトークンなしでビルドして、
labhub/leak:v2を作成します(イメージのEnvにAPP_TOKENがない必要があります)。そして、labhub/leak:v2でsec-runtimeコンテナを起動し、APP_TOKENを実行時に注入します。 /root/sec2/scan.mdに、次の3行を正確にこの形式で書きます。package_count=<2단계 파일의 줄 수>(プレースホルダーはステップ2のファイルの行数です)denied_hits=0secret_in_image=no
参考
- alpine系イメージのインストール済みパッケージは、
docker run --rm <이미지> apk info -vで、이름-버전が1行ずつ出力されます(プレースホルダーはイメージ、名前、バージョンです)。そのコマンドが読むのは、イメージの中の普通のテキストファイル/lib/apk/db/installedで、気になるなら自分で開いても同じ内容が出てきます。脆弱性スキャナーが行うことの最初の段階が、まさにこのファイルを読むことです。 - バージョンを除くには、
sed 's/-[0-9].*$//'のように、最初の数字の前で切ります。並べ替えたあと、片方にだけある項目はcomm -13で数えます。 - イメージの環境変数の確認は、
docker image inspect labhub/leak:v1 | jq -r '.[0].Config.Env[]'で行います。 - ビルド引数は
docker build --build-arg APP_TOKEN=<값>、実行時の注入はdocker run -e APP_TOKEN=<값>です(プレースホルダーは値です)。 - よくあるミス1: ステップ1・2を異なる形式で取り出すと、ステップ3の比較がすべてずれます。
- よくあるミス2: ステップ5で、違反のときに何も出力しないと、通過できません。ゲートは原因を伝えて初めてゲートです。
- よくあるミス3: ステップ7で
labhub/leak:v1でコンテナを起動すると失敗します。必ずlabhub/leak:v2である必要があります。
ベースイメージのパッケージインベントリ
/root/sec2ディレクトリを作成し、alpine:3.20にインストールされたパッケージを、이름-버전の1行形式(プレースホルダーは名前とバージョンです)で/root/sec2/alpine-pkgs.txtに保存してください。10行以上である必要があり、muslが入っている必要があります。
スキャナーが最初に行うことが、これです。alpineは、apkでインストールされたパッケージを参照でき、名前とバージョンが付いた1行の形式で出力するオプションがあります。ネットワークなしでローカルのDBだけを読むので、オフラインでも動作します。
ランタイムイメージと比較する
同じ形式で、nginx:1.27-alpineのパッケージ一覧を/root/sec2/nginx-pkgs.txtに保存してください。10行以上である必要があり、nginxが入っている必要があり、ステップ1より行数が多い必要があります。
ステップ1とまったく同じ形式で出力しないと、次のステップで比較できません。同じalpineベースなのに、nginxイメージ側の行数のほうが多いのが正常です。その差が、そのまま増えた攻撃対象領域です。
増えた攻撃対象領域を数える
2つの一覧を、バージョンを除いたパッケージ名を基準に比較して、nginx側にだけあるパッケージの個数を/root/sec2/extra-count.txtに数字だけで保存してください(誤差2以内は許容)。
バージョンの文字列が付いていると、同じパッケージも違って見えます。이름-버전からバージョンを除いて、名前だけで比較してください(プレースホルダーは名前とバージョンです)。並べ替えたあと、片方にだけある項目を数える標準的なツールがあります。ファイルには、数字だけを残してください。
SBOMを自分で作ってみる
/root/sec2/sbom.jsonを作成してください。最上位のimageの値はnginx:1.27-alpine、packagesは配列で、項目数がステップ2のファイルの行数と同じである必要があります。各項目には、空ではないnameとversionがある必要があります。
ツールを使うのではなく、構造を理解するステップです。ステップ2の一覧の各行を、名前とバージョンに分けて、JSONの配列にしてください。項目数がステップ2の行数とまったく同じである必要があり、名前だけがあってバージョンが空の項目が1つでもあると、照合が不可能なので失敗します。
ポリシーゲートのスクリプト
/root/sec2/deny.txtに、禁止パッケージの名前を1行に1つずつ書いてください(必ずcurlを含めます)。そして、実行権限のある/root/sec2/gate.shを作成します。第1引数で受け取ったパッケージ一覧のファイルに禁止パッケージがなければ終了コード0、あれば0以外の終了コードで終了しつつ、該当したパッケージ名を出力する必要があります。
引数で受け取った一覧のファイルを検査して、通過なら0、違反なら0以外の値で終了する必要があります。違反のとき、どのパッケージが該当したかを出力しないと、誰も原因がわかりません。そして、バージョンを除いた名前で正確に比較してください。curlのルールがlibcurlを誤って捕捉してはいけません。
ENVで入れたトークンはイメージに焼き付けられる
ARGで受け取った値をENV APP_TOKENに渡すDockerfileでlabhub/leak:v1をビルドし、イメージ設定からそのまま読み取れるトークンの値を/root/sec2/leak.txtに保存してください。
ARGで受け取った値をENVに渡すDockerfileを作成し、ビルド引数でトークンを渡してください。そして、その値がイメージ設定からそのまま読み取れることを確認して、ファイルに書きます。イメージを作った人でなくても読めるというのが、要点です。
イメージはきれいに、シークレットは実行時に
同じアプリをトークンなしでビルドして、labhub/leak:v2を作成してください(イメージのEnvにAPP_TOKENがない必要があります)。そして、labhub/leak:v2でsec-runtimeコンテナを起動し、APP_TOKENを実行時に注入します。
同じアプリをトークンなしで再ビルドして、イメージのEnvに何も残らないようにし、必要な値はコンテナを起動するときに注入してください。イメージに残っていると、このステップは失敗します。
スキャンの要約
/root/sec2/scan.mdに、次の3行を正確にこの形式で書いてください。
package_count=<2단계 파일의 줄 수>(プレースホルダーはステップ2のファイルの行数です)denied_hits=0secret_in_image=no
3行とも、키=값の形式です(プレースホルダーはキーと値です)。パッケージの数は、ステップ2のファイルの行数と同じである必要があり、最終的なイメージに禁止パッケージがなく、シークレットもない必要があるため、残りの2つの値は、それぞれ0とnoになります。実際の状態と違えば、採点が見つけ出します。