Docker 이미지 보안 스캔과 수정: Trivy 실전 튜토리얼 및 CI/CD 통합 가이드

2021년 12월 10일, 모니터링 대시보드의 경보 곡선이 갑자기 빨간색으로 치솟았습니다. 운영 채팅방에 누군가 CVE 번호 하나를 올렸습니다. 바로 이후 기술 업계 전체를 뒤흔든 Log4Shell 취약점인 CVE-2021-44228이었습니다. 10개가 넘는 마이크로서비스가 이 취약점이 포함된 베이스 이미지를 사용하고 있었고, 팀 전체가 밤새 긴급히 이미지를 업그레이드하고 다시 배포했습니다.
사후 회고에서 대표가 한마디 물었습니다. “우리가 쓰는 이미지에 어떤 취약점이 있는지도 어떻게 몰랐죠?”
NSFOCUS의 한 연구에 따르면 Docker Hub 이미지의 76%에는 알려진 보안 취약점이 있습니다. 지금 사용 중인 python:3.9 이미지나 nginx:latest에도 수십 개의 CVE 취약점이 숨어 있을 가능성이 큽니다.
아마 이런 질문이 떠오를 겁니다. 이미지에 어떤 취약점이 있는지 어떻게 알 수 있을까요? 취약점을 발견한 뒤에는 어떻게 수정해야 할까요? 취약한 이미지를 운영 환경에 배포하지 않도록 CI/CD 과정에서 자동으로 탐지하려면 어떻게 해야 할까요?
저는 운영 환경에서 2년 동안 직접 시행착오를 겪으며 이 질문들의 답을 찾았습니다. 이 글에서는 다음 내용을 단계별로 설명합니다.
- Trivy 같은 도구로 Docker 이미지 취약점을 빠르게 스캔하는 방법
- 단순히 ‘이미지 버전 업데이트’에 그치지 않고 취약점을 체계적으로 수정하는 방법
- GitHub Actions, GitLab CI 같은 CI/CD 플랫폼에 자동 스캔을 통합하는 방법
모든 명령과 설정은 그대로 복사해 실행할 수 있는 실전 코드입니다. 이 글을 다 읽고 나면 불필요한 시행착오를 크게 줄일 수 있을 겁니다.
"NSFOCUS가 2018년 3월 발표한 연구 보고서에 따르면 Docker Hub에서 무작위로 조사한 이미지 중 4분의 3이 넘는 76%에 알려진 보안 취약점이 있었습니다."
Docker 이미지 보안 위협의 전체 모습
Docker 이미지에는 실제로 어떤 보안 문제가 생길까요?
솔직히 저도 컨테이너를 처음 접했을 때는 Docker 이미지를 그저 ‘패키징된 프로그램’이라고 생각했습니다. 실행만 되면 된다고 여겼죠. 하지만 운영 환경이 사흘 동안 스캔 공격을 받은 뒤에야 이미지 취약점도 여러 종류로 나뉘고, 어느 것 하나 가볍지 않다는 사실을 알았습니다.
운영체제 패키지 취약점 - 가장 흔한 유형입니다. Alpine이나 Ubuntu 베이스 이미지에 들어 있는 OpenSSL, glibc, curl 같은 시스템 라이브러리에서는 주기적으로 새 취약점이 발견됩니다. 예를 들어 2022년 OpenSSL 3.0.x 고위험 취약점은 수많은 이미지에 영향을 줬습니다.
애플리케이션 의존성 취약점 - 더 은밀합니다. Python 프로젝트가 Flask에 의존하고, Flask는 다시 Werkzeug에 의존하는데 특정 Werkzeug 버전에 코드 실행 취약점이 있을 수 있습니다. Python 버전만 올리면 안전해질까요? 꼭 그렇지는 않습니다. 의존성 트리 안에 숨어 있는 취약점을 아예 모르고 지나칠 수도 있습니다. Log4j가 대표적인 사례입니다. 수많은 Java 프로젝트가 자신이 Log4j를 사용하는지조차 몰랐습니다.
잘못된 구성과 민감 정보 유출 - 편의를 위해 데이터베이스 비밀번호를 Dockerfile에 직접 적거나 .git 디렉터리를 지우지 않은 채 패키징하는 개발자도 있습니다. root 사용자로 컨테이너를 실행하면서 지나치게 큰 권한을 주는 경우도 있습니다.
공급망 위험 - 가장 예방하기 어려운 유형입니다. Docker Hub에서 겉보기에는 정상적인 이미지를 받았는데 나중에 채굴 프로그램이나 백도어가 심어져 있었다는 사실이 밝혀질 수도 있습니다. 2018년 연구에서는 편의를 위해 곧바로 pull하는 사용자를 노린 ‘악성 이미지’가 Docker Hub에서 다수 발견됐습니다.
Docker Hub 이미지를 무조건 신뢰하면 안 되는 이유
저도 76%라는 수치를 처음 봤을 때 정말 놀랐습니다.
데이터가 다소 오래됐다고 생각할 수 있습니다. 문제는 당시의 오래된 이미지가 지금도 사용된다는 점입니다. 게다가 많은 ‘공식 이미지’도 실시간으로 업데이트되지 않습니다. python:3.9 태그가 반년 전에 빌드된 것이라면 내부 시스템 패키지는 이미 낡았을 수 있습니다.
더 골치 아픈 점은 Docker Hub 이미지의 품질이 제각각이라는 사실입니다. 개인 개발자가 관리해 반년 동안 업데이트되지 않은 이미지도 있고, ‘공식’이라고 표시돼 있지만 실제로는 특정 회사가 직접 업로드한 이미지도 있습니다. 통일된 보안 감사 표준이 없으니 운에 맡기고 사용하는 셈입니다.
예전에 한 동료가 편의를 위해 Docker Hub에서 Node.js 환경이 포함된 이미지를 가져왔습니다. 나중에 스캔해 보니 고위험 취약점이 30개 넘게 나왔습니다. 이미지 작성자에게 문의했더니 “이 이미지는 2년 동안 관리하지 않았으니 직접 수정하세요”라는 답이 돌아왔습니다. 황당해서 웃음밖에 나오지 않았습니다.
그래서 지금 제 원칙은 간단합니다. Docker Hub에서 받은 이미지는 사용하기 전에 반드시 한 번 스캔합니다. ‘공식’ 태그가 붙어 있어도 예외는 없습니다.
주요 이미지 스캔 도구 비교와 선택
Trivy: 가장 추천하는 오픈 소스 도구
Docker 이미지 보안 스캔 도구는 다양하지만 지금은 거의 Trivy만 사용합니다. 다른 도구가 나빠서가 아니라 Trivy가 ‘사용 편의성’과 ‘기능성’의 균형을 정말 잘 맞췄기 때문입니다.
처음 Trivy를 썼을 때는 속도에 놀랐습니다. 수백 MB짜리 이미지를 처음 스캔하는 데 약 10초가 걸리고, 다음부터는 몇 초면 충분했습니다. 무엇보다 로컬 데이터베이스를 직접 관리하거나 별도 서비스를 구성할 필요 없이 설치 직후 사용할 수 있습니다.
Trivy가 탐지하는 범위는 예상보다 넓습니다.
- 취약점 탐지: 여러 운영체제 패키지(Alpine, Ubuntu, Debian, CentOS 등)와 애플리케이션 의존성(npm, pip, Maven, Go Modules 등)을 지원합니다.
- 잘못된 구성 스캔: Dockerfile과 Kubernetes 구성 파일의 보안 문제를 검사합니다.
- Secret 유출 탐지: 키나 비밀번호 같은 민감 정보가 이미지에 실수로 포함됐는지 알려 줍니다.
- 라이선스 규정 준수: 의존 패키지의 오픈 소스 라이선스까지 검사해 법적 위험을 줄여 줍니다.
가장 만족스러운 점은 Trivy가 이미 GitHub Actions, Harbor 같은 주요 플랫폼에 통합돼 있다는 것입니다. 직접 통합 방식을 연구하지 않아도 공식 도구를 바로 사용할 수 있습니다.
다른 도구도 간단히 살펴보기
Snyk - 기능이 매우 폭넓은 상용 제품입니다. IDE에 바로 통합해 코딩하는 동안 의존성 취약점을 실시간으로 알려 주고 수정 PR도 자동 생성합니다. 다만 무료 버전에는 제한이 많고 본격적으로 사용하려면 비용이 들어 예산이 충분한 대기업에 알맞습니다.
Clair - Quay 이미지 저장소의 오픈 소스 스캔 엔진으로 깊이 커스터마이징할 수 있습니다. 하지만 솔직히 구성은 꽤 복잡합니다. 여러 Linux 배포판의 보안 팀으로부터 CVE 정보를 가져와 위협 탐지에 집중하는 방식입니다. 전담 보안 엔지니어가 있고 고도로 맞춤화된 솔루션이 필요하다면 좋은 선택이지만 일반 개발팀에는 학습 비용이 높습니다.
Anchore - 정책 관리를 지원하는 엔터프라이즈급 솔루션입니다. 예를 들어 “CRITICAL 등급 취약점이 발견되면 배포를 차단한다”는 규칙을 설정할 수 있습니다. 금융이나 의료처럼 규정 준수 요건이 특히 엄격한 환경에 적합하지만 구성과 유지 관리 부담도 큽니다.
Docker Scout / Hardened Images (DHI) - Docker가 2025년 5월 선보인 새 제품으로, CVE가 거의 없고 이미지 크기도 95% 줄일 수 있다고 소개됐습니다. 기술 방식을 살펴보면 distroless 런타임을 사용하는데 확실히 유망합니다. 다만 출시된 지 얼마 되지 않아 생태계가 아직 성장 중이므로 관심을 두고 지켜볼 만합니다.
제 선택 기준
중소 팀이나 개인 개발자라면 Trivy를 바로 사용하세요. 무료이고 사용하기 쉬우며 기능도 충분합니다. 설치 명령 하나, 스캔 명령 하나면 10분 안에 시작할 수 있습니다.
대기업이고 보안 규정 준수 요건이 높으며 예산도 충분하다면 Snyk를 고려할 수 있습니다. 취약점 데이터베이스가 빠르게 업데이트되고 전문 지원을 제공하며 여러 개발 도구와 깊이 통합됩니다.
전담 보안 엔지니어가 있고 깊이 커스터마이징할 수 있는 오픈 소스 솔루션이 필요하다면 Clair를 연구해 볼 만합니다. 다만 구성에 충분한 시간을 투자할 준비는 해야 합니다.
극한의 보안과 최소 크기 이미지를 추구한다면 Docker Hardened Images의 발전을 지켜보세요. 차세대 표준 솔루션이 될 가능성도 있습니다.
제 경우 일상 개발과 CI/CD에는 빠르게 피드백을 주는 Trivy를 사용하고, 운영 배포 전에는 Harbor에 통합된 Clair로 다시 스캔해 이중으로 확인합니다.
Trivy 실전 튜토리얼
설치와 기본 사용법
Trivy의 설치는 처음에는 믿기 어려울 정도로 간단합니다. macOS를 사용한다면 다음 명령을 실행하면 됩니다.
brew install trivy
Linux 사용자는 바이너리를 직접 다운로드하거나 패키지 관리자로 설치할 수 있습니다. 자세한 내용은 GitHub releases 페이지에서 확인하세요. https://github.com/aquasecurity/trivy/releases
설치가 끝나면 바로 이미지를 하나 스캔해 봅시다.
# Docker Hub 이미지 스캔
trivy image python:3.9
# 로컬에서 빌드한 이미지 스캔
trivy image myapp:latest
# tar 파일로 저장한 이미지 스캔
trivy image --input ruby-3.1.tar
처음 실행하면 Trivy가 수십 MB 정도인 취약점 데이터베이스(trivy-db)를 자동으로 다운로드하므로 잠시 기다려야 합니다. 다운로드가 끝난 뒤부터는 스캔 속도가 매우 빨라집니다.
스캔 결과는 취약점 심각도에 따라 분류돼 표시됩니다.
- CRITICAL(심각): 즉시 수정해야 하며 시스템 전체 제어로 이어질 수 있습니다.
- HIGH(위험): 공격에 악용될 수 있으므로 가능한 한 빨리 수정해야 합니다.
- MEDIUM(보통): 일정 수준의 보안 위험이 있어 수정을 권장합니다.
- LOW(낮음): 우선순위가 낮아 일정에 맞춰 처리할 수 있습니다.
각 취약점에는 CVE 번호, 영향을 받는 패키지명, 현재 버전, 수정 버전(있는 경우)이 표시됩니다.
고급 스캔 옵션: Trivy를 더 효율적으로 사용하기
실무에서는 모든 취약점, 특히 아직 수정할 방법이 없는 낮은 위험도의 취약점을 전부 보고 싶지 않을 수 있습니다. 이때 고급 옵션을 사용하면 됩니다.
HIGH와 CRITICAL 취약점만 표시하기:
trivy image --severity HIGH,CRITICAL nginx:latest
저도 CI/CD에서 자주 사용하는 옵션입니다. 모든 취약점을 한꺼번에 수정하기는 현실적으로 어려우므로 위험도가 높은 항목부터 해결해야 합니다.
아직 수정되지 않은 취약점 무시하기:
trivy image --ignore-unfixed redis:latest
공식 패치가 나오지 않은 취약점은 서두른다고 해결할 수 없습니다. 이 옵션을 추가하면 Trivy가 이미 수정 방법이 있는 취약점만 표시하므로 실제로 해결할 수 있는 문제에 집중할 수 있습니다.
심각한 취약점 발견 시 0이 아닌 종료 코드 반환하기:
trivy image --exit-code 1 --severity CRITICAL myapp:latest
매우 유용한 옵션입니다. CI/CD pipeline에서 심각한 취약점이 발견되면 명령이 0이 아닌 종료 코드를 반환해 빌드가 실패합니다. 이미지 앞에 보안 게이트를 추가해 취약한 이미지가 운영 환경에 들어가지 못하게 하는 셈입니다.
다른 도구와 쉽게 통합하도록 JSON으로 출력하기:
trivy image -f json -o results.json myapp:latest
스캔 결과를 보안 플랫폼으로 가져오거나 추가 자동 처리를 하려면 JSON 형식이 편리합니다.
오프라인 환경에서 스캔하기:
trivy image --skip-db-update myapp:latest
일부 회사의 내부망은 외부 인터넷에 접속할 수 없습니다. 인터넷이 되는 장비에서 취약점 데이터베이스를 미리 내려받아 내부망 장비로 복사한 뒤 이 옵션을 사용하면 업데이트를 시도하지 않습니다.
스캔 결과 이해하기: 무엇을 중점적으로 봐야 할까요?
처음 운영 이미지를 스캔했을 때 화면을 가득 채운 취약점이 100개 넘게 나왔습니다. 이걸 언제 다 고치나 싶어 눈앞이 캄캄했습니다.
하지만 경험이 쌓인 뒤에는 결과를 읽는 데도 요령이 있다는 것을 알게 됐습니다.
수정 버전이 있는 취약점을 먼저 확인하세요. ‘Fixed Version’ 열에 ‘none’이 표시되거나 비어 있다면 아직 패치가 없다는 뜻이므로 당장 할 수 있는 일이 없습니다. 수정 방법이 있는 취약점부터 처리합니다.
CRITICAL과 HIGH에 집중하세요. LOW와 MEDIUM 취약점은 외부에 공개된 서비스가 아니라면 조금 뒤로 미룰 수 있습니다. 하지만 CRITICAL은 즉시 수정해야 합니다. 이런 취약점은 공개 exploit 코드가 있어 공격자가 바로 악용할 때가 많습니다.
CVE 세부 정보를 확인하세요. 특정 취약점의 위험을 판단하기 어렵다면 https://cve.mitre.org/ 에서 CVE 번호를 검색해 구체적인 문제, 영향 범위, 악용 난도를 확인하세요. 겉으로는 심각해 보여도 매우 까다로운 조건에서만 악용할 수 있는 취약점도 있습니다.
Trivy의 작동 원리를 이해하세요. Trivy는 여러 소프트웨어 패키지의 알려진 취약점을 기록한 JSON 취약점 데이터베이스를 다운로드합니다. 그리고 패키지 관리자 데이터베이스를 분석해 이미지 안의 패키지 목록을 얻은 뒤 서로 대조합니다. 패키지명과 버전 번호가 알려진 취약점과 일치하면 경고합니다.
따라서 오탐이 발생할 때도 있습니다. 이미지에 취약한 패키지가 있지만 실제 코드가 문제가 있는 함수를 전혀 호출하지 않아도 Trivy는 경고합니다. 이때는 실제 코드까지 함께 검토해 위험을 판단해야 합니다.
취약점을 체계적으로 수정하는 방법
올바른 베이스 이미지 선택부터 시작하기
취약점 수정은 때로는 ‘고치는 것’보다 ‘교체하는 것’에 가깝습니다. 시스템 패키지 취약점이 수십 개 발견됐다면 하나씩 업그레이드하기보다 더 안전한 베이스 이미지로 교체하는 편이 훨씬 효율적입니다.
예전에 저희 팀은 5.87MB로 아주 작은 Alpine Linux를 베이스 이미지로 즐겨 사용했습니다. 하지만 나중에 Alpine에도 문제가 있다는 것을 알게 됐습니다. 일반적인 glibc 대신 muslc 표준 라이브러리를 사용해 호환성 문제가 생길 수 있고, 작다고 해서 취약점까지 사라지는 것은 아닙니다.
지금은 Distroless 이미지를 더 선호합니다. 3.06MB에 불과해 Alpine보다도 작고, Shell이나 패키지 관리자, 불필요한 도구를 모두 제거해 극도로 간소화했습니다.
왜 더 안전할까요? 간단합니다. 공격자가 애플리케이션 취약점을 발견해도 컨테이너 안에 shell 자체가 없으므로 shell 명령을 실행할 수 없습니다. 공격 표면을 최소한으로 줄이는 것입니다.
Google Distroless 이미지는 여러 언어용 버전을 제공합니다.
# Python 애플리케이션
FROM gcr.io/distroless/python3
# Node.js 애플리케이션
FROM gcr.io/distroless/nodejs
# Go 애플리케이션(정적 컴파일이면 static 버전도 사용 가능)
FROM gcr.io/distroless/static
물론 Distroless에는 불편한 점도 있습니다. ls, cat 같은 기본 명령조차 없어 디버깅이 어렵습니다. 저는 개발 환경에서는 디버깅하기 쉬운 일반 이미지를 사용하고, 운영 환경에서는 안전성을 위해 Distroless로 전환합니다.
Docker가 2025년에 선보인 **Hardened Images (DHI)**는 한층 더 과감합니다. CVE가 거의 없고 이미지 크기도 95% 더 줄일 수 있다고 소개됐습니다. 아직 생태계가 성숙하지 않았지만 차세대 표준 솔루션이 될 가능성이 있어 관심을 둘 만합니다.
의존 패키지를 업그레이드해 취약점을 수정하는 실전 방법
베이스 이미지를 바꾸는 것만으로는 부족합니다. 애플리케이션 계층의 의존성도 관리해야 합니다. 다음은 몇 가지 실용적인 방법입니다.
방법 1: 베이스 이미지 버전 업데이트
# 너무 모호한 latest는 사용하지 않기
FROM python:3.9
# 구체적인 마이너 버전을 사용하고 정기적으로 업데이트
FROM python:3.11.7
편의를 위해 latest 태그를 쓰는 사람이 많지만 좋지 않은 습관입니다. latest가 몇 달 동안 업데이트되지 않으면 내부 시스템 패키지는 이미 낡게 됩니다. 구체적인 버전 번호를 사용하고 매월 새 마이너 버전이나 패치 버전이 나왔는지 능동적으로 확인하세요.
방법 2: Dockerfile에서 시스템 패키지 업그레이드
FROM ubuntu:22.04
# 이미지 빌드 시 모든 시스템 패키지 업데이트
RUN apt-get update && \
apt-get upgrade -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
여기에는 요령이 있습니다. update, upgrade, clean을 하나의 RUN 명령에 넣으면 이미지 레이어 수를 줄일 수 있습니다. 마지막에 apt 캐시를 삭제하면 용량도 수십 MB 절약할 수 있습니다.
방법 3: 애플리케이션 의존성 버전 업데이트
가장 흔한 상황입니다. Trivy 스캔에서 Flask 취약점이 나오면 requirements.txt의 버전 번호를 수정합니다.
# 수정 전
Flask==2.0.1
# 수정 후(2.3.0에서 취약점이 수정됐다고 가정)
Flask==2.3.0
다만 주의할 점이 있습니다. Flask를 바로 업그레이드하면 호환성 문제가 생길 수 있습니다. 따라서 먼저 테스트 환경에서 실행해 문제가 없는지 확인한 다음 운영에 반영하는 것이 좋습니다.
더 안정적인 방법은 패치 버전만 업그레이드하는 것입니다. 예를 들어 Flask 2.0.1에 취약점이 있다면 바로 2.3.0으로 건너뛰기보다 먼저 최신 2.0.x를 사용해 보세요.
놓치면 안 되는 모범 사례
구체적인 취약점을 수정하는 것 외에도 이미지를 오랫동안 안전하게 유지하는 습관이 있습니다.
멀티 스테이지 빌드 사용:
# 빌드 단계
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# 실행 단계(컴파일된 바이너리만 포함)
FROM gcr.io/distroless/static
COPY --from=builder /app/myapp /
CMD ["/myapp"]
이렇게 하면 최종 이미지에 Go 컴파일러, 소스 코드, 중간 파일이 포함되지 않습니다. 이미지가 깔끔해지고 공격 표면도 크게 줄어듭니다.
root 사용자로 실행하지 않기:
RUN useradd -m myuser
USER myuser
많은 이미지는 기본적으로 root로 애플리케이션을 실행하는데 이는 매우 위험합니다. 공격자가 애플리케이션을 침해하면 곧바로 root 권한까지 얻어 무엇이든 할 수 있습니다. 일반 사용자를 만들고 그 권한으로 애플리케이션을 실행하는 편이 훨씬 안전합니다.
정기적으로 이미지 재빌드하기:
쉽게 놓치는 부분입니다. 코드가 반년 동안 바뀌지 않아도 베이스 이미지의 시스템 패키지에는 계속 새 패치가 나옵니다. 최소한 매월 한 번 이미지를 다시 build해 최신 보안 패치를 적용하세요.
저희 팀은 매주 일요일 모든 운영 이미지를 자동으로 다시 빌드하고 스캔하는 예약 작업을 설정했습니다. 문제가 있으면 월요일에 바로 발견해 수정할 수 있습니다.
.dockerignore로 민감 파일 제외하기:
.git
.env
*.log
secrets/
.env 파일을 실수로 이미지에 넣으면 데이터베이스 비밀번호가 유출됩니다. .dockerignore는 .gitignore처럼 민감 파일이 이미지에 포함되지 않도록 막아 줍니다.
latest 태그는 절대 사용하지 않기:
다시 한번 강조하겠습니다. FROM python:latest는 시한폭탄과 같습니다. 어떤 버전을 받게 될지, 그 안에 취약점이 있는지 알 수 없습니다. FROM python:3.11.7-slim처럼 명확한 태그를 사용하면 예측하고 추적할 수 있습니다.
CI/CD 자동 보안 스캔 절차 구축
GitHub Actions 통합: 5분이면 충분합니다
이미지를 수동으로 스캔하는 일은 번거롭고 잊기도 쉽습니다. 가장 확실한 방법은 CI/CD에 스캔을 통합해 코드가 제출되거나 이미지가 빌드될 때마다 자동으로 검사하는 것입니다.
GitHub Actions에 Trivy를 통합하는 일은 놀랄 만큼 간단합니다. 저장소에 .github/workflows/docker-scan.yml을 만드세요.
name: Docker Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'table'
severity: 'CRITICAL,HIGH'
exit-code: '1' # 심각한 취약점 발견 시 빌드 실패
- name: Upload scan results
if: always()
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
이 구성은 다음 작업을 수행합니다.
- main 또는 develop 브랜치에 push하거나 PR이 생성될 때마다 자동 실행됩니다.
- Docker 이미지를 빌드합니다. commit SHA를 태그로 사용해 빌드마다 고유한 이미지를 만듭니다.
- Trivy로 이미지를 스캔하고 CRITICAL과 HIGH 취약점만 검사합니다.
- 심각한 취약점이 발견되면
exit-code: '1'이 workflow를 실패시켜 병합을 차단합니다. - 스캔 결과를 GitHub Security 탭에 업로드해 쉽게 확인할 수 있습니다.
구성을 마친 뒤 처음으로 코드를 push해 보면 GitHub Actions 탭에서 스캔 결과를 확인할 수 있습니다. 취약점이 있으면 빌드가 빨간색으로 표시되고 주 브랜치에 병합할 수 없습니다. 코드 저장소 앞에 자동 보안 게이트를 하나 추가한 셈입니다.
GitLab CI/CD 통합: 마찬가지로 간단합니다
GitLab을 사용한다면 통합 방식도 비슷합니다. 저장소 루트에 .gitlab-ci.yml을 만들거나 기존 파일을 수정하세요.
stages:
- build
- test
- security
build_image:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
security_scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false
dependencies:
- build_image
allow_failure: false는 보안 스캔이 실패하면 전체 pipeline도 실패하게 한다는 뜻입니다. 이렇게 엄격하게 운영하고 싶지 않다면 allow_failure: true로 바꿔 취약점이 나와도 경고만 하고 배포는 차단하지 않게 할 수 있습니다.
하지만 운영 환경의 pipeline은 반드시 false로 설정할 것을 권합니다. 개발 환경에서는 조금 느슨해도 되지만 운영 환경의 보안은 타협하면 안 됩니다.
Harbor 이미지 저장소로 이중 보호하기
회사에서 Harbor를 사설 이미지 저장소로 사용한다면 더 편리합니다. 많은 대기업이 사용하는 Harbor는 v1.2부터 Trivy와 Clair를 통합해 push된 이미지를 자동으로 스캔할 수 있습니다.
Harbor 프로젝트 설정에서 다음 스캔 정책을 구성할 수 있습니다.
- 이미지 push 시 자동 스캔
- 새 취약점 공개에 대비해 매일 모든 이미지 정기 스캔
- CRITICAL 취약점이 있는 이미지의 pull을 금지하는 식으로 취약점 임계값 설정
저희 팀의 정책은 다음과 같습니다.
- 개발 환경에서 Harbor로 이미지를 push하면 Trivy 스캔을 자동 실행합니다.
- HIGH 이상 취약점이 발견되면 Harbor가 ‘취약점 있음’ 태그를 붙입니다.
- 운영 환경 K8s 클러스터에는 admission webhook을 구성해 이 태그가 있는 이미지의 배포를 거부합니다.
누군가 CI/CD를 우회해 직접 배포하더라도 Harbor가 마지막 게이트에서 차단합니다.
완전한 보안 스캔 절차 권장안
앞의 내용을 연결하면 완전한 이미지 보안 절차는 다음과 같습니다.
개발 단계:
- IDE에 Snyk 또는 Trivy 플러그인을 통합해 코딩 중 의존성 취약점을 확인합니다.
- 로컬에서 이미지를 빌드한 뒤
trivy image를 한 번 수동으로 실행합니다.
빌드 단계:
- GitHub Actions / GitLab CI가 새로 빌드한 이미지를 자동 스캔합니다.
- CRITICAL 취약점이 발견되면 즉시 실패 처리해 병합을 차단합니다.
- 스캔 결과를 Security Dashboard에 업로드해 팀이 추세를 확인하게 합니다.
저장소 단계:
- Harbor 같은 이미지 저장소에서 2차로 스캔합니다. CI/CD가 놓친 항목이나 새로 공개된 취약점을 보완할 수 있습니다.
- 취약점 정책을 설정해 위험 이미지의 pull을 차단합니다.
런타임:
- 운영 환경에서 실행 중인 이미지를 주 1회 정기적으로 스캔합니다.
- 새 취약점이 발견되면 알림을 보내고 수정 일정을 잡습니다.
정기 검토:
- 매월 한 번 취약점 수정 현황을 검토합니다.
- 스캔 정책과 취약점 임계값을 업데이트합니다.
복잡해 보이지만 한 번 구성하면 이후에는 자동으로 돌아갑니다. 저희 팀은 구성부터 안정적인 운영까지 약 한 Sprint(2주)를 들였지만 그 결과 장기적인 보안을 확보했습니다.
솔직히 이 절차를 구성한 뒤에는 밤에도 훨씬 마음 편히 잘 수 있게 됐습니다. 어느 날 갑자기 큰 취약점이 터졌을 때 우리 이미지가 이미 영향을 받고 있었다는 사실을 뒤늦게 알게 될 걱정을 덜었기 때문입니다.
결론
글의 첫머리에 나온 Log4Shell 사례로 돌아가 보겠습니다. 당시에 이런 이미지 보안 스캔 절차가 있었다면 취약점은 CI/CD 단계에서 발견됐고 운영 환경까지 들어오지 못했을 겁니다. 가슴 철렁했던 그 새벽도, 대표의 뼈아픈 질문도 없었겠죠.
Docker 이미지 보안은 어려운 기술이 아닙니다. 핵심은 세 가지입니다.
- Trivy 같은 도구로 이미지 취약점 스캔: 설치가 간단하고 빠르며 기능도 충분합니다.
- 취약점을 체계적으로 수정: 안전한 베이스 이미지를 선택하고 의존성을 업그레이드하며 모범 사례를 따릅니다.
- CI/CD에서 스캔 자동화: GitHub Actions / GitLab CI에 5분이면 통합할 수 있습니다.
Docker Hub 이미지의 76%에 취약점이 있다는 수치는 처음에는 확실히 두려웠습니다. 하지만 달리 보면 아직 많은 팀이 이 문제를 중요하게 다루지 않는다는 뜻이기도 합니다. 지금 바로 행동한다면 적어도 그 76%보다 더 잘할 수 있다는 점은 꽤 뿌듯한 일 아닐까요?
처음부터 모든 취약점을 한꺼번에 수정하려고 하지는 마세요. 현실적으로 어렵습니다. 가장 간단한 작업부터 시작하는 것이 좋습니다.
- 오늘 퇴근 전: Trivy를 설치하고 프로젝트의 Docker 이미지를 스캔해 취약점이 몇 개인지 확인합니다.
- 이번 주 안에: CRITICAL 취약점을 수정합니다. 베이스 이미지 버전 하나만 올리면 해결될 수도 있습니다.
- 다음 Sprint: CI/CD에 Trivy를 통합하고 심각한 취약점 발견 시 배포를 차단하도록 구성합니다.
- 이번 달 안에: 이미지 보안 기준선을 정하고 정기적으로 검토하고 업데이트합니다.
컨테이너 보안은 한 번으로 끝나는 일이 아니라 지속적인 과정입니다. 하지만 절차를 세워 두면 이후 유지 관리에 큰 노력이 들지 않습니다. 저희 팀은 매주 자동 스캔을 실행하고 문제가 있으면 주간 회의에서 확인하는데 보통 30분이면 처리가 끝납니다.
마지막으로 다시 강조합니다. 운영 환경 이미지는 반드시 스캔하세요. Docker Hub에서 받은 이미지도 반드시 스캔하세요. 사고가 터진 뒤 후회하면 이미 늦습니다.
Docker 보안 실무 경험이나 시행착오가 있다면 댓글로 공유해 주세요. 함께 배우며 컨테이너 애플리케이션을 더 안전하게 만들어 봅시다.
Docker 이미지 보안 스캔 및 수정 전체 절차
Trivy 설치부터 CI/CD 통합까지 스캔, 수정, 자동화 절차를 모두 다루는 단계별 가이드
⏱️ Estimated time: 2 hr
- 1
Step 1: Trivy 스캔 도구 설치
Trivy 스캔 도구 설치:
• macOS 사용자: brew install trivy
• Linux 사용자: GitHub releases 페이지에서 바이너리를 다운로드하거나 패키지 관리자로 설치
• 다운로드 주소: https://github.com/aquasecurity/trivy/releases
• 처음 실행하면 취약점 데이터베이스(trivy-db, 수십 MB 정도)를 자동으로 다운로드합니다. - 2
Step 2: 기본 이미지 취약점 스캔
기본 이미지 취약점 스캔:
• Docker Hub 이미지 스캔: trivy image python:3.9
• 로컬 이미지 스캔: trivy image myapp:latest
• tar 파일 스캔: trivy image --input ruby-3.1.tar
스캔 결과는 심각도에 따라 분류됩니다:
• CRITICAL(즉시 수정 필수)
• HIGH(가능한 한 빨리 수정)
• MEDIUM(수정 권장)
• LOW(일정에 맞춰 처리 가능) - 3
Step 3: 고급 스캔 옵션 구성
고급 스캔 옵션 구성:
• 위험 및 심각 취약점만 표시: trivy image --severity HIGH,CRITICAL nginx:latest
• 수정되지 않은 취약점 무시: trivy image --ignore-unfixed redis:latest
• 심각 취약점 발견 시 명령이 0이 아닌 종료 코드를 반환하도록 설정(CI/CD용):
trivy image --exit-code 1 --severity CRITICAL myapp:latest
• JSON 형식으로 출력: trivy image -f json -o results.json myapp:latest - 4
Step 4: 체계적으로 취약점 수정
체계적으로 취약점 수정:
안전한 베이스 이미지 선택:
• Distroless 이미지 사용(gcr.io/distroless/python3/nodejs/static, 3.06MB에 불과하며 Shell과 패키지 관리자가 없음)
• 또는 CVE가 거의 없는 2025년 Docker Hardened Images 사용
베이스 이미지 버전 업데이트:
• latest 대신 구체적인 버전 번호(python:3.11.7) 사용
Dockerfile에서 시스템 패키지 업그레이드:
• RUN apt-get update && apt-get upgrade -y && apt-get clean && rm -rf /var/lib/apt/lists/*
애플리케이션 의존성 버전 업데이트:
• requirements.txt 같은 의존성 파일을 수정하고 패치 버전을 우선 업그레이드합니다. - 5
Step 5: GitHub Actions에 자동 스캔 통합
.github/workflows/docker-scan.yml 파일을 만들고 다음과 같이 구성합니다:
• push/pull_request 발생 시 실행
• Docker 이미지 빌드
• aquasecurity/trivy-action으로 스캔
• severity: 'CRITICAL,HIGH'와 exit-code: '1' 설정(심각한 취약점 발견 시 빌드 실패)
• 스캔 결과를 GitHub Security 탭에 업로드 - 6
Step 6: GitLab CI/CD 통합
.gitlab-ci.yml을 만들거나 수정하고 다음과 같이 구성합니다:
• stages에 build, test, security 포함
• security 단계에서 aquasec/trivy:latest 이미지 사용
• trivy image --exit-code 1 --severity HIGH,CRITICAL 명령 실행
• allow_failure: false 설정(보안 스캔 실패 시 전체 pipeline 실패) - 7
Step 7: 완전한 보안 스캔 절차 구축
개발 단계:
• IDE에 Snyk/Trivy 플러그인 통합
• 로컬 이미지 빌드 후 수동 스캔
빌드 단계:
• CI/CD 자동 스캔
• CRITICAL 취약점이 있으면 즉시 실패 처리해 병합 차단
• 스캔 결과를 Security Dashboard에 업로드
저장소 단계:
• Harbor 같은 이미지 저장소에서 2차 스캔
• 취약점 정책을 설정해 위험 이미지 pull 차단
런타임:
• 운영 환경 이미지를 정기적으로 스캔(주 1회)
• 새 취약점 발견 시 알림 실행
정기 검토:
• 매월 취약점 수정 현황 검토
• 스캔 정책과 취약점 임계값 업데이트
FAQ
Docker 이미지에는 어떤 보안 문제가 생길 수 있나요?
1) 운영체제 패키지 취약점:
• Alpine, Ubuntu 베이스 이미지의 OpenSSL, glibc, curl 같은 시스템 라이브러리
• 예: 2022년 OpenSSL 3.0.x 고위험 취약점
2) 애플리케이션 의존성 취약점:
• Log4j 사례처럼 의존성 트리 안에 숨은 취약점
• 많은 Java 프로젝트는 자신이 Log4j를 사용한다는 사실조차 몰랐습니다.
3) 잘못된 구성과 민감 정보 유출:
• 데이터베이스 비밀번호를 Dockerfile에 작성
• .git 디렉터리를 삭제하지 않음
• root 사용자로 컨테이너 실행
4) 공급망 위험:
• Docker Hub의 악성 이미지
• 2018년 연구에서 사용자를 노린 악성 이미지가 발견됐습니다.
Docker Hub 이미지를 무조건 신뢰하면 안 되는 이유는 무엇인가요? 76%라는 수치는 정확한가요?
다소 오래된 데이터이지만 당시의 오래된 이미지가 지금도 사용됩니다. 많은 '공식 이미지'도 실시간으로 업데이트되지 않으며, python:3.9 태그가 반년 전에 빌드돼 시스템 패키지가 이미 낡았을 수도 있습니다.
Docker Hub 이미지의 품질은 제각각입니다:
• 개인 개발자가 관리해 반년 동안 업데이트되지 않은 이미지도 있음
• '공식'이라고 표시돼도 실제로는 특정 회사가 직접 업로드한 이미지일 수 있음
• 통일된 보안 감사 표준이 없음
원칙: Docker Hub에서 받은 이미지는 '공식' 태그라도 사용하기 전에 반드시 한 번 스캔해야 합니다.
Trivy와 다른 스캔 도구(Snyk, Clair, Anchore)는 어떻게 다른가요? 무엇을 선택해야 하나요?
• 가장 추천하는 오픈 소스 도구로 무료이며 사용이 쉽고 기능도 충분함
• 설치가 간단함(brew install trivy)
• 스캔이 빠름(처음에는 10초, 이후에는 몇 초)
• 취약점 탐지, 잘못된 구성 스캔, Secret 유출 탐지, 라이선스 규정 준수 지원
• GitHub Actions와 Harbor에 통합됨
• 10분이면 시작할 수 있어 중소 팀과 개인 개발자에게 적합
Snyk:
• 기능이 폭넓은 상용 제품
• IDE에 통합해 실시간 알림 제공
• 수정 PR 자동 생성 지원
• 무료 버전의 제한이 많아 예산이 충분한 대기업에 적합
Clair:
• Quay 이미지 저장소의 스캔 엔진으로 오픈 소스이며 심층 커스터마이징 가능
• 구성이 복잡하고 학습 비용이 높음
• 전담 보안 엔지니어가 있는 팀에 적합
Anchore:
• 정책 관리를 지원하는 엔터프라이즈급 솔루션
• 규정 준수 요건이 엄격한 금융·의료 업종에 적합
권장: 중소 팀은 Trivy, 대기업은 Snyk, 보안 엔지니어가 있는 팀은 Clair를 검토하세요.
Trivy 스캔 결과는 어떻게 읽고 무엇을 중점적으로 봐야 하나요?
• CRITICAL(즉시 수정 필수, 시스템 전체 제어로 이어질 수 있음)
• HIGH(가능한 한 빨리 수정, 공격에 악용될 수 있음)
• MEDIUM(수정 권장, 일정 수준의 보안 위험 존재)
• LOW(우선순위가 낮아 일정에 맞춰 처리 가능)
결과를 읽는 요령:
1) 수정 버전이 있는 취약점을 우선 확인합니다. Fixed Version에 none이 표시되거나 비어 있으면 아직 패치가 없으므로 해결 방법이 있는 항목부터 처리합니다.
2) CRITICAL과 HIGH에 집중합니다. CRITICAL은 공개 exploit 코드가 있어 공격자가 바로 악용할 수 있으므로 즉시 수정해야 합니다.
3) CVE 세부 정보를 확인합니다. https://cve.mitre.org/ 에서 CVE 번호를 검색해 구체적인 문제, 영향 범위, 악용 난도를 확인합니다.
4) Trivy의 작동 원리를 이해합니다. Trivy는 취약점 데이터베이스 JSON 파일을 다운로드해 이미지의 소프트웨어 패키지 목록과 대조하고 패키지명 및 버전이 알려진 취약점과 일치하면 경고합니다. 오탐이 있을 수 있으므로 실제 코드를 함께 살펴 위험을 판단해야 합니다.
Docker 이미지 취약점을 체계적으로 수정하려면 어떻게 해야 하나요?
1) 안전한 베이스 이미지 선택:
• Distroless 이미지는 3.06MB로 Alpine보다 작고 Shell과 패키지 관리자가 없어 공격 표면이 최소화됨
• Google Distroless는 python3/nodejs/static 버전 제공
• 2025년 Docker Hardened Images는 CVE가 거의 없고 이미지 크기도 95% 줄일 수 있다고 소개됨
2) 베이스 이미지 버전 업데이트:
• latest 대신 python:3.11.7 같은 구체적인 버전 번호 사용
• latest는 몇 달 동안 시스템 패키지가 업데이트되지 않을 수 있음
• 매월 새 마이너 버전이나 패치 버전이 있는지 능동적으로 확인
3) Dockerfile에서 시스템 패키지 업그레이드:
• RUN apt-get update && apt-get upgrade -y && apt-get clean && rm -rf /var/lib/apt/lists/*
• update/upgrade/clean을 하나의 RUN 명령에 넣어 이미지 레이어 감소
4) 애플리케이션 의존성 버전 업데이트:
• requirements.txt 등을 수정
• Flask 2.0.1에 취약점이 있다면 바로 2.3.0으로 올리기보다 최신 2.0.x처럼 패치 버전을 우선 업그레이드
• 운영에 반영하기 전 테스트 환경에서 먼저 확인
모범 사례: 멀티 스테이지 빌드 사용, root 사용자로 실행하지 않기, 정기적으로 이미지 재빌드(최소 월 1회), .dockerignore로 민감 파일 제외, latest 태그 사용 금지.
CI/CD에 자동 보안 스캔을 어떻게 통합하나요?
• .github/workflows/docker-scan.yml 생성
• push/pull_request 발생 시 실행하도록 구성
• Docker 이미지를 빌드하고 aquasecurity/trivy-action으로 스캔
• severity: 'CRITICAL,HIGH'와 exit-code: '1' 설정(심각한 취약점 발견 시 빌드 실패)
• 스캔 결과를 GitHub Security 탭에 업로드
GitLab CI 통합:
• .gitlab-ci.yml 생성 또는 수정
• stages에 build/test/security 구성
• security 단계에서 aquasec/trivy:latest 이미지 사용
• trivy image --exit-code 1 --severity HIGH,CRITICAL 명령 실행
• allow_failure: false 설정(보안 스캔 실패 시 전체 pipeline 실패, 운영 환경에서는 반드시 false 사용)
Harbor 이미지 저장소:
• v1.2부터 Trivy와 Clair 통합
• 이미지 push 시 자동 스캔, 매일 모든 이미지 정기 스캔 구성 가능
• 취약점 임계값 설정(예: CRITICAL 취약점이 있는 이미지 pull 금지)
• 개발 환경에서 이미지 push 시 자동으로 스캔하고 HIGH 이상 취약점이 발견되면 '취약점 있음' 태그 부여
• 운영 환경 K8s 클러스터에서 admission webhook을 구성해 취약점 태그가 있는 이미지의 배포 거부
완전한 Docker 이미지 보안 스캔 절차는 어떻게 구성해야 하나요?
개발 단계:
• IDE에 Snyk 또는 Trivy 플러그인을 통합해 코딩 중 의존성 취약점 확인
• 로컬 이미지 빌드 후 trivy image를 수동 실행
빌드 단계:
• GitHub Actions/GitLab CI로 새 이미지를 자동 스캔
• CRITICAL 취약점 발견 시 즉시 실패 처리해 병합 차단
• 스캔 결과를 Security Dashboard에 업로드해 팀이 추세 확인
저장소 단계:
• Harbor 같은 이미지 저장소에서 2차 스캔
• CI/CD가 놓쳤거나 새로 공개된 취약점 보완
• 취약점 정책으로 위험 이미지 pull 차단
런타임:
• 운영 환경에서 실행 중인 이미지를 주 1회 정기 스캔
• 새 취약점 발견 시 알림을 보내고 수정 일정 수립
정기 검토:
• 매월 취약점 수정 현황 검토
• 스캔 정책과 취약점 임계값 업데이트
이 절차는 한 번 구성하면 자동으로 돌아갑니다. 팀에서 구성부터 안정적인 운영까지 약 한 Sprint(2주)가 걸렸지만 장기적인 보안을 확보할 수 있었습니다.
3분 읽기 · 게시일: 2025년 12월 18일 · 수정일: 2026년 9월 4일
Docker 실전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Docker로 Nginx 배포하기 완벽 가이드: 설정 파일 마운트, HTTPS 설정과 리버스 프록시 실전
Docker로 Nginx를 배포할 때 필요한 설정 파일 마운트, HTTPS 인증서 설정, 리버스 프록시를 모두 다룹니다. 설정이 적용되지 않거나 컨테이너가 서로 통신하지 못하는 흔한 문제를 해결하고, Let's Encrypt 자동 갱신과 프로덕션 환경 모범 사례까지 설명합니다.
33편 중 24편
다음
Docker 보안 설정: 컨테이너를 root로 실행하지 않는 완전한 실전 가이드
Docker 컨테이너를 기본 root로 실행하면 심각한 보안 위험이 발생합니다. 이 글에서는 컨테이너 탈출 원리와 Dockerfile USER 명령, --user 옵션, Capabilities 세분화 권한 제어, AppArmor 설정까지 프로덕션급 안전한 컨테이너를 구축하는 방법을 자세히 설명합니다.
33편 중 26편



댓글
GitHub로 로그인하여 댓글을 남기세요