요약

SUMMARY

고객사에 솔루션을 반입할 때마다 컨테이너 이미지에 알려진 취약점(CVE)을 검사하는 프로세스를 진행해야 했다. 매 반입마다 되풀이되는 일이므로 그때그때 처리하지 않고 프로세스로 정착시켰습니다. trivy로 스캔하고, 무작정 버전을 올리는 대신 공식 업그레이드→하드닝 이미지→직접 빌드→대체 이미지→추적(no-fix)으로 이어지는 대응 사다리를 따르며, 결정을 원장에 남긴다.

직접 만든 이미지가 아니라 가져다 쓰는 OSS 이미지가 대상입니다. CVE 하나 때문에 무작정 최신 버전으로 올리면 breaking change 나 라이선스 문제가 함께 발생하기 쉽습니다.

1. 도입 배경

고객사에 솔루션을 반입할 때 항상 거치는 절차가 있는데, 바로 이미지 취약점(CVE) 검사 및 대응이다. 기존에는 고객사 프로젝트 담당자가 직접 처리하는 방식으로 진행했기 때문에 대응 방식이 제각각 달라지고 히스토리 또한 제대로 유지되지 못했습니다. 따라서 대응 방식을 공통된 방식으로 일원화하고 기록도 함께 유지하는 프로세스가 필요하다고 판단했습니다.

그래서 세 가지 사항을 고정했습니다: 무엇을 스캔하는지, 어떤 순서로 대응을 시도하는지, 결정을 어디에 남기는지.

2. 스캔 대상과 결과 해석

배포하는 이미지 목록을 추출하여 각각 trivy image 로 스캔합니다. OS 패키지(apt/apk)와 언어 패키지(Go 바이너리·npm·pip 등)를 모두 검사합니다. 기본적으로는 CRITICAL 등급만 검사하고, 필요하면 HIGH 등급까지 범위를 넓힙니다. 이미지별 상태는 대략 ok(0건), vuln(검출), failed(pull 실패)로 구분됩니다. failed 는 대개 사설 레지스트리라서 인증이 필요한 경우입니다(로그인 후 재시도).

vuln 으로 표시된 CVE 에서는 fixed version 이 존재하는지 여부를 가장 먼저 확인합니다.

  • fixed 버전이 있으면: 해당 버전 이상으로 올렸을 때 해소됩니다. 대응의 출발점입니다.
  • fixed 버전이 없으면(no fix): 업스트림이 아직 수정하지 않은 것입니다. 버전을 올려도 사라지지 않으므로 억지로 조치하기보다는 추적 대상으로 관리합니다.

스캔 결과는 시점 산출물이라는 점을 늘 염두에 둡니다. CVE 데이터베이스가 갱신되면 어제까지 깨끗하던 이미지가 오늘 취약한 것으로 표시됩니다. 그래서 스캔 결과 자체는 회차별로 쌓되 git 에는 올리지 않고, 대응 결정만 원장에 남깁니다.

(직접 작성한 Go 코드와 그 의존성의 취약점은 성격이 다른 문제이므로 govulncheck·gosec 으로 따로 검사합니다: 여기. 이 글은 가져다 쓰는 OSS 이미지에 관한 내용입니다.)

3. 대응 사다리

원칙은 하나입니다. 무작정 최신 버전으로 올리지 않습니다. 후보를 정하고 각각 trivy 로 스캔하여 CVE 가 실제로 사라지는 최선의 옵션을 고릅니다. 위 단계부터 시도하고, 막히면 한 칸 아래 단계로 내려갑니다.

1. 공식 업그레이드   ── 후보(최신 stable·LTS·동일 메이저 최신 패치)를 스캔 → 해소되면 채택
       │ (breaking / license / no-fix 로 막히면)
2. 하드닝 이미지     ── 벤더가 미리 취약점을 줄여둔 대체 이미지를 스캔 → 해소되면 채택
       │ (미수록이거나 여전히 안 되면)
3. self-built        ── upstream 기반 + 취약 패키지만 패치한 Dockerfile → 빌드 → 재스캔으로 검증
       │ (라이선스·재컴파일 제약으로 막히면)
4. replace           ── 유지보수되는 대체 이미지로 갈아탐 (EOL 이미지 탈출)
       │ (전부 소진되면)
5. no-fix            ── 업스트림 미해결. 근거·잔여 CVE·도달성을 적어 추적

1. 공식 업그레이드. 실험적 버전은 피하고 LTS 나 동일 메이저의 최신 패치를 우선합니다. 새 메이저로 점프하면 CVE 는 사라지더라도 breaking change 가 딸려오기 쉽습니다. 후보를 스캔하여 해소되었고 breaking·라이선스 이슈가 없다면 이 단계에서 마무리합니다.

