IT/DevSecOps

Trivy Operator로 클러스터 취약점 지속 스캔 세팅하기

gfrog 2026. 9. 3. 12:15

이미지 빌드 파이프라인에 Trivy를 붙여놨다고 클러스터가 안전한 건 아니다. 새 CVE는 매일 나오고, 어제까지 clean이었던 이미지가 오늘 갑자기 Critical 3개짜리로 바뀐다. CI에서 스캔한 결과는 그 시점의 스냅샷일 뿐이라, 실제 클러스터에서 도는 파드가 지금 어떤 상태인지 알려면 클러스터 안에서 계속 돌아가는 스캐너가 하나 있어야 한다.

Trivy Operator는 딱 그 역할이다. 클러스터에 배포된 워크로드를 감시하다가 새 Pod가 뜨면 자동으로 스캔하고, 결과를 VulnerabilityReport, ConfigAuditReport 같은 CRD로 저장한다. 이번 글에서는 EKS에 Trivy Operator를 얹고, 결과를 Prometheus로 뽑아내서 Grafana에서 보는 데까지 세팅하는 과정을 정리한다. 최근 릴리즈된 chart 0.34.0(내부적으로 trivy v0.71.0 사용) 기준이다.

왜 CI 스캔만으로는 부족한가

우리 팀에서 예전에 겪은 케이스가 하나 있다. nginx:1.25.3 이미지를 6월에 CI에서 스캔했을 때 High 이슈가 2개였다. 그 이미지를 프로덕션에 넣고 3개월간 그대로 두었는데, 8월에 새로 발견된 libxml2 CVE가 붙으면서 어느 순간 Critical 1개로 승격됐다. CI는 다시 안 돌았으니까 아무도 몰랐다. 클러스터 사이드에서 계속 재스캔해주는 뭔가가 있어야 이런 걸 잡는다.

Trivy Operator가 하는 일을 정리하면 이렇다.

  • 클러스터에 있는 워크로드(Deployment, StatefulSet, DaemonSet, CronJob 등)의 이미지 취약점 스캔
  • ConfigAudit — SecurityContext, hostNetwork, 권한 관련 misconfiguration 감지
  • Exposed Secret — 이미지 안에 실수로 들어간 시크릿 탐지
  • Compliance 리포트 (CIS Benchmark, NSA 등)
  • Trivy DB가 갱신되면 자동으로 재스캔

CRD로 저장한다는 게 중요하다. kubectl로 조회 가능하고, exporter만 붙이면 Prometheus로 metric이 흐른다. 별도 UI 없이 기존 관측성 스택에 편입된다.

설치 — 그냥 Helm이 정답

공식 chart 이름이 trivy-operator다. Helm 3 기준으로 이렇게 붙인다.

helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update

helm install trivy-operator aqua/trivy-operator \
  --namespace trivy-system \
  --create-namespace \
  --version 0.34.0 \
  --set trivy.ignoreUnfixed=true \
  --set operator.scanJobTimeout=10m \
  --set trivy.severity=CRITICAL,HIGH

몇 가지 설정 이유를 짚고 넘어가자.

trivy.ignoreUnfixed=true는 픽스가 없는 CVE를 건너뛴다. 이게 논쟁적일 수 있는데, 우리 팀에서는 켠다. 이유는 간단하다. 픽스가 없는 걸 리포트에 계속 띄워봐야 액션할 게 없고, 노이즈만 쌓인다. 다만 Critical만이라도 unfixed를 잡고 싶으면 ignoreUnfixed는 끄고 Grafana 쪽에서 필터를 걸어도 된다.

trivy.severity는 스캔 자체는 다 하되 리포트에 어떤 severity를 포함할지 정한다. 클러스터에 파드가 수백 개면 MEDIUM 이하까지 켰다가 CRD 사이즈가 폭발한다. 처음엔 Critical/High만 보는 게 낫다.

