Inventory, SBOM and the Gate
한국어 원문으로 표시합니다.
이 실습은 진짜 VM 에서 돕니다
이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이
따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라
진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가
되고 docker exec 도 docker logs 도 그대로 동작합니다.
예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라 컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어 보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.
알아 둘 것이 둘 있습니다.
- 처음 뜨는 데 1분 남짓 걸립니다. VM 이 부팅하고 도커를 설치하기 때문입니다. 파드 실습(보통 40초)보다 느립니다.
- 브라우저 미리보기가 없습니다. VM 으로 들어오는 연결은 채점 포트
하나만 열려 있습니다. 웹 서버를 띄웠다면 VM 안에서
curl로 확인하세요.
목표
스캐너가 실제로 하는 일(패키지 인벤토리 → DB 대조)을 손으로 재현합니다. 베이스와 런타임 이미지의 패키지 목록을 뽑아 늘어난 공격 표면을 숫자로 세고, SBOM 을 직접 만들고, 정책 게이트 스크립트를 작성한 뒤, 이미지에 박제된 시크릿을 찾아 고칩니다.
왜 중요한가
스캐너를 마법 상자로 두면 두 가지 잘못된 결론에 빠집니다. "깨끗하니 안전하다"와
"1,247건 전부 고쳐야 한다". 스캐너는 이미지를 실행하지도 코드를 분석하지도 않고,
설치된 패키지의 이름·버전을 취약점 DB 와 대조할 뿐입니다. 그래서 curl | tar xz
로 넣은 바이너리는 아예 보이지 않고, 한 번도 호출하지 않는 패키지의 취약점은
그대로 올라옵니다. 이 원리를 손으로 재현해 보면 "무엇을 고칠 것인가"보다
"왜 이 패키지가 이미지에 있는가" 가 먼저 나와야 한다는 것이 분명해집니다.
애플리케이션이 링크하는 공유 라이브러리가 8개인데 이미지에 패키지가 432개
들어 있다면, 문제는 취약점이 아니라 이미지 구성입니다.
단계
/root/sec2디렉터리를 만들고,alpine:3.20에 설치된 패키지를이름-버전한 줄 형식으로/root/sec2/alpine-pkgs.txt에 저장합니다. 10줄 이상이어야 하고musl이 들어 있어야 합니다.- 같은 형식으로
nginx:1.27-alpine의 패키지 목록을/root/sec2/nginx-pkgs.txt에 저장합니다. 10줄 이상이어야 하고nginx가 들어 있어야 하며 1단계보다 줄 수가 많아야 합니다. - 두 목록을 버전을 뗀 패키지 이름 기준으로 비교해, nginx 쪽에만 있는 패키지 개수를
/root/sec2/extra-count.txt에 숫자만 저장합니다 (오차 2 이내 허용). /root/sec2/sbom.json을 만듭니다. 최상위image값은nginx:1.27-alpine,packages는 배열이며 항목 수가 2단계 파일의 줄 수와 같아야 합니다. 각 항목에는 비어 있지 않은name과version이 있어야 합니다./root/sec2/deny.txt에 금지 패키지 이름을 한 줄에 하나씩 적습니다(반드시curl포함). 그리고 실행 권한이 있는/root/sec2/gate.sh를 작성합니다. 첫 번째 인자로 받은 패키지 목록 파일에 금지 패키지가 없으면 종료코드 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에 다음 세 줄을 정확히 이 형식으로 적습니다.package_count=<2단계 파일의 줄 수>denied_hits=0secret_in_image=no
참고
- alpine 계열 이미지의 설치 패키지는
docker run --rm <이미지> apk info -v로이름-버전한 줄씩 나옵니다. 그 명령이 읽는 것은 이미지 안의 평범한 텍스트 파일/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 에 설치된 패키지를 이름-버전 한 줄 형식으로 /root/sec2/alpine-pkgs.txt 에 저장합니다. 10줄 이상이어야 하고 musl 이 들어 있어야 합니다.
스캐너가 제일 먼저 하는 일이 이것입니다. alpine 은 apk 로 설치된 패키지를 조회할 수 있고, 이름과 버전이 붙은 한 줄 형식으로 뽑는 옵션이 있습니다. 네트워크 없이 로컬 DB 만 읽으므로 오프라인에서도 동작합니다.
런타임 이미지와 비교
같은 형식으로 nginx:1.27-alpine 의 패키지 목록을 /root/sec2/nginx-pkgs.txt 에 저장합니다. 10줄 이상이어야 하고 nginx 가 들어 있어야 하며 1단계보다 줄 수가 많아야 합니다.
1단계와 정확히 같은 형식으로 뽑아야 다음 단계에서 비교가 됩니다. 같은 alpine 베이스인데 nginx 이미지 쪽 줄 수가 더 많아야 정상입니다. 그 차이가 곧 늘어난 공격 표면입니다.
늘어난 공격 표면 세기
두 목록을 버전을 뗀 패키지 이름 기준으로 비교해, nginx 쪽에만 있는 패키지 개수를 /root/sec2/extra-count.txt 에 숫자만 저장합니다 (오차 2 이내 허용).
버전 문자열이 붙어 있으면 같은 패키지도 다르게 보입니다. 이름-버전 에서 버전을 떼어 이름만으로 비교하세요. 정렬 후 한쪽에만 있는 항목을 세는 표준 도구가 있습니다. 파일에는 숫자만 남기세요.
SBOM 직접 만들어 보기
/root/sec2/sbom.json 을 만듭니다. 최상위 image 값은 nginx:1.27-alpine, packages 는 배열이며 항목 수가 2단계 파일의 줄 수와 같아야 합니다. 각 항목에는 비어 있지 않은 name 과 version 이 있어야 합니다.
도구를 쓰는 것이 아니라 구조를 이해하는 단계입니다. 2단계 목록의 각 줄을 이름과 버전으로 갈라 JSON 배열로 만드세요. 항목 수가 2단계 줄 수와 정확히 같아야 하고, 이름만 있고 버전이 빈 항목이 하나라도 있으면 대조가 불가능하므로 실패합니다.
정책 게이트 스크립트
/root/sec2/deny.txt 에 금지 패키지 이름을 한 줄에 하나씩 적습니다(반드시 curl 포함). 그리고 실행 권한이 있는 /root/sec2/gate.sh 를 작성합니다. 첫 번째 인자로 받은 패키지 목록 파일에 금지 패키지가 없으면 종료코드 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 에 다음 세 줄을 정확히 이 형식으로 적습니다.
package_count=<2단계 파일의 줄 수>denied_hits=0secret_in_image=no
세 줄 모두 키=값 형식입니다. 패키지 수는 2단계 파일의 줄 수와 같아야 하고, 최종 이미지에 금지 패키지가 없고 시크릿도 없어야 하므로 나머지 두 값은 각각 0 과 no 가 됩니다. 실제 상태와 다르면 채점이 잡아냅니다.