13년차의 서버실

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

[태그:] SBOM

  • [보안] 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 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 파이프라인을 만드는 방법도 다뤄볼 예정입니다. 이전 글에서 다뤘던 이미지 경량화와 베이스 이미지 관리 전략을 같이 보면 더 연결이 잘 되실 겁니다. ✅