operator.scanJobTimeout은 스캔 잡 하나가 10분 안에 안 끝나면 죽인다. 큰 이미지(예: nvidia CUDA 이미지 몇 GB짜리)는 기본값 5분으론 부족하다.

어떻게 도는지 살짝 뜯어보기

설치하고 나면 trivy-system 네임스페이스에 두 개의 Deployment가 뜬다. trivy-operator(컨트롤러)와 trivy-server(스캐너, serverMode를 켰을 때만). 기본 세팅은 client-server 모드 없이 잡마다 새 trivy 컨테이너를 띄우는 방식인데, 파드 수가 많으면 DB 다운로드 비용이 반복돼서 부담이 크다.

파드가 300개 넘는 클러스터라면 serverMode를 켜는 편이 훨씬 낫다.

trivy:
  mode: ClientServer
  storageClassName: gp3

이러면 trivy-server가 취약점 DB를 한 번만 pull해서 캐시하고, 잡들은 클라이언트로만 붙는다. 실제로 우리 클러스터(파드 ~400개)에서 이 스위치 하나로 스캔 완료 시간이 40분에서 12분으로 줄었다.

내부적으로는 이렇게 흐른다. 컨트롤러가 파드 이벤트를 watch → 파드에 대응되는 워크로드(Deployment 등) 찾음 → 이미 리포트 있으면 스킵, 없으면 스캔 Job 생성 → Job이 이미지 다이제스트 기준으로 스캔 → 결과를 CRD로 write. 다이제스트 기준이라 같은 tag를 pull해도 실제 이미지가 안 바뀌면 재스캔 안 한다.

결과 확인

몇 분 지나면 CRD가 생긴다.

kubectl get vulnerabilityreports -A
kubectl get configauditreports -A
kubectl get exposedsecretreports -A

특정 워크로드 하나를 파헤쳐보려면:

kubectl get vulnerabilityreports -n prod \
  -o custom-columns='NAME:.metadata.name,CRITICAL:.report.summary.criticalCount,HIGH:.report.summary.highCount'

리포트 하나를 상세히 보면 이렇게 나온다.

report:
  artifact:
    repository: nginx
    tag: "1.25.3"
    digest: sha256:4c0fdaa8b6341bfdeca5f18f7837462c80cff90527ee35ef185571e1c327beac
  summary:
    criticalCount: 1
    highCount: 3
    mediumCount: 0
  vulnerabilities:
  - vulnerabilityID: CVE-2024-56171
    severity: CRITICAL
    resource: libxml2
    installedVersion: 2.9.14
    fixedVersion: 2.9.14-r2
    title: "libxml2: use-after-free in XMLReader"

이거 그대로 두면 안 본다. 반드시 metric으로 뽑아야 한다.

Prometheus로 metric 뽑기

Trivy Operator 자체가 /metrics 엔드포인트를 노출한다. Helm value에서 serviceMonitor.enabled=true로 켜면 자동으로 ServiceMonitor CRD를 만든다(kube-prometheus-stack 쓸 때).

serviceMonitor:
  enabled: true
  interval: 60s

노출되는 대표 메트릭 몇 개.

  • trivy_image_vulnerabilities{severity, namespace, name, resource_kind} — 워크로드별 CVE 카운트
  • trivy_resource_configaudits{severity, namespace} — 미스컨피그 카운트
  • trivy_exposedsecrets{severity, namespace} — 노출된 시크릿 카운트

여기서 실용적인 쿼리 몇 개.

# 클러스터 전체 Critical CVE 총합
sum(trivy_image_vulnerabilities{severity="Critical"})

# 네임스페이스별 High + Critical
sum by (namespace) (
  trivy_image_vulnerabilities{severity=~"Critical|High"}
)

# 최근 24시간 동안 새로 생긴 Critical
sum(increase(trivy_image_vulnerabilities{severity="Critical"}[24h]))

Alertmanager 규칙은 이 정도만 걸어놨다.

