13년차의 서버실

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

[태그:] 보안 자동화

  • [보안] Kubernetes 보안 체크리스트: 컨테이너 이미지 스캔 자동화

    [보안] Kubernetes 보안 체크리스트: 컨테이너 이미지 스캔 자동화

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

    Kubernetes 보안 체크리스트: 컨테이너 이미지 스캔 자동화

    Kubernetes 보안 체크리스트를 운영하다 보면 병목이 꽤 자주 이미지 스캔에서 드러납니다. 배포 파이프라인은 분 단위로 빨라졌는데 보안 판정이 여전히 사람 손에 걸려 있으면, 속도와 통제가 같이 무너지더라고요. 저도 홈랩이든 사내 테스트 클러스터든 처음에는 릴리스 직전 일회성 스캔으로 시작했는데, 운영으로 넘어가면 금방 한계가 보였습니다. 이미지 변경 속도, 베이스 이미지 교체 주기, 레지스트리 정책, 클러스터 반입 기준이 따로 놀면 자동화는 이름만 자동화가 되거든요.

    실무에서는 스캐너를 한 번 붙이는 것으로 끝나지 않습니다. 빌드 시점, 레지스트리 저장 시점, 클러스터 반입 시점, 운영 중 재평가 시점을 나눠서 봐야 합니다. 이유는 단순합니다. 배포 당시엔 통과한 이미지라도, 며칠 뒤 취약점 데이터베이스가 갱신되면 같은 다이제스트가 다른 판정을 받을 수 있습니다. 이 차이를 놓치면 팀은 바로 “어제는 괜찮았는데 왜 오늘 막히지?”라는 혼란에 빠집니다.

    CI, 레지스트리, Kubernetes 클러스터, 보고 체계를 한눈에 보여주는 전체 구성도입니다.

    Kubernetes 보안 체크리스트에서 이미지 스캔이 중요한 이유

    현장에서 필요한 건 CVE 숫자 나열이 아닙니다. 무엇을 기준으로 배포를 막을지, 무엇은 추적만 하고 무엇은 즉시 조치할지, 그 판정을 누가 재현할 수 있는지가 핵심입니다. 실제 스캔 결과를 보면 문제는 대체로 세 부류로 갈립니다. 베이스 이미지 교체로 끝나는 항목, 애플리케이션 의존성 업데이트가 필요한 항목, 이미지가 아니라 배포 매니페스트나 실행 권한 설정을 손봐야 하는 항목입니다. 같은 High라도 대응 경로가 완전히 다릅니다.

    그래서 체크리스트를 만들 때 제가 먼저 적는 건 도구 이름이 아니라 다음 네 가지입니다. 첫째, 어떤 시점에 검사할지. 둘째, 어떤 대상을 검사할지. 셋째, 어떤 심각도와 조건에서 실패로 볼지. 넷째, 예외를 누구 승인으로 얼마나 오래 둘지. 이 네 줄이 빠지면 결과는 늘 많고, 책임은 늘 흐려집니다.

    Kubernetes 보안 체크리스트로 정리한 컨테이너 이미지 스캔 자동화 체크리스트

    1. 이미지 태그보다 다이제스트를 기준으로 기록: repo:tag만 저장하지 말고 repo@sha256:...를 남겨야 스캔 결과와 배포 대상을 정확히 연결할 수 있습니다.
    2. 빌드 직후 스캔: CI에서 이미지 생성 직후 취약점과 시크릿 혼입 여부를 확인합니다.
    3. 실패 기준을 코드로 고정: CRITICAL 즉시 차단, HIGH는 팀 정책에 따라 차단 또는 예외 관리 등으로 명시합니다.
    4. 데이터베이스 갱신 시점 기록: 로컬, CI, 클러스터의 스캔 결과가 다른 흔한 원인입니다.
    5. 레지스트리 인증 분리: 빌드용 자격 증명과 스캔용 읽기 전용 자격 증명을 분리해 두면 사고 범위를 줄이기 좋습니다.
    6. 클러스터 내 재스캔: 이미 떠 있는 워크로드를 주기적으로 다시 평가해야 운영 중 신규 공개 취약점을 따라잡을 수 있습니다.
    7. 예외 처리 만료일 강제: 예외는 허가가 아니라 부채입니다. 사유, 승인자, 만료일이 없으면 결국 영구 면제가 됩니다.
    8. 결과 저장 위치 통일: CI 로그만 믿지 말고 리포트 시스템이나 Kubernetes 리소스로 남겨 반복 조회 가능하게 해야 합니다.
    9. 로컬과 CI 판정 일치: 같은 옵션, 같은 대상, 가능하면 같은 DB 갱신 흐름을 써야 팀이 결과를 신뢰합니다.
    10. 최소 이미지 우선: Distroless나 slim 계열 검토는 보안 때문이기도 하지만, 스캔 노이즈와 패치 범위를 줄이는 데도 효과가 큽니다.
    11. Admission 단계 연계 여부 결정: 규정이 엄격하면 배포 허가 단계에서 차단하고, 초기 도입기라면 관찰 모드부터 들어가는 편이 덜 부서집니다.
    12. 성능 예산 확보: 대형 이미지 재스캔은 노드 자원과 레지스트리 호출을 먹습니다. 운영 클러스터에서 무작정 스캔 빈도를 높이면 다른 워크로드가 피해를 볼 수 있습니다.

    도구 선택 전에 먼저 정할 기준

    Trivy, Grype처럼 널리 쓰이는 스캐너는 출발점으로 충분히 좋습니다. 저는 빠르게 붙일 때는 Trivy를 자주 쓰는 편입니다. 로컬 점검, CI, Kubernetes 연계까지 흐름을 만들기 편해서 이거 진짜 실무에서 손이 덜 가더라고요. 다만 도구보다 먼저 정해야 할 것이 있습니다. 결과 해석 단위가 이미지인지, 레포지토리인지, 실제 클러스터 워크로드인지부터 정해야 합니다. 이 기준이 흔들리면 스캐너를 바꿔도 운영은 그대로 혼란스럽습니다.

    선택 포인트 A를 택할 때 B를 택할 때 제가 실제로 권하는 기준
    스캔 시점 CI만 스캔: 배포 속도와 단순성이 우선일 때 CI + 클러스터 재스캔: 운영 중 노출 추적이 중요할 때 운영 클러스터가 있으면 병행이 맞습니다. CI만으로는 신규 공개 취약점을 놓치기 쉽습니다.
    식별 방식 태그 기준: 초기에 단순하게 시작할 때 다이제스트 기준: 결과 재현성과 감사 추적이 필요할 때 실서비스는 다이제스트 기준으로 가는 편이 안전합니다. 태그는 너무 쉽게 바뀝니다.
    실패 정책 Critical만 차단: 도입 초기에 반발을 줄여야 할 때 Critical + High 차단: 규정과 배포 품질 기준이 이미 성숙했을 때 처음부터 전부 막기보다 Critical 차단, High는 만료일 있는 예외 관리가 현실적입니다.
    스캔 대상 OS 패키지 위주: 베이스 이미지 관리가 주 이슈일 때 OS + 애플리케이션 의존성 + 시크릿: 공급망 전체를 봐야 할 때 실무에서는 최소한 취약점과 시크릿은 같이 보는 편이 낫습니다.
    운영 방식 관찰 모드: 현재 상태 파악이 먼저일 때 차단 모드: 배포 기준을 강제해야 할 때 새로 도입하는 팀은 1~2주 정도 관찰 모드로 데이터부터 모으는 편이 덜 아픕니다.

    실전 구현 1: Kubernetes 보안 체크리스트 기준으로 로컬과 CI 맞추기

    가장 먼저 할 일은 개발자 로컬과 CI가 가능한 한 같은 판정을 내도록 맞추는 것입니다. 로컬에선 통과했는데 CI에서만 실패하면 팀은 금방 스캐너보다 파이프라인을 더 싫어하게 됩니다. 그래서 이 단계에서는 “옵션을 많이 주는 것”보다 “같은 옵션을 고정하는 것”이 더 중요합니다. 이 기준만 맞춰도 운영 피로도가 꽤 줄어듭니다.

    1. 이미지 빌드 후 Trivy로 즉시 검사

    아래 예시는 로컬에서 이미지 빌드 직후 취약점과 시크릿을 같이 보는 가장 단순한 흐름입니다. 팀 공통 기준을 잡을 때는 이런 명령부터 통일하는 게 훨씬 편합니다.

    docker build -t myapp:scan-test .
    trivy image \
      --severity HIGH,CRITICAL \
      --ignore-unfixed \
      --scanners vuln,secret \
      --format table \
      myapp:scan-test

    여기서 중요한 건 옵션 의미보다 운영상의 함정입니다. --ignore-unfixed는 노이즈를 줄이는 데 유용하지만, “아직 수정본이 없으니 위험이 없다”는 뜻은 아닙니다. 패치가 없어도 우회책이나 완화 조치가 필요한 경우가 있습니다. 그래서 저는 이 옵션을 쓰더라도 예외 문서에는 “미조치”보다 “공급사 수정 대기”처럼 상태를 분리해 적는 편입니다.

    --scanners vuln,secret 조합도 실무 효율이 좋습니다. 이미지 레이어에 테스트 토큰, 오래된 인증서, 샘플 키가 남아 있는 경우가 의외로 자주 나옵니다. 이건 CVE보다 발견 즉시 우선순위를 높게 잡아야 하는 유형입니다. 공격 난도가 낮고 노출 즉시 영향이 커질 수 있어서 그렇습니다.

    2. CI에서 실패 기준을 강제하고 결과를 아티팩트로 남기기

    CI에서는 사람 눈으로만 보는 표 형식과, 후처리용 JSON 결과를 같이 남겨 두는 편이 좋습니다. 나중에 추세 비교나 예외 검토할 때 이 차이가 꽤 크게 느껴집니다.

    IMAGE_REF="registry.example.com/team/myapp:${GIT_COMMIT}"
    
    trivy image \
      --severity CRITICAL,HIGH \
      --exit-code 1 \
      --ignore-unfixed \
      --scanners vuln,secret \
      --format json \
      --output trivy-report.json \
      "$IMAGE_REF"
    
    trivy image \
      --severity CRITICAL,HIGH \
      --ignore-unfixed \
      --scanners vuln,secret \
      --format table \
      "$IMAGE_REF"

    --exit-code 1이 없으면 자동화는 반쪽입니다. 로그에 경고가 많아도 배포가 계속되면 운영팀은 곧 “보안 경고는 참고용”으로 받아들이게 됩니다. 그 순간부터 정책은 문서에만 남습니다. 그리고 JSON 결과를 별도 파일로 남기는 이유도 분명합니다. 사람이 읽기 쉬운 표와 기계가 다시 처리하기 쉬운 형식을 분리해야 나중에 재검증이 편해집니다.

    초기에 자주 보는 실패 모드는 “로컬은 어제 DB로 검사했고, CI는 오늘 DB로 검사했다”는 불일치입니다. 겉으로는 같은 이미지, 같은 명령처럼 보여도 결과가 달라집니다. 그래서 운영 문서에는 최소한 이미지 다이제스트, 스캔 시각, 스캐너 버전, DB 갱신 시각 네 가지를 같이 남겨 두는 게 좋습니다.

    컨테이너 이미지 스캔이 포함된 Kubernetes 보안 체크리스트 CI 흐름

    빌드, 스캔, 실패 기준 적용, 배포 차단으로 이어지는 파이프라인 흐름 예시입니다.

    실전 구현 2: Kubernetes 클러스터 안에서 재스캔 자동화

    CI 스캔은 배포 직전의 사진 한 장에 가깝습니다. 운영은 동영상에 더 가깝고요. 이미지 자체는 그대로인데 취약점 데이터베이스가 갱신되거나, 오래된 워크로드가 남아 있거나, 수동 롤백으로 이전 태그가 다시 떠 있을 수 있습니다. 그래서 운영 클러스터에서는 주기 재평가가 꼭 필요합니다.

    1. Helm으로 Trivy Operator 설치

    Trivy Operator는 Kubernetes 안에서 보안 리포트를 CRD 형태로 남겨 주기 때문에, 클러스터 관점 추적이 필요한 팀에는 꽤 잘 맞습니다. 별도 콘솔 없이도 kubectl로 상태를 확인할 수 있다는 점도 편합니다.

    helm repo add aqua https://aquasecurity.github.io/helm-charts/
    helm repo update
    helm upgrade --install trivy-operator aqua/trivy-operator \
      --namespace trivy-system \
      --create-namespace

    이 방식의 장점은 결과가 Kubernetes 리소스로 남는다는 점입니다. CI 로그는 흩어지기 쉽지만 리소스는 조회 경로가 고정됩니다. 운영팀과 플랫폼팀이 같은 화면을 본다는 점도 생각보다 큽니다. 인수인계할 때 특히 편하더라고요.

    2. 스캔 결과 조회

    설치 후에는 먼저 실제로 어떤 리포트가 생성되는지 확인하는 게 좋습니다. 환경과 활성화된 기능에 따라 보이는 리포트 종류가 조금씩 다를 수 있습니다.

    kubectl get vulnerabilityreports -A
    kubectl describe vulnerabilityreport -n default <report-name>

    결과를 읽을 때는 숫자보다 맥락을 먼저 봅니다. 저는 보통 세 가지를 바로 확인합니다. 첫째, 어떤 워크로드의 어떤 컨테이너인지. 둘째, 문제가 베이스 이미지 계층인지 애플리케이션 의존성인지. 셋째, 지금 당장 수정 가능한지입니다. 같은 High라도 베이스 이미지 교체로 끝나는 항목과 코드 호환성 검토가 필요한 라이브러리 문제는 대응 일정이 다릅니다.

    3. 실제 배포 중인 이미지 목록부터 확인

    보고서가 아니라 실제 실행 대상을 먼저 보는 습관이 중요합니다. 선언상 최신 이미지가 적혀 있어도, 운영 클러스터에는 예전 이미지가 남아 있는 경우가 꽤 있습니다.

    kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .spec.containers[*]}{.image}{" "}{end}{"\n"}{end}'

    이 명령은 단순해 보여도 중요합니다. Git 저장소 기준으로는 최신 태그를 본다고 해도, 실제 클러스터에선 예전 태그나 수동 롤백된 이미지가 남아 있을 수 있습니다. 스캔 체계가 배포 선언만 보고 실제 실행 대상을 안 보면, 보고서는 깨끗한데 운영은 더러운 상태가 됩니다.

    4. 사설 레지스트리 인증 문제가 있는지 먼저 분리 진단

    리포트가 안 생길 때는 스캐너 자체를 의심하기 전에 인증 흐름부터 확인하는 편이 빠릅니다. 여기서 시간 많이 쓰는 팀이 정말 많습니다.

    kubectl get events -A --sort-by=.lastTimestamp
    kubectl get secrets -n trivy-system
    kubectl describe serviceaccount -n trivy-system trivy-operator

    클러스터 내 스캐너가 리포트를 못 만드는 가장 흔한 이유는 인증 연결 누락입니다. 이미지 풀은 애플리케이션 서비스 계정이 잘하는데, 스캐너는 다른 서비스 계정이라 접근을 못 하는 경우가 잦습니다. 표면 증상은 “리포트가 안 생긴다”인데 근본 원인은 권한 경계가 다른 데 있습니다. 이건 설정 몇 줄 더 넣는다고 해결되지 않고, 누가 어느 레지스트리를 어떤 자격 증명으로 읽는지 모델을 분리해서 봐야 합니다.

    재현 가능한 시나리오로 보는 운영 포인트

    이런 시나리오는 실제 운영에서 자주 나옵니다. 내부 API 서비스가 registry.example.com/team/api:2026-09-01로 배포됐고, 배포 당시 CI 스캔은 통과했습니다. 며칠 뒤 베이스 이미지에 대한 신규 취약점 정보가 스캐너 DB에 반영됩니다. 코드도, 이미지 다이제스트도 바뀌지 않았는데 클러스터 리포트에는 새로운 Critical이 생길 수 있습니다. CI만 보는 팀은 “왜 갑자기 숫자가 늘었지?”에서 멈추고, 클러스터 재스캔이 있는 팀은 “지금 떠 있는 워크로드 중 어느 네임스페이스가 영향권인지”까지 바로 확인합니다.

    이 지점에서 중요한 것은 억울함을 줄이는 기록입니다. 결과가 바뀌었는데 이미지가 안 바뀌었을 때 스캐너 오작동으로 몰아가는 경우를 여러 번 봤습니다. 실제로는 취약점 DB 갱신이나 메타데이터 해석 차이인 경우가 많았습니다. 그래서 운영 기준에는 반드시 스캔 시각, DB 갱신 시각, 대상 다이제스트를 함께 남겨 두는 게 좋습니다.

    실제로 많이 막히는 문제와 해결법

    Private Registry 인증이 안 붙는 경우

    증상은 단순합니다. 리포트가 생성되지 않거나 이벤트에 인증 실패가 남습니다. 그런데 근본 원인은 보통 “앱 배포에 쓰는 이미지 풀 자격 증명”과 “스캐너가 읽는 자격 증명”을 같은 것으로 착각한 데 있습니다. 해결은 권한을 더 크게 주는 것이 아니라, 스캐너 전용 읽기 권한이 어디에 연결돼 있는지 확인하는 것입니다.

    취약점이 너무 많이 떠서 운영이 멈추는 경우

    초기 도입기에는 거의 반드시 겪습니다. 이때 모든 심각도를 한 번에 차단하면 실제로는 보안 수준이 올라가는 게 아니라 파이프라인이 멈춥니다. 저는 보통 Critical 즉시 차단, High는 예외 문서와 만료일 부여, Medium 이하는 추세 관찰로 시작합니다. 운영이 감당할 수 있는 속도로 올라가야 자동화도 습관으로 남습니다.

    태그만 보고 이미지를 추적하는 경우

    latest나 재사용되는 릴리스 태그는 감사 추적에 불리합니다. 스캔 결과가 어느 바이너리 집합을 가리키는지 불명확해집니다. 실제 배포 검증이나 사고 대응까지 생각하면 다이제스트 기준 저장이 맞습니다. 태그는 사람이 보기 좋고, 다이제스트는 시스템이 책임지기 좋습니다.

    운영 판단 없이 숫자만 보는 경우

    스캔 수치는 우선순위의 단서일 뿐, 우선순위 그 자체는 아닙니다. 외부에 노출된 서비스의 런타임 라이브러리 문제와 내부 배치 컨테이너의 개발 도구 패키지 문제는 같은 High여도 대응 순서가 달라집니다. 저는 아래 네 항목을 같이 봅니다.

    • 노출 면적: Ingress 뒤 서비스인지, 내부 잡인지 구분합니다.
    • 권한 수준: root 실행 여부, 추가 capability, privileged 여부를 확인합니다.
    • 수정 가능성: 베이스 이미지 교체로 끝나는지, 코드 수정과 검증이 필요한지 나눕니다.
    • 예외 만료: 지금 못 고치는 항목은 다시 볼 날짜가 없으면 영구 미해결이 됩니다.

    스캔이 클러스터 자원을 갉아먹는 경우

    운영 규모가 커지면 이것도 무시하기 어렵습니다. 큰 이미지가 많고 네임스페이스가 많으면 스캔 자체가 CPU, 메모리, 레지스트리 호출량을 소모합니다. 증상은 오퍼레이터가 느려지거나 리포트 생성이 밀리는 식으로 나타납니다. 재스캔은 많이 돌린다고 무조건 좋은 게 아니라, 업데이트 빈도, 클러스터 자원, 조치 가능한 인력에 맞춰야 합니다.

    클러스터 보안 관점의 Kubernetes 보안 체크리스트 리포트 구성도

    오퍼레이터가 워크로드를 스캔하고 리포트를 남기는 클러스터 내부 흐름 예시입니다.

    검증: 자동화가 제대로 작동했는지 확인하는 방법

    자동화 검증은 명령 성공 여부만 보면 부족합니다. 실패해야 할 때 진짜 실패하는지, 운영 중 판정 변화가 반영되는지, 팀원이 바뀌어도 같은 경로로 확인 가능한지가 핵심입니다. 제가 점검할 때는 다음 순서로 봅니다.

    1. 테스트용 이미지를 하나 빌드하고 로컬 결과를 저장합니다.
    2. 같은 이미지를 CI에서 같은 심각도 기준으로 스캔해 동일하게 실패 또는 통과하는지 확인합니다.
    3. 가능하면 배포 시점의 이미지 다이제스트를 기록해 로컬 결과와 CI 결과를 같은 대상으로 묶습니다.
    4. 클러스터에 배포한 뒤 vulnerabilityreports 리소스가 생성되는지 확인합니다.
    5. 사설 레지스트리 이미지는 인증 실패 없이 조회되는지 이벤트와 서비스 계정 기준으로 검증합니다.
    6. 예외 처리한 항목이 문서, CI 정책, 클러스터 결과에서 같은 의미로 반영되는지 비교합니다.

    환경에 따라 추가 리포트가 생성될 수 있으니, 아래처럼 어떤 CRD가 실제로 보이는지 함께 확인하면 좋습니다. 다만 활성화된 기능과 Trivy Operator 설정에 따라 결과는 달라질 수 있습니다.

    kubectl get vulnerabilityreports -A
    kubectl get configauditreports -A
    kubectl get clustercompliancereports -A

    중요한 것은 결과를 반복 조회 가능한 형태로 남기는 것입니다. 사람이 한 번 보고 지나가는 로그는 운영 자산이 아닙니다. 조회 경로가 고정되고 같은 명령으로 다시 확인할 수 있어야 팀 지식이 됩니다. 그리고 Pod Security Standards, RBAC, NetworkPolicy 관련 글도 함께 보면 Kubernetes 보안 체크리스트를 더 입체적으로 잡는 데 도움이 됩니다.

    Kubernetes 보안 체크리스트 결과 검증용 이미지 스캔 대시보드

    배포된 워크로드별 취약점 현황과 확인 포인트를 보여주는 결과 화면 예시입니다.

    FAQ: 현장에서 자주 받는 질문

    Q. 스캔 결과가 바뀌었는데 이미지는 안 바뀌었습니다.

    A. 충분히 가능한 일입니다. 가장 흔한 원인은 취약점 데이터베이스 갱신입니다. 그래서 스캔 시각, DB 갱신 시각, 이미지 다이제스트를 같이 기록해야 나중에 원인 추적이 쉬워집니다.

    Q. 모든 High를 바로 차단해야 하나요?

    A. 팀 상태에 따라 다릅니다. 초기 도입기라면 Critical 우선 차단, High는 예외와 만료일 관리가 현실적입니다. 이미 배포 품질 게이트가 정착된 조직이라면 High까지 차단해도 됩니다. 중요한 건 기준을 문서가 아니라 파이프라인에 넣는 것입니다.

    Q. Kubernetes 보안 체크리스트에서 이미지 스캔만 하면 충분한가요?

    A. 부족합니다. 이미지 스캔은 공급망과 배포 전 검증의 한 축일 뿐입니다. Pod Security Standards, RBAC, NetworkPolicy, Secret 관리, 감사 로그까지 함께 봐야 운영 리스크가 줄어듭니다. 다만 시작점으로는 이미지 스캔이 체감 효과가 빠른 편입니다.

    운영 기준은 이렇게 잡으시면 됩니다

    제가 오래 운영을 보면서 내린 기준은 단순합니다. 팀이 아직 수동 검토에 기대고 있다면 Trivy CLI로 CI 차단부터 붙이세요. 이 단계에서는 로컬과 CI 판정 일치, 다이제스트 기록, 예외 만료일 관리가 핵심입니다. 운영 클러스터가 여러 개이거나 롤백이 잦다면 클러스터 재스캔도 같이 가져가세요. 배포 당시엔 멀쩡했던 이미지가 나중에 문제로 바뀌는 순간을 잡아내려면 이 경로가 필요합니다.

    반대로 아직 팀이 결과 해석도 못 따라가는데 Admission 단계에서 전부 막는 것은 추천하지 않습니다. 그건 통제가 아니라 마찰이 됩니다. 이럴 땐 관찰 모드로 현재 상태 파악, 그다음 Critical 차단, 마지막으로 High까지 확대 순서가 덜 부서집니다. 규정이 엄격하고 변경 이력 감사가 중요하다면, 그때 배포 허가 단계 연계까지 가면 됩니다.

    제 권고를 한 줄로 줄이면 이렇습니다. 작게 시작하는 팀은 CI 스캔 + 실패 코드 적용, 운영 가시성이 필요한 팀은 클러스터 재스캔 추가, 강제 통제가 필요한 조직은 Admission 정책 연계. 이 순서가 실무에서 가장 덜 아프고, 결과가 남고, 다음 담당자도 이해하기 쉽습니다.

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

    CI 스캔, 클러스터 재스캔, 예외 관리, 정책 연계를 한 장으로 정리한 요약 이미지입니다.

  • [보안] Trivy IaC 보안 전환: tfsec 마이그레이션과 운영비 분석

    [보안] Trivy IaC 보안 전환: tfsec 마이그레이션과 운영비 분석

    목차

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

    Trivy IaC 보안 전환: tfsec 마이그레이션과 운영비 분석

    Trivy IaC 보안을 검토하는 팀이라면 대개 비슷한 시점이 옵니다. Terraform만 보던 흐름에서 Kubernetes manifest, Helm, 컨테이너 이미지, secret 노출까지 같이 챙겨야 하거든요. 저도 그 구간에서 tfsec 중심 운영을 오래 끌고 갔는데, 결국 Trivy로 표준화를 잡는 쪽이 더 편했습니다. 핵심은 단순히 도구를 하나 더 붙인 게 아니라, CI 실패 기준과 예외 관리, 결과 소비 방식까지 다시 설계했다는 점입니다.

    이번 글은 Trivy IaC 보안을 단순 소개로 끝내지 않겠습니다. 실제 마이그레이션에서 어디서 시간이 새는지, 어떤 옵션이 운영비를 줄이는지, tfsec를 바로 걷어내면 왜 반발이 생기는지까지 실무 기준으로 정리해보겠습니다. 이미 tfsec를 쓰는 분이라면 더 중요하죠. 필요한 건 “Trivy도 됩니다”가 아니라, 언제 바꾸고 언제 병행할지 판단할 재료니까요.

    결론부터 짧게 말하면 이렇습니다. Terraform만 안정적으로 검사하면 되는 단일 저장소라면 tfsec 병행 검증 후 천천히 넘어가도 됩니다. 반대로 IaC, secret, 파일시스템, 이미지 스캔을 한 CLI 체계로 묶고 싶다면 Trivy 쪽이 운영 밀도가 더 좋습니다. 라이선스 비용보다 사람 시간이 비싼 팀일수록 이 차이가 크게 느껴지더라고요.

    Trivy IaC 보안 전환 아키텍처를 보여주는 다이어그램

    Terraform, Kubernetes, CI 파이프라인, 결과 리포트가 한 흐름으로 연결된 IaC 보안 전환 개요 이미지입니다.

    1. 왜 Trivy IaC 보안으로 옮기게 되었는가

    tfsec은 Terraform 보안 검사 도구로 여전히 익숙하고 빠릅니다. 다만 팀 요구사항은 보통 Terraform에서 끝나지 않더라고요. 실제 운영에서는 “Terraform misconfiguration은 tfsec”, “secret은 다른 스캐너”, “이미지는 또 다른 스캐너”처럼 갈라지기 쉽습니다. 이 상태가 길어지면 보안 품질보다 도구 조합 관리가 일이 됩니다.

    Trivy로 옮기며 가장 크게 본 장점도 기능 수 자체는 아니었습니다. 같은 CLI 습관으로 misconfiguration, secret, filesystem, image 스캔을 묶을 수 있다는 점이 컸습니다. 신규 팀원이 들어왔을 때도 “저 저장소는 이 도구, 저 저장소는 저 도구”가 아니라 “우리는 우선 Trivy를 기준으로 본다”고 설명하면 훨씬 빨랐습니다. 이거 진짜 온보딩에서 차이가 납니다.

    • tfsec 유지가 맞는 경우: Terraform만 안정적으로 검사하고 있고, 다른 보안 영역은 이미 별도 체계가 자리 잡은 팀입니다.
    • Trivy 전환이 맞는 경우: IaC 외에 secret, 파일시스템, 이미지, SBOM 흐름까지 한 도구권으로 정리하려는 팀입니다.
    • 가장 흔한 착각: 오픈소스라서 비용이 거의 없다고 보는 것입니다. 실제 비용은 도구 수, 예외 재검토 시간, CI 노이즈, 신규 입사자 온보딩에서 나옵니다.

    2. tfsec 마이그레이션의 본질은 명령어 치환이 아니라 정책 재설계입니다

    Aqua Security는 tfsec을 계속 공개해 두고 있지만, 공식적으로는 스캔 역량을 Trivy로 통합하는 방향을 안내하고 있습니다. 그래서 마이그레이션을 어렵게 만드는 건 설치가 아니라 같은 문제를 어떤 기준으로 실패 처리할지, 그리고 예외를 어디에 어떤 기한으로 남길지입니다. 저는 tfsec에서 Trivy로 옮길 때 아래 네 항목부터 고정했습니다.

    1. 현재 tfsec가 막고 있는 위험이 무엇인지 목록화
    2. Trivy에서 동일하거나 더 나은 방식으로 재현 가능한지 확인
    3. CI 실패 기준을 룰 이름보다 심각도와 자산 노출도 기준으로 재정의
    4. 예외를 무기한 방치하지 않도록 만료일이 있는 방식으로 정리

    여기서 중요한 건 tfsec 룰 이름과 Trivy 결과를 1:1로 맞추겠다는 집착을 버리는 겁니다. 운영 관점에서는 그게 핵심이 아니거든요. 실제로 중요한 건 두 가지였습니다. 이 PR이 위험한 변경을 추가했는가, 개발자가 왜 막혔는지 스스로 이해하고 수정할 수 있는가. 이 기준으로 보면 도구 이름보다 결과 전달 방식이 더 중요합니다.

    또 하나, tfsec에서 오래 누적된 예외는 그대로 가져오지 않는 편이 좋습니다. 경험상 문제였던 예외는 두 종류였습니다. “급해서 일단 무시”한 것, 그리고 “당시에는 합리적이었지만 지금은 맥락이 사라진 것”입니다. 이걸 새 도구로 그대로 옮기면 전환 첫 주부터 신뢰가 흔들립니다.

    3. Trivy IaC 보안 기본 구성: 로컬 재현부터 잡아야 덜 흔들립니다

    CI부터 붙이면 로그만 길어지고 해석은 느려집니다. 저는 반드시 로컬에서 같은 결과가 나오는 최소 루틴부터 만듭니다. Trivy는 Terraform 디렉터리, Terraform plan 파일, plan JSON까지 공식 문서 기준으로 지원해서 시작점이 비교적 분명합니다.

    3-1. Terraform 디렉터리 스캔: 가장 먼저 붙일 최소 명령

    trivy config ./terraform
    
    trivy config \
      --severity HIGH,CRITICAL \
      --exit-code 1 \
      --format table \
      ./terraform

    첫 번째 줄은 탐색용이고, 두 번째 줄은 CI 차단용 기본형입니다. 실무에서 특히 중요한 옵션은 세 가지입니다.

    • --severity HIGH,CRITICAL: 초기 도입 단계에서 잡음이 과도하게 늘어나는 걸 막아줍니다.
    • --exit-code 1: 결과가 발견되면 빌드를 실패시켜 정책을 기술적으로 강제합니다.
    • --format table: 사람이 바로 읽기 좋습니다. 개발자 피드백 단계에서는 아직도 꽤 유효합니다.

    초기부터 MEDIUM까지 한꺼번에 막고 싶어질 수 있는데, 저는 대체로 말립니다. 첫 도입의 목적은 모든 문제를 한 번에 청소하는 게 아니라, 새로운 고위험 변경이 무심코 들어오는 걸 차단하는 것이기 때문입니다. 이 순서를 뒤집으면 도구가 바로 미움받습니다.

    3-2. tfvars와 다운로드 모듈 처리: 실제 결과 품질을 좌우하는 옵션

    trivy config \
      --tf-vars env/prod.tfvars \
      --tf-exclude-downloaded-modules \
      --severity HIGH,CRITICAL \
      --exit-code 1 \
      ./infra/env/prod

    이 명령은 보기보다 중요합니다. --tf-vars를 주지 않으면 기본값 기준으로 해석돼 실제 배포 맥락과 다른 결과가 나올 수 있습니다. 반대로 다운로드된 모듈까지 전부 스캔하면, 현재 PR과 직접 관련 없는 외부 모듈 이슈가 섞여 리뷰 집중도가 떨어질 수 있습니다. 그래서 저는 애플리케이션 팀 저장소에서는 --tf-exclude-downloaded-modules를 먼저 켜고, 공통 플랫폼 팀 저장소에서는 별도 검증 잡으로 모듈까지 본다는 식으로 나눕니다.

    이 지점이 운영비와 바로 연결됩니다. 도구가 무료여도 리뷰 시간이 비싸면 이미 손해입니다. 실제 위험과 직접 수정 가능성이 높은 결과부터 보여줘야 개발자도 반응합니다.

    3-3. JSON 아티팩트 저장: 운영 자동화는 여기서 시작됩니다

    mkdir -p artifacts
    trivy config \
      --format json \
      --output artifacts/trivy-iac.json \
      ./terraform
    
    jq '.Results[] | {target: .Target, misconfig_count: (.Misconfigurations | length)}' artifacts/trivy-iac.json

    표 형식은 사람이 보기 좋고, JSON은 시스템이 다루기 좋습니다. 저도 Trivy로 넘어오면서 이 부분이 꽤 편해졌습니다. 같은 결과를 PR 코멘트, 대시보드, 주간 리포트에 재사용하기 쉬워지거든요. 인프라 코드 보안 자동화를 넓히려면 결국 여기까지 가게 됩니다.

    Trivy IaC 보안 검사와 JSON 아티팩트 생성 흐름 이미지

    개발자가 커밋하면 CI에서 Trivy가 IaC를 검사하고 결과 JSON과 요약 리포트를 남기는 흐름을 보여주는 이미지입니다.

    4. tfsec에서 Trivy로 옮길 때 실제로 손보게 되는 지점

    현업에서 삽질을 줄이려면 차이를 추상적으로 보면 안 됩니다. “둘 다 IaC 검사 도구” 수준으로는 의사결정이 안 나옵니다. 운영 포인트를 기준으로 봐야 합니다.

    항목 tfsec 중심 운영 Trivy 중심 운영 제가 권하는 판단 기준
    주력 범위 Terraform 검사에 집중 IaC, filesystem, secret, image까지 확장 가능 Terraform만 보면 tfsec도 충분하지만, 도구 표준화가 목표면 Trivy가 유리합니다.
    도입 리스크 기존 팀 습관 유지가 쉬움 정책과 출력 소비 방식을 같이 바꿔야 함 전환 자체보다 예외 재정비 비용이 더 큽니다.
    결과 소비 Terraform 검사 결과 중심 JSON, SARIF, 테이블을 다른 스캔과 함께 운영하기 쉬움 PR 자동화나 보안 대시보드 연계가 목표면 Trivy 쪽 손이 덜 갑니다.
    실패 기준 설계 기존 룰·예외 유지가 쉬움 심각도 기반 단계적 차단이 자연스러움 처음엔 HIGH, CRITICAL만 차단하고 점진적으로 넓히는 편이 안정적입니다.
    운영비 도구 추가 시 문서와 파이프라인이 분산 한 CLI 습관으로 통합 가능 소규모 팀일수록 사람 시간 절감 효과가 큽니다.
    예외 관리 기존 무시 목록 답습 가능 만료일·속성 기반 예외 재설계가 가능 예외를 기술 부채로 관리하려면 Trivy 방식이 더 낫습니다.

    표에서 핵심은 기능 수가 아닙니다. 운영비가 어디서 줄고 어디서 새는지입니다. 저도 도구를 비교할 때 늘 “한 달 뒤 누가 덜 귀찮은가”를 봅니다. Trivy는 그 질문에 꽤 강한 편입니다.

    4-1. GitHub Actions 예시: SARIF 업로드 단계까지 넣어야 보안 탭 연계가 됩니다

    name: trivy-iac-scan
    
    on:
      pull_request:
      push:
        branches:
          - main
    
    jobs:
      scan:
        runs-on: ubuntu-latest
        permissions:
          contents: read
          security-events: write
        steps:
          - name: Checkout
            uses: actions/checkout@v4
    
          - name: Run Trivy config scan
            uses: aquasecurity/[email protected]
            with:
              scan-type: 'config'
              scan-ref: '.'
              hide-progress: true
              format: 'sarif'
              output: 'trivy-iac.sarif'
              severity: 'HIGH,CRITICAL'
              exit-code: '1'
    
          - name: Upload SARIF to GitHub code scanning
            if: always()
            uses: github/codeql-action/upload-sarif@v4
            with:
              sarif_file: trivy-iac.sarif
              category: trivy-iac
    
          - name: Upload artifact
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: trivy-iac-sarif
              path: trivy-iac.sarif

    여기서 버전 고정을 강조하는 이유가 있습니다. 보안 도구 도입 시 액션 참조를 떠다니는 브랜치에 두면 운영 안정성이 떨어집니다. 스캔 결과 변화가 코드 변경 때문인지, 액션 변경 때문인지 구분이 어려워지거든요. 실무에서는 탐지 정확도만큼이나 결과의 재현성이 중요합니다.

    하나 더 짚으면, SARIF 파일을 만드는 것만으로 GitHub 보안 탭에 자동 반영되지는 않습니다. github/codeql-action/upload-sarif 같은 업로드 단계가 있어야 코드 스캐닝 결과로 연결됩니다. 이 부분을 빼먹는 팀이 꽤 많았습니다.

    4-2. trivy.yaml로 정책을 저장소에 고정해두면 운영이 덜 흔들립니다

    format: json
    exit-code: 1
    severity:
      - HIGH
      - CRITICAL
    scan:
      skip-dirs:
        - modules/legacy
    misconfiguration:
      scanners:
        - terraform
    terraform:
      vars:
        - env/prod.tfvars
      exclude-downloaded-modules: true

    옵션이 CI 파일 곳곳에 흩어지면 유지보수가 피곤합니다. 반대로 정책 파일로 묶어두면 저장소 단위 코드 리뷰가 됩니다. 저는 이 방식을 선호합니다. 보안 정책이 파이프라인 구현 세부사항 속에 숨어 있으면, 변경 이력이 결국 사람 기억에만 남기 때문입니다.

    4-3. 재현 가능한 시나리오 하나: 왜 막혔는지가 보여야 수정이 빨라집니다

    AWS security group에 SSH를 0.0.0.0/0로 열어둔 Terraform 변경이 PR에 들어왔다고 가정해보겠습니다. 이런 건 교육용 환경, 임시 장애 대응, 외부 벤더 점검 같은 이유로 종종 등장합니다. 문제는 대부분 넣은 이유보다 그 상태가 운영에 남는 것입니다.

    제가 해보니 개발자가 가장 빨리 반응하는 메시지는 단순한 “빌드 실패”가 아니었습니다. 어느 리소스가 왜 위험한지, 이 변경이 인터넷 노출인지 내부 한정인지를 같이 보여줄 때 반응 속도가 훨씬 빨랐습니다. 그래서 PR 자동화도 개수 집계만 하지 말고 Target, 리소스 위치, 심각도, 수정 우선순위를 함께 전달하는 쪽이 낫습니다.

    Trivy IaC 보안 관점에서 안전한 설정과 위험한 설정을 비교한 이미지

    안전한 설정과 위험한 설정을 나란히 보여주며, IaC 보안 검사에서 어떤 패턴을 주의해야 하는지 설명하는 이미지입니다.

    5. Trivy IaC 보안에서 비용 효율은 라이선스가 아니라 운영 마찰에서 갈립니다

    오픈소스 보안 비용을 설명할 때 “무료니까 이득”으로 끝내면 실제 예산 설명이 잘 안 됩니다. 보안 리더나 팀장이 궁금한 건 구매비보다 사람 시간을 얼마나 아끼거나 태우는가입니다. 저는 비용을 다섯 덩어리로 나눠 봅니다.

    • 학습 비용: CLI 사용법, 결과 해석, 예외 작성 방식에 팀이 적응하는 시간
    • 파이프라인 유지 비용: 액션 버전 고정, 캐시, 출력 포맷, 실패 기준 조정 비용
    • 예외 관리 비용: 무시한 항목의 사유, 소유자, 만료 시점을 추적하는 비용
    • 도구 중복 비용: Terraform, secret, image를 다른 도구로 운영하며 문서와 자동화가 흩어지는 비용
    • 리뷰 비용: 결과는 많은데 실제 수정 우선순위가 안 보여 triage 시간이 불어나는 비용

    여기서 Trivy 장점은 도구 수를 줄여 문맥 전환 비용을 낮춘다는 데 있습니다. 반면 한 CLI에 여러 스캔을 몰아넣기 시작하면 초기에 결과량이 급격히 많아져 시끄럽게 느껴질 수 있습니다. 그래서 저는 “통합은 하되 차단은 분리”를 권합니다. 명령 체계는 하나로 모으되, IaC misconfiguration 차단과 secret 경고, image 취약점 차단은 운영 단계를 다르게 두는 방식입니다.

    6. 흔한 실패 모드와 근본 원인: 여기서 시간이 제일 많이 샙니다

    문서만 보면 보안 스캔은 명령 한 줄처럼 보이지만, 실제 장애는 대부분 입력 경계와 해석 방식에서 납니다. 제가 자주 본 문제를 원인 중심으로 정리해보겠습니다.

    6-1. 결과가 너무 많아져서 아무도 안 보는 문제

    근본 원인은 도구 성능보다 정책 설계 실패인 경우가 많습니다. 처음부터 모든 심각도와 모든 스캐너를 한 번에 차단하면, 개발자는 보안 경고를 “수정해야 할 문제”가 아니라 “우회해야 할 소음”으로 인식합니다.

    현실적인 해법은 이 순서가 좋았습니다.

    1. 1단계: HIGH,CRITICAL만 차단
    2. 2단계: MEDIUM은 JSON 또는 SARIF로 남기고 주간 triage
    3. 3단계: 반복 발생 항목은 공통 모듈 수정 또는 코딩 가이드로 환류

    차단 기준이 곧 팀의 메시지입니다. 이걸 너무 넓게 잡으면 보안 경고가 금방 배경 소음이 됩니다.

    6-2. 로컬은 통과하는데 CI만 실패하는 문제

    이건 보통 세 가지 원인으로 갈립니다. 첫째, 스캔 루트가 다릅니다. 둘째, tfvars가 다릅니다. 셋째, Terraform이 참조하는 파일 경계가 다릅니다. Trivy는 스캔 루트를 신뢰 경계로 보기 때문에, 하위 디렉터리만 스캔하면 상위 경로의 file() 참조가 깨질 수 있습니다.

    # 저장소 루트에서 실행해 참조 파일 경계를 맞춥니다.
    trivy config --skip-dirs envs/staging ./
    
    # 배포 단위별로 동일한 규칙을 강제합니다.
    for dir in infra/env/dev infra/env/prod; do
      echo "Scanning ${dir}"
      trivy config --tf-vars "${dir}/terraform.tfvars" --severity HIGH,CRITICAL --exit-code 1 "${dir}"
    done

    이럴 때 저는 “왜 CI만 다르지?”보다 “스캔 루트를 누가 정의하고 있지?”부터 봅니다. 대부분 거기서 답이 나옵니다.

    6-3. downloaded module까지 스캔되어 현재 PR과 무관한 노이즈가 섞이는 문제

    근본 원인은 책임 범위가 섞였기 때문입니다. 애플리케이션 팀 PR에서 외부 모듈 내부 구현까지 한 번에 막아버리면, 수정 권한이 없는 이슈 때문에 파이프라인이 실패할 수 있습니다.

    이럴 땐 애플리케이션 저장소 차단 잡에서는 --tf-exclude-downloaded-modules를 사용하고, 공통 모듈 검증은 모듈 저장소 자체에서 별도 강제하는 편이 낫습니다. 문제를 만든 저장소에서 문제를 막는 구조가 운영비도 가장 낮습니다.

    6-4. private module 때문에 CI에서 스캔이 깨지는 문제

    문제는 스캐너보다 인증입니다. Terraform이 private module을 내려받아야 하는데 CI가 그 권한을 갖고 있지 않으면 결과가 비거나 오류가 섞입니다. 이 경우 스캔 실패를 보안 이슈로 오해하면 안 됩니다. 먼저 모듈 접근 경로부터 복구해야 합니다.

    실무에서는 체크아웃 뒤에 필요한 Git 인증이나 Terraform registry 토큰을 명시적으로 설정하는 방식이 가장 덜 헷갈렸습니다. 포인트는 이걸 보안 도구 오작동으로 분류하지 않는 것입니다. 입력 데이터를 못 읽는 스캔은 검사 품질 이전의 문제입니다.

    6-5. secret도 본다고 생각했는데 사실은 misconfiguration만 돌고 있는 문제

    이건 생각보다 자주 나옵니다. trivy config는 IaC misconfiguration 검사에 초점을 둡니다. secret까지 같이 보려면 filesystem 스캔에서 스캐너를 명시적으로 지정하는 편이 더 명확합니다.

    trivy fs \
      --scanners misconfig,secret \
      --severity HIGH,CRITICAL \
      --exit-code 1 \
      ./infra

    제가 권하는 운영 방식은 이렇습니다. PR 차단은 우선 trivy config로 IaC misconfiguration에 집중하고, secret 검사는 별도 잡으로 분리하세요. remediation 주체와 사고 대응 절차가 달라서 분리하는 편이 실제 운영에서는 편합니다.

    6-6. plan 파일과 HCL 결과가 달라 보이는 문제

    Terraform plan을 스캔하면 배포 직전 상태를 본다는 장점이 있습니다. 다만 Trivy 공식 문서도 plan JSON에서 for_each나 count 표현식 관련 한계를 안내하고 있습니다. 그래서 저는 HCL 스캔과 plan 스캔을 경쟁 관계로 보지 않습니다.

    • HCL 스캔: 작성 습관과 코드 리뷰 단계에서 빠르게 막기 좋습니다.
    • plan 스캔: 실제 적용 직전 리소스 상태를 한 번 더 확인하기 좋습니다.
    terraform plan --out tfplan
    trivy config tfplan
    
    terraform show -json tfplan > tfplan.json
    trivy config tfplan.json

    운영 경험상 PR 단계에서는 HCL, 배포 직전에는 plan을 추가하는 2단 구성이 가장 안정적이었습니다.

    6-7. 예외가 무기한 방치되는 문제

    예외는 필요합니다. 문제는 만료일 없는 예외가 제도화되는 순간입니다. Trivy는 인라인 ignore에 만료일을 붙일 수 있어서 이 부분이 실무에서 꽤 유용합니다.

    #trivy:ignore:aws-s3-enable-logging:exp:2026-12-31
    resource "aws_s3_bucket" "example" {
      bucket = "example-bucket"
    }

    이 방식이 좋은 이유는 간단합니다. 왜 무시했는지와 언제 다시 봐야 하는지가 코드 옆에 남기 때문입니다. 중앙 ignore 파일만 계속 키우면 몇 달 뒤엔 아무도 그 사유를 설명하지 못합니다.

    7. 검증과 결과 확인: 전환 성공 여부는 탐지와 운영 부담을 같이 봐야 합니다

    도구 전환이 끝났다고 말하려면 두 가지를 같이 확인해야 합니다. 위험한 변경을 실제로 잡는가, 그리고 팀이 그 결과를 소화할 수 있는가입니다. 저는 아래 순서로 검증합니다.

    1. 의도적으로 위험한 Terraform 변경을 샘플 브랜치에 넣어 탐지되는지 확인
    2. HIGH,CRITICAL 결과에서만 CI가 실패하는지 확인
    3. JSON 또는 SARIF 아티팩트가 남아 후처리에 재사용되는지 확인
    4. 개발자가 경고 메시지를 보고 수정 포인트를 바로 찾을 수 있는지 확인
    5. 예외가 코드 옆 또는 정책 파일에 이유와 만료일을 남기는 구조인지 확인

    마지막 항목이 빠지면 전환은 반쪽입니다. 단순 탐지는 누구나 붙일 수 있지만, 결과가 조직 안에서 오래 유지되는 방식까지 설계해야 운영 품질이 올라갑니다.

    제가 Trivy 전환 뒤 가장 크게 느낀 변화는 “보안 도구가 늘어나는 속도”가 늦어졌다는 점이었습니다. 문서도 짧아지고, 신규 입사자 설명도 간단해지고, 파이프라인 장애를 볼 때 원인 축도 줄어듭니다. 무료 도구의 가성비는 결국 이런 데서 갈립니다. Kubernetes나 컨테이너 이미지 쪽까지 넓힐 계획이라면 관련 보안 글도 함께 읽어보시면 흐름을 잡는 데 도움이 됩니다.

    Trivy IaC 보안 스캔 결과와 심각도 분류를 보여주는 대시보드 이미지

    심각도별 결과 요약, 실패 여부, 아티팩트 저장 상태를 한눈에 확인하는 검증 결과 이미지입니다.

    8. 현업 추천 시나리오와 다음 단계

    선택 기준은 명확할수록 좋습니다. 제가 현장에서 자주 권하는 방식은 아래와 같습니다.

    • Terraform 위주 소규모 저장소: tfsec를 당장 걷어내기보다 같은 경로를 Trivy로 병행 스캔해 결과 차이부터 확인하세요.
    • Terraform과 Kubernetes, 이미지 스캔까지 같이 보는 팀: Trivy를 중심축으로 묶는 편이 유지비가 낮습니다.
    • CI 실패에 민감한 조직: 차단은 HIGH,CRITICAL부터 시작하고, 나머지는 리포트만 남기세요.
    • 예외가 많은 조직: 전환 전에 예외 사유 정리부터 해야 합니다. 이걸 건너뛰면 새 도구가 아니라 새 혼란만 생깁니다.
    • 공통 모듈을 별도 저장소로 관리하는 조직: 애플리케이션 저장소 차단과 모듈 저장소 차단을 분리하세요.

    제 추천은 단순합니다. 도구 표준화가 목표면 Trivy, 변경 리스크를 최소화해야 하면 tfsec와 병행 검증 후 단계 전환입니다. secret, image, filesystem까지 한 CLI 체계로 묶고 싶다면 운영 일관성 쪽 가치가 생각보다 큽니다.

    다음 단계도 분명합니다. 1) 로컬에서 trivy config 최소 명령을 고정하고, 2) 저장소별 차단 심각도를 정하고, 3) JSON 또는 SARIF 아티팩트 소비처를 정하고, 4) 예외에 만료일을 붙이세요. 여기까지 가야 전환이 끝납니다. 설치만 끝난 상태는 아직 운영이 아닙니다.

    언제 tfsec 병행이 맞고, 언제 Trivy 중심 전환이 유리한지 빠르게 판단할 수 있도록 정리한 요약 이미지입니다.

    9. 짧은 FAQ

    Q. tfsec를 바로 버려도 될까요?

    저는 바로 교체하기보다 병행 검증을 권합니다. 같은 저장소를 일정 기간 둘 다 돌려보면, 어떤 결과가 달라지는지와 예외가 어디서 충돌하는지가 먼저 보입니다.

    Q. Trivy IaC 보안만으로 secret까지 같이 해결되나요?

    IaC misconfiguration 관점에서는 trivy config가 출발점이 맞습니다. 다만 secret까지 같은 단계에서 다루려면 trivy fs --scanners misconfig,secret처럼 스캐너 구성을 의도적으로 나누는 편이 운영상 더 낫습니다.

    Q. 오픈소스 보안 비용은 어떻게 설명해야 할까요?

    라이선스보다 운영 마찰로 설명하는 편이 낫습니다. 도구 수, 예외 재검토 시간, 리뷰 잡음, CI 유지비, 신규 인원 교육비가 핵심입니다.

    Q. Trivy IaC 보안은 누구에게 특히 맞나요?

    Terraform만 보는 팀보다, Terraform과 Kubernetes, 파일시스템, secret, 이미지 스캔을 한 체계로 가져가려는 팀에 더 잘 맞습니다.

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

  • [보안 사례] 컨테이너 이미지 보안 강화: Trivy를 활용한 CI/CD 통합 운영 후기

    [보안 사례] 컨테이너 이미지 보안 강화: Trivy를 활용한 CI/CD 통합 운영 후기

    [보안 사례] 컨테이너 이미지 보안 강화: Trivy를 활용한 CI/CD 통합 운영 후기

    컨테이너 보안 사례를 찾다 보면 다들 비슷한 이야기만 하더라고요. “스캔하세요”, “취약점 관리하세요” 같은 말은 맞는데, 막상 운영 환경에 넣으려면 어디서부터 손대야 할지 애매합니다. 저도 홈랩과 사내 비슷한 구조의 테스트 환경에서 Trivy(트리비, 오픈소스 취약점 스캐너)를 붙여서 이미지 스캔을 자동화해봤는데, 처음엔 이게 생각보다 손이 많이 가더라고요. 특히 CI/CD 보안 관점에서 “스캔은 했는데 배포는 누가 막지?”, “베이스 이미지가 바뀌면 기존 정책은 어떻게 하지?” 같은 현실적인 문제가 꼭 나옵니다.

    이번 글은 그런 분들을 위한 컨테이너 보안 사례 정리입니다. Trivy를 활용해서 CI/CD 보안 흐름에 이미지 검사를 붙이고, Docker 보안과 Kubernetes 보안까지 운영 관점에서 어떻게 연결했는지 경험 기반으로 풀어보겠습니다. 제가 직접 해보니 도구 자체보다도 언제 검사하고, 어떤 기준으로 실패 처리할지가 진짜 핵심이었습니다.

    컨테이너 보안 사례 기반 Trivy CI/CD 파이프라인 아키텍처 이미지

    CI 단계에서 이미지 빌드, Trivy 스캔, 레지스트리 푸시, Kubernetes 배포 전 검증까지 이어지는 전체 흐름을 보여주는 이미지입니다.

    1. 왜 컨테이너 이미지 보안이 운영에서 중요할까

    쉽게 말해 컨테이너 이미지는 서버의 골격을 압축해 둔 결과물입니다. 애플리케이션 코드만 들어가는 게 아니라, 베이스 OS 패키지, 런타임, 시스템 라이브러리까지 같이 들어가거든요. 그래서 이미지 안에 취약한 패키지가 하나만 있어도, 실제 운영에서는 꽤 민감한 이슈가 됩니다.

    예전엔 VM(Virtual Machine, 가상머신) 단위로 관리하던 걸 컨테이너로 옮기면서 배포 속도는 빨라졌는데, 그만큼 이미지가 더 자주 만들어지고 더 자주 배포됩니다. 문제는 속도가 빨라질수록 사람이 수동으로 체크할 여유가 없어집니다. 저도 처음엔 배포 직전에 한 번만 확인하면 되겠지 싶었는데, 실제로 써보니까 그 방식은 오래 못 갑니다. 빌드마다 자동으로 검사하고, 위험 수준이 높은 건 파이프라인에서 바로 걸러야 하더라고요.

    • 빌드 시점: 취약한 패키지가 이미지에 포함되는지 확인
    • 푸시 시점: 레지스트리(Registry, 이미지 저장소)에 위험한 이미지가 쌓이지 않게 제어
    • 배포 시점: Kubernetes(쿠버네티스, 컨테이너 오케스트레이션)에서 검증되지 않은 이미지 실행 방지

    여기서 중요한 포인트! 컨테이너 보안 사례를 보면 대부분 “도구 설치”에서 끝나는데, 운영은 그 다음부터 시작입니다.

    2. Trivy가 뭐고, 왜 많이 쓰는가

    Trivy는 Aqua Security에서 공개한 오픈소스 보안 스캐너로, 컨테이너 이미지 안의 OS 패키지와 애플리케이션 의존성 취약점을 확인할 수 있습니다. 그리고 이게 편한 이유가 뭔가 하면, 명령어가 비교적 단순하고 CI/CD에 넣기 쉽습니다. 저도 처음엔 Clair나 다른 스캐너도 같이 봤었는데, 실무에서는 빠르게 붙고 운영 부담이 적은 쪽이 오래 갑니다.

    쉽게 말해 Trivy는 “이 이미지 안에 알려진 취약점이 있는지”를 검사해주는 첫 번째 관문입니다. 그리고 필요하면 설정 파일 오탐도 잡고, 파일시스템이나 Git 저장소 스캔에도 활용할 수 있죠. 다만 오늘은 범위를 넓히기보다 이미지 스캔 중심의 CI/CD 보안 이야기에 집중하겠습니다.

    항목 Trivy 적용 전 Trivy 적용 후
    취약점 발견 시점 배포 후 또는 수동 점검 빌드 단계에서 자동 확인
    운영자 개입 높음 정책 기반 자동화 가능
    재현성 담당자 경험 의존 파이프라인 기준으로 일관됨
    Docker 보안 관리 개별 확인 공통 정책으로 통제

    3. 운영 기준 먼저 정하기: 실패 조건을 정하지 않으면 자동화가 흔들립니다

    이건 제가 꽤 삽질했던 부분입니다. 처음엔 취약점이 하나라도 나오면 실패 처리해봤거든요. 결과는 뻔했습니다. 팀이 바로 우회 플래그를 찾기 시작합니다. 왜냐하면 현실적으로 베이스 이미지 하나만 바뀌어도 Low나 Medium 수준이 여러 개 잡힐 수 있기 때문입니다.

    그래서 운영 기준을 이렇게 나눴습니다.

    1. 개발 브랜치: 결과는 남기되 즉시 차단하지 않음
    2. 메인 브랜치: High, Critical 취약점이 있으면 실패
    3. 릴리스 태그: 스캔 결과 아티팩트 보관 + 예외 승인된 항목만 통과
    4. 운영 배포: 승인된 이미지 다이제스트(Digest, 내용 기반 식별자)만 사용

    이렇게 나누니까 팀도 덜 답답해하고, 보안팀이나 인프라 담당도 기준을 설명하기 쉬워졌습니다. 결국 CI/CD 보안은 기술보다 합의가 먼저더라고요.

    4. 실전 구현: Trivy를 CI/CD 파이프라인에 붙이는 방식

    제가 가장 단순하게 시작했던 흐름은 이렇습니다. Docker 이미지 빌드 후 Trivy로 스캔하고, 결과가 기준을 넘으면 푸시나 배포를 막는 구조입니다. 여기서는 예시로 GitHub Actions 스타일 YAML을 보여드리지만, GitLab CI나 Jenkins에서도 개념은 거의 같습니다.

    4-1. 로컬에서 먼저 이미지 스캔해보기

    자동화 전에 로컬에서 결과를 보는 게 좋습니다. 그래야 나중에 파이프라인 로그를 봐도 덜 헷갈립니다.

    docker build -t sample-app:local .
    trivy image sample-app:local
    

    조금 더 실무적으로 보려면 심각도(Severity, 취약점 등급)를 제한해서 보는 게 편합니다.

    trivy image --severity HIGH,CRITICAL sample-app:local
    

    JSON 결과를 파일로 저장해두면 후처리에도 좋습니다.

    trivy image --format json --output trivy-result.json sample-app:local
    

    4-2. GitHub Actions 예시

    name: container-security
    
    on:
      push:
        branches:
          - main
      pull_request:
    
    jobs:
      build-and-scan:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v4
    
          - name: Set up Docker Buildx
            uses: docker/setup-buildx-action@v3
    
          - name: Build image
            run: docker build -t sample-app:${{ github.sha }} .
    
          - name: Run Trivy scan
            run: |
              trivy image \
                --exit-code 1 \
                --severity HIGH,CRITICAL \
                --format table \
                sample-app:${{ github.sha }}
    
          - name: Save JSON report
            if: always()
            run: |
              trivy image \
                --format json \
                --output trivy-report.json \
                sample-app:${{ github.sha }}
    
          - name: Upload report artifact
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: trivy-report
              path: trivy-report.json
    

    여기서 중요한 건 –exit-code 1입니다. 이 옵션이 있어야 기준에 걸렸을 때 파이프라인이 실제로 실패합니다. 처음엔 결과만 출력하고 끝내는 식으로 구성했었는데, 그러면 운영에서 아무도 안 봅니다. 진짜입니다. 로그는 쌓이는데 아무도 안 봐요.

    컨테이너 보안 사례에서 Trivy 이미지 스캔이 포함된 CI/CD 보안 구성 이미지

    개발 브랜치, 메인 브랜치, 릴리스 태그에 따라 서로 다른 스캔 정책이 적용되는 예시 화면 또는 구성 다이어그램용 이미지입니다.

    4-3. 무시할 취약점은 최소한으로 관리하기

    운영하다 보면 당장 패치가 어려운 항목이 나옵니다. 이럴 때 무작정 무시하지 말고, 예외를 추적 가능한 방식으로 남겨야 합니다.

    # .trivyignore 예시
    CVE-2023-00000
    CVE-2023-11111
    

    저는 이 파일을 넣을 때 리뷰 규칙을 따로 뒀습니다.

    • 왜 무시하는지 이슈 번호 남기기
    • 대체 패치 일정 적기
    • 베이스 이미지 교체 가능 여부 확인하기

    안 그러면 .trivyignore가 점점 커지고, 나중엔 형식적인 보안이 되어버립니다.

    4-4. Kubernetes 배포 전 검증 포인트

    Kubernetes 보안 관점에서는 “스캔 완료된 이미지인가”와 “검증된 이미지 버전만 쓰는가”가 핵심입니다. 가장 기본은 태그(tag)보다 다이제스트 고정입니다. latest 같은 태그는 진짜 편해 보여도 운영에서는 추적이 어려워집니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sample-app
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: sample-app
      template:
        metadata:
          labels:
            app: sample-app
        spec:
          containers:
            - name: sample-app
              image: registry.example.com/sample-app@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
              imagePullPolicy: IfNotPresent
              securityContext:
                runAsNonRoot: true
                readOnlyRootFilesystem: true
    

    이미지 스캔만으로 Kubernetes 보안이 끝나는 건 아니고요, securityContext(시큐리티 컨텍스트), 비루트 실행, 읽기 전용 파일시스템 같은 기본 하드닝(hardening, 보안 강화)도 같이 가야 합니다.

    5. 제가 실제로 겪은 문제들: 트러블슈팅과 운영 팁

    여기서부터가 좀 현실적인 이야기입니다. 도구 소개 글에는 잘 안 나오는데, 실제로 붙이면 은근히 이런 문제를 많이 겪습니다.

    5-1. 베이스 이미지가 원인인 경우가 많습니다

    애플리케이션 코드는 멀쩡한데 계속 High가 뜨는 경우가 있었습니다. 알고 보니 대부분 베이스 이미지 문제였어요. 예를 들어 오래된 패키지를 포함한 이미지라면 애플리케이션 수정으로 해결이 안 됩니다. 이럴 땐 Dockerfile부터 봐야 합니다.

    FROM python:3.11-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    CMD ["python", "app.py"]
    

    여기서도 끝이 아닙니다. 같은 slim 계열이라도 시점에 따라 포함 패키지가 달라질 수 있거든요. 그래서 정기 재빌드가 중요합니다. 코드를 안 바꿔도 다시 빌드하면 해결되는 취약점이 생각보다 많습니다.

    5-2. 스캔 시간 때문에 파이프라인이 느려질 수 있습니다

    처음엔 모든 브랜치에서 전체 스캔을 돌렸더니 CI 시간이 확 늘어났습니다. 이거 진짜 불만 많이 나옵니다. 그래서 저는 전략을 나눴습니다.

    • Pull Request 단계: High, Critical 중심 빠른 검사
    • Main 병합 후: 전체 이미지 스캔 + 결과 보관
    • 야간 배치: 주요 이미지 일괄 재스캔

    이렇게 하면 개발 흐름을 너무 막지 않으면서도 운영 기준은 유지할 수 있습니다.

    5-3. 취약점 수보다 맥락이 중요합니다

    숫자만 보고 “취약점 27개”라고 하면 다들 놀랍니다. 근데 실제로는 실행 경로와 무관한 패키지일 수도 있고, 이미 수정판이 준비된 경우도 있습니다. 반대로 개수가 적어도 원격 코드 실행(RCE, Remote Code Execution) 관련이면 우선순위가 훨씬 높죠. 그래서 대시보드에는 단순 개수보다 Critical 존재 여부, 고정 가능한지 여부, 운영 노출도를 같이 봤습니다.

    컨테이너 보안 사례의 Trivy 취약점 결과와 이미지 스캔 대시보드 이미지

    Critical, High, 수정 가능 여부, 베이스 이미지 기인 여부 등을 기준으로 결과를 정리한 보안 리포트 대시보드 이미지입니다.

    6. 검증과 결과: 무엇이 달라졌나

    몇 달 운영해보니 체감되는 변화가 꽤 분명했습니다. 가장 큰 건 “배포 후 발견”이 줄어든 겁니다. 이전에는 이미지가 이미 레지스트리에 올라가고 나서 보안 이슈를 알게 되는 경우가 있었는데, 지금은 빌드 단계에서 대부분 걸러집니다. 이 차이가 큽니다.

    • 개발자가 PR 단계에서 위험한 이미지 변경을 바로 인지
    • 운영자는 승인된 기준으로만 배포 가능
    • 레지스트리에 누적되는 불량 이미지 감소
    • Docker 보안 점검이 개별 담당자 감에 덜 의존

    또 하나 좋았던 건 커뮤니케이션입니다. 예전엔 보안 이슈가 나오면 “누가 확인했나요?”부터 시작했는데, 이제는 “이 이미지는 Trivy 기준을 통과했는가”로 대화가 정리됩니다. 기준이 생기니까 감정 소모가 줄더라고요.

    검증 체크리스트

    1. 빌드 직후 Trivy 스캔이 자동 실행되는가
    2. High, Critical 기준에서 파이프라인 실패가 동작하는가
    3. JSON 결과가 아티팩트로 저장되는가
    4. 운영 배포에서 다이제스트 기반 이미지를 사용하는가
    5. Kubernetes 보안 설정이 최소 기준을 만족하는가
    trivy image --severity HIGH,CRITICAL --exit-code 1 registry.example.com/sample-app:release
    kubectl get deploy sample-app -o yaml
    

    위 두 줄만 봐도 1차 확인은 꽤 됩니다. 간단하지만 실전에서는 이 기본기가 중요합니다.

    7. 운영하면서 정리한 추천 정책

    혹시 이제 막 도입하시는 분이라면, 처음부터 너무 거창하게 가지 않는 걸 추천드립니다. 저도 처음엔 모든 걸 다 통제하려다 오히려 반발만 샀거든요.

    단계 추천 정책 이유
    1단계 메인 브랜치만 차단 개발 흐름 충격 최소화
    2단계 리포트 아티팩트 보관 추적성과 감사 대응 확보
    3단계 예외 관리 프로세스 도입 무분별한 ignore 방지
    4단계 Kubernetes 배포 정책 연계 CI와 런타임 보안 연결

    이 흐름으로 가면 도입 난이도도 적당하고, 팀이 적응할 시간도 벌 수 있습니다. 특히 컨테이너 보안 사례는 도구 선정보다 운영 규칙 설계가 더 중요하다는 점, 꼭 기억하시면 좋겠습니다.

    8. 마무리: Trivy는 시작점이고, 운영 기준이 완성도를 만듭니다

    정리해보면 Trivy 자체는 비교적 가볍게 시작할 수 있는 좋은 도구입니다. 다만 진짜 차이는 이미지 스캔 결과를 어떻게 의사결정에 반영하느냐에서 나옵니다. 제가 직접 해보니, 단순히 “스캔했다”보다 “배포를 막을 수 있다”, “예외를 기록할 수 있다”, “Kubernetes 보안까지 연결된다”가 훨씬 중요했습니다.

    그리고 이건 꼭 말씀드리고 싶네요. 처음부터 완벽하게 하려고 하면 오래 못 갑니다. 먼저 CI/CD 보안 흐름에 Trivy를 넣고, 그 다음 Docker 보안 기준을 정리하고, 마지막으로 Kubernetes 보안 정책과 연결하는 순서가 현실적이었습니다. 드디어 됐다 싶었던 순간도 있었고, 베이스 이미지 하나 때문에 몇 시간 날린 적도 있었는데요, 그런 삽질이 결국 운영 기준을 더 단단하게 만들어주더라고요.

    컨테이너 보안 사례 도입 전후와 CI/CD 보안 체크포인트 요약 이미지

    도입 전후 차이, 운영 체크리스트, 다음 단계 액션 아이템을 한눈에 보여주는 요약 인포그래픽용 이미지입니다.

    다음 단계로 해볼 만한 것

    • 레지스트리 단계 스캔 자동화 추가
    • SBOM(Software Bill of Materials, 소프트웨어 구성 명세) 관리 연계
    • Admission Controller(어드미션 컨트롤러, 배포 승인 제어) 기반 배포 정책 강화
    • 이전 글의 Dockerfile 경량화 내용과 연결해 베이스 이미지 개선

    다음 글에서는 컨테이너 보안 사례를 이어서, Kubernetes 배포 정책과 Admission Controller를 어떻게 엮으면 좋은지 좀 더 깊게 다뤄보려고 합니다. 그 부분도 운영에서 체감이 크거든요.

    정리 FAQ

    Q1. Trivy는 어느 시점에 넣는 게 가장 효과적인가요?

    가장 먼저는 이미지 빌드 직후입니다. 그 다음이 레지스트리 푸시 전, 마지막이 배포 전 검증입니다.

    Q2. 모든 취약점을 차단해야 하나요?

    아닙니다. 운영에서는 보통 High, Critical부터 시작하는 편이 현실적입니다. 대신 예외 기준은 명확해야 합니다.

    Q3. 이미지 스캔만 하면 Kubernetes 보안도 충분한가요?

    아쉽지만 아닙니다. 비루트 실행, 읽기 전용 파일시스템, 권한 최소화 같은 런타임 보안 설정이 같이 필요합니다.