13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] 컨테이너 보안

  • [보안] Trivy 이미지 스캔 시 흔히 발생하는 오류와 해결 방법

    [보안] Trivy 이미지 스캔 시 흔히 발생하는 오류와 해결 방법

    [컨테이너 보안] Trivy 이미지 스캔 오류 해결, 삽질 끝에 찾은 해법

    안녕하세요, 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가 통합된 워크플로우 다이어그램

    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 데몬 미실행.
    • 해결 방법:
      1. Docker 데몬 실행 확인: sudo systemctl status docker 명령으로 Docker 서비스가 활성화되어 있는지 확인합니다. 만약 실행 중이 아니라면 sudo systemctl start docker로 시작하세요.
      2. 사용자에게 Docker 그룹 권한 부여: 현재 사용자를 docker 그룹에 추가하여 소켓 파일에 접근할 수 있도록 합니다.
      3. 
        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
    • 원인: 레지스트리 인증 정보 누락, 네트워크 문제(프록시, 방화벽), 이미지 이름 오타.
    • 해결 방법:
      1. 레지스트리 로그인: 프라이빗 레지스트리인 경우 docker login <your-registry> 명령으로 로그인 정보를 Trivy가 사용할 수 있도록 해줍니다.
      2. 네트워크 설정 확인: 기업 환경에서는 프록시 서버(Proxy Server)를 통해 인터넷에 접속해야 하는 경우가 많아요. Trivy는 HTTP_PROXY, HTTPS_PROXY 환경 변수를 지원하므로 이를 설정해 주세요.
      3. 
        export HTTP_PROXY="http://proxy.example.com:8080"
        export HTTPS_PROXY="http://proxy.example.com:8080"
        trivy image my-private-repo/my-image:latest
        
      4. 방화벽 규칙 검토: 필요한 포트(예: 443, 80)가 열려 있는지 확인합니다.

    3. 취약점 데이터베이스(DB) 초기화 오류 (`failed to initialize vulnerability DB: …`)

    Trivy는 최신 취약점 정보를 스캔하기 위해 자체 데이터베이스를 사용합니다. 이 DB를 업데이트하거나 초기화하는 과정에서 문제가 생기면 스캔을 시작할 수 없어요.

    • 문제 상황: FATAL: failed to initialize vulnerability DB: failed to download vulnerability DB: failed to download DB from ...
    • 원인: 네트워크 연결 불량, Trivy DB 서버 접근 불가, 디스크 공간 부족.
    • 해결 방법:
      1. 네트워크 연결 확인: Trivy DB를 다운로드하는 URL(보통 GitHub 또는 Aqua Security CDN)에 접근 가능한지 ping이나 curl로 확인합니다.
      2. 디스크 공간 확인: df -h 명령으로 Trivy가 설치된 디렉토리(보통 ~/.cache/trivy)의 디스크 공간이 충분한지 확인해요.
      3. 수동 DB 업데이트 시도: 문제가 지속되면 DB를 수동으로 업데이트해 보세요.
      4. 
        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이 설치되어 있지 않아 데몬을 찾지 못하는 경우예요.
    • 해결 방법:
      1. Docker Desktop 설치: 가장 권장되는 방법입니다. Docker Desktop을 설치하면 WSL2와 원활하게 통합되어 Trivy가 Docker 데몬을 쉽게 사용할 수 있어요.
      2. WSL2 내에 Docker Engine 직접 설치: 좀 더 복잡하지만, WSL2 배포판 안에 직접 Docker Engine을 설치하고 실행하는 방법도 있습니다. 이 경우 sudo service docker start 등으로 데몬을 수동으로 시작해야 할 수 있어요.
      3. Trivy를 Docker 컨테이너로 실행: 이 방법은 호스트의 Docker 데몬에 의존하지 않고 Trivy 자체를 컨테이너 안에서 실행하는 방식이라 유용합니다.
      4. 
        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 파이프라인에서 자동화된 분석에 아주 유용하게 쓰여요.

    
    trivy image --format json -o results.json my-image:latest
    
    Trivy 컨테이너 이미지 취약점 스캔 결과 예시

    Trivy 스캔 결과 보고서의 심각도별 취약점 목록을 시각화한 이미지입니다.

    결과를 볼 때는 다음 기준들을 중점적으로 살펴보세요.

    항목 설명 해석 및 권고
    Severity (심각도) CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN CRITICAL, HIGH는 즉시 조치 필요. 심각한 보안 위협으로 이어질 수 있어요.
    Vulnerability ID (취약점 ID) CVE-YYYY-XXXXX 등의 고유 식별자 해당 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 오류 유형 및 해결 방법 요약 인포그래픽

    Trivy 사용 중 발생할 수 있는 주요 오류 유형과 그 해결책을 요약한 인포그래픽입니다.

    만약 여러분의 CI/CD 파이프라인에서 Trivy가 자꾸 에러를 뿜어낸다면, 이 글에서 제시된 해결 방법들을 하나씩 적용해 보세요. 대부분의 문제는 Docker 데몬 권한, 네트워크 설정, 또는 Trivy DB 업데이트 문제에서 비롯됩니다. 특히, Docker 데몬 연결 문제와 레지스트리 인증 문제는 제가 가장 많이 마주쳤던 케이스들이니, 이 부분들을 먼저 확인해 보시는 것을 추천합니다.

    저도 여전히 홈랩에서 새로운 기술을 실험하며 삽질을 거듭하고 있습니다. 그 과정에서 얻은 소중한 경험들은 앞으로도 ’13년차의 서버실’ 블로그를 통해 꾸준히 공유해 드릴게요. 다음번에는 Trivy를 이용한 Kubernetes 클러스터 보안 스캔에 대한 이야기를 다뤄볼까 합니다. 그때까지 모두 안전한 컨테이너 환경을 만드시길 바랍니다! 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

  • [CI/CD 보안] Trivy로 컨테이너 보안 취약점 자동 탐지 구축하기

    [CI/CD 보안] Trivy로 컨테이너 보안 취약점 자동 탐지 구축하기

    [CI/CD 보안] Trivy로 컨테이너 보안 취약점 자동 탐지 구축하기

    안녕하세요, 13년차 인프라 엔지니어입니다. 요즘 같은 DevOps 환경에서는 CI/CD 파이프라인이 선택이 아닌 필수가 되었죠. 그런데 이렇게 빠르게 빌드하고 배포하는 과정에서 보안(Security)은 제대로 챙기고 계신가요?

    현업과 홈랩에서 일하다 보니 느끼는 게, 보안은 항상 뒷전으로 밀리거나 나중에 터지고 나서야 허둥지둥 해결하는 경우가 많더라고요. 특히 컨테이너 이미지는 한 번 빌드되면 어떤 취약점이 있는지 제대로 확인하지 않고 프로덕션 환경에 배포되는 일이 허다합니다. 그러다 터지면… 상상하기도 싫죠.

    이런 문제를 미리 막고 싶어서, CI/CD 파이프라인에 보안 취약점 자동 탐지(Automated Vulnerability Scanning)를 도입하는 방법을 계속 고민해왔습니다. 그리고 찾은 게 바로 Trivy(트리비)입니다. 오늘은 Trivy를 활용해서 CI/CD 파이프라인에 컨테이너 이미지 보안 스캔을 자동화하는 방법을 제 경험을 바탕으로 솔직하게 풀어보려 합니다.

    참고: 본 글은 보안 학습과 자신이 관리하는 시스템 방어를 위한 교육 목적입니다. 타인의 시스템에 무단 접근하는 행위는 법률상 불법이며 처벌 대상이니 꼭 기억해두세요.

    CI/CD 파이프라인에 Trivy가 통합되어 컨테이너 이미지의 보안 취약점을 자동 탐지하는 아키텍처 다이어그램

    그림 1: Trivy를 활용한 CI/CD 보안 파이프라인 개요

    Trivy란 무엇인가? 컨테이너 보안 스캔 도구 개론

    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 환경에서는 캐시 활용이 빌드 시간을 줄이는 데 아주 중요하거든요.
    GitLab CI/CD에서 Trivy 스캔 작업이 실행되고 CRITICAL, HIGH 취약점이 표시된 결과 화면

    그림 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 파이프라인에서 성공적으로 스캔이 완료된 모습이나, 혹은 취약점 때문에 파이프라인이 실패한 모습을 보면 정말 뿌듯하더라고요. 저는 주로 이런 식으로 결과를 확인합니다.

    Trivy 스캔 결과가 심각도별로 요약되고 해결 방안을 제시하는 대시보드 리포트

    그림 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) 보안 검증, 초기 개발 단계에서 설정 오류 방지 코드 레벨에서 보안 취약점 조기 발견, 설정 파일 검증 코드 내부의 라이브러리 취약점은 탐지 불가, 설정 파일 문법 의존적
    Trivy의 컨테이너 이미지, 파일 시스템, Git 저장소 스캔 대상별 특징 비교 인포그래픽

    그림 4: Trivy 스캔 대상별 특징 비교

    마무리하며: Trivy를 통한 지속적인 보안 확보

    Trivy를 CI/CD 파이프라인에 통합하는 것은 컨테이너 기반 환경에서 보안을 강화하고, 개발 및 운영 효율성을 높이는 중요한 단계라고 생각합니다. 저도 처음엔 ‘이걸 언제 다 구축하나’ 싶었는데, 한 번 구축해두니 정말 든든하더라고요. 든든한 방패를 얻은 기분이랄까요?

    물론 Trivy 하나만으로 모든 보안 위협을 막을 수는 없습니다. 하지만 가장 기본적인 방어선을 구축하고, 자동화된 방식으로 지속적인 보안 검증을 수행한다는 점에서 그 가치는 충분하다고 봅니다. Critical, High 레벨의 취약점만이라도 배포 전에 걸러낼 수 있다면, 야간 비상 호출(On-call) 횟수를 확 줄일 수 있을 거예요. 저도 덕분에 잠을 좀 더 잘 수 있게 됐습니다.

    만약 여러분의 CI/CD 파이프라인에 아직 자동화된 보안 스캔이 없다면, 지금 당장 Trivy를 도입해보시길 강력히 추천합니다. 초기에는 오탐이나 너무 많은 보고서 때문에 조금 번거로울 수도 있지만, 꾸준히 규칙을 개선하고 피드백을 반영하다 보면 훨씬 견고하고 효율적인 보안 프로세스를 만들 수 있을 겁니다. 다음번에는 Trivy를 활용한 SBOM(Software Bill Of Materials) 생성 및 관리 방법에 대해 이야기해볼까 합니다. 기대해주세요!

  • [보안] Trivy 도입 1년 회고: DevSecOps 워크플로우 변화와 개선점 분석

    [보안] Trivy 도입 1년 회고: DevSecOps 워크플로우 변화와 개선점 분석

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.

    Trivy 도입 회고를 1년치로 다시 적어보면, 제가 얻은 가장 큰 변화는 탐지율보다도 의사결정 방식의 변화였습니다. 전에는 취약점 공지가 뜨면 운영팀이 먼저 불안해했고, 개발팀은 “우리 코드 문제인지 베이스 이미지 문제인지”부터 감으로 추정했었죠. Trivy를 붙인 뒤에는 최소한 그 대화가 구조화됐습니다. 어떤 단계에서 막을지, 무엇을 예외로 둘지, 어떤 결과는 즉시 수정이고 어떤 결과는 추적 대상으로 둘지 기준선이 생겼거든요.

    중요한 건 여기입니다. Trivy는 보안을 완성해주는 도구가 아니라, 컨테이너와 IaC 영역에서 팀의 판단을 표준화해주는 도구에 가깝습니다. 이걸 잘못 기대하면 “스캔은 도는데 사고는 왜 나지?”라는 반응이 나오고, 반대로 위치를 정확히 잡으면 릴리스 직전의 불필요한 공황을 꽤 줄여주더라고요. 저도 홈랩과 업무 환경에서 비슷한 패턴으로 굴려보면서, Trivy를 도입한 팀과 그렇지 않은 팀의 차이는 기능 수보다 운영 규칙의 유무에서 갈린다고 느꼈습니다.

    이번 글은 단순 사용기가 아니라, 1년 동안 실제로 남은 습관과 버린 습관을 같이 적는 회고입니다. 특히 CI에서 보안 스캔이 형식화되기 쉬운 팀, 결과는 쌓이는데 누구도 우선순위를 정하지 못하는 팀이라면 바로 적용 가능한 형태로 정리해봤습니다.

    Trivy 도입 회고 기반 DevSecOps 워크플로우 개요 이미지

    Trivy 도입 회고와 DevSecOps 워크플로우 변화를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 Trivy를 넣었는지: 제가 원한 건 “더 많은 경고”가 아니라 “더 빠른 판별”이었습니다

    제가 처음 Trivy를 넣은 이유는 단순했습니다. 컨테이너 이미지를 배포하는 속도는 빨라졌는데, 취약점 확인은 여전히 사람 손을 많이 탔기 때문입니다. 패키지 목록을 보고 CVE를 대조하는 식의 확인은 운영 주기가 짧아질수록 바로 무너지더라고요. 그때 필요한 건 화려한 플랫폼이 아니라, 이미지와 설정을 같은 문법으로 반복 점검할 수 있는 스캐너였습니다.

    Trivy가 실무에서 먹히는 이유는 범위가 넓어서가 아니라 도입 순서를 잘게 쪼갤 수 있어서입니다. 이미지 스캔만 먼저 넣고, 이후에 IaC와 secret 탐지로 넓혀도 워크플로우가 크게 흔들리지 않습니다. 반면 SAST나 DAST는 언어, 프레임워크, 런타임, 테스트 환경에 더 강하게 묶이는 경우가 많아서 첫 진입 비용이 큽니다.

    다만 여기서 선을 분명히 그어야 합니다. 애플리케이션 로직 취약점, 예를 들면 인증 우회나 권한 검증 실수 같은 건 Trivy가 대신 찾아주지 않습니다. 제 경험상 Trivy는 아래 상황에서 특히 강하고, 아래 상황에서는 과도한 기대를 버리는 편이 맞았습니다.

    상황 Trivy를 우선 쓸 때 다른 도구를 같이 봐야 할 때 제 판단
    컨테이너 배포 파이프라인 베이스 이미지, OS 패키지, 언어 패키지 점검 런타임 행위 분석이 필요할 때 Trivy를 기본 게이트로 둡니다
    Kubernetes / Terraform 운영 매니페스트, Terraform, Dockerfile 오설정 점검 조직 정책 강제와 예외 승인 체계가 별도일 때 Trivy config 또는 fs 스캔을 먼저 붙입니다
    애플리케이션 코드 보안 의존성 수준의 위험 신호 확인 인증, 권한, 비즈니스 로직 취약점 검토 SAST, 코드리뷰, 위협 모델링이 필요합니다
    비밀값 관리 저장소 내 하드코딩 secret 탐지 이미 유출된 키 회전, 감사 추적 탐지 후 운영 절차가 더 중요합니다

    한마디로 말하면, Trivy는 “전체 보안 솔루션”이 아니라 배포 경로에 가까운 위험을 빠르게 분류하는 도구로 볼 때 가장 실용적이었습니다.

    2. Trivy를 1년 써보니 바뀐 DevSecOps 워크플로우

    도입 전에는 릴리스 직전에 한 번 몰아서 확인하는 습관이 있었습니다. 이 방식의 문제는 취약점 자체보다 발견 시점이 늦다는 데 있습니다. 이미지 하나에서 HIGH나 CRITICAL이 나와도, 그게 애플리케이션 의존성인지 베이스 이미지 레이어인지 구분하는 데 시간이 들고, 그다음에는 재빌드와 회귀 테스트가 줄줄이 따라옵니다. 보안 이슈가 일정 이슈로 번지는 전형적인 패턴이죠.

    Trivy를 붙인 뒤에는 스캔 위치를 늘린 게 아니라, 질문이 다른 세 지점으로 나눴습니다.

    1. 개발 중: 지금 만든 변경이 새 위험을 들여왔는가
    2. CI 빌드 단계: 이 커밋은 배포 금지선에 걸리는가
    3. 배포 전 또는 정기 점검: 허용한 예외가 아직도 타당한가

    이 세 지점을 구분하고 나니 팀 대화가 훨씬 현실적으로 바뀌었습니다. 예전에는 “보안팀이 막았다”는 식으로 받아들였는데, 지금은 “이건 신규 유입이라 지금 고쳐야 한다”, “이건 기존 부채라 추적하되 이번 배포는 진행한다”처럼 이야기할 수 있게 됐습니다.

    특히 제가 강하게 느낀 건, 모든 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 이미지 스캔과 설정 스캔 구성도

    빌드, 스캔, 배포 승인 단계로 이어지는 Trivy 기반 CI 흐름을 설명하는 이미지입니다.

    3-3. CI 파이프라인 예시: 속도와 신뢰도를 같이 잡으려면 캐시 전략이 먼저입니다

    초기에는 매 파이프라인마다 DB를 처음부터 내려받아서 체감 속도가 꽤 나빴습니다. 나중에 알게 된 건, 느린 원인이 Trivy 자체의 스캔 엔진보다도 DB cold start와 캐시 운영 방식에 있는 경우가 많다는 점입니다. 저는 이후에 “업데이트 단계”와 “실제 스캔 단계”를 분리하는 식으로 바꿨습니다.

    stages:
      - build
      - security
    
    variables:
      TRIVY_CACHE_DIR: .trivycache
    
    trivy_db_warmup:
      stage: security
      image: aquasec/trivy:latest
      script:
        - trivy image --download-db-only --cache-dir "$TRIVY_CACHE_DIR"
      cache:
        key: trivy-cache
        paths:
          - .trivycache
    
    trivy_scan:
      stage: security
      image: aquasec/trivy:latest
      script:
        - trivy image --cache-dir "$TRIVY_CACHE_DIR" --skip-db-update --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --format json --output trivy-report.json "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
        - trivy config --severity HIGH,CRITICAL .
      cache:
        key: trivy-cache
        paths:
          - .trivycache
      artifacts:
        when: always
        paths:
          - trivy-report.json

    여기서 핵심은 세 가지입니다. 첫째, --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 조합을 보면 “패키지 업그레이드로 해결 가능한지”, “베이스 이미지 교체가 먼저인지” 판단이 빨라집니다. 리포트는 많아도, 실제로 손대야 하는 지점은 보통 몇 군데로 수렴합니다.

    Trivy 도입 회고 결과 검증과 예외 관리 대시보드 이미지

    스캔 결과, 심각도 분류, 예외 검토 흐름을 보여주는 결과 화면 이미지입니다.

    6. 재현 가능한 시나리오 하나: 코드보다 베이스 이미지가 먼저인 경우

    실제 현장에서 자주 본 장면을 하나 말씀드리겠습니다. 애플리케이션 코드는 크게 바뀌지 않았는데, 어느 날 이미지 스캔에서 OS 패키지 쪽 HIGH/CRITICAL이 갑자기 눈에 띄게 늘어납니다. 이때 개발팀이 바로 애플리케이션 의존성을 뒤지기 시작하면 시간이 꽤 낭비됩니다. 제가 먼저 확인하는 순서는 거의 항상 같습니다.

    1. 취약점이 애플리케이션 패키지인지 OS 패키지인지 구분합니다
    2. 현재 베이스 이미지 태그가 오래됐는지 확인합니다
    3. 가능하면 공식 베이스 이미지를 최신 태그 계열로 교체해 재빌드합니다
    4. 재스캔 후에도 남는 항목만 애플리케이션 레벨에서 봅니다

    예를 들어 JSON 결과에서 동일한 OS 계열 패키지가 여러 항목에 반복 등장하면, 그때는 코드보다 베이스 레이어를 먼저 의심하는 편이 맞았습니다. 반대로 언어 패키지 매니저 쪽 결과가 중심이면 애플리케이션 의존성 정리가 먼저일 때가 많았습니다. 이 판단을 빨리 내리면 보안 대응이 코드 수리전이 아니라 레이어 선택 문제로 바뀝니다.

    다만 베이스 이미지 교체가 만능은 아닙니다. 베이스를 올리면 libc나 openssl 같은 런타임 동작 차이로 회귀가 날 수 있습니다. 그래서 저는 Trivy 결과를 보고 베이스 이미지를 바꿀 때는, 보안 수정과 기능 검증을 분리하지 않고 같은 변경 세트로 다루는 편이 낫다고 봅니다. 취약점 수치만 줄이고 서비스가 깨지면 운영 관점에서는 실패니까요.

    7. 1년 운영 결과: 좋아진 점과 아직 아쉬운 점

    좋아진 점은 분명했습니다. 첫째, 보안 이슈가 배포 막판에야 드러나는 빈도가 줄었습니다. 둘째, 컨테이너 보안 검토가 특정 담당자의 기억이나 감에 덜 의존하게 됐습니다. 셋째, IaC 오설정이 템플릿 수준에서 반복되는 패턴으로 보이기 시작했습니다. 이건 단순히 경고를 더 본다는 의미가 아니라, 조직이 같은 실수를 덜 반복한다는 쪽에 가깝습니다.

    반면 한계도 뚜렷했습니다.

    • Trivy만으로 애플리케이션 보안이 완성되지는 않습니다
    • 예외 관리가 느슨해지면 스캔 결과는 빠르게 형식화됩니다
    • 캐시와 업데이트 전략을 안 잡으면 CI 병목이 바로 눈에 띕니다
    • 팀 성숙도보다 앞선 강한 차단 정책은 우회를 부릅니다

    그래서 지금의 제 결론은 꽤 명확합니다. Trivy는 만능 도구가 아니라 컨테이너와 설정 검증의 기본 레일로 두는 게 가장 잘 맞습니다. 이 위치를 벗어나면 기대가 과해지고, 이 위치를 정확히 잡으면 투자 대비 효용이 좋습니다.

    Trivy 도입 회고의 도입 전후 비교와 개선 포인트 인포그래픽

    도입 전 수동 점검과 도입 후 자동화된 DevSecOps 워크플로우를 비교하는 요약 이미지입니다.

    8. 마무리: 이런 팀이라면 저는 이렇게 적용하라고 권합니다

    제 추천은 상황별로 꽤 분명합니다. 이제 막 시작하는 팀이라면 이미지 스캔부터 붙이세요. 처음부터 모든 경고를 막지 말고, HIGH/CRITICAL 결과를 읽고 분류하는 루틴부터 만드시는 편이 낫습니다. 이미 CI가 있는 팀이라면 --exit-code 1과 심각도 기준을 먼저 명시하세요. 배포 차단 기준이 불명확하면 스캔은 돌아도 운영은 바뀌지 않습니다.

    Kubernetes나 Terraform 비중이 큰 팀이라면 trivy config . 또는 trivy fs --scanners vuln,misconfig,secret .를 같이 두는 편이 맞습니다. 실무에서 더 자주 사고를 내는 건 코드보다 설정인 경우가 많기 때문입니다. 반대로 애플리케이션 인증, 권한, 비즈니스 로직 취약점까지 Trivy 하나로 해결하려고 하시면 방향이 틀어집니다. 그건 다른 검토 체계가 필요합니다.

    장기 운영 팀이라면 선택이 더 단순합니다. 신규 유입 위험은 차단하고, 기존 부채는 추적하되, 예외는 만료시켜 다시 심사하세요. 이 세 가지만 지켜도 Trivy는 충분히 값어치를 합니다. 결국 Trivy 도입 회고는 도구 평가라기보다 운영 습관 평가에 가까웠습니다. 저에게 남은 교훈도 하나였습니다. 스캐너는 많이 도는 것보다, 팀이 어떤 기준으로 멈추고 어떤 기준으로 진행할지를 분명히 할 때 비로소 도움이 됩니다.

    마지막으로 다시 적어둡니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. Trivy를 도입하실 때는 기능 목록보다 운영 규칙부터 설계해보세요. 그 순서가 맞아야 1년 뒤에도 파이프라인에 남습니다.

  • [보안] Trivy False Positive 줄이기: VEX 활용 및 설정 최적화 전략

    [보안] Trivy False Positive 줄이기: VEX 활용 및 설정 최적화 전략

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 오늘 내용도 어디까지나 제가 관리하는 이미지와 파이프라인에서 Trivy False Positive와 취약점 오탐을 줄이기 위해 정리한 방식입니다.

    Trivy False Positive, 왜 이렇게 피곤할까요

    CI에 Trivy를 붙여두면 초반에는 꽤 속이 시원합니다. 그런데 조금 지나면 다른 문제가 생기더라고요. 실제 서비스 경로에서는 쓰이지 않는 라이브러리인데도 계속 빨간 줄이 뜨고, 배포판 패치가 아직 없는 이슈까지 매번 반복해서 올라옵니다. 바로 이런 지점에서 Trivy False Positive 대응 기준이 필요합니다.

    저도 처음에는 .trivyignore에 CVE를 바로 넣어버렸습니다. 그런데 나중에 보니 이 방식은 너무 거칠더라고요. 왜 무시했는지 근거가 남지 않고, 이미지 태그가 바뀌어도 예외가 그대로 살아남는 경우가 있었습니다. 실제 운영에서는 VEX(Vulnerability Exploitability eXchange)와 .trivyignore.yaml을 역할별로 나눠 쓰는 쪽이 훨씬 관리하기 편했습니다.

    Trivy False Positive를 VEX와 ignore 정책으로 정리하는 전체 흐름 이미지

    Trivy 스캔 결과가 바로 차단으로 이어지지 않고, VEX CSAF와 ignore 정책을 거쳐 검증된 이슈만 남는 흐름을 보여주는 이미지입니다.

    Trivy False Positive를 줄이기 위한 VEX CSAF 이해

    쉽게 말해 VEX는 "이 취약점이 존재하는 건 맞지만, 우리 제품이나 패키지 조합에서는 실제 영향이 없다"를 표준 형식으로 설명하는 문서입니다. 그냥 메모가 아니라 스캐너가 읽을 수 있는 구조화된 예외죠.

    Trivy는 로컬 VEX 파일로 CycloneDX, OpenVEX, CSAF를 지원합니다. 이 중 CSAF(Common Security Advisory Framework)는 SBOM 형식에 덜 종속적인 편이라 컨테이너 이미지, 파일시스템, 저장소, Kubernetes, SBOM 대상에 두루 적용하기 좋습니다. 여러 팀이 함께 보는 환경에서는 CSAF가 설명력도 좋고, 감사 대응 때도 근거를 정리하기 수월했습니다.

    여기서 중요한 포인트가 하나 있습니다. VEX는 단순 숨김이 아니라 왜 영향이 없는지를 기록하는 레이어라는 점입니다. 반대로 --ignore-unfixed나 --ignore-status는 결과를 빠르게 줄이는 운영 옵션에 가깝습니다. 둘을 같은 개념으로 다루면 나중에 기준이 꼬이기 쉽습니다.

    옵션부터 정리해보겠습니다: 무엇을 언제 쓰면 되나

    방법 적합한 상황 장점 주의점
    --ignore-unfixed 배포판 패치가 아직 없는 이슈를 우선 제외할 때 가장 빠르게 노이즈를 줄임 영향도 근거까지 대신해주지는 않습니다
    --ignore-status under_investigation, will_not_fix, fix_deferred 같은 상태 기반 필터가 필요할 때 공급자 메타데이터를 운영 정책에 반영하기 쉬움 패키지별 상세 설명은 약합니다
    .trivyignore.yaml 특정 CVE를 경로나 PURL 단위로 제한해서 무시할 때 만료일, statement 기록이 가능 실험적 기능이라 --ignorefile를 명시하는 편이 안전합니다
    --vex + CSAF 영향 없음의 근거를 문서화해야 할 때 감사 대응과 재사용에 유리함 문서 구조를 이해해야 하고 실험적 기능입니다

    제가 권장하는 순서는 이렇습니다. 1) unfixed 정리, 2) 상태 기반 필터, 3) 그래도 남는 케이스를 VEX CSAF로 설명, 4) 정말 로컬 예외만 필요한 건 .trivyignore.yaml. 이 순서를 지켜야 정책이 덜 엉킵니다.

    실전 1단계: 기준 스캔 결과를 먼저 저장합니다

    처음부터 예외부터 만들면 안 됩니다. 기준 보고서가 있어야 나중에 무엇이 줄었는지, 무엇이 실수로 사라졌는지 비교할 수 있거든요. 저는 보통 JSON과 테이블 출력을 둘 다 남깁니다.

    1. 대상 이미지를 취약점 스캐너만으로 먼저 스캔합니다.
    2. JSON 결과를 저장합니다.
    3. 사람이 확인하기 좋은 테이블 결과도 따로 봅니다.
    trivy image --scanners vuln --format json --output result.json debian:11.8
    trivy image --scanners vuln --severity HIGH,CRITICAL debian:11.8

    여기서 판단 기준은 단순 개수보다 같은 CVE가 어떤 패키지와 어떤 상태로 반복되는지입니다. 패치 버전이 비어 있고 배포판 공급자 이슈만 남아 있는 경우는 --ignore-unfixed 후보일 수 있고, 특정 패키지가 실제 실행 경로에 없으면 VEX 후보가 됩니다.

    제가 홈랩에서 자주 헷갈렸던 건 "발견됨"과 "영향 있음"을 같은 뜻으로 보는 부분이었습니다. 스캐너는 존재 여부를 잘 찾지만, 실행 경로나 제품 조합까지 자동으로 다 판단해주지는 않더라고요. 이 차이를 구분해야 보안 스캔 최적화가 제대로 됩니다.

    실전 2단계: VEX CSAF 문서로 오탐 근거를 남깁니다

    Trivy는 --vex /path/to/file 형식으로 로컬 VEX 파일을 읽습니다. CSAF에서는 product_tree에 패키지 식별자(PURL)를 두고, vulnerabilities 아래에서 어떤 제품이 known_not_affected인지 지정하면 됩니다.

    아래 예시는 Trivy 공식 문서 흐름을 바탕으로, Debian 패키지 하나를 영향 없음(not affected)으로 설명하는 구조입니다.

    {
      "document": {
        "category": "csaf_vex",
        "csaf_version": "2.0",
        "publisher": {
          "category": "vendor",
          "name": "Example Company ProductCERT",
          "namespace": "https://psirt.example.com"
        },
        "title": "Example VEX document",
        "tracking": {
          "id": "2024-EVD-UC-01-A-001",
          "status": "final",
          "version": "1",
          "initial_release_date": "2024-01-01T11:00:00.000Z",
          "current_release_date": "2024-01-01T11:00:00.000Z",
          "revision_history": [
            {
              "number": "1",
              "date": "2024-01-01T11:00:00.000Z",
              "summary": "Initial version."
            }
          ]
        }
      },
      "product_tree": {
        "branches": [
          {
            "category": "vendor",
            "name": "Debian",
            "branches": [
              {
                "category": "product_name",
                "name": "Database Libraries",
                "branches": [
                  {
                    "category": "product_version",
                    "name": "5.3",
                    "product": {
                      "name": "Database Libraries 5.3",
                      "product_id": "LIBDB-5328",
                      "product_identification_helper": {
                        "purl": "pkg:deb/debian/[email protected]%2Bdfsg1-0.8?arch=amd64&distro=debian-11.8"
                      }
                    }
                  }
                ]
              }
            ]
          }
        ]
      },
      "vulnerabilities": [
        {
          "cve": "CVE-2019-8457",
          "product_status": {
            "known_not_affected": [
              "LIBDB-5328"
            ]
          },
          "threats": [
            {
              "category": "impact",
              "details": "Vulnerable code not in execute path.",
              "product_ids": [
                "LIBDB-5328"
              ]
            }
          ]
        }
      ]
    }

    실제로 써보면 핵심은 두 가지입니다. PURL 매칭이 정확해야 하고, 영향 없음의 이유를 threats/details에 남겨야 나중에 팀원들이 봐도 납득이 됩니다. 그냥 CVE를 가리는 문서가 아니어야 하거든요.

    Trivy False Positive 대응을 위한 VEX CSAF 구조 다이어그램

    CSAF VEX에서 product_tree와 vulnerabilities가 어떻게 연결되는지, product_id와 PURL이 어떤 역할을 하는지 설명하는 이미지입니다.

    문서를 만들었다면 스캔에 바로 붙여봅니다.

    trivy image debian:11.8 --vex ./debian11.vex.csaf
    trivy image debian:11.8 --vex ./debian11.vex.csaf --show-suppressed

    --show-suppressed를 같이 쓰면 사라진 항목과 적용된 source를 확인하기 편합니다. 이 단계에서 CVE가 없어졌다고 바로 안심하지 마시고, 왜 suppress 되었는지를 꼭 보셔야 합니다. 저도 예전에 PURL 범위를 너무 넓게 잡아서 다른 버전까지 같이 가려버린 적이 있었는데, 그때 이 옵션이 꽤 도움이 됐습니다.

    실전 3단계: .trivyignore.yaml과 상태 필터를 같이 최적화합니다

    VEX만으로 모든 노이즈를 처리하려고 하면 오히려 관리가 무거워집니다. 반대로 모든 걸 ignore 파일에 몰아넣으면 근거가 약해지고요. 그래서 저는 용도를 분리합니다.

    • VEX CSAF: 영향 없음의 근거가 있는 취약점
    • .trivyignore.yaml: 특정 경로나 패키지에만 한정한 임시 예외
    • –ignore-unfixed: 배포판 패치가 아직 없는 이슈를 줄이는 운영 옵션
    • –ignore-status: 상태가 명확한 공급자 메타데이터 기반 정리

    예를 들어 임시 예외가 필요하면 이렇게 갑니다.

    vulnerabilities:
      - id: CVE-2022-40897
        paths:
          - "usr/local/lib/python3.9/site-packages/setuptools-58.1.0.dist-info/METADATA"
        statement: "Accept the risk during image transition"
      - id: CVE-2023-3817
        purls:
          - "pkg:deb/debian/libssl1.1"
      - id: CVE-2023-29491
        expired_at: 2026-09-30
        statement: "Temporary exception until base image refresh"

    그리고 스캔은 이렇게 묶습니다.

    trivy image \
      --ignore-unfixed \
      --ignore-status under_investigation,will_not_fix,fix_deferred \
      --ignorefile ./.trivyignore.yaml \
      --vex ./debian11.vex.csaf \
      --show-suppressed \
      debian:11.8

    여기서 중요한 포인트가 있습니다. .trivyignore.yaml은 실험적 기능이라 명시적으로 --ignorefile를 붙이는 습관이 좋습니다. 그리고 expired_at을 꼭 넣어두세요. 안 그러면 예외가 영구화되기 쉽습니다. 이런 게 나중에 제일 무섭더라고요.

    Trivy False Positive 트러블슈팅

    1. VEX를 넣었는데 결과가 안 줄어드는 경우

    대부분은 PURL 불일치입니다. 패키지 버전, distro qualifier, arch qualifier가 맞는지 다시 보셔야 합니다. 특히 Debian/Ubuntu 계열은 qualifier가 빠지면 다른 패키지 변형과 매칭이 달라질 수 있습니다.

    2. 너무 넓게 suppress 되는 경우

    PURL에서 버전을 뺀 예외는 여러 버전에 넓게 적용될 수 있습니다. 편할 때도 있지만 운영 환경에서는 위험할 때가 더 많습니다. 저는 배포 이미지에는 가능하면 버전까지 넣는 편입니다.

    3. ignore 파일과 VEX가 섞여서 이유를 모르겠는 경우

    --show-suppressed를 켜면 어느 소스에서 suppress 되었는지 확인하기 쉬워집니다. 테이블 출력의 source를 먼저 보고, JSON 보고서를 쓰는 파이프라인이면 수정된 findings도 같이 보관하는 편이 좋습니다.

    4. unfixed를 너무 믿는 경우

    --ignore-unfixed는 노이즈를 줄이는 데는 좋지만, 리스크가 사라진다는 뜻은 아닙니다. 배포판 패치가 아직 없을 뿐이고, 실제 영향 분석은 여전히 필요합니다. 특히 외부 공개 서비스용 이미지는 여기서 한 번 더 눈으로 확인하는 게 안전합니다.

    Trivy False Positive 검증을 위해 suppressed 결과를 확인하는 터미널 이미지

    스캔 결과에서 suppressed vulnerabilities와 source 정보가 보여서 어떤 규칙이 적용됐는지 검증하는 장면의 이미지입니다.

    검증은 이렇게 보시면 됩니다

    완성된 결과를 볼 때는 총 개수만 보면 안 됩니다. 저는 아래 순서로 읽습니다.

    1. 기준 보고서 대비 어떤 CVE가 빠졌는지 확인합니다.
    2. 빠진 이유가 VEX인지 ignorefile인지 구분합니다.
    3. 남아 있는 HIGH/CRITICAL 중 실제 조치 대상만 추립니다.
    4. 예외 문서의 만료일과 변경 이력을 같이 봅니다.

    판단 기준도 명확해야 합니다. 같은 CVE가 결과에서 사라졌다고 끝내면 안 되고, suppressed source가 기대한 정책인지를 확인해야 합니다. VEX로 처리하려던 항목이 ignorefile에서 먼저 가려졌다면 정책 계층이 뒤집힌 상태거든요.

    제가 직접 해보니 가장 안정적인 검증 방식은 기준 JSON 보고서 + suppress 확인용 테이블 출력을 함께 남기는 방식이었습니다. 사람은 테이블로 읽고, 파이프라인 비교는 JSON으로 하는 거죠. 이거 진짜 편하더라고요.

    운영 추천안: 이런 경우엔 이렇게 가시면 됩니다

    Trivy False Positive 줄이기 전략을 비교한 요약 인포그래픽

    오탐 성격에 따라 어떤 옵션을 먼저 선택해야 하는지 한눈에 보여주는 요약 이미지입니다.

    • 배포판 패치가 아직 없는 경우: --ignore-unfixed를 먼저 적용하고, 주기적으로 재검토하세요.
    • 공급자 상태 정보로 충분한 경우: --ignore-status로 운영 정책을 단순화하세요.
    • 영향 없음의 근거를 남겨야 하는 경우: VEX CSAF로 문서화하세요. 감사 대응이나 팀 간 공유에 특히 강합니다.
    • 특정 파일 경로나 패키지에만 임시 예외가 필요한 경우: .trivyignore.yaml에 expired_at과 statement를 꼭 넣으세요.

    제 추천은 분명합니다. 조직 차원의 표준 예외는 VEX CSAF, 팀 로컬의 짧은 유예는 .trivyignore.yaml입니다. 여기에 --ignore-unfixed와 --ignore-status를 운영 보조 수단으로 얹는 방식이 가장 덜 망가집니다.

    다음 글에서는 Trivy 결과를 CI에서 fail-open / fail-close로 나누는 기준도 다뤄보겠습니다. 이전 글에서 다뤘던 컨테이너 보안 글과 함께 보면 더 잘 연결됩니다.

    FAQ처럼 자주 받는 질문 몇 가지

    VEX만 쓰면 ignore 파일은 필요 없나요

    아닙니다. VEX는 설명 가능한 예외에 강하고, ignore 파일은 로컬 임시 예외에 강합니다. 둘의 목적이 다릅니다.

    CSAF와 OpenVEX 중 무엇을 먼저 볼까요

    팀이 작고 단순하면 OpenVEX도 좋습니다. 다만 여러 대상과 제품 구조를 설명해야 하면 CSAF가 더 잘 맞는 경우가 많습니다.

    Trivy False Positive를 완전히 없앨 수 있나요

    완전히 없애는 건 어렵습니다. 대신 오탐을 문서화 가능한 예외와 정말 처리해야 할 취약점으로 나누는 건 충분히 가능합니다. 그 차이가 운영 품질을 바꿉니다.

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 실제 운영 환경에서는 변경 이력과 승인 절차를 함께 관리하시길 권합니다.

  • [보안] Trivy 성능 최적화로 대규모 컨테이너 스캔 가속하기

    [보안] Trivy 성능 최적화로 대규모 컨테이너 스캔 가속하기

    [보안] Trivy 성능 최적화로 대규모 컨테이너 스캔 가속하기

    대규모 컨테이너 환경을 운영하다 보면 Trivy 성능 최적화가 생각보다 빨리 중요한 과제가 됩니다. 처음에는 이미지 하나, 둘 스캔할 때는 괜찮거든요. 근데 마이크로서비스가 늘고, CI/CD 파이프라인마다 이미지 스캔이 붙기 시작하면 갑자기 빌드 시간이 확 늘어납니다. 저도 홈랩이랑 실무 환경에서 비슷한 상황을 꽤 겪었는데요. 처음엔 “보안 검사니까 원래 느린가 보다” 하고 넘겼다가, 나중엔 배포 병목이 되더라고요. 특히 컨테이너 보안 정책과 이미지 스캔을 체계적으로 관리하려다 보니, 취약점 관리의 효율성까지 함께 챙겨야 하는 팀들이라면 이 부분을 그냥 두면 안 됩니다.

    오늘은 제가 직접 적용하면서 효과를 봤던 방향 위주로, Trivy를 더 빠르게 돌리는 방법을 정리해보겠습니다. 핵심은 무작정 옵션을 많이 붙이는 게 아니라, 어디서 시간이 쓰이는지 구간을 나눠서 보는 것입니다. 데이터베이스(DB) 다운로드, 레이어 분석, 불필요한 스캐너 실행, CI/CD 캐시 미활용, 중복 스캔. 보통 여기서 시간 대부분이 나갑니다.

    대규모 컨테이너 환경에서 Trivy 스캔이 어디서 느려지는지 한눈에 보여주는 개요 이미지입니다.

    1. 왜 Trivy 성능 최적화가 필요한가

    쉽게 말해 Trivy는 취약점 데이터베이스를 내려받고, 이미지 레이어를 분석하고, 패키지 정보를 대조해서 취약점을 찾는 도구입니다. 이 과정 자체는 합리적이죠. 문제는 서비스 수가 많아질수록 같은 작업을 너무 자주 반복한다는 데 있더라고요.

    • 같은 베이스 이미지를 여러 서비스가 반복 스캔
    • CI/CD 러너(runner, 빌드 실행기)가 매번 새로 떠서 캐시가 없음
    • 필요 없는 스캐너까지 같이 실행
    • 모든 브랜치에서 동일한 깊이로 검사

    저도 처음엔 각 저장소마다 Trivy를 독립 실행하게 해뒀었는데, 실제로 써보니까 DB 다운로드와 캐시 미스(cache miss) 때문에 시간이 꽤 날아가더라고요. 특히 ephemeral runner(에페메럴 러너, 작업 후 사라지는 실행 환경) 쓰는 곳에서는 더 체감이 큽니다.

    2. Trivy가 느려지는 구간을 먼저 이해해보죠

    여기서 중요한 포인트가 있습니다. Trivy를 빠르게 만들려면 먼저 느린 구간을 분리해서 봐야 하더라고요. 보통 아래 네 가지입니다.

    1. DB 준비 시간: 취약점 데이터베이스를 매번 새로 가져오면 느립니다.
    2. 이미지 분석 시간: 레이어가 많거나 패키지 종류가 다양하면 분석 시간이 늘어납니다.
    3. 스캐너 범위: vuln(취약점), secret(시크릿), config(설정)까지 한 번에 다 돌리면 당연히 무거워집니다.
    4. 중복 실행: 같은 이미지를 브랜치마다, 잡(job)마다 반복 스캔하면 비효율이 크더라고요.

    그래서 제가 권하는 접근은 이렇습니다. “DB 캐시 최적화 → 스캔 범위 축소 → 실행 구조 개선 → 서버 모드 검토” 순서로 보시면 됩니다. 이 순서가 좋은 이유는, 앞 단계일수록 적용 난이도 대비 체감 효과가 큰 경우가 많기 때문입니다.

    3. 실전 구현 1: DB 캐시와 캐시 디렉터리부터 잡습니다

    Trivy 성능 최적화에서 제일 먼저 손볼 건 캐시입니다. 이건 진짜 기본인데, 의외로 놓치는 팀이 많아요. 러너가 매번 깨끗한 상태로 시작하면 Trivy 입장에서는 매번 처음부터 다시 해야 하거든요.

    제가 직접 해보니 가장 단순하고 효과적인 방법은 캐시 디렉터리(cache directory)를 고정하고, CI 캐시와 연결하는 거더라고요.

    export TRIVY_CACHE_DIR=.trivycache
    trivy image --cache-dir .trivycache my-registry.example.com/sample-app:latest

    파이프라인에서는 보통 이 디렉터리를 캐시 대상으로 잡습니다. 예를 들면 이런 식이죠.

    steps:
      - name: Restore Trivy cache
        uses: actions/cache@v4
        with:
          path: .trivycache
          key: trivy-cache-${{ runner.os }}-${{ hashFiles('**/Dockerfile') }}
          restore-keys: |
            trivy-cache-${{ runner.os }}-
    
      - name: Scan image
        run: |
          export TRIVY_CACHE_DIR=.trivycache
          trivy image --cache-dir .trivycache my-image:latest

    여기서 팁 하나 더 있습니다. 빌드 잡과 스캔 잡을 분리했다면, 스캔 직전에 DB를 미리 받아두는 방식도 꽤 유용하더라고요.

    trivy image --download-db-only
    trivy image my-image:latest

    이렇게 해두면 네트워크 상태가 애매한 환경에서 시간 분산이 되더라고요. “왜 어떤 날은 빠르고 어떤 날은 느리지?” 싶은 분들, 사실 DB 다운로드 타이밍 영향이 큽니다.

    4. 실전 구현 2: 필요한 스캐너만 돌리고 스캔 범위를 줄입니다

    Trivy는 기능이 많은 만큼, 아무 생각 없이 다 켜면 무거워집니다. 물론 보안 관점에서는 이것저것 다 보고 싶죠. 저도 처음엔 그랬습니다. 근데 CI/CD 전체 시간을 생각하면 목적별 분리가 필요하더라고요.

    예를 들어 PR 단계에서는 취약점(vulnerability, 취약성) 위주로 빠르게 보고, 야간 배치나 메인 브랜치에서는 secret(시크릿, 노출된 비밀정보)나 config(설정 검사)까지 확장하는 식입니다.

    trivy image \
      --scanners vuln \
      --severity HIGH,CRITICAL \
      --ignore-unfixed \
      my-image:latest

    이렇게 하면 노이즈가 꽤 줄어듭니다. 특히 취약점 관리를 실무에서 운영할 때 당장 조치 가능한 항목 위주로 보는 데 도움이 되더라고요.

    또 하나 많이 쓰는 방법이 skip 옵션입니다. 파일 시스템(fs, 파일 시스템) 스캔이나 저장소(repo, 리포지토리) 스캔에서는 불필요한 디렉터리를 건너뛰는 것만으로도 시간이 줄 수 있습니다.

    trivy fs \
      --skip-dirs node_modules \
      --skip-dirs vendor \
      --skip-files '*.log' \
      .

    물론 무턱대고 제외하면 안 돼요. 여기서 중요한 포인트! 실제 배포 산출물에 포함되는 경로인지 먼저 확인해야 합니다. 예전에 제가 빌드 컨텍스트(build context)를 제대로 안 보고 디렉터리 제외했다가, 필요한 패키지 경로까지 날려서 결과가 이상하게 나온 적이 있었어요. 삽질 좀 했습니다 ㅎㅎ

    Trivy 성능 최적화를 위한 CI/CD 캐시 및 스캐너 분리 구성 이미지

    CI 파이프라인에서 Trivy 캐시, 스캐너 범위, 스캔 경로를 어떻게 나누는지 설명하는 구성 이미지입니다.

    5. 실전 구현 3: 서버 모드로 중복 작업을 줄입니다

    이미지가 많고 팀이 여러 파이프라인에서 Trivy를 동시에 쓰는 환경이라면, server mode(서버 모드)도 검토할 만하더라고요. 이 방식은 공용 Trivy 서버가 DB와 캐시를 들고 있고, 클라이언트가 거기로 요청을 보내는 구조입니다. 제가 실제로 여러 서비스 이미지를 반복 검사하던 환경에서 이 구조를 써보니까, 매번 러너마다 준비 작업을 다시 하는 비용이 줄어서 운영이 한결 편해졌어요.

    trivy server --listen 0.0.0.0:4954
    trivy image --server http://trivy-server.internal:4954 my-image:latest

    이 구조의 장점은 명확합니다.

    • DB와 캐시를 중앙에서 관리 가능
    • 여러 CI/CD 잡이 같은 준비 상태를 재활용
    • 에페메럴 러너 환경에서도 일관된 속도 확보

    다만 모든 환경에 정답은 아닙니다. 소규모 팀이나 저장소 수가 적은 경우에는 오히려 운영 포인트만 늘어날 수 있거든요. 그래서 저는 보통 아래 기준으로 판단합니다.

    환경 추천 방식 이유
    소규모 단일 저장소 로컬 캐시 중심 구성이 단순하고 관리가 쉽습니다
    여러 서비스가 있는 CI/CD 공용 캐시 + 잡 분리 중복 다운로드를 줄이기 좋습니다
    대규모 멀티 프로젝트 Trivy 서버 모드 중앙 캐시 재사용 효과가 큽니다

    6. ⚠️ 실제로 자주 만나는 문제와 트러블슈팅

    이 섹션은 정말 중요해요. 문서만 보면 다 쉬워 보이는데, 실제 운영에서는 예상 밖의 병목이 꼭 나옵니다. 제가 겪었던 것과 많이 상담받았던 케이스를 정리해보겠습니다.

    6-1. 캐시를 붙였는데도 체감이 없다

    대부분은 캐시 경로가 매 실행마다 달라지거나, 러너가 캐시 복원을 제대로 못 하고 있는 경우였어요. CI 로그에서 실제로 캐시 restore가 성공했는지 꼭 보셔야 합니다. 경로만 같다고 끝이 아니더라고요.

    6-2. secret 스캔 때문에 예상보다 오래 걸린다

    이건 자주 있습니다. secret 스캔은 용도가 분명하지만, 모든 PR 단계에서 항상 필요한 건 아닐 수 있어요. 빠른 피드백이 우선인 구간과 정밀 검사가 필요한 구간을 분리해보세요.

    6-3. 같은 베이스 이미지를 계속 스캔한다

    이 경우는 이미지 계층(layer) 구조를 먼저 보셔야 합니다. 베이스 이미지가 공통이라면 변경 없는 브랜치에서 풀스캔을 반복하는 구조가 맞는지 점검해보는 게 좋아요. 실제로 써보니까 애플리케이션 변경이 거의 없는데도 이미지만 다시 빌드해서 전체 스캔하는 경우가 많았거든요.

    6-4. 네트워크가 느린 날만 유독 전체 시간이 튄다

    DB 다운로드와 외부 레지스트리 접근 시간이 흔들리면 전체 결과도 흔들린답니다. 이럴 때는 사전 DB 준비, 사내 레지스트리 미러, 스캔 전용 실행 구간 분리가 도움이 돼요.

    • 캐시가 복원되는지 로그로 확인
    • 스캐너 범위를 목적별로 나눌 것
    • 불필요한 디렉터리 제외는 실제 배포 경로 기준으로 검토
    • 중앙 캐시가 필요할 정도인지 운영 복잡도와 함께 판단

    7. 결과 검증: 빨라졌는지 어떻게 확인할까

    최적화는 느낌으로 하면 안 돼요. “조금 빨라진 것 같아요”는 운영에서 별 의미가 없거든요. 최소한 아래 정도는 비교해보시는 걸 권합니다.

    1. DB 다운로드 포함/미포함 전체 소요 시간
    2. PR 스캔과 메인 브랜치 스캔의 평균 실행 시간
    3. 이미지 크기별 스캔 편차
    4. 동시 실행 시 대기 시간 증가 여부

    예를 들어 간단하게는 CI 로그에서 전후 시간을 모아 비교할 수 있어요. 더 여유가 있으면 잡 실행 시간 추이를 대시보드로 모아보세요. 이거 진짜 편하더라고요. 어느 날 갑자기 느려졌을 때 원인 찾는 속도가 달라집니다.

    time trivy image --cache-dir .trivycache my-image:latest

    결과를 볼 때는 단순 평균보다 최악의 경우(worst case)도 같이 보셔야 합니다. 실무에서는 평소 2분이어도 가끔 9분 튀는 게 더 문제거든요. 특히 배포 직전 파이프라인에서 그러면 팀 전체가 멈춘 느낌이 납니다.

    Trivy 성능 최적화 적용 전후 스캔 성능 결과 대시보드

    Trivy 최적화 적용 전후의 스캔 시간, 캐시 적중률, 병목 감소를 시각화한 결과 이미지입니다.

    8. 정리: 상황별 추천 전략과 다음 단계

    지금까지 내용을 정리하면, Trivy 성능 최적화는 거창한 튜닝보다 기본기에서 시작돼요. 캐시를 제대로 쓰고, 필요한 스캐너만 돌리고, 중복 작업을 줄이는 것. 이 세 가지만 해도 체감 차이가 꽤 큽니다. 제가 직접 해보니 특히 CI/CD에서 빌드 대기열이 긴 팀일수록 효과가 빨리 보였어요.

    상황별로 간단히 추천하면 이렇습니다.

    상황 우선 적용할 것 비고
    러너가 매번 새로 생성됨 캐시 디렉터리 고정 + CI 캐시 복원 가장 먼저 볼 포인트입니다
    PR이 너무 느림 severity, scanners 범위 축소 빠른 피드백용 정책 분리
    서비스 수가 많음 공용 캐시 또는 서버 모드 중복 작업 절감 효과가 큽니다
    노이즈가 많음 ignore-unfixed와 정책 재정비 속도와 운영 피로를 함께 줄입니다

    혹시 지금 Trivy를 돌리고 있는데 “느리긴 한데 어디서부터 손대야 할지 모르겠다” 싶으시면, 오늘 글 기준으로는 1) 캐시 확인, 2) 스캐너 범위 분리, 3) 중복 스캔 구조 점검 이 세 가지부터 해보시면 돼요. 이 순서가 실패 확률도 낮고, 결과 확인도 쉽습니다.

    다음 글에서는 CI/CD 파이프라인에서 이미지 스캔 정책을 단계별로 분리하는 방법도 다뤄볼 예정입니다. 이전 글에서 다뤘던 레지스트리 구조 설계와 함께 보시면 훨씬 이해가 쉬우실 거예요.

    Trivy 성능 최적화 핵심 전략을 정리한 요약 인포그래픽

    Trivy 성능 최적화 핵심 포인트를 빠르게 복습할 수 있도록 정리한 요약 인포그래픽입니다.

    9. 자주 묻는 질문

    Q1. Trivy 성능 최적화는 캐시만 잘 써도 충분한가요?

    아닙니다. 캐시는 시작점이에요. 대규모 환경에서는 스캔 범위 조정과 중복 실행 구조 개선까지 같이 봐야 합니다.

    Q2. 모든 CI 단계에서 secret 스캔을 돌려야 할까요?

    반드시 그럴 필요는 없어요. 보안 요구사항에 따라 다르지만, 빠른 피드백이 중요한 구간과 정밀 검사가 필요한 구간을 나누는 편이 현실적입니다.

    Q3. 서버 모드는 언제 고려하면 좋을까요?

    여러 프로젝트가 동일한 취약점 데이터와 캐시를 반복해서 쓰는 환경이라면 검토 가치가 크더라고요. 반대로 작은 팀이라면 운영 복잡도가 더 클 수도 있습니다.

    결국 핵심은 하나입니다. 보안을 포기하지 않으면서도 배포 속도를 지키는 구조를 만드는 것. 이 균형을 맞추는 게 인프라 엔지니어 일의 재미이기도 하죠.

  • [보안] Trivy vs Clair: 우리 팀에 맞는 컨테이너 이미지 보안 스캐너는?

    [보안] Trivy vs Clair: 우리 팀에 맞는 컨테이너 이미지 보안 스캐너는?

    [보안] Trivy vs Clair: 우리 팀에 맞는 컨테이너 이미지 보안 스캐너는?

    컨테이너 이미지 보안 이야기를 하면 결국 한 번은 취약점 스캐너(vulnerability scanner, 알려진 보안 취약점을 찾아주는 도구) 선택 문제로 돌아오게 됩니다. 특히 팀에서 CI/CD를 굴리기 시작하면, 이미지 빌드까지는 잘 되는데 배포 직전에 “이 이미지를 그냥 올려도 되나?” 하는 순간이 꼭 오거든요. 저도 홈랩하고 회사 환경에서 이것저것 붙여보면서 삽질을 꽤 했습니다. 처음엔 스캐너면 다 비슷한 거 아닌가 싶었는데, 실제로 써보니까 Trivy와 Clair는 접근 방식이 꽤 다르더라고요. 이번 글에서는 컨테이너 이미지 보안 관점에서 두 도구를 비교하고, 어떤 팀에 어떤 선택이 맞는지 경험 기반으로 정리해보겠습니다.

    컨테이너 이미지 보안 아키텍처에서 Trivy와 Clair의 역할을 보여주는 개요 이미지

    Trivy와 Clair가 개발 파이프라인과 레지스트리 사이에서 어디에 붙는지 한눈에 보여주는 개요 이미지입니다.

    1. 왜 컨테이너 이미지 보안이 먼저냐

    쉽게 말해 컨테이너 이미지는 애플리케이션 실행에 필요한 파일 묶음입니다. 여기 안에는 애플리케이션 코드만 들어있는 게 아니라, 베이스 이미지(base image, 실행 기반 운영체제 레이어), 패키지 관리자 설치물, 언어별 라이브러리까지 같이 들어가죠. 그러다 보니 한 줄의 애플리케이션 코드보다 이미지 안에 포함된 오래된 패키지가 더 큰 리스크인 경우도 많습니다.

    제가 직접 해보니 현업에서 자주 놓치는 포인트는 딱 두 가지였습니다. 첫째, 개발팀은 애플리케이션 dependency(디펜던시, 의존 패키지)는 보는데 OS 패키지는 잘 안 봅니다. 둘째, 운영팀은 이미지 태그만 보고 안심하는 경우가 있는데, 같은 애플리케이션이어도 베이스 이미지가 달라지면 결과가 완전히 달라집니다. 그래서 컨테이너 이미지 보안은 배포 뒤가 아니라 빌드 직후부터 챙겨야 하거든요.

    2. Trivy와 Clair, 이 두 스캐너는 뭐가 다를까

    Trivy는 실행이 빠르고 진입장벽이 낮은 올인원 성격의 스캐너에 가깝습니다. 공식 문서 기준으로 컨테이너 이미지뿐 아니라 파일시스템, Git 리포지토리, Kubernetes, SBOM(Software Bill of Materials, 소프트웨어 구성 명세)까지 다뤄요. 취약점뿐 아니라 misconfiguration(미스컨피규레이션, 잘못된 보안 설정)과 secret(시크릿, 노출된 비밀정보) 검사도 같이 가져갈 수 있어서 DevSecOps 도입 초반에 특히 편합니다.

    Clair는 조금 결이 다릅니다. 컨테이너 취약점 정적 분석(static analysis, 실행 없이 구조와 패키지를 분석하는 방식)에 집중한 서비스형 구조에 더 가깝습니다. 공식 문서와 프로젝트 설명을 보면 API 기반으로 이미지를 인덱싱(indexing, 패키지 정보를 수집해 저장)하고, 이후 알려진 취약점과 매칭하는 흐름이 핵심입니다. 그래서 단일 CLI를 바로 실행하는 느낌보다는, 레지스트리나 내부 플랫폼에 연결해서 쓰는 그림이 더 자연스럽습니다.

    여기서 중요한 포인트! 둘 중 누가 절대적으로 더 좋다고 보기는 어렵습니다. 우리 팀의 운영 방식이 더 중요합니다.

    3. Trivy vs Clair: 우리 팀에 중요한 비교 포인트

    비교 항목 Trivy Clair
    첫 도입 난이도 낮음. CLI 중심으로 바로 시작 가능 상대적으로 높음. 서비스와 설정 이해 필요
    운영 형태 로컬 CLI, CI, 서버 모드 모두 가능 API 기반 서비스 운용에 더 적합
    주요 강점 취약점, misconfiguration, secret, SBOM 범위가 넓음 컨테이너 이미지 취약점 분석 파이프라인에 집중
    데이터베이스 운영 자체 DB를 받아 캐시하며 사용 PostgreSQL 기반 영속 저장 구조 사용
    잘 맞는 팀 작은 팀, 플랫폼 초기 팀, 빠른 CI 통합이 필요한 팀 내부 보안 플랫폼/레지스트리 통합을 운영하는 팀
    학습 곡선 완만함 구조 이해가 필요해서 가파른 편

    실제로 써보니까 Trivy는 시작이 쉽고 범용적이었습니다. 반면 Clair는 아키텍처 안에 넣었을 때 힘을 발휘하더라고요. 그래서 팀의 질문도 달라야 합니다. “무엇이 더 강력한가”보다 “우리는 CLI 중심으로 갈 건가, 서비스 중심으로 갈 건가”가 먼저입니다.

    4. Trivy로 바로 시작하는 실전 구현

    처음 도입하는 팀이라면 저는 거의 무조건 Trivy부터 붙여봅니다. 이유가 단순합니다. 빨리 결과가 나오고, 개발팀이 거부감 없이 받아들이기 쉽거든요.

    1. 스캔 대상 이미지를 하나 정합니다. 예시는 nginx:latest처럼 공개 이미지로 해도 됩니다.
    2. 로컬에서 한 번 돌려보고 결과 형식을 익힙니다.
    3. 그다음 CI에 붙여서 High/Critical 기준으로 fail 시킵니다.
    docker run --rm \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v $HOME/.cache/trivy:/root/.cache/ \
      aquasec/trivy image nginx:latest

    이 명령은 Docker 소켓을 마운트해서 로컬 이미지 또는 엔진에서 접근 가능한 이미지를 검사합니다. 처음엔 DB를 내려받는 시간이 조금 걸릴 수 있어요. 저도 처음엔 “왜 이렇게 느리지?” 했었는데, 캐시가 잡히고 나면 훨씬 빨라지더라고요.

    trivy image --severity HIGH,CRITICAL --exit-code 1 nginx:latest

    이건 CI에 넣기 좋은 형태입니다. 지정한 심각도(severity, 위험도) 이상이 나오면 종료 코드를 1로 반환하니까 파이프라인에서 바로 실패 처리할 수 있어요. 단순하지만 실무에서는 이게 제일 중요합니다. 개발팀이 결과를 보는 것보다, 기준 이상이면 못 지나가게 막는 것이 더 효과가 크거든요.

    trivy image --format spdx-json --output sbom.json nginx:latest

    SBOM 생성도 됩니다. 여기서 DevSecOps 흐름이 한 단계 올라갑니다. 단순히 취약점 숫자만 보는 게 아니라, 이미지 안에 무엇이 들어있는지 자산 관리를 같이 할 수 있거든요.

    컨테이너 이미지 보안을 위해 Trivy 스캔이 CI에서 동작하는 모습

    Trivy를 CI에 붙여서 이미지 스캔이 자동으로 돌아가고, 기준 이상 취약점에서 빌드가 멈추는 흐름을 설명하는 이미지입니다.

    Trivy를 추천하는 경우

    • 빠르게 시작해야 하는 팀
    • 보안 전담 인력이 많지 않은 팀
    • 취약점 스캔 외에 misconfiguration, secret 검사까지 같이 보고 싶은 팀
    • 개발자 로컬 환경과 CI에서 같은 도구를 쓰고 싶은 팀

    5. Clair는 언제 빛나나: 서비스형 운영이 필요할 때

    Clair는 “개발자가 CLI 하나 실행하는 도구”라기보다, 내부 서비스처럼 다루는 쪽이 더 맞습니다. 공식 문서 기준으로 Clair는 여러 모드(indexer, matcher, notifier, combo mode)를 가지며, PostgreSQL을 사용해요. 즉, 단순 바이너리 하나보다 스캐닝 서비스를 운영한다는 관점이 필요합니다.

    저도 처음엔 이게 뭔가 싶었는데, 구조를 이해하고 나니 왜 이런 설계를 했는지 보이더라고요. 이미지 메타데이터를 인덱싱하고, 취약점 데이터와 매칭하는 과정을 API 기반으로 분리하면 레지스트리나 사내 플랫폼에 붙이기 좋습니다. 여러 팀이 공통 서비스로 재사용하기도 좋고요.

    1. PostgreSQL을 준비합니다.
    2. Clair 설정 파일을 작성합니다.
    3. combo mode로 먼저 띄워봅니다.
    4. clairctl로 이미지를 제출해서 결과를 확인합니다.
    http_listen_addr: :6060
    introspection_addr: :8089
    log_level: info
    indexer:
      connstring: host=clair-db port=5432 user=clair dbname=clair sslmode=disable
      migrations: true
    matcher:
      connstring: host=clair-db port=5432 user=clair dbname=clair sslmode=disable
      migrations: true
    notifier:
      connstring: host=clair-db port=5432 user=clair dbname=clair sslmode=disable
      migrations: true

    이 예시는 개념 설명용 최소 구성입니다. 실서비스에서는 인증, 네트워크, 스토리지, 운영 정책을 따로 잡아야 합니다. Clair 쪽은 특히 “일단 설치”보다 “운영 구조를 어떻게 가져갈지”를 먼저 생각해야 해요.

    CLAIR_MODE=combo
    CLAIR_CONF=/etc/clair/config.yaml
    clair -conf /etc/clair/config.yaml -mode combo
    clairctl report --host http://localhost:6060 ubuntu:focal

    Clair는 이렇게 API 서비스와 클라이언트 도구가 분리된 흐름을 이해하면 훨씬 덜 헷갈려요. 반대로 이 구조가 부담스럽다면, 억지로 Clair부터 시작할 필요는 없다고 봅니다.

    컨테이너 이미지 보안에서 Clair의 indexer matcher PostgreSQL 구조를 설명하는 이미지

    Clair가 단순 CLI가 아니라 indexer, matcher, 데이터 저장소가 분리된 구조라는 점을 보여주는 아키텍처 이미지입니다.

    6. ⚠️ 실제로 겪었던 주의사항과 트러블슈팅

    첫 번째, 결과 숫자만 보고 놀라지 마세요. 취약점 스캐너는 패키지 소스와 배포판 메타데이터를 해석하는 방식에 따라 결과가 달라질 수 있습니다. 같은 이미지라도 스캐너마다 개수가 다르게 보일 수 있거든요. 저도 초반엔 “왜 A는 12개인데 B는 20개지?” 하면서 한참 봤습니다. 이건 도구가 틀렸다기보다 탐지 기준과 데이터 소스 차이를 봐야 하는 경우가 많습니다.

    두 번째, 스캔 시점을 통일해야 합니다. 개발자 로컬, CI, 레지스트리, 운영 배포 후 스캔이 다 따로 놀면 팀이 금방 지쳐요. 제 경험상 가장 덜 싸우는 방식은 이렇습니다.

    • 개발자 로컬: 빠른 사전 확인
    • CI: 정책 강제
    • 레지스트리 또는 플랫폼: 중앙 모니터링

    세 번째, False Positive(오탐)와 False Negative(미탐)를 운영 규칙으로 다뤄야 합니다. 취약점 스캐너는 만능이 아니거든요. 패키지가 실제로 exploitable(익스플로이터블, 실제 공격 가능 상태)한지와는 또 다른 문제예요. 그래서 보안팀이나 플랫폼팀이 예외 처리 기준을 문서화해야 합니다.

    네 번째, 베이스 이미지 갱신 주기를 같이 가져가야 합니다. 스캐너만 붙여놓고 이미지가 매달 그대로면 결국 경고판만 늘어나요. 알림은 쌓이는데 고쳐지진 않죠. 이거 진짜 많이 봤습니다 ㅎㅎ

    7. 검증: 어떤 결과가 나오면 잘 붙은 걸까

    스캐너 도입이 끝났다고 판단하는 기준은 단순 설치 여부가 아닙니다. 아래 체크리스트가 돌아가야 합니다.

    1. 이미지 빌드 직후 자동 스캔이 실행된다.
    2. High 또는 Critical 기준으로 실패 정책이 적용된다.
    3. 어떤 패키지가 문제인지 개발자가 바로 확인할 수 있다.
    4. 재빌드 후 취약점 감소 여부를 비교할 수 있다.
    5. 예외 처리 사유가 기록된다.

    예를 들어 Trivy라면 결과 테이블에서 패키지명, 취약점 ID, 설치 버전, 수정 버전이 보여야 하고, Clair라면 제출한 이미지에 대해 보고서가 안정적으로 반환되어야 합니다. 여기까지 되면 컨테이너 이미지 보안 체계가 최소한의 자동화 단계에는 올라온 겁니다. 🎉

    컨테이너 이미지 보안 검증 결과와 취약점 감소 추이를 보여주는 대시보드 이미지

    스캔 결과를 한 번에 파악할 수 있는 대시보드와 재빌드 후 취약점 수가 줄어드는 검증 장면을 보여주는 이미지입니다.

    8. 그래서 우리 팀은 뭘 고르면 되나

    제가 멘토링하듯 정리하면 이렇습니다.

    • Trivy: 작은 팀, 스타트업, 플랫폼 초기 단계, 개발자 자율 사용이 중요한 팀에 잘 맞아요.
    • Clair: 중앙 서비스형 스캔, 레지스트리 통합, 내부 보안 플랫폼 운영이 중요한 팀에 잘 맞습니다.

    조금 더 현실적으로 말하면, 대부분의 팀은 Trivy로 시작해서 운영 성숙도가 올라가면 Clair 같은 서비스형 구성까지 검토하는 흐름이 자연스럽습니다. 물론 이미 레지스트리 중심 운영이 잘 잡혀 있고, 보안 서비스 운영 경험이 있다면 Clair도 충분히 좋은 선택이 될 수 있습니다.

    팀 상황 추천 이유
    개발팀이 직접 CLI를 돌려야 함 Trivy 배포 전 빠른 피드백이 쉬워요
    CI에서 바로 차단 정책이 필요함 Trivy 간단한 exit code 기반 통합이 쉬워요
    사내 공통 스캔 서비스를 운영함 Clair API 기반 구조가 잘 맞아요
    레지스트리와 강하게 결합한 운영을 원함 Clair 이미지 인덱싱/매칭 구조가 자연스러워요
    컨테이너 이미지 보안 도구인 Trivy와 Clair의 선택 기준을 요약한 인포그래픽

    팀 규모, 운영 방식, DevSecOps 성숙도에 따라 Trivy와 Clair 중 무엇을 고를지 빠르게 판단할 수 있게 정리한 요약 이미지입니다.

    9. 마무리: 도구보다 운영 습관이 더 오래 갑니다

    정리해보면, Trivy vs Clair 비교는 기능표 한 장으로 끝나는 문제가 아니었습니다. 직접 써보니까 결국 승부는 운영 방식에서 나더라고요. Trivy는 시작이 빠르고 범위가 넓어서 도입 속도가 좋았어요. Clair는 서비스형 구조를 이해하고 운영할 수 있을 때 더 의미가 있었습니다.

    혹시 지금 팀에서 취약점 스캐너를 막 도입하려는 상황이라면, 저는 이렇게 권하고 싶습니다. 먼저 Trivy로 CI 차단 정책 하나를 확실히 세우세요. 그다음 SBOM과 예외 처리 기준을 정리하세요. 그리고 조직 규모가 커지면서 중앙 서비스가 필요해질 때 Clair 같은 구조를 검토하면 됩니다. 이 순서가 실제로 덜 아프고, 덜 헷갈립니다.

    다음 글에서는 Trivy를 GitHub Actions 같은 CI에 붙여서 DevSecOps 파이프라인을 만드는 방법도 다뤄볼 예정입니다. 이전 글에서 다뤘던 이미지 경량화와 베이스 이미지 관리 전략을 같이 보면 더 연결이 잘 되실 겁니다. ✅

  • [Kubernetes] Kubernetes PodSecurity 도입 전후 비교: 보안 강화 사례 연구

    [Kubernetes] Kubernetes PodSecurity 도입 전후 비교: 보안 강화 사례 연구

    [Kubernetes] Kubernetes PodSecurity 도입 전후 비교: 보안 강화 사례 연구

    Kubernetes PodSecurity 도입을 고민하시는 분들은 아마 한 번쯤 이런 순간이 있으셨을 겁니다. 워크로드는 잘 돌아가는데, 막상 보안 점검을 해보면 privileged(특권 모드) 컨테이너가 섞여 있거나, hostPath(호스트 경로 마운트) 같은 위험한 설정이 아무 제어 없이 배포되는 경우 말이죠. 저도 처음엔 “네임스페이스만 잘 나누면 되지 않을까?” 싶었는데, 실제로 운영 환경과 홈랩에서 계속 만져보니 결국 배포 시점의 기본 보안 가드레일이 있어야 하더라고요. 그래서 이번 글에서는 Kubernetes PodSecurity 도입 전후를 어떻게 비교했는지, 어떤 문제가 줄었는지, 그리고 적용하면서 어디서 삽질했는지 경험 기반으로 정리해보겠습니다.

    특히 이 글은 Kubernetes 보안, Pod Security Standards(파드 보안 표준), 보안 정책을 실제 운영에 녹이는 관점으로 읽으시면 도움이 됩니다. 이론만 설명하는 글이 아니라, 제가 직접 해보니 어디서 걸리고 어떤 기준으로 정리해야 덜 힘든지 그 흐름을 보여드릴게요.

    PodSecurity 도입 전에는 제어가 느슨하고, 도입 후에는 네임스페이스 정책과 허용 범위가 명확해진 구조를 한눈에 보여주는 개요 이미지입니다.

    Kubernetes PodSecurity 도입이 왜 중요한가요

    쉽게 말해 PodSecurity는 “이 파드(Pod)는 어디까지 위험한 설정을 써도 되는가”를 네임스페이스 단위로 관리하는 방식입니다. 예전에는 팀마다 매니페스트를 제각각 작성하다 보니, 어떤 워크로드는 runAsNonRoot가 잘 들어가 있고, 어떤 건 아예 빠져 있고, 또 어떤 건 디버깅한다고 privileged: true를 넣어둔 채 남아 있기도 했습니다. 이게 한두 개면 눈으로 보겠는데, 서비스가 늘어나면 사람의 기억으로 통제하는 건 불가능하더라고요.

    여기서 중요한 포인트! 보안은 좋은 설정을 권장하는 것만으로는 부족합니다. 실제로는 잘못된 설정이 들어왔을 때 막아줘야 합니다. 제가 현장에서 느낀 것도 그거였습니다. 리뷰에서 빠지고, 급한 장애 대응 중엔 원칙이 밀리고, 결국 가장 약한 워크로드가 전체 클러스터 위험도를 끌어올리거든요.

    Pod Security Standards(파드 보안 표준)를 먼저 이해해야 합니다

    Pod Security Standards는 파드 보안 수준을 세 단계로 나눠서 생각하게 해줍니다. 저도 처음엔 이름만 보고 좀 딱딱하다고 느꼈는데, 실제로 써보니까 기준점이 분명해서 오히려 팀 합의가 빨라졌습니다.

    수준 설명 적합한 상황
    Privileged 제한이 거의 없는 수준입니다. 특수한 시스템 워크로드, 아주 제한적인 운영 도구
    Baseline 명백히 위험한 설정을 막는 기본선입니다. 일반적인 애플리케이션 네임스페이스의 첫 단계
    Restricted 가장 엄격한 수준으로, 안전한 기본값을 강하게 요구합니다. 보안 민감 서비스, 표준화가 잘 된 앱 워크로드

    제가 보통 권하는 방식은 이렇습니다.

    • 처음부터 전부 Restricted로 밀어붙이지 않습니다.
    • 기존 레거시 워크로드가 많다면 먼저 Baseline으로 현재 상태를 정리합니다.
    • 새로 만드는 네임스페이스나 표준화된 앱은 Restricted를 기본으로 잡습니다.

    이 접근이 중요한 이유는, 보안 정책은 강하면 좋은 게 아니라 운영에 정착해야 좋은 것이기 때문입니다. 너무 급하게 올리면 예외만 늘어나고 결국 정책이 무력화되더라고요.

    도입 전 환경에서 실제로 보였던 문제들

    제가 PodSecurity를 넣기 전에 먼저 한 일은 “무슨 설정이 돌아다니고 있는지”를 보는 것이었습니다. 막연히 위험할 것 같다가 아니라, 실제 매니페스트와 실행 중인 파드를 기준으로 봐야 하거든요. 그때 자주 보였던 문제는 아래와 같았습니다.

    • securityContext가 아예 없는 배포가 생각보다 많았습니다.
    • allowPrivilegeEscalation 설정이 빠져 있었습니다.
    • 루트(root) 권한 기반 이미지가 관성적으로 사용되고 있었습니다.
    • 운영 중 잠깐 쓰려고 만든 디버그용 파드가 그대로 남아 있었습니다.
    • 예외가 문서화되지 않아, 왜 허용했는지 아무도 설명 못 하는 설정이 있었습니다.

    이런 상태에서 보안 점검이 들어오면 굉장히 피곤해집니다. “왜 이 컨테이너는 루트로 뜨나요?” 같은 질문이 나오면, 실제 이유가 있어서가 아니라 그냥 기본값이라서 그런 경우가 많거든요. 사실 제일 무서운 건 악의적인 공격보다도 무심코 허용된 기본값입니다.

    Kubernetes PodSecurity 도입 절차: 제가 실제로 밟은 순서

    여기서는 복잡하게 가지 않고, 운영에서 비교적 덜 아픈 순서로 설명해볼게요. 핵심은 한 번에 차단(enforce)하지 말고 먼저 관찰부터 하는 겁니다.

    1. 보호할 네임스페이스를 분류합니다.
    2. audit(감사)와 warn(경고) 중심으로 먼저 적용합니다.
    3. 실패하는 워크로드를 수정합니다.
    4. 문제가 없는 네임스페이스부터 enforce(강제)로 전환합니다.
    5. 예외 워크로드는 별도 네임스페이스로 분리합니다.

    예를 들어 애플리케이션용 네임스페이스에 먼저 Baseline 경고와 감사 정책을 걸어볼 수 있습니다.

    kubectl label namespace app-team \
      pod-security.kubernetes.io/audit=baseline \
      pod-security.kubernetes.io/warn=baseline --overwrite

    이 상태에서는 배포가 바로 막히지는 않지만, 어떤 설정이 기준을 벗어나는지 확인하기 좋아요. 저는 처음부터 차단했다가 CI/CD 파이프라인이 우르르 깨지는 바람에 한 번 크게 삽질했었습니다. 그 뒤로는 무조건 경고와 감사부터 봅니다.

    조금 정리됐다 싶으면 강제로 올립니다.

    kubectl label namespace app-team \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest --overwrite

    운영에서는 latest 대신 조직에서 검증한 기준 버전을 명시적으로 관리하는 팀도 많습니다. 저는 팀 표준이 아직 흔들릴 때는 버전 기준을 문서화해두는 쪽이 더 낫다고 봅니다. 기준이 자주 바뀌면 개발팀이 체감상 더 힘들어하거든요.

    Kubernetes PodSecurity 도입 시 네임스페이스 정책 적용 흐름 이미지

    네임스페이스에 audit, warn, enforce 라벨을 붙이면서 단계적으로 정책을 강화하는 흐름을 설명하는 이미지입니다.

    Restricted(제한) 수준을 목표로 할 때 매니페스트 예시

    다음은 비교적 안전한 기본 형태의 예시입니다. 모든 앱이 이대로 되진 않겠지만, 팀 표준의 시작점으로 잡기 좋습니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: web-app
      namespace: app-team
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: web-app
      template:
        metadata:
          labels:
            app: web-app
        spec:
          securityContext:
            runAsNonRoot: true
            seccompProfile:
              type: RuntimeDefault
          containers:
            - name: web
              image: nginx:stable
              ports:
                - containerPort: 80
              securityContext:
                allowPrivilegeEscalation: false
                capabilities:
                  drop:
                    - ALL

    여기서 자주 보는 포인트는 아래입니다.

    • runAsNonRoot: true로 비루트 실행을 강제합니다.
    • allowPrivilegeEscalation: false로 권한 상승 가능성을 줄입니다.
    • capabilities.drop: [ALL]로 불필요한 리눅스 capability(리눅스 커널 권한 집합)를 제거합니다.
    • seccompProfile.type: RuntimeDefault로 기본 시스템 호출 필터를 사용합니다.

    도입 전후 비교: 무엇이 달라졌나

    정량 수치를 억지로 붙이기보다, 운영 체감 기준으로 말씀드리는 게 더 정확할 것 같습니다. Kubernetes PodSecurity 도입 전에는 “문제 있는 매니페스트가 배포된 뒤 나중에 발견되는 구조”였다면, 도입 후에는 “기준에 맞지 않으면 배포 단계에서 바로 드러나는 구조”로 바뀌었습니다. 이 차이가 생각보다 큽니다.

    비교 항목 도입 전 도입 후
    보안 기준 문서나 리뷰에 의존 네임스페이스 정책으로 강제
    위험 설정 발견 시점 배포 후 또는 점검 시 배포 직전 또는 배포 중
    팀 간 일관성 사람마다 다름 정책 기준으로 통일
    예외 관리 구두 합의가 많음 별도 네임스페이스와 문서로 관리

    제가 직접 해보니 제일 편했던 건, 리뷰에서 감정 소모가 줄어든다는 점이었습니다. 예전엔 “이 설정 위험하지 않나요?” 같은 대화가 사람 대 사람의 의견 충돌처럼 번질 때가 있었는데, 정책을 두고 나면 “이건 Baseline 기준에서 걸립니다”처럼 훨씬 명확해집니다. 이거 진짜 편하더라고요.

    ⚠️ 적용하면서 많이 막혔던 문제와 해결 방법

    이 섹션은 꼭 넣고 싶었습니다. 문서만 보면 간단해 보이는데, 실제로는 여기서 다들 한 번씩 막히거든요.

    1. 기존 이미지가 non-root 실행을 전제로 만들어지지 않은 경우

    Restricted 수준으로 올리면 가장 먼저 터지는 부분 중 하나입니다. 애플리케이션은 문제 없어 보여도, 이미지 내부 디렉터리 권한이나 엔트리포인트 스크립트가 루트 권한을 전제하는 경우가 있습니다. 저도 처음엔 “분명 앱은 잘 뜨는데 왜 이게 안 되지?” 싶었는데, 알고 보니 컨테이너 시작 시 쓰기 권한이 필요한 경로가 있었어요.

    이럴 때는 아래 순서로 봤습니다.

    1. 이미지가 비루트 실행을 지원하는지 확인합니다.
    2. 쓰기 경로가 꼭 필요한지 줄입니다.
    3. 필요하면 Dockerfile 단계에서 권한을 정리합니다.
    4. 정말 예외가 필요한 워크로드만 별도 분리합니다.

    2. 운영 도구와 일반 앱을 같은 기준으로 묶은 경우

    로그 수집기, CNI 관련 컴포넌트, 노드 접근이 필요한 에이전트는 일반 앱과 요구 조건이 다를 수 있습니다. 그런데 이걸 한 네임스페이스 정책으로 퉁치면 꼭 어딘가서 충돌이 납니다. 저는 한동안 “왜 어떤 건 꼭 예외가 생기지?” 하다가, 결국 애플리케이션 네임스페이스와 인프라 네임스페이스를 분리하면서 정리가 되더라고요.

    3. warn만 보고 끝내는 경우

    이건 진짜 자주 봅니다. 경고는 보이는데 바빠서 미루다 보면, 3개월 뒤에도 같은 경고가 그대로 쌓여 있어요. 그래서 저는 warn → audit → enforce 전환 일정을 미리 잡아두는 편입니다. 일정이 없으면 정책은 거의 장식품이 됩니다.

    Kubernetes PodSecurity 도입 후 경고와 배포 실패 분석 이미지

    경고 단계에서 어떤 항목이 걸리는지 확인하고, enforce 전환 전에 수정 포인트를 찾는 과정을 보여주는 이미지입니다.

    검증은 이렇게 했습니다

    보안 정책은 넣는 것보다 검증이 더 중요합니다. 정책이 존재해도 실제로 막히지 않으면 의미가 없으니까요. 제가 보통 하는 검증은 두 가지입니다. 하나는 정상 매니페스트가 잘 배포되는지, 다른 하나는 의도적으로 위험한 설정을 넣었을 때 차단되는지 확인하는 겁니다.

    예를 들어 아래처럼 위험한 설정이 들어간 테스트 파드를 만들어 봅니다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: privileged-test
      namespace: app-team
    spec:
      containers:
        - name: test
          image: busybox:1.36
          command: ["sh", "-c", "sleep 3600"]
          securityContext:
            privileged: true

    그리고 배포를 시도합니다.

    kubectl apply -f privileged-test.yaml

    정상이라면 보안 정책 위반으로 거부되는 흐름이 나와야 합니다. 반대로, 팀 표준 매니페스트는 무리 없이 통과해야 하고요. 여기서 중요한 건 단순히 “막혔다”가 아니라, 왜 막혔는지 개발자도 이해할 수 있어야 한다

    추가로 확인했던 항목은 아래와 같습니다.

    • 새 네임스페이스 생성 시 기본 라벨 정책이 누락되지 않는지
    • CI/CD에서 배포 실패 메시지가 충분히 읽히는지
    • 예외 네임스페이스가 무분별하게 늘어나지 않는지
    • 운영 문서와 실제 클러스터 라벨 상태가 일치하는지
    Kubernetes PodSecurity 도입 후 배포 검증 결과 이미지

    정상 배포와 정책 위반 배포가 어떻게 구분되는지, 운영자가 어떤 식으로 결과를 확인하는지 보여주는 이미지입니다.

    운영 기준을 문서화해야 도입이 끝납니다

    사실 PodSecurity는 라벨 몇 개 붙인다고 끝나지 않습니다. 진짜 끝은 팀 운영 기준이 문서로 남고, 다음 사람도 같은 방식으로 적용할 수 있을 때입니다. 저도 처음엔 클러스터에만 반영해두고 안심했었는데, 시간이 지나니 “이 네임스페이스는 왜 baseline이지?” 같은 질문이 계속 나오더라고요.

    그래서 저는 아래 항목을 최소 문서 세트로 남깁니다.

    • 네임스페이스별 목표 수준: baseline 또는 restricted
    • 예외 허용 기준: 누가 승인하고 언제 재검토하는지
    • 개발팀용 체크리스트: 보안 컨텍스트 기본 예시
    • 배포 실패 시 확인 순서: 어떤 필드를 먼저 볼지

    혹시 이런 경험 있으신가요? 정책은 분명 있는데, 새 프로젝트가 생길 때마다 같은 설명을 또 하고 또 하게 되는 상황 말이죠. 이런 반복을 줄이려면 결국 문서화와 템플릿화가 답입니다. 다음 글에서는 OPA Gatekeeper(오픈 정책 에이전트 게이트키퍼)나 Kyverno(카이버노) 같은 정책 엔진과 PodSecurity를 어떻게 역할 분담할지 다뤄볼 예정입니다. 이전 글에서 다룬 네임스페이스 표준화 내용이 있으시다면 같이 연결해서 보시는 것도 좋습니다.

    도입 전후의 차이, 운영 체크리스트, 다음 단계까지 한 장으로 요약한 인포그래픽 이미지입니다.

    정리: Kubernetes 보안은 작은 강제에서 시작됩니다

    Kubernetes PodSecurity 도입은 거창한 보안 프로젝트처럼 보일 수 있지만, 실제로는 “위험한 기본값을 그냥 두지 않겠다”는 아주 현실적인 출발점에 가깝습니다. 제가 여러 번 적용해보니 가장 효과적인 방식은 이렇습니다. 먼저 경고와 감사로 현재 상태를 파악하고, 그다음 Baseline으로 정리한 뒤, 준비된 워크로드부터 Restricted로 올리는 흐름이었습니다. 처음엔 이게 뭔가 싶었는데, 한 번 기준이 잡히고 나면 운영이 훨씬 편해집니다. 드디어 됐다! 싶은 순간이 오더라고요.

    마지막으로 한 줄 요약하면 이겁니다. Pod Security Standards는 문서가 아니라 운영 습관으로 들어와야 합니다. 보안 정책이 실제 배포 과정에 녹아들면, 리뷰 품질도 좋아지고 예외도 줄고, 나중에 감사 대응도 덜 힘들어집니다. Kubernetes 보안을 어디서부터 손대야 할지 막막하셨다면, PodSecurity부터 차근차근 시작해보세요.

    FAQ: 자주 묻는 질문

    모든 네임스페이스를 바로 Restricted로 가야 하나요?

    아닙니다. 레거시 워크로드가 많다면 Baseline부터 정리하는 편이 현실적입니다. 무리하게 올리면 예외만 늘어날 수 있습니다.

    PodSecurity만 있으면 보안 정책이 충분한가요?

    기본 보안 가드레일로는 매우 유용하지만, 조직별 세부 규칙까지 모두 대신하진 못합니다. 필요한 경우 별도 정책 엔진과 역할을 나눠가는 게 좋습니다.

    가장 먼저 점검할 필드는 무엇인가요?

    runAsNonRoot, allowPrivilegeEscalation, privileged, capabilities, hostPath 같은 항목부터 보는 걸 권합니다.

  • [k8s] Pod Security Standards 마이그레이션: Kubernetes PSP에서 안전하게 전환하기

    [k8s] Pod Security Standards 마이그레이션: Kubernetes PSP에서 안전하게 전환하기

    Pod Security Standards 마이그레이션: Kubernetes PSP에서 안전하게 전환하기

    Kubernetes 클러스터를 오래 운영했다면 PSP(PodSecurityPolicy) 때문에 한 번쯤 머리가 아팠을 겁니다. 저도 그랬습니다. 그런데 PSP는 Kubernetes 1.21에서 deprecated 되었고, Kubernetes 1.25에서 완전히 제거됐습니다. 그래서 Pod Security Standards 마이그레이션은 이제 선택이 아니라 업그레이드와 운영 안정성을 위해 꼭 챙겨야 할 작업이 됐습니다. 이번 글에서는 Kubernetes 보안 관점에서 PSP 대체 수단인 PSS(Pod Security Standards)와 Pod Security Admission으로 어떻게 안전하게 전환하는지, 실무 기준으로 차근차근 정리해보겠습니다.

    처음엔 저도 PSS가 너무 단순해 보여서 오히려 불안했습니다. PSP처럼 세부 필드를 다 적는 방식에 익숙하면 더 그렇게 느껴지거든요. 그런데 실제로 운영해보니 팀 공통 보안 기준을 맞추는 데는 훨씬 현실적이더라고요. 대신 전환 순서를 잘못 잡으면 배포가 갑자기 막힐 수 있습니다. 여기서 핵심은 처음부터 enforce로 가지 말고 warn, audit로 먼저 관찰하는 것입니다.

    Pod Security Standards 마이그레이션 개요를 보여주는 Kubernetes 보안 아키텍처 이미지

    PSP에서 PSS와 Pod Security Admission으로 넘어가는 흐름을 한눈에 보여주는 개요 이미지입니다.

    왜 Pod Security Standards 마이그레이션이 중요한가

    예전 방식인 PSP는 표현력은 강했지만 운영 피로도도 높았습니다. 반면 PSS는 Privileged, Baseline, Restricted라는 공통 보안 레벨을 제공해서 팀 간 기준을 맞추기 훨씬 쉽습니다. 여러 네임스페이스를 운영하는 환경에서는 긴 정책 객체를 계속 유지하는 것보다, 보안 수준을 레벨로 선언하는 편이 관리가 훨씬 단순합니다.

    제가 직접 겪어보니 Pod Security Standards 마이그레이션의 장점은 분명했습니다. 신규 팀원에게 설명하기 쉽고, 클러스터 업그레이드 때 정책 호환성 이슈가 크게 줄어듭니다. 다만 PSP처럼 아주 세밀한 예외 제어를 기대하면 아쉬울 수 있습니다. 그런 경우에는 OPA Gatekeeper나 Kyverno 같은 정책 엔진을 함께 쓰는 구성이 더 잘 맞습니다.

    Pod Security Standards 마이그레이션 기준: PSP와 PSS는 무엇이 다를까

    처음 보면 이름만 바뀐 것처럼 느껴질 수 있는데 구조는 꽤 다릅니다. PSP는 정책 리소스를 만들고 RBAC으로 사용자나 서비스어카운트와 연결하는 방식이었습니다. 반면 PSS는 네임스페이스에 보안 수준을 라벨로 선언하고, Pod Security Admission이 그 기준으로 파드를 검사합니다.

    항목 PSP PSS + Pod Security Admission
    정책 방식 세부 필드 기반 정책 객체 사전 정의된 보안 표준 레벨
    적용 범위 RBAC과 연결해 사용 네임스페이스 라벨 기반 적용
    운영 난이도 높은 편 상대적으로 단순
    이식성 클러스터별 편차 큼 표준화하기 좋음
    세밀한 예외 처리 강점 제한적, 필요 시 별도 정책 엔진 보완

    PSS의 핵심 레벨은 아래처럼 이해하면 편합니다.

    • Privileged: 사실상 제한이 거의 없는 수준입니다. 시스템 워크로드나 인프라성 컴포넌트에 가깝습니다.
    • Baseline: 일반 애플리케이션에 적용하기 좋은 현실적인 최소 보호선입니다.
    • Restricted: 현재 권장되는 강한 하드닝 기준입니다. 대신 기존 애플리케이션은 여기서 많이 걸릴 수 있습니다.

    Pod Security Admission 적용 방법: 전환 전에 꼭 확인할 것

    Pod Security Admission은 파드 생성 시점에 PSS 기준을 검사하는 내장 Admission Controller입니다. 다만 여기서 하나 짚고 갈 점이 있습니다. warn과 audit는 Deployment 같은 워크로드 리소스에도 경고를 보여주지만, enforce는 최종적으로 생성되는 Pod에 적용됩니다. 이 차이를 알고 있어야 배포 시 메시지를 덜 헷갈립니다.

    1. enforce: 정책을 어기면 Pod 생성이 거부됩니다.
    2. audit: 생성은 허용되지만 감사 로그에 위반 정보가 남습니다.
    3. warn: 생성은 허용되지만 클라이언트에 경고가 표시됩니다.

    실무에서 자주 하는 실수가 바로 여기입니다. 바로 enforce부터 켜버리면 그동안 관성적으로 돌아가던 워크로드가 한꺼번에 실패할 수 있거든요. 저도 테스트 환경에서 ingress controller가 안 떠서 꽤 오래 본 적이 있습니다. 알고 보니 hostNetwork와 securityContext 설정이 기준에 안 맞았더라고요. 이런 경험을 한 번 하면 warn, audit부터 가야 하는 이유가 확실히 보입니다.

    전환 전 체크리스트

    • 현재 클러스터에서 PSP를 실제로 쓰는 네임스페이스가 어디인지 확인합니다.
    • 권한 상승 허용 여부를 점검합니다.
    • root 실행, hostPath 마운트, hostNetwork 사용 여부를 확인합니다.
    • DaemonSet, CSI, 모니터링 에이전트처럼 예외가 필요한 워크로드를 분리합니다.
    • 애플리케이션 네임스페이스와 시스템 네임스페이스를 같은 기준으로 묶지 않습니다.

    실전 구현: Pod Security Standards 마이그레이션 순서

    여기서는 제가 실제로 권장하는 순서를 정리해보겠습니다. 핵심은 단순합니다. 관찰 먼저, 강제는 나중. 이 순서만 지켜도 사고 확률이 확실히 줄어듭니다.

    1. 현재 네임스페이스 라벨 상태 확인

    kubectl get ns --show-labels

    기존에 이미 pod-security 관련 라벨이 붙어 있는지 먼저 확인합니다. 다른 운영자가 먼저 설정해둔 경우도 생각보다 자주 있습니다. 이 상태를 모르고 덮어쓰면 원인 파악이 더 어려워집니다.

    2. 먼저 warn, audit 모드로 Baseline 적용

    kubectl label ns my-app \
      pod-security.kubernetes.io/warn=baseline \
      pod-security.kubernetes.io/audit=baseline \
      --overwrite

    처음부터 Restricted로 가고 싶어도 일단 Baseline부터 보는 편이 안전합니다. 여기서 어떤 워크로드가 경고를 내는지 파악해야 다음 단계가 보입니다. 이 단계가 생각보다 중요하더라고요.

    3. 버전 라벨도 함께 명시해 드리프트 줄이기

    kubectl label ns my-app \
      pod-security.kubernetes.io/warn-version=v1.36 \
      pod-security.kubernetes.io/audit-version=v1.36 \
      --overwrite

    버전 라벨은 선택 사항이지만 운영에선 거의 필수에 가깝습니다. PSS 해석 기준은 Kubernetes 마이너 버전에 따라 달라질 수 있어서, 테스트와 운영 클러스터의 결과가 어긋나는 일을 줄여주거든요. 다만 예시 값을 그대로 복붙하기보다는 현재 클러스터의 마이너 버전에 맞춰 적는 게 맞습니다.

    4. 경고 메시지로 문제 워크로드 찾기

    kubectl apply -f deployment.yaml

    배포 시 warn 메시지가 뜨면 그 내용을 기준으로 수정합니다. warn과 audit는 Deployment 같은 워크로드 리소스에도 적용되기 때문에, 이 단계에서 꽤 많은 힌트를 얻을 수 있습니다. 흔히 손보게 되는 포인트는 아래와 같습니다.

    • runAsNonRoot: non-root 실행 강제
    • allowPrivilegeEscalation: false 설정
    • seccompProfile: RuntimeDefault 적용
    • capabilities: 불필요한 Linux capability 제거
    Pod Security Admission과 네임스페이스 라벨 기반 PSS 적용 방법을 설명하는 이미지

    네임스페이스에 warn, audit, enforce 라벨을 붙이는 방식과 적용 흐름을 보여주는 이미지입니다.

    5. 매니페스트 보안 컨텍스트 정리

    아래 예시는 Restricted로 가기 전에 가장 자주 쓰는 기본 골격입니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: web-app
      namespace: my-app
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: web-app
      template:
        metadata:
          labels:
            app: web-app
        spec:
          securityContext:
            runAsNonRoot: true
            seccompProfile:
              type: RuntimeDefault
          containers:
            - name: web-app
              image: nginx:stable
              securityContext:
                allowPrivilegeEscalation: false
                capabilities:
                  drop:
                    - ALL
              ports:
                - containerPort: 80

    이 예시는 아주 화려하지는 않지만, 실제로 많이 걸리는 조건을 담고 있습니다. 특히 runAsNonRoot는 이미지 자체가 non-root 실행을 지원해야 제대로 동작합니다. 오래된 이미지라면 매니페스트만 고쳐서는 해결이 안 되고, 컨테이너 이미지 베이스부터 손봐야 할 때가 있습니다.

    6. 문제 없으면 enforce 전환

    kubectl label ns my-app \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=v1.36 \
      --overwrite

    여기서도 한 번에 모든 네임스페이스를 바꾸는 건 추천하지 않습니다. 서비스 영향이 낮은 곳부터 묶어서 적용하고, 모니터링으로 이상이 없는지 확인한 뒤 확대하는 방식이 제일 덜 아픕니다. 이거 진짜 차이가 큽니다.

    Restricted까지 가려면 어떤 점을 더 봐야 하나

    Pod Security Standards 마이그레이션을 시작하면 많은 팀이 최종 목표를 Restricted로 잡습니다. 방향은 맞습니다. 다만 현실적으로 모든 워크로드가 바로 들어가지는 않더라고요. 특히 아래 항목은 자주 발목을 잡습니다.

    1. 레거시 이미지가 root 실행을 전제로 만들어져 있음
    2. initContainer에서 과한 권한을 사용함
    3. 모니터링, 스토리지, 네트워크 에이전트가 호스트 접근을 필요로 함
    4. hostPath 사용이 습관처럼 들어가 있음

    그래서 저는 보통 네임스페이스를 이렇게 나눕니다.

    네임스페이스 유형 권장 레벨 비고
    일반 애플리케이션 Baseline 또는 Restricted 신규 서비스는 Restricted 목표
    운영 도구 Baseline 필요 권한 확인 후 점진 강화
    시스템 워크로드 Privileged 또는 별도 예외 관리 CNI, CSI, 일부 에이전트 주의

    트러블슈팅: Pod Security Standards 마이그레이션에서 자주 막히는 지점

    문서만 보면 금방 끝날 것 같지만, 운영 환경에 붙이면 여기서 한 번씩 다 막힙니다. 저도 몇 번은 꽤 돌아갔습니다. 그래서 아래 포인트는 미리 보고 들어가는 편이 좋습니다.

    1. 배포가 갑자기 거부될 때

    enforce가 켜진 네임스페이스에 정책 위반 Pod가 들어가면 바로 거부됩니다. 이 경우 이벤트와 매니페스트를 같이 봐야 합니다.

    kubectl describe pod <pod-name> -n my-app
    kubectl get events -n my-app --sort-by=.lastTimestamp

    경고 문구만 보고 대충 고치면 같은 문제를 반복하기 쉽습니다. 어떤 필드가 위반인지 정확히 확인하고, Pod 템플릿 기준으로 수정해야 합니다.

    2. 시스템 네임스페이스까지 묶어서 막힐 때

    이 부분은 정말 조심해야 합니다. kube-system 같은 곳에 일반 앱과 같은 Restricted를 걸면 예상치 못한 장애가 날 수 있습니다. 네트워크 플러그인, 스토리지 드라이버, 노드 에이전트가 영향을 받는 경우가 대표적입니다. 실제 운영에선 시스템성 워크로드를 별도 기준으로 다루는 게 훨씬 안전합니다.

    3. warn은 뜨는데 배포가 되니까 그냥 넘길 때

    처음엔 “배포는 되네” 하고 넘기기 쉽습니다. 그런데 나중에 enforce로 바꾸는 순간 한꺼번에 터집니다. 그래서 warn 단계에서 바로 티켓을 만들고, 팀별 수정 일정을 잡아두는 편이 훨씬 낫습니다.

    4. PSP 기능과 1:1 대응을 기대할 때

    PSP 대체라고 해서 완전히 같은 경험을 기대하면 실망할 수 있습니다. PSS는 표준 보안 레벨을 제공하는 방식이고, 세밀한 조직별 규칙은 별도 정책 도구가 더 적합합니다. 이 차이를 이해하고 설계해야 전환 이후 불만도 줄어듭니다.

    검증: Pod Security Admission이 제대로 동작하는지 확인하는 방법

    적용 후에는 라벨만 붙였다고 끝이 아닙니다. 검증이 꼭 필요합니다. 제가 보통 확인하는 항목은 아래와 같습니다.

    1. 네임스페이스 라벨이 의도한 값으로 들어갔는지 확인
    2. warn 메시지가 줄거나 사라졌는지 확인
    3. 문제 Pod 없이 롤링 업데이트가 완료되는지 확인
    4. 시스템 워크로드가 영향 없이 유지되는지 확인
    kubectl get ns my-app --show-labels
    kubectl rollout status deploy/web-app -n my-app
    kubectl get pods -n my-app

    조금 더 확실히 보려면 의도적으로 정책 위반 Pod를 하나 던져보는 것도 방법입니다. 다만 이 테스트는 운영 네임스페이스가 아니라 테스트 네임스페이스에서만 하는 편이 안전합니다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: pss-test
      namespace: my-app
    spec:
      hostNetwork: true
      containers:
        - name: busybox
          image: busybox:1.36
          command: ["sh", "-c", "sleep 3600"]
          securityContext:
            allowPrivilegeEscalation: true

    Baseline이나 Restricted enforce가 걸려 있다면 이런 Pod는 거부되는 게 정상입니다. hostNetwork는 Baseline부터 금지되고, allowPrivilegeEscalation은 Restricted에서 금지되기 때문에 테스트용으로 확인하기 좋습니다.

    Pod Security Standards 마이그레이션 검증 결과를 보여주는 Kubernetes 보안 이미지

    정책 위반 테스트와 정상 배포가 어떻게 다르게 보이는지 검증 결과를 시각화한 이미지입니다.

    운영 팁: 네임스페이스 전략을 먼저 정하면 훨씬 편합니다

    제가 여러 번 해보니, PSS 적용 방법에서 제일 중요한 건 YAML 문법보다 운영 기준이었습니다. 다시 말해 “어떤 네임스페이스를 어떤 신뢰 수준으로 볼 것인가”를 먼저 정해야 합니다. 이 기준이 없으면 팀마다 예외 요청이 쌓이고, 결국 정책이 누더기가 되더라고요.

    • 신규 서비스는 기본적으로 Baseline 또는 Restricted로 시작합니다.
    • 예외가 필요한 시스템성 워크로드는 별도 네임스페이스로 분리합니다.
    • warn과 audit 결과를 주기적으로 점검해 enforce 후보를 올립니다.
    • 정책 예외는 문서화하고 만료 시점을 정합니다.

    이런 방식으로 가면 Kubernetes 보안이 특정 담당자 감에 의존하지 않고 팀 표준으로 자리 잡습니다. 이전에 정리한 RBAC 글이 있다면 같이 연결해두는 것도 좋습니다. 내부 링크 흐름이 생기면 독자 입장에서도 이해가 더 빨라지고, SEO 측면에서도 분명 도움이 됩니다.

    정리: PSP에서 PSS로 갈아탈 때 기억할 것

    오늘 내용을 짧게 정리하면 이렇습니다. Pod Security Standards 마이그레이션은 단순한 기능 교체가 아니라, 보안 운영 방식을 표준화하는 작업입니다. PSP보다 세밀한 제어는 줄었지만, 네임스페이스 단위로 기준을 맞추기 쉬워졌고, Pod Security Admission으로 warn, audit, enforce를 단계적으로 운영할 수 있게 됐습니다.

    저도 처음엔 이 구조가 너무 단순한 것 아닌가 싶었습니다. 그런데 실제로는 팀 단위 운영에 더 잘 맞더라고요. 다만 무작정 enforce부터 걸면 안 됩니다. 꼭 warn, audit으로 현재 상태를 먼저 파악하고, Restricted는 목표로 두되 시스템 워크로드 예외를 현실적으로 분리해 가져가면 훨씬 수월합니다.

    지금 클러스터 업그레이드를 앞두고 있다면 이번 주에라도 테스트 네임스페이스 하나 만들어 Baseline warn부터 붙여보세요. 생각보다 빨리 문제가 보입니다. 그리고 이전에 다뤘던 RBAC 정리 글도 함께 보면 흐름이 더 잘 잡힐 겁니다. 다음 글에서는 Kyverno나 Gatekeeper로 PSS의 빈틈을 어떻게 보완하는지도 이어서 정리해보겠습니다.

    PSP 대체와 Pod Security Standards 보안 레벨 비교를 정리한 인포그래픽 이미지

    PSP와 PSS의 차이, 적용 순서, 운영 체크포인트를 한 장으로 정리한 요약 인포그래픽입니다.

    자주 묻는 질문

    PSS만으로 PSP를 완전히 대체할 수 있나요?

    기본적인 표준화 목적에는 충분한 경우가 많습니다. 다만 세밀한 조건 제어나 예외 검증이 많다면 Kyverno나 Gatekeeper 같은 정책 엔진을 함께 검토하는 편이 좋습니다.

    처음부터 Restricted로 가도 되나요?

    신규 서비스만 있고 이미지와 매니페스트를 처음부터 통제할 수 있는 환경이라면 가능합니다. 하지만 기존 워크로드가 있다면 Baseline warn, audit부터 시작하는 편이 훨씬 안전합니다.

    Pod Security Admission은 어디에 적용되나요?

    네임스페이스 라벨 기준으로 해당 네임스페이스의 Pod 생성에 영향을 줍니다. 또 warn과 audit는 Deployment 같은 워크로드 리소스에도 힌트를 주기 때문에, 네임스페이스 전략과 배포 검증 루틴을 함께 가져가는 게 중요합니다.

  • [K8s] Pod Security Admission 1년 사용 후기 및 실수담

    [K8s] Pod Security Admission 1년 사용 후기 및 실수담

    K8s Pod Security Admission, 1년 사용 후기 및 실수담

    안녕하세요, 13년차 서버실 주인장입니다. 오늘은 Kubernetes(이하 K8s) 환경에서 보안을 한층 강화해주는 Pod Security Admission(PSA)에 대해 이야기해보려 합니다. 제가 운영하는 홈랩 환경에서 PSA를 도입한 지 어느덧 1년이 되었는데요, 처음에는 조금 헷갈렸지만 지금은 없어서는 안 될 기능이 되었답니다. 오늘은 1년 동안 PSA를 사용하면서 겪었던 시행착오와 함께, 어떻게 하면 좀 더 효율적으로 PSA를 활용할 수 있을지에 대한 경험을 솔직하게 공유해 드릴게요. 혹시 K8s 보안에 대해 고민하고 계셨다면, 오늘 제 이야기가 작게나마 도움이 되기를 바랍니다. 😊

    Kubernetes 클러스터의 전반적인 아키텍처와 Pod Security Admission이 어떤 위치에 있는지 보여주는 다이어그램

    K8s Pod Security Admission, 왜 필요할까요?

    K8s는 유연하고 강력한 컨테이너 오케스트레이션 도구이지만, 그만큼 보안 설정에 신경 써야 할 부분이 많습니다. 특히 Pod(파드)는 K8s의 가장 기본적인 배포 단위인데요, 이 Pod가 실행될 때 어떤 보안 정책을 따라야 하는지 명확하게 제어하지 않으면 예상치 못한 보안 취약점이 발생할 수 있습니다. 예를 들어, 특권(privileged) 컨테이너가 실수로 실행되거나, 호스트 파일 시스템에 접근하는 등의 위험한 상황이 생길 수 있죠.

    이런 문제를 해결하기 위해 K8s에서는 Pod Security Admission(PSA)이라는 기능을 제공합니다. 쉽게 말해, Pod를 생성하거나 업데이트할 때 미리 정의된 보안 기준에 부합하는지 자동으로 검사하고, 기준에 맞지 않으면 해당 Pod의 생성을 막아주는(Admission Controller) 기능이라고 생각하시면 됩니다. 💡

    Pod Security Admission, 어떻게 작동하나요?

    PSA는 K8s API 서버의 Admission Controller 중 하나로 작동합니다. Pod 생성 요청이 API 서버에 전달되면, PSA는 해당 Pod가 특정 Pod Security Standards(PSS)를 준수하는지 확인합니다. PSS에는 크게 세 가지 레벨이 있습니다:

    • Restricted (제한): 가장 엄격한 보안. Pod는 격리되고 제한된 권한만 가지며, 최신 보안 기능(예: Seccomp, AppArmor, SELinux)을 사용해야 합니다.
    • Baseline (기준): 최소한의 보안을 보장. 알려진 취약점을 악용하기 어려운 수준으로, 대부분의 워크로드에 권장됩니다.
    • Privileged (특권): 가장 낮은 수준의 보안. 컨테이너가 호스트 시스템에 대한 거의 모든 권한을 갖게 되므로, 매우 신중하게 사용해야 합니다.

    이 세 가지 레벨 외에도, 각 네임스페이스(Namespace)별로 Enforce, Audit, Warn 모드를 설정할 수 있습니다.

    • 1. Enforce (강제): 보안 정책을 위반하는 Pod의 생성을 차단합니다. 가장 강력한 모드죠.
    • 2. Audit (감사): 정책을 위반하더라도 Pod 생성을 차단하지는 않지만, 해당 이벤트를 로그로 기록합니다. 보안 감사에 유용합니다.
    • 3. Warn (경고): 정책을 위반하는 Pod에 대해 사용자에게 경고 메시지를 보냅니다. Pod 생성은 허용됩니다.

    실전! Pod Security Admission 설정하기 (제 경험담)

    제가 처음 PSA를 도입할 때 가장 헷갈렸던 부분이 바로 이 네임스페이스별 정책 설정이었습니다. 저희 홈랩에는 개발용, 테스트용, 운영용 등 다양한 환경의 네임스페이스가 있었기 때문에, 각기 다른 보안 정책을 적용하고 싶었거든요.

    K8s 클러스터 전체에 PSA를 활성화하는 것은 API 서버 설정에서 간단히 할 수 있습니다. 하지만 실제로는 각 네임스페이스별로 정책을 세밀하게 조정하는 것이 중요합니다. 저는 다음과 같은 방식으로 설정을 진행했습니다.

    1단계: 네임스페이스별 레이블 설정

    먼저, 각 네임스페이스에 어떤 보안 정책을 적용할지 명시하는 레이블을 붙여줍니다. 예를 들어, 보안이 중요한 운영 네임스페이스에는 pod-security.kubernetes.io/enforce=baseline과 같이 설정할 수 있습니다.

    kubectl label --overwrite ns production pod-security.kubernetes.io/enforce=baseline
    kubectl label --overwrite ns production pod-security.kubernetes.io/audit=baseline
    kubectl label --overwrite ns production pod-security.kubernetes.io/warn=baseline
    
    kubectl label --overwrite ns development pod-security.kubernetes.io/enforce=privileged
    kubectl label --overwrite ns development pod-security.kubernetes.io/audit=baseline
    kubectl label --overwrite ns development pod-security.kubernetes.io/warn=baseline
    
    네임스페이스별 Pod Security Admission 정책 설정 예시

    각 네임스페이스에 할당된 Pod Security Standards 레벨과 모드를 보여주는 예시 화면

    2단계: PSA 동작 확인 (Audit 모드 활용)

    처음부터 enforce 모드로 설정하면, 예상치 못한 Pod 배포 실패로 인해 서비스 장애가 발생할 수 있습니다. 그래서 저는 먼저 audit 모드를 활용하여 어떤 Pod들이 보안 정책을 위반하는지 파악하는 단계를 거쳤습니다. K8s API 서버 감시 로그를 주기적으로 확인하면서, 어떤 Pod가 어떤 이유로 위반되는지 분석했죠.

    💡 팁: kube-apiserver의 감시 로그(audit log)를 통해 PSA 관련 감사 정보를 확인할 수 있습니다. 로컬 클러스터에서는 kubectl describe nodes 또는 클러스터 설정에 따라 로그 파일에서 감시 로그를 조회할 수 있습니다.

    3단계: Enforce 모드로 전환 및 검증

    Audit 모드를 통해 문제를 파악하고 수정했다면, 이제 enforce 모드로 전환할 차례입니다. enforce 모드로 전환한 후, 중요한 워크로드부터 순차적으로 배포해보면서 문제가 없는지 꼼꼼하게 검증했습니다.

    kubectl label --overwrite ns production pod-security.kubernetes.io/enforce=baseline
    

    Pod Security Admission 정책을 위반했을 때 API 서버 로그에 기록되는 내용

    1년간 사용해보니… 삽질과 깨달음

    PSA를 1년 가까이 사용하면서 정말 많은 것을 배웠습니다. 처음에는 단순히 PSS 레벨을 설정하면 되는 줄 알았는데, 실제로는 Pod의 설정 하나하나가 PSS와 충돌할 수 있다는 것을 깨달았죠.

    가장 흔하게 겪었던 실수담은 바로 privileged: true 설정이었습니다. 저희 팀에서 사용하는 특정 사이드카(Sidecar) 컨테이너가 네트워킹 관련 기능을 위해 이 설정을 필요로 했는데, restricted 또는 baseline PSS에서는 당연히 막히더라고요. 처음에는 왜 Pod 생성이 안 되는지 한참을 헤맸습니다. 😂

    이런 문제를 해결하기 위해 다음과 같은 방법을 사용했습니다:

    • Pod Security Admission 이해하기: Pod Security Policies(PSP)는 K8s 1.25에서 deprecated 되었고 1.28에서 완전히 제거되었습니다. 대신 PSA로 전환하는 과정에서, PSA는 훨씬 간결하고 명확하게 정책을 적용할 수 있다는 걸 체감했습니다.
    • Pod Spec 검토 및 수정: 사이드카 컨테이너의 Pod Spec을 면밀히 검토하여, privileged: true 설정 없이도 동일한 기능을 구현할 수 있는지 확인했습니다. 결국, 필요한 권한만 최소한으로 부여하도록 수정하여 baseline PSS를 만족시킬 수 있었습니다.
    • Audit 모드를 적극 활용: 새로운 애플리케이션을 배포하거나 기존 설정을 변경할 때, 바로 enforce 모드로 전환하기보다는 audit 모드로 먼저 테스트하면서 잠재적인 충돌을 미리 파악하는 습관을 들였습니다.

    PSA는 K8s 클러스터의 보안 수준을 크게 향상시켜주는 강력한 기능입니다. 하지만 모든 Pod가 완벽하게 PSS를 준수하는 것은 아니므로, Service Account, RBAC(Role-Based Access Control)와 함께 종합적으로 고려하여 보안 전략을 수립하는 것이 중요합니다.

    Pod Security Admission과 Pod Security Policy의 주요 차이점 비교

    Pod Security Admission과 이전의 Pod Security Policy의 주요 차이점을 비교한 표

    결론: K8s 보안, PSA로 한 단계 업그레이드하세요!

    Pod Security Admission은 K8s 환경의 보안을 강화하는 데 필수적인 요소입니다. 1년간의 경험을 통해 PSA가 단순히 복잡한 설정이 아니라, 실질적인 보안 위협을 줄여주는 강력한 방어선이라는 것을 체감했습니다.

    물론 초기 설정 과정에서 시행착오를 겪을 수 있습니다. 하지만 audit 모드를 활용하고, Pod Spec을 꼼꼼히 검토하는 습관을 들인다면, 충분히 안정적으로 PSA를 운영할 수 있더라고요.

    혹시 아직 PSA를 도입하지 않으셨다면, 지금 바로 시작해보시는 것을 강력히 추천합니다! 먼저 개발 네임스페이스부터 baseline PSS로 설정하고 audit 모드로 운영해보면서 점차 확대해나가는 것을 권장합니다.

    다음 글에서는 K8s 네트워크 보안의 또 다른 핵심 요소인 Network Policy에 대해 다뤄볼 예정이니, 많은 기대 부탁드립니다! 😉

  • [k8s] 쿠버네티스 보안 강화: 베스트 프랙티스 가이드

    쿠버네티스 보안, 왜 지금 더 중요해졌나요?

    솔직히 말씀드리면, 저도 처음 쿠버네티스를 도입했을 때는 “일단 돌아가면 됐지”라는 마음이 컸어요. Pod가 뜨고, 서비스가 노출되고, 배포 파이프라인이 연결되면 그걸로 충분하다고 생각했거든요. 근데 운영 2년차쯤 됐을 때 실제로 클러스터 하나가 크립토마이닝 공격에 당하는 걸 목격하고 나서… 그때부터 쿠버네티스 보안을 진지하게 공부하기 시작했습니다.

    최근 들어 클라우드 네이티브 환경이 보편화되면서 쿠버네티스(Kubernetes) 클러스터를 겨냥한 공격도 눈에 띄게 늘었어요. 잘못 설정된 RBAC(Role-Based Access Control, 역할 기반 접근 제어), 노출된 API 서버, 권한 과잉 서비스 계정… 이런 것들이 실제로 침해 사고로 이어지는 경우가 많더라고요.

    이 글에서는 제가 13년간 인프라를 운영하면서 직접 적용해보고, 실제로 효과를 확인한 쿠버네티스 보안 베스트 프랙티스를 정리해드릴게요. 개념만 나열하는 게 아니라 실제 적용 가능한 YAML 설정과 명령어도 함께 다룰 겁니다.

    ▲ 쿠버네티스 클러스터의 주요 보안 레이어 — API 서버, RBAC, 네트워크 정책, Pod 보안이 어떻게 계층적으로 동작하는지 보여주는 아키텍처 다이어그램


    쿠버네티스 보안의 핵심 개념 먼저 짚고 가기

    쿠버네티스 보안은 크게 4가지 영역으로 나뉘어요. 흔히 “4C 보안 모델”이라고 부르는데, 쉽게 말해 Cloud → Cluster → Container → Code 순서로 각 레이어를 지켜야 한다는 개념이에요.

    레이어 설명 주요 관리 포인트
    Cloud 클라우드 인프라 수준 보안 IAM, VPC, 방화벽, 노드 OS 보안
    Cluster 쿠버네티스 클러스터 수준 보안 API 서버 접근 제어, RBAC, Audit 로깅
    Container 컨테이너 런타임 수준 보안 이미지 취약점 스캔, securityContext 설정
    Code 애플리케이션 코드 수준 보안 시크릿 관리, TLS 통신, 의존성 취약점

    이 중에서 Cluster와 Container 레이어가 실제로 가장 많이 놓치는 부분이에요. Cloud는 CSP(클라우드 서비스 제공자)가 어느 정도 기본 보호를 해주고, Code는 개발팀이 챙기는 경우가 많은데, 정작 클러스터 내부 설정은 인프라팀도 개발팀도 서로 남의 일이라고 생각하는 경우가 많거든요. 제가 딱 그랬었어요 ㅎㅎ.


    RBAC 설정: 최소 권한 원칙을 철저히 지켜라

    RBAC(Role-Based Access Control)는 쿠버네티스 보안의 핵심 중 핵심입니다. “필요한 권한만, 필요한 리소스에만”이라는 최소 권한 원칙(Principle of Least Privilege)을 RBAC로 구현하는 거예요.

    흔히 보이는 실수가 뭔지 아세요? 빠르게 개발하려고 ServiceAccount에 cluster-admin 권한을 줘버리는 거예요. 저도 초반에 “일단 개발 환경이니까”라며 그냥 넘어간 적이 있는데, 그 습관이 프로덕션까지 이어지면 진짜 큰일 납니다.

    나쁜 예시 — 절대 이렇게 하지 마세요

    # ⚠️ 절대 금지: 모든 권한을 서비스 계정에 부여
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: dangerous-binding
    subjects:
    - kind: ServiceAccount
      name: my-app
      namespace: default
    roleRef:
      kind: ClusterRole
      name: cluster-admin  # 이거 쓰면 안 됩니다!
      apiGroup: rbac.authorization.k8s.io
    

    좋은 예시 — 최소 권한으로 Role 정의하기

    # ✅ 권장: 특정 네임스페이스의 특정 리소스에만 접근 허용
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      namespace: production
      name: pod-reader
    rules:
    - apiGroups: [""]  # core API 그룹
      resources: ["pods", "pods/log"]
      verbs: ["get", "list", "watch"]  # 읽기 전용만 허용
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: read-pods
      namespace: production
    subjects:
    - kind: ServiceAccount
      name: monitoring-agent
      namespace: production
    roleRef:
      kind: Role
      name: pod-reader
      apiGroup: rbac.authorization.k8s.io
    

    💡 팁: 현재 클러스터에서 과도한 권한이 있는 바인딩을 빠르게 확인하려면 아래 명령어를 써보세요.

    # cluster-admin 권한을 가진 바인딩 전체 조회
    kubectl get clusterrolebindings -o json | \
      jq '.items[] | select(.roleRef.name == "cluster-admin") | .metadata.name'
    
    # 특정 서비스 계정의 권한 확인
    kubectl auth can-i --list --as=system:serviceaccount:production:my-app
    

    네트워크 정책으로 Pod 간 통신 제어하기

    기본적으로 쿠버네티스 클러스터 내 모든 Pod는 서로 통신할 수 있어요. 이게 편리하긴 한데, 보안 관점에서는 꽤 위험한 기본값이에요. 만약 하나의 Pod가 침해당하면 클러스터 내 모든 서비스로 횡적 이동(Lateral Movement)이 가능하거든요.

    NetworkPolicy(네트워크 정책)는 Pod 간 또는 Pod와 외부 간의 트래픽을 화이트리스트 방식으로 제어하는 k8s 리소스예요. 처음엔 이게 뭔가 복잡해 보였는데, 개념만 잡으면 생각보다 직관적이더라고요.

    기본 원칙: 먼저 모든 트래픽 차단 후 필요한 것만 열기

    # 1단계: 특정 네임스페이스의 모든 인바운드/아웃바운드 트래픽 차단
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: default-deny-all
      namespace: production
    spec:
      podSelector: {}  # 네임스페이스 내 모든 Pod에 적용
      policyTypes:
      - Ingress
      - Egress
    
    # 2단계: 필요한 트래픽만 허용 (예: frontend -> backend 통신)
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: allow-frontend-to-backend
      namespace: production
    spec:
      podSelector:
        matchLabels:
          app: backend  # 이 정책이 적용될 Pod
      policyTypes:
      - Ingress
      ingress:
      - from:
        - podSelector:
            matchLabels:
              app: frontend  # frontend Pod에서 오는 트래픽만 허용
        ports:
        - protocol: TCP
          port: 8080
    

    ⚠️ 주의사항: NetworkPolicy는 CNI(Container Network Interface) 플러그인이 지원해야 동작해요. Calico, Cilium, Weave Net 등은 지원하지만, 기본 flannel은 NetworkPolicy를 지원하지 않아요. 클러스터 구성 전에 꼭 확인하세요!

    ▲ NetworkPolicy 적용 전후 Pod 간 트래픽 흐름 비교 — 기본 개방 상태와 화이트리스트 방식 적용 후 허용된 경로만 표시된 다이어그램


    컨테이너 보안 컨텍스트 설정하기

    컨테이너 보안에서 제일 많이 놓치는 부분이 바로 SecurityContext(시큐리티 컨텍스트)예요. 이 설정 하나만 제대로 해도 컨테이너 탈출(Container Escape) 공격을 상당 부분 방어할 수 있거든요.

    제가 보안 감사를 해드린 한 팀의 클러스터를 보니까, 거의 모든 Pod가 root 권한으로 실행되고 있었어요. 개발 편의를 위해 그렇게 설정했다고 하는데, 그건 컨테이너 안에서 root 권한을 얻은 공격자가 호스트 노드까지 공격할 수 있는 발판을 주는 거나 마찬가지예요.

    apiVersion: v1
    kind: Pod
    metadata:
      name: secure-pod
      namespace: production
    spec:
      securityContext:
        runAsNonRoot: true          # root로 실행 금지
        runAsUser: 1000             # 특정 UID로 실행
        runAsGroup: 3000            # 특정 GID로 실행
        fsGroup: 2000               # 볼륨 파일시스템 그룹
        seccompProfile:
          type: RuntimeDefault      # 기본 seccomp 프로파일 적용
      containers:
      - name: app
        image: my-app:1.0.0
        securityContext:
          allowPrivilegeEscalation: false  # 권한 상승 금지 (핵심!)
          readOnlyRootFilesystem: true     # 루트 파일시스템 읽기 전용
          capabilities:
            drop:
            - ALL                          # 모든 Linux 기능(capability) 제거
            add:
            - NET_BIND_SERVICE             # 필요한 것만 다시 추가
        resources:
          limits:
            cpu: "500m"
            memory: "128Mi"
          requests:
            cpu: "250m"
            memory: "64Mi"
    

    💡 핵심 포인트! allowPrivilegeEscalation: false는 반드시 설정하세요. 이 설정이 없으면 컨테이너 내부에서 sudo나 setuid 바이너리를 통해 권한 상승이 가능해요.

    Pod Security Admission으로 클러스터 전체에 정책 적용하기

    쿠버네티스 1.25부터 기존의 PodSecurityPolicy(PSP)가 제거되고 Pod Security Admission(PSA)이 정식 출시됐어요. 네임스페이스 레벨에서 보안 프로파일을 강제할 수 있는데, 세 가지 레벨이 있어요.

    레벨 설명 적용 권장 환경
    privileged 제한 없음 (기본값) 시스템 네임스페이스 (kube-system 등)
    baseline 최소한의 보안 제한 적용 일반 애플리케이션 네임스페이스
    restricted 가장 강력한 보안 제한 보안이 중요한 프로덕션 환경
    # 네임스페이스에 PSA 레벨 적용 (enforce: 위반 시 Pod 생성 거부)
    kubectl label namespace production \
      pod-security.kubernetes.io/enforce=restricted \
      pod-security.kubernetes.io/enforce-version=latest
    
    # warn: 경고만 (생성은 허용), audit: 감사 로그만 기록
    kubectl label namespace staging \
      pod-security.kubernetes.io/warn=baseline \
      pod-security.kubernetes.io/audit=restricted
    

    시크릿 관리와 etcd 암호화

    이건 진짜 많은 분들이 모르고 지나치는 부분인데요. 쿠버네티스 Secret은 기본적으로 암호화되지 않은 채로 etcd에 저장돼요. Base64 인코딩이 되어 있긴 하지만, 이건 암호화가 아니라 그냥 인코딩이에요. etcd에 직접 접근하면 바로 읽을 수 있거든요.

    # etcd에서 시크릿 평문 확인 (이게 되면 안 되는 거예요!)
    ETCDCTL_API=3 etcdctl get /registry/secrets/default/my-secret \
      --endpoints=https://127.0.0.1:2379 \
      --cacert=/etc/kubernetes/pki/etcd/ca.crt \
      --cert=/etc/kubernetes/pki/etcd/server.crt \
      --key=/etc/kubernetes/pki/etcd/server.key | hexdump -C
    

    etcd 암호화 설정하기

    # /etc/kubernetes/encryption-config.yaml
    apiVersion: apiserver.config.k8s.io/v1
    kind: EncryptionConfiguration
    resources:
    - resources:
      - secrets
      - configmaps  # configmap도 같이 암호화 권장
      providers:
      - aescbc:     # AES-CBC 암호화 방식
          keys:
          - name: key1
            secret: 
      - identity: {}  # 암호화 안 된 기존 데이터 읽기 위해 fallback
    
    # kube-apiserver에 암호화 설정 파일 적용 (kubeadm 기준)
    # /etc/kubernetes/manifests/kube-apiserver.yaml에 아래 플래그 추가
    # --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
    
    # 기존 시크릿 모두 재암호화 (설정 후 반드시 실행!)
    kubectl get secrets --all-namespaces -o json | kubectl replace -f -
    

    ⚠️ 중요: 암호화 키는 반드시 안전하게 보관하세요. 키를 잃어버리면 모든 시크릿을 복구할 수 없어요. 실제 프로덕션에서는 AWS KMS, HashiCorp Vault, Azure Key Vault 같은 외부 키 관리 서비스(KMS)와 연동하는 걸 강력히 권장합니다.


    감사 로깅과 런타임 보안 모니터링

    보안은 예방도 중요하지만, 이상 행동을 빠르게 탐지하는 것도 똑같이 중요해요. Audit Logging(감사 로깅)을 설정하면 API 서버에 대한 모든 요청을 기록할 수 있어요.

    # /etc/kubernetes/audit-policy.yaml
    apiVersion: audit.k8s.io/v1
    kind: Policy
    rules:
    # 시크릿 접근은 반드시 RequestResponse 레벨로 상세 기록
    - level: RequestResponse
      resources:
      - group: ""
        resources: ["secrets"]
    
    # 인증 실패 이벤트 기록
    - level: Request
      users: ["system:anonymous"]
    
    # exec 명령어 실행 기록 (공격자가 주로 사용)
    - level: RequestResponse
      resources:
      - group: ""
        resources: ["pods/exec", "pods/attach", "pods/portforward"]
    
    # 그 외 일반 요청은 메타데이터만 기록
    - level: Metadata
      omitStages:
      - RequestReceived
    

    런타임 보안 모니터링 도구로는 Falco를 많이 사용해요. Falco는 CNCF(Cloud Native Computing Foundation) 프로젝트로, 커널 시스템 콜을 분석해서 컨테이너 내부의 이상 행동을 실시간으로 탐지해줘요.

    # Helm으로 Falco 설치
    helm repo add falcosecurity https://falcosecurity.github.io/charts
    helm repo update
    helm install falco falcosecurity/falco \
      --namespace falco \
      --create-namespace \
      --set falco.grpc.enabled=true \
      --set falco.grpc_output.enabled=true
    
    # Falco 로그 확인 (이상 탐지 이벤트)
    kubectl logs -l app.kubernetes.io/name=falco -n falco -f
    

    ▲ Falco 런타임 보안 모니터링 — 컨테이너 내부의 의심스러운 시스템 콜과 이상 행동이 실시간으로 탐지되는 대시보드 예시


    이미지 보안: 공급망 공격 방어

    최근 몇 년간 컨테이너 이미지를 통한 공급망 공격이 크게 늘었어요. 신뢰할 수 없는 이미지를 클러스터에 배포하면 그 자체가 침해의 시작이 될 수 있거든요.

    • ✅ 이미지 취약점 스캔: Trivy, Grype 같은 도구로 CI/CD 파이프라인에서 이미지 스캔 필수화
    • ✅ 이미지 서명 검증: Cosign + Sigstore로 이미지 서명 및 검증 (공급망 무결성 보장)
    • ✅ 신뢰할 수 있는 레지스트리만 허용: Admission Webhook으로 허용된 레지스트리 이미지만 배포 가능하도록 제한
    • ✅ latest 태그 금지: 이미지 버전을 명확하게 고정 (mutable tag는 보안 위험)
    • ✅ distroless 또는 minimal 이미지 사용: 불필요한 바이너리가 없는 이미지를 쓰면 공격 표면 자체가 줄어요
    # Trivy로 이미지 취약점 스캔
    trivy image --severity HIGH,CRITICAL nginx:1.25.3
    
    # Cosign으로 이미지 서명
    cosign sign --key cosign.key my-registry.io/my-app:1.0.0
    
    # Cosign으로 이미지 서명 검증
    cosign verify --key cosign.pub my-registry.io/my-app:1.0.0
    

    전체 보안 체크리스트 정리 및 다음 단계

    ▲ 쿠버네티스 보안 베스트 프랙티스 전체 체크리스트 — RBAC, 네트워크 정책, 컨테이너 보안, 시크릿 관리, 모니터링 항목이 단계별로 정리된 요약 인포그래픽

    여기까지 꽤 많은 내용을 다뤘는데요, 핵심을 정리해드릴게요.

    1. RBAC 최소 권한 원칙 — cluster-admin은 진짜 필요한 경우에만, 서비스 계정은 네임스페이스 범위 Role로 제한
    2. 네트워크 정책(k8s 보안의 방화벽) — 기본 차단 후 필요한 트래픽만 허용, CNI 지원 여부 먼저 확인
    3. SecurityContext 설정 — root 실행 금지, 권한 상승 차단, 읽기 전용 파일시스템 적용
    4. etcd 암호화 — Secret은 반드시 암호화, 가능하면 외부 KMS 연동
    5. 감사 로깅 + 런타임 모니터링 — 예방만큼 탐지도 중요, Falco 도입 검토
    6. 이미지 보안 — 스캔, 서명, 신뢰 레지스트리 제한까지 CI/CD에 통합

    처음부터 모든 걸 한 번에 적용하려고 하면 오히려 지쳐서 포기하게 돼요. 제 경험상 RBAC 정리 → NetworkPolicy → SecurityContext 순서로 하나씩 적용해나가는 게 가장 현실적이더라고요.

    다음 글에서는 OPA Gatekeeper를 활용한 쿠버네티스 정책 자동화를 다룰 예정이에요. Admission Webhook으로 보안 정책을 코드로 관리하는 방법인데, 이걸 도입하면 위에서 설명한 SecurityContext 설정 같은 것들을 개발자가 실수로 빠뜨려도 자동으로 차단하거나 수정할 수 있거든요. 꽤 강력합니다.

    혹시 이 글 읽으면서 “우리 클러스터 이거 안 돼 있는데…” 싶은 항목 있으셨나요? 댓글로 공유해주시면 같이 고민해볼게요. 저도 아직 배우는 중이거든요 😄


    자주 묻는 질문 (FAQ)

    Q. RBAC 설정이 복잡한데, 자동으로 감사하는 도구가 있나요?

    네! rbac-tool이나 kubectl-who-can 같은 플러그인을 사용하면 현재 클러스터의 RBAC 설정을 시각화하고 과도한 권한을 쉽게 찾을 수 있어요. kubectl krew install rbac-tool로 설치할 수 있습니다.

    Q. 관리형 쿠버네티스(EKS, GKE, AKS)를 쓰면 보안 설정이 덜 필요한가요?

    아니요. 관리형 서비스는 컨트롤 플레인(마스터 노드) 보안을 CSP가 담당하지만, RBAC, NetworkPolicy, SecurityContext, 시크릿 관리 같은 워크로드 수준의 보안은 여전히 사용자 책임이에요. 오히려 관리형이라고 방심하는 경우가 더 위험할 수 있어요.

    Q. 컨테이너 보안 스캔은 언제 하는 게 좋나요?

    이상적으로는 Shift Left 보안 원칙에 따라 개발자 로컬 환경 → CI 파이프라인 → 레지스트리 푸시 전 → 배포 전 총 4단계에서 스캔하는 게 좋아요. 최소한 CI 파이프라인에는 반드시 포함시키세요.