- alert: NewCriticalVulnerabilityInProd
  expr: |
    sum by (namespace, name) (
      trivy_image_vulnerabilities{severity="Critical", namespace="prod"}
    ) > 0
  for: 30m
  labels:
    severity: warning
  annotations:
    summary: "prod에 Critical CVE 있음: {{ $labels.namespace }}/{{ $labels.name }}"

for: 30m을 준 건 스캔 Job이 갓 돈 직후에는 리포트가 아직 partial일 수 있어서 flapping이 종종 있었기 때문이다.

세팅하다 자주 걸리는 것들

Private 레지스트리 인증. 사내 Harbor에서 이미지를 pull한다면, 워크로드에 붙은 imagePullSecrets을 Trivy Operator가 자동으로 재사용한다. 다만 서비스어카운트 레벨 시크릿은 놓친다. operator.serviceAccount.imagePullSecrets을 별도로 지정해야 하는 케이스가 있으니 첫 스캔이 authentication required로 실패하면 여기부터 의심.

Trivy DB 다운로드 실패. ghcr.io 레이트 리밋에 걸리는 클러스터가 종종 있다. DB 미러를 사내에 두는 게 정신건강에 좋다. trivy.dbRepository=harbor.internal/aquasecurity/trivy-db 이런 식으로 지정 가능.

CRD가 너무 많이 쌓임. 리포트는 워크로드별로 하나씩 생기는데, replicaset이 뒤에서 계속 도는 상황에서 GC 안 하면 몇 만 개도 쌓인다. operator.excludeNamespaces=kube-system,kube-node-lease 정도는 기본으로 걸고, operator.vulnerabilityReportsPlugin=Trivy에서 operator.builtInTrivyServer=true로 서버 모드까지 켜면 부담이 확 준다.

노드 컴포넌트 스캔은 노이즈가 심하다. 기본으로 kubelet, apiserver 스캔이 켜져 있는데, 관리형 EKS 쓰면 어차피 우리가 못 고친다. nodeCollector.enabled=false로 꺼두는 게 낫다.

대시보드 한 장

Grafana에 이렇게 구성해뒀다. panel 5개짜리 심플한 대시보드.

1. 클러스터 총 CVE 카운트(severity별 stacked bar) 2. 네임스페이스 top 10 (High+Critical 합산) 3. 이미지별 top 20 취약점 워크로드 (table) 4. 지난 7일간 Critical 증감 추이 (time series) 5. ConfigAudit 실패 top 10 (table)

여기서 담당자가 매주 월요일에 top 20만 훑어보고 티켓 잘라 돌리는 워크플로우로 정착시켰다. 자동으로 PR 만들어주는 툴(Renovate + trivy 연동)도 붙일 수는 있는데, 우리 팀은 아직 사람 손으로 우선순위를 판단하는 걸 선호한다.

이거 다음에 뭘 볼 것인가

Trivy Operator만으로도 상당 부분 커버되지만, 몇 가지 한계도 있다.

  • 러닝 프로세스 관점의 런타임 위협은 못 잡는다. 이건 Falco나 Tetragon 영역
  • SBOM 자체를 관리 대상으로 볼 거면 Trivy Operator보다는 Kubeclarity나 Dependency-Track이 낫다
  • 정책 위반 자체를 admission에서 막고 싶으면 Kyverno + Trivy 조합으로 확장

우리 팀도 아직 런타임 쪽은 못 붙였다. 다음 분기에 Tetragon PoC 돌려볼 예정인데, 그 이야기는 나중에 정리해서 올리겠다.

혹시 사내에서 이미 다른 스캐너(Prisma Cloud, Aqua, Sysdig 등) 쓰고 있으면 굳이 Trivy Operator 얹을 필요는 없다. 다만 오픈소스만으로 관측성 스택에 자연스럽게 편입시키기에는 이만한 게 없다는 게 지금까지의 경험이다.


Tags: Kubernetes, DevSecOps, Trivy, 취약점스캔, 보안, Prometheus