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