Inventory, SBOM and the Gate
This lab runs on a real VM
This box is not a Pod but a virtual machine started by KubeVirt. A Linux kernel
runs separately, systemd actually manages services, and docker is not an imitation but
a real Docker engine. A container started with docker run actually becomes a process,
and docker exec and docker logs work as expected.
This lab used to run inside a Pod. The box had dropped all kernel privileges, so the step that starts a container was blocked, and the lab worked around that by unpacking the image archive directly. The workaround is no longer needed.
There are two things to know.
- The first start takes a little over a minute. This is because the VM boots and installs Docker. It is slower than a Pod lab (usually 40 seconds).
- There is no browser preview. Only one grading port is open for connections into the VM. If you started a web server, check it with
curlfrom inside the VM.
Goal
Reproduce by hand what a scanner actually does (package inventory, then DB matching). Extract the package lists of a base and a runtime image, count the increased attack surface as a number, build an SBOM yourself, write a policy gate script, and then find and fix a secret baked into an image.
Why it matters
If you leave a scanner as a magic box, you fall into two wrong conclusions: "it is clean, so it is safe" and
"all 1,247 findings must be fixed". A scanner neither runs the image nor analyzes the code;
it only matches the names and versions of installed packages against a vulnerability DB. So a binary put in with curl | tar xz
is not visible at all, and vulnerabilities in packages that are never called
still show up. When you reproduce this principle by hand, it becomes clear that the question
"why is this package in the image at all?" has to come before "what should we fix?".
If the application links 8 shared libraries but the image contains 432 packages,
the problem is not the vulnerabilities but the image composition.
Steps
- Create the
/root/sec2directory, and save the packages installed inalpine:3.20to/root/sec2/alpine-pkgs.txtin the one-line이름-버전format (the placeholders are the package name and its version). It must have at least 10 lines and must containmusl. - In the same format, save the package list of
nginx:1.27-alpineto/root/sec2/nginx-pkgs.txt. It must have at least 10 lines, must containnginx, and must have more lines than step 1. - Compare the two lists by package name with the version stripped, and save only the number of packages that exist only on the nginx side to
/root/sec2/extra-count.txt(an error of up to 2 is allowed). - Create
/root/sec2/sbom.json. The top-levelimagevalue isnginx:1.27-alpine,packagesis an array, and the number of entries must equal the number of lines in the step 2 file. Each entry must have a non-emptynameandversion. - In
/root/sec2/deny.txt, write denied package names, one per line (it must includecurl). Then write/root/sec2/gate.shwith execute permission. When the package list file received as the first argument contains no denied package, it must exit with code 0; when it does, it must exit with a nonzero code and print the names of the packages that matched. - Build
labhub/leak:v1with a Dockerfile that passes a value received viaARGtoENV APP_TOKEN, and save the token value that can be read as-is from the image configuration to/root/sec2/leak.txt. - Build the same app without the token to make
labhub/leak:v2(the image Env must not containAPP_TOKEN). Then start asec-runtimecontainer fromlabhub/leak:v2, injectingAPP_TOKENat run time. - In
/root/sec2/scan.md, write the following three lines in exactly this format.package_count=<2단계 파일의 줄 수>(the placeholder is the number of lines in the step 2 file)denied_hits=0secret_in_image=no
Notes
- The installed packages of an alpine-family image come out one per line in
이름-버전format (the placeholders are the name and the version) withdocker run --rm <이미지> apk info -v(the placeholder is the image). What that command reads is the plain text file/lib/apk/db/installedinside the image, and if you are curious, you can open it yourself and see the same content — the first step of what a vulnerability scanner does is exactly reading this file. - To strip the version, cut before the first digit, as in
sed 's/-[0-9].*$//'. After sorting, count the items on only one side withcomm -13. - Check an image's environment variables with
docker image inspect labhub/leak:v1 | jq -r '.[0].Config.Env[]'. - The build argument is
docker build --build-arg APP_TOKEN=<값>, and injection at run time isdocker run -e APP_TOKEN=<값>(the placeholder is the token value). - Common mistake 1: if you extract steps 1 and 2 in different formats, the step 3 comparison is entirely off.
- Common mistake 2: if step 5 prints nothing on a violation, it does not pass. A gate is only a gate if it states the cause.
- Common mistake 3: if you start the container from
labhub/leak:v1in step 7, it fails. It must belabhub/leak:v2.
Base image package inventory
Create the /root/sec2 directory, and save the packages installed in alpine:3.20 to /root/sec2/alpine-pkgs.txt in the one-line 이름-버전 format (the placeholders are the package name and its version). It must have at least 10 lines and must contain musl.
This is the very first thing a scanner does. alpine can list the packages installed with apk, and there is an option that prints them in a one-line format with the name and version attached. It reads only the local DB without a network, so it works offline too.
Compare with the runtime image
In the same format, save the package list of nginx:1.27-alpine to /root/sec2/nginx-pkgs.txt. It must have at least 10 lines, must contain nginx, and must have more lines than step 1.
You must extract it in exactly the same format as step 1 for the next step's comparison to work. It is normal for the nginx image side to have more lines even though the alpine base is the same. That difference is precisely the increased attack surface.
Count the increased attack surface
Compare the two lists by package name with the version stripped, and save only the number of packages that exist only on the nginx side to /root/sec2/extra-count.txt (an error of up to 2 is allowed).
With a version string attached, even the same package looks different. Strip the version from 이름-버전 (the placeholders are the name and the version) and compare by name only. There is a standard tool that counts the items on only one side after sorting. Leave only the number in the file.
Build an SBOM yourself
Create /root/sec2/sbom.json. The top-level image value is nginx:1.27-alpine, packages is an array, and the number of entries must equal the number of lines in the step 2 file. Each entry must have a non-empty name and version.
This step is about understanding the structure, not about using a tool. Split each line of the step 2 list into a name and a version and build a JSON array. The number of entries must be exactly the same as the number of lines in step 2, and if even one entry has a name but an empty version, matching becomes impossible and the check fails.
Policy gate script
In /root/sec2/deny.txt, write denied package names, one per line (it must include curl). Then write /root/sec2/gate.sh with execute permission. When the package list file received as the first argument contains no denied package, it must exit with code 0; when it does, it must exit with a nonzero code and print the names of the packages that matched.
Check the list file received as an argument, and end with 0 on a pass and a nonzero value on a violation. If you do not print which package matched on a violation, nobody knows the cause. And compare exactly by the name with the version stripped — a curl rule must not wrongly catch libcurl.
A token passed in with ENV is baked into the image
Build labhub/leak:v1 with a Dockerfile that passes a value received via ARG to ENV APP_TOKEN, and save the token value that can be read as-is from the image configuration to /root/sec2/leak.txt.
Create a Dockerfile that passes a value received via ARG to ENV, and pass the token as a build argument. Then confirm that the value can be read as-is from the image configuration and write it to the file. The point is that someone who did not build the image can read it too.
Keep the image clean, inject the secret at run time
Build the same app without the token to make labhub/leak:v2 (the image Env must not contain APP_TOKEN). Then start a sec-runtime container from labhub/leak:v2, injecting APP_TOKEN at run time.
Rebuild the same app without the token so that nothing remains in the image Env, and inject the value you need when you start the container. If it remains in the image, this step fails.
Scan summary
In /root/sec2/scan.md, write the following three lines in exactly this format.
package_count=<2단계 파일의 줄 수>(the placeholder is the number of lines in the step 2 file)denied_hits=0secret_in_image=no
All three lines are in 키=값 format (the placeholders are the key and the value). The package count must equal the number of lines in the step 2 file, and since the final image must have no denied package and no secret, the other two values become 0 and no respectively. If they differ from the actual state, grading will catch it.