3. self-built. 1·2 단계로 해결되지 않으면 직접 빌드합니다. 이미지 전체를 새로 작성하는 것이 아니라, upstream 이미지를 베이스로 두고 취약한 바이너리 하나만 걷어내는 방식입니다. 예를 들어 어떤 공식 DB 이미지의 CVE 가 DB 본체가 아니라 동봉된 gosu 의 Go stdlib 에서 발생한 적이 있습니다. 그럴 때는 본체·entrypoint·UID·볼륨은 그대로 두고, 취약한 gosu 만 OS 기본 유틸(setpriv) 기반 호환 wrapper 로 교체하여 최종 이미지에서 제외했습니다. 그리고 재스캔으로 CVE 가 실제로 사라졌는지 검증한 뒤에 채택합니다.

4. replace. 문제가 이미지 하나에 국한되지 않고 계열 전체가 EOL 이라면 다른 이미지로 전환합니다. 널리 쓰이던 어떤 벤더 이미지 묶음이 유지보수 종료(legacy/동결)로 전환된 적이 있었는데, 이 경우 개별 CVE 를 하나씩 잡는 일은 근본적인 해결책이 되지 못합니다. 유지보수가 지속되는 공식 이미지로 차트째 마이그레이션하는 것이 답이었습니다.

5. no-fix. 사다리를 모두 소진해도 해결되지 않는 경우가 있습니다. 업스트림이 자체 코드의 CVE 를 아직 수정하지 않았거나, fix 는 존재하지만 바이너리에 내장된 의존성이라 소스 재컴파일이 필요하고 그 재컴파일이 라이선스(예: AGPL 소스 공개 의무)와 충돌하는 경우입니다. 이럴 때는 억지로 조치하지 않고, 수정할 수 없는 이유와 잔여 CVE, 실제 도달 가능성(예: 폐쇄망이라 해당 경로가 노출되지 않음)을 근거와 함께 기록하여 추적 항목으로 남깁니다.

4. 하드닝 이미지(DHI)를 언제 쓸까 — 잠정 결론

사다리 2단계의 하드닝 이미지는 처음에 가장 기대했던 선택지였습니다. 벤더가 취약점을 미리 줄여 둔 이미지(예: Docker Hardened Image)를 최대한 활용하면 self-built 의 수고가 없을 것이라고 생각했습니다. 실제로 운용해 보니 공짜가 아니었습니다.

  • 실행 UID 가 강제되는 경우가 있습니다. 하드닝 이미지가 특정 non-root UID 로 고정되어 있기 때문에, 기존 배포의 데이터 볼륨(PVC) 소유권과 맞지 않으면 그대로는 교체할 수 없습니다. (CRITICAL 등급 0건인 대체 이미지가 있었는데도 UID 가 달라 기존 볼륨과 충돌하여 단순 교체가 불가능했던 적이 있습니다.)
  • 일부 패키지가 누락되어 있습니다. 공격 면을 줄이려고 최소 구성으로 만들다 보니, 원본 이미지가 사용하던 유틸·라이브러리가 없어서 동작이 깨지기도 합니다.

그래서 잠정 결론은 다음과 같습니다. 기능이 단순하고 볼륨 마운트가 없는(상태를 들고 있지 않고 UID·소유권 이슈가 없는) 이미지에만 하드닝 이미지를 사용합니다. 상태를 유지하면서 볼륨을 붙이거나 부가 패키지에 의존하는 이미지는, 하드닝 이미지로 전환하기보다 공식 업그레이드나 self-built 로 진행하는 편이 문제가 적었습니다. DHI 를 “만능 치트키”로 밀지 않고 적용 조건을 좁힌 것이 이번 프로세스의 실무 교훈입니다.

5. 결정을 원장에 남기기

반복 업무를 프로세스로 만드는 핵심은 결국 기록입니다. 스캔 결과는 시점마다 달라지지만, “이 CVE 를 이렇게 대응했다”는 결정은 남아 있어야 합니다. 그래서 이미지 단위의 원장(YAML)을 두고 항목마다 다음과 같이 기재합니다.

  • 대응 전 이미지 ref 와 사용처(어떤 차트들이 공유하는지)
  • 대상 CVE 목록
  • 대응 방식(upgrade / hardened / self-built / replace / no-fix)과 결과 ref
  • 사유(breaking·마이그레이션·라이선스·잔여 CVE)와 근거 URL

이 원장이 있으면 반년 뒤에 같은 이미지가 다시 취약한 것으로 표시되더라도, 다른 고객사에 같은 이미지를 반입할 때 “저번에 왜 이렇게 했는지”를 다시 조사하지 않아도 됩니다. 여러 차트가 같은 이미지를 공유할 때 한 번 대응하고 사용처 전부에 반영하는 작업도 원장 덕분에 놓치지 않습니다.

6. 남은 것

반복되는 CVE 대응을 사다리와 원장으로 정착시키고 나니, 반입 때마다 헤매던 일이 “스캔 → 사다리 적용 → 원장 기록”의 절차가 되었습니다. CVE 대응은 “버전 올리기”가 아니라 의사결정이므로, 어디까지 시도하고 어디서 멈출지를 미리 정해 둔 것이 큰 도움이 되었습니다. 다음으로는 스캔·원장 기록을 CI 로 더 자동화하고, self-built 이미지의 빌드·검증을 파이프라인에 추가할 계획입니다.

참고