TT Lab
Get started
Learn Learning paths Courses

Container Security

Inventory, SBOM and the Gate

Continue in TT Lab

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.

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

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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=0
    • secret_in_image=no

Notes

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.

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.