13년차의 서버실

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

[태그:] CI/CD 보안

  • [보안] Trivy, Aqua Security, Snyk: 컨테이너 보안 스캐너 비용 및 기능 분석

    [보안] Trivy, Aqua Security, Snyk: 컨테이너 보안 스캐너 비용 및 기능 분석

    안녕하세요, 13년차 서버실 지킴이입니다. 🤓

    요즘 인프라 이야기에서 컨테이너(Container)를 빼놓을 수 없죠? Docker(도커)와 Kubernetes(쿠버네티스)가 대세가 되면서 개발 속도는 엄청나게 빨라졌지만, 그만큼 보안(Security)에 대한 고민도 깊어졌습니다. 제가 직접 여러 환경을 구축하고 운영하면서 느낀 건데, ‘개발 속도만 빠르면 장땡이지!’ 했다가 크게 데인 적이 한두 번이 아니거든요. 😅

    특히 컨테이너 이미지는 한 번 만들면 여러 환경에서 재사용되기 때문에, 이미지 안에 숨겨진 취약점은 나중에 큰 사고로 이어질 수 있습니다. 그래서 오늘은 컨테이너 이미지의 취약점을 미리 스캔하고 관리해주는 도구들, 그중에서도 Trivy, Aqua Security, Snyk 세 가지 솔루션의 비용(Cost)과 기능(Feature)을 제 경험을 바탕으로 심층 분석해보려고 합니다. 어떤 도구가 우리 환경에 가장 적합할지 함께 고민해보시죠!

    ⚠️ 면책 조항: 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근이나 공격은 불법이며 법적 처벌 대상이 될 수 있습니다. 모든 실습은 반드시 허가된 환경에서만 진행해주세요.

    컨테이너 보안 스캔 워크플로우 다이어그램: 개발부터 CI/CD, 스캐너를 통한 취약점 발견까지

    컨테이너 보안 스캐너는 개발-배포 파이프라인의 핵심 방어선입니다.

    컨테이너 보안 스캐너, 왜 필요할까요? (Concept Explanation)

    쉽게 말해, 컨테이너 보안 스캐너(Container Security Scanner)는 우리가 만든 도커 이미지(Docker Image) 안에 혹시 모를 취약점(Vulnerability)이나 잘못된 설정(Misconfiguration)이 없는지 꼼꼼히 검사해주는 도구입니다. 운영체제 라이브러리, 애플리케이션 종속성(dependencies), 심지어 설정 파일까지 싹 다 뒤져서 알려진 문제점들을 찾아주죠.

    이런 스캐너가 중요한 이유는 다음과 같거든요:

    • 조기 발견 및 수정(Early Detection & Remediation): 개발 단계에서 미리 취약점을 찾아서 고치면, 나중에 운영 환경에서 문제가 터지는 것보다 훨씬 적은 비용과 노력으로 해결할 수 있어요.
    • 규정 준수(Compliance): 특정 산업군에서는 보안 규정 준수가 필수인데, 스캐너를 활용하면 이런 요구사항을 더 쉽게 만족시킬 수 있거든요.
    • DevSecOps(데브섹옵스) 구현: 개발(Dev), 보안(Sec), 운영(Ops)을 통합하는 DevSecOps 문화에서 보안 스캐닝은 CI/CD 파이프라인에 필수적으로 통합되어야 할 요소입니다.

    Trivy로 컨테이너 이미지 스캔하기 (Hands-on Implementation)

    자, 그럼 제가 홈랩에서 가장 즐겨 쓰는 오픈소스 스캐너인 Trivy(트리비)를 직접 써보면서 어떻게 동작하는지 알아볼까요? Trivy는 Aqua Security에서 만든 오픈소스 취약점 스캐너인데, 빠르고 사용하기 쉽다는 장점 때문에 많은 개발자/운영자들에게 사랑받고 있습니다. 저도 처음엔 ‘이거 정말 공짜 맞아?’ 싶을 정도로 만족하며 쓰고 있거든요. 👍

    1. Trivy 설치

    macOS 기준으로 Homebrew(홈브루)를 사용하면 아주 간단하게 설치할 수 있습니다.

    
    # Trivy 설치 (macOS 예시)
    brew install aquasecurity/trivy/trivy
    
    # 설치 확인
    trivy --version
    # Output 예시:
    # Version: 0.49.1
    # ...
    

    다른 운영체제나 Docker 컨테이너로 실행하는 방법은 Trivy 공식 문서를 참고해주세요!

    2. Docker 이미지 스캔

    이제 설치된 Trivy로 특정 Docker 이미지를 스캔해볼까요? 저는 안정적인 버전의 Nginx(엔진엑스) 이미지를 예시로 사용하겠습니다. `nginx:1.21.6`은 비교적 널리 알려진 버전이라, 학습 데이터 컷오프 이전에도 충분히 존재했던 이미지입니다.

    
    # Nginx 1.21.6 이미지 스캔
    trivy image nginx:1.21.6
    

    명령어를 실행하면 Trivy가 해당 이미지의 레이어들을 분석하고, 발견된 취약점들을 severity(심각도)별로 정리해서 보여줄 겁니다. 처음엔 이게 뭔가 싶었는데, 자세히 보면 CVE(Common Vulnerabilities and Exposures) 번호와 함께 어떤 패키지에 어떤 문제가 있는지, 그리고 권고되는 해결책까지 친절하게 알려줘요. 정말 편하더라고요!

    Trivy 컨테이너 이미지 스캔 결과 예시: 심각도별 취약점 목록과 해결책

    Trivy 스캔 결과는 심각도와 함께 취약점 정보, 해결책을 명확하게 보여줍니다.

    삽질 경험: 너무 많은 경고, 어떻게 다루지? (Troubleshooting & Best Practices)

    Trivy를 처음 써봤을 때, ‘와, 세상에 이렇게 취약점이 많았다고?!’ 하면서 놀랐던 기억이 있습니다. 특히 공식 이미지인데도 CRITICAL(치명적), HIGH(높음) 등급의 경고가 수십 개씩 쏟아져 나오는 걸 보고 멘붕이 오기도 했었죠. 🤯

    여기서 중요한 포인트! 모든 경고를 당장 다 해결할 수는 없습니다. 중요한 건 우선순위(Priority)를 정하고, 우리 서비스에 미치는 영향(Impact)을 분석하는 겁니다.

    1. 미해결 취약점 필터링 (`–ignore-unfixed`)

    간혹 어떤 취약점은 아직 패치(patch)가 나오지 않아 해결할 수 없는 경우가 있습니다. 이런 경우까지 경고로 계속 뜨면 노이즈가 너무 심해져요. 이럴 땐 `–ignore-unfixed` 옵션을 사용해서 아직 수정되지 않은 취약점은 제외하고 볼 수 있습니다.

    2. 심각도별 필터링 (`–severity`)

    모든 MEDIUM(중간)이나 LOW(낮음) 레벨의 취약점을 당장 해결하기는 어렵습니다. 보통은 CRITICAL이나 HIGH 레벨부터 우선 처리하는 게 일반적이거든요. `–severity` 옵션으로 원하는 심각도만 지정해서 볼 수 있어요.

    
    # CRITICAL, HIGH 심각도의 미해결 취약점 제외 스캔
    trivy image --severity HIGH,CRITICAL --ignore-unfixed nginx:1.21.6
    

    이렇게 옵션을 조합해서 사용하면, 정말 중요한 취약점만 집중해서 볼 수 있고, 불필요한 노이즈를 줄여서 실제 액션으로 이어질 가능성이 훨씬 높아지더라고요. 제가 직접 해보니, 이 필터링 작업이 엄청 중요하더라고요. 삽질 좀 했습니다 ㅎㅎ

    스캔 결과 해석 및 후속 조치 (Verification & Results)

    Trivy 스캔 결과는 보통 다음과 같은 정보를 포함합니다.

    • CVE ID: 고유한 취약점 식별 번호 (예: CVE-2023-XXXX)
    • Severity: 취약점의 심각도 (CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN)
    • Package: 취약점이 발견된 패키지 이름 및 버전
    • Installed Version: 현재 이미지에 설치된 패키지 버전
    • Fixed Version: 취약점이 해결된 패키지 버전 (이 버전 이상으로 업데이트해야 함)
    • Title/Description: 취약점에 대한 간략한 설명

    스캔 결과를 보고 나면 어떻게 해야 할까요?

    1. 가장 먼저 Fixed Version 확인: 해당 패키지를 Fixed Version 이상으로 업데이트할 수 있는지 확인하고, Dockerfile(도커파일)을 수정해서 이미지를 다시 빌드(rebuild)합니다.
    2. 영향 분석: 패치 적용이 어렵다면, 해당 취약점이 우리 서비스에 실제로 어떤 영향을 미칠 수 있는지 심층 분석합니다. 예를 들어, 특정 포트가 외부에 노출되지 않는다면 일부 네트워크 관련 취약점은 우선순위가 낮아질 수 있겠죠.
    3. 예외 처리(Suppression): 불가피하게 당장 해결할 수 없거나, 우리 서비스에 영향이 없다고 판단되면, 문서화된 근거를 가지고 해당 취약점을 일시적으로 무시(suppress)할 수 있습니다. 하지만 이는 최후의 수단이며, 지속적으로 재평가해야 합니다.

    이런 과정을 통해 저는 ‘보안은 개발의 한 부분’이라는 것을 다시 한번 깨달았습니다. 단순히 스캔만 하고 끝내는 게 아니라, 결과를 이해하고 적절한 조치를 취하는 것이 중요하더라고요.

    Trivy, Aqua Security, Snyk 컨테이너 보안 솔루션 기능 및 비용 모델 비교 인포그래픽

    세 가지 컨테이너 보안 스캐너의 핵심 기능과 비용 모델을 한눈에 비교할 수 있습니다.

    Trivy, Aqua Security, Snyk: 비용 및 기능 비교 (Cost & Feature Analysis)

    이제 Trivy 외에 다른 상용 솔루션인 Aqua Security와 Snyk는 어떤 특징을 가지고 있는지, 그리고 비용(Cost) 측면에서는 어떻게 다른지 비교해보겠습니다. 이들은 단순 취약점 스캔을 넘어, 더 넓은 범위의 컨테이너 보안(Container Security) 기능을 제공합니다.

    컨테이너 보안 스캐너 주요 솔루션 비교

    제가 직접 사용해보고, 또 주변 동료들의 경험담을 들으면서 느낀 점들을 바탕으로 비교표를 만들어봤습니다.

    항목 Trivy Aqua Security (Aqua Cloud Native Security Platform) Snyk (스닉)
    주 사용 목적 개발/CI/CD 단계의 빠른 취약점 스캔 및 SBOM 생성 엔터프라이즈급 통합 Cloud Native 보안 플랫폼 (스캔, 런타임, 네트워크, 워크로드 보호 등) 개발자 중심의 코드, 종속성, 컨테이너, IaC 보안 및 취약점 자동 수정 제안
    라이선스 오픈소스 (Apache 2.0) 상용 솔루션 (유료 구독) 상용 솔루션 (유료 구독, 제한적 무료 플랜 제공)
    통합 용이성 CLI, Docker, Kubernetes, CI/CD 파이프라인 (GitHub Actions, GitLab CI 등) CI/CD, Kubernetes, Cloud Platform, Registry 등 전방위 통합 IDE, Git Repositories, CI/CD, Container Registries 등 개발자 워크플로우 중심 통합
    스캔 범위 OS 패키지, 프로그래밍 언어 종속성, IaC(Infrastructure as Code), 설정 파일, SBOM 이미지, 컨테이너, 서버리스, Kubernetes, 런타임 환경, 클라우드 자산 등 광범위 코드, 오픈소스 종속성, 컨테이너 이미지, IaC 설정
    부가 기능 SBOM(Software Bill of Materials) 생성, 이미지 서명 검증 런타임 보호, 네트워크 방화벽, KSPM(Kubernetes Security Posture Management), 데이터 보호, 통합 대시보드 및 보고서 자동 취약점 수정 제안, 라이선스 준수 검사, 코드 보안, 개발자 교육 자료
    비용 모델 무료 (오픈소스) 구독 기반 (스캔 대상 이미지 수, 보호하는 노드/워크로드 수, 사용량 기반 등 복합적) 구독 기반 (개발자 수, 월별 테스트 횟수, 프로젝트 수 등 복합적)

    비용 측면에서 보면, Trivy는 오픈소스이므로 직접 운영하는 데 드는 인프라 비용 외에는 무료입니다. 하지만 Aqua Security나 Snyk 같은 상용 솔루션은 기업 규모와 사용량에 따라 상당한 구독료가 발생할 수 있습니다. 예를 들어, 수백 개의 컨테이너 이미지를 관리하고 수십 대의 Kubernetes 노드에서 런타임 보호까지 필요하다면, 연간 수천만 원에서 억대까지도 비용이 들 수 있더라고요. 물론 이 금액은 제품마다, 계약 조건마다 천차만별입니다. 제가 특정 가격을 확언할 수는 없지만, ‘오픈소스와 상용 솔루션의 가격 차이는 크다’는 점은 분명합니다.

    따라서 컨테이너 보안 스캐너를 선택할 때는 단순히 기능만 볼 것이 아니라, 우리 조직의 규모, 예산, 기존 인프라와의 통합 용이성, 그리고 필요한 보안 범위 등을 종합적으로 고려해야 합니다. 무조건 비싼 솔루션이 좋다고 할 수는 없거든요. “우리 팀은 개발 속도가 생명인데, 보안 스캐너 때문에 속도가 느려지면 안 돼!” 같은 고민도 충분히 할 수 있습니다. 이럴 땐 Snyk처럼 개발자 워크플로우에 녹아드는 솔루션이 더 효과적일 수 있고요.

    Trivy, Aqua Security, Snyk 장단점 및 추천 사용 사례 요약 비교

    Trivy, Aqua Security, Snyk 각 솔루션의 장단점과 추천 사용 시나리오를 요약한 비교표입니다.

    마무리: 우리에게 맞는 컨테이너 보안 스캐너는? (Conclusion)

    오늘은 컨테이너 보안의 중요한 축인 Trivy, Aqua Security, Snyk 세 가지 솔루션에 대해 기능과 비용(Cost) 관점에서 자세히 알아봤습니다. 제가 직접 써보고, 삽질하면서 느낀 점들을 솔직하게 공유해드렸는데요.

    결론적으로 어떤 도구를 선택할지는 여러분의 상황에 따라 달라집니다.

    • 작은 프로젝트, 개인 홈랩, 또는 제한된 예산으로 기본적인 취약점 스캔을 시작하고 싶다면? ✅ Trivy가 최고의 선택입니다. CLI 기반으로 가볍게 시작하고, CI/CD에 통합하기도 쉽습니다.
    • 개발 초기 단계부터 보안을 녹여내고 싶고, 개발자 중심의 자동화된 워크플로우와 코드 레벨의 빠른 피드백이 중요하다면? 💡 Snyk를 고려해보세요. IDE 통합과 자동 수정 제안 기능이 개발 생산성을 해치지 않으면서 보안을 강화하는 데 도움을 줄 겁니다.
    • 대규모 엔터프라이즈 환경에서 통합적인 Cloud Native 보안 플랫폼이 필요하고, 취약점 스캔을 넘어 런타임 보호, 네트워크 보안, 규정 준수까지 전방위적인 관리가 필요하다면? 🚀 Aqua Security가 적합합니다. 물론 그만큼 비용 투자는 필요하겠죠.

    어떤 도구를 선택하든 중요한 건, 보안은 한 번 하고 끝나는 게 아니라 지속적인 관심과 개선이 필요하다는 점입니다. 스캐너는 도구일 뿐, 그 결과를 이해하고 적절한 조치를 취하는 것이 가장 중요합니다. 여러분의 컨테이너 환경이 더욱 안전해지기를 바라며, 다음 글에서는 컨테이너 런타임 보안에 대해 더 자세히 다뤄볼게요. 궁금한 점이 있다면 언제든 댓글 남겨주세요! 😉

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

  • [보안 사례] 컨테이너 이미지 보안 강화: 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 보안도 충분한가요?

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

  • [Cloud] GitHub Actions OIDC로 AWS 인증하기: 보안 강화 및 자격 증명 관리

    [Cloud] GitHub Actions OIDC로 AWS 인증하기: 보안 강화 및 자격 증명 관리

    GitHub Actions에서 AWS 인증, 아직도 IAM Access Key 쓰시나요?

    안녕하세요, 13년차 인프라 엔지니어이자 서버실 주인장입니다. 지난 몇 년간 수많은 CI/CD 파이프라인(Continuous Integration/Continuous Delivery Pipeline)을 구축하고 관리해왔는데, GitHub Actions와 AWS를 연동해서 사용하는 경우가 정말 많았거든요. 처음에는 저도 많은 분들처럼 AWS IAM(Identity and Access Management) 사용자의 Access Key와 Secret Key를 GitHub Secrets에 넣어 사용했습니다. 편리하긴 했죠. 근데 이게 뭔가 찜찜하더라고요.

    Access Key는 말 그대로 영구적인 자격 증명(Long-lived Credentials)이라 탈취 위험이 늘 존재하고, 정기적으로 Key를 교체(Rotation)해야 하는 관리 부담도 만만치 않았습니다. 만약 여러 레포지토리에서 같은 키를 쓴다면 더더욱 골치 아파지고요. 혹시 이런 경험 있으신가요? 저도 처음엔 이 방식밖에 없나 싶었는데, 다행히 더 안전하고 효율적인 방법이 등장했습니다. 바로 GitHub Actions OIDC(OpenID Connect)를 활용한 AWS 인증입니다! 오늘은 이 OIDC를 이용해 GitHub Actions에서 AWS에 안전하게 접근하는 방법을 제 경험을 바탕으로 자세히 알려드릴게요. CI/CD 파이프라인의 보안을 한 단계 끌어올리는 중요한 포인트가 될 겁니다.

    GitHub Actions OIDC를 이용한 AWS 인증의 전체적인 아키텍처 다이어그램입니다. GitHub Actions가 OIDC Provider 역할을 하고, AWS IAM이 이를 신뢰하여 임시 자격 증명을 발급하는 과정을 보여줍니다.

    OIDC(OpenID Connect)와 워크로드 아이덴티티(Workload Identity)가 뭔가요?

    본격적인 설정에 들어가기 전에, OIDC와 워크로드 아이덴티티라는 개념을 잠시 짚고 넘어가면 좋습니다. 복잡하게 들릴 수 있지만, 쉽게 말해볼게요.

    • OIDC (OpenID Connect, 오픈아이디 커넥트): OAuth 2.0 위에 구축된 인증(Authentication) 프로토콜입니다. 사용자(혹은 서비스)가 어떤 Identity Provider(신원 제공자)에게 “나 누구야”라고 인증받으면, Identity Provider는 ID Token이라는 걸 발급해줍니다. 이 토큰 안에는 “이 사람이 누구고, 어떤 방식으로 인증했는지” 같은 정보가 담겨있죠. AWS의 경우, GitHub Actions가 Identity Provider 역할을 하고, AWS IAM이 이 ID Token을 신뢰하는 Relying Party(의존 당사자)가 되는 겁니다.
    • 워크로드 아이덴티티 (Workload Identity): CI/CD 파이프라인의 워크로드(Workload)나 컨테이너처럼 사람이 아닌 시스템에 신원(Identity)을 부여하는 개념입니다. 기존의 Access Key 방식처럼 영구적인 자격 증명을 주지 않고, 필요할 때만 임시 자격 증명(Temporary Credentials)을 발급받아 사용하는 방식이에요. 이렇게 하면 자격 증명 탈취 위험을 최소화하고, 키 관리 부담을 없앨 수 있습니다.

    결론적으로, GitHub Actions OIDC를 사용하면 GitHub Actions 워크플로우가 직접 Access Key 없이 AWS에 “나 GitHub Actions의 이 레포지토리, 이 브랜치에서 실행된 애야!”라고 증명하고, AWS는 이 신원을 확인한 후 특정 권한을 가진 임시 자격 증명을 주는 방식이라고 이해하시면 됩니다. 진짜 편하고 안전하더라고요!

    GitHub Actions OIDC로 AWS 인증 설정 단계별 가이드

    자, 이제 실제로 GitHub Actions OIDC를 이용해 AWS에 인증하는 방법을 단계별로 따라해 볼까요? 제가 직접 해보니 몇 가지 포인트만 잘 기억하면 어렵지 않게 설정할 수 있었습니다.

    1단계: AWS IAM Identity Provider 생성하기

    가장 먼저 AWS IAM에서 GitHub Actions를 신뢰할 수 있는 OIDC Identity Provider로 등록해야 합니다. AWS 콘솔에서 진행하는 것이 가장 직관적입니다.

    1. AWS 콘솔에 로그인 후 IAM 서비스로 이동합니다.
    2. 왼쪽 탐색 메뉴에서 Access management (액세스 관리) > Identity Providers (자격 증명 공급자)를 클릭합니다.
    3. Add provider (공급자 추가) 버튼을 클릭합니다.
    4. Provider type (공급자 유형)으로 OpenID Connect를 선택합니다.
    5. Provider URL (공급자 URL)에 <code>https://token.actions.githubusercontent.com 을 입력합니다.
    6. Get thumbprint (지문 가져오기) 버튼을 클릭하여 서버 인증서 지문을 자동으로 가져옵니다.
    7. Audience (대상)에는 sts.amazonaws.com 을 입력하고 Add provider (공급자 추가)를 클릭하여 완료합니다.

    💡 팁: sts.amazonaws.com은 AWS Security Token Service의 기본 대상입니다. 다른 용도로 특정 서비스에만 OIDC를 사용한다면 해당 서비스의 Audience를 사용할 수도 있습니다.

    AWS IAM 콘솔에서 OIDC Identity Provider를 생성하는 화면입니다. Provider URL과 Audience를 정확히 입력하는 것이 중요합니다.

    2단계: AWS IAM Role 생성하기

    이제 이 OIDC Identity Provider를 통해 GitHub Actions가 임시 자격 증명을 요청할 수 있는 IAM Role을 생성해야 합니다. 이 역할에는 GitHub Actions가 AWS에서 수행할 작업에 대한 권한을 부여하게 됩니다.

    1. IAM 콘솔에서 Access management (액세스 관리) > Roles (역할)로 이동합니다.
    2. Create role (역할 생성) 버튼을 클릭합니다.
    3. Select type of trusted entity (신뢰할 수 있는 개체 유형 선택)에서 Custom trust policy (사용자 지정 신뢰 정책)를 선택합니다.
    4. 다음과 같이 신뢰 정책을 작성합니다. 여기서 StringEquals 조건이 핵심입니다!
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
          },
          "Action": "sts:AssumeRoleWithWebIdentity",
          "Condition": {
            "StringEquals": {
              "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
              "token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_ORG/YOUR_GITHUB_REPO:ref:refs/heads/main"
            }
          }
        }
      ]
    }

    ⚠️ 주의사항:

    • YOUR_AWS_ACCOUNT_ID: 여러분의 AWS 계정 ID로 교체해야 합니다.
    • YOUR_GITHUB_ORG/YOUR_GITHUB_REPO: GitHub 조직(Organization) 이름과 레포지토리(Repository) 이름으로 교체해야 합니다.
    • ref:refs/heads/main: 이 부분은 특정 브랜치(Branch)에서만 역할 가정을 허용하겠다는 의미입니다. *를 사용하여 모든 브랜치를 허용할 수도 있지만, 보안을 위해 특정 브랜치를 지정하는 것을 권장합니다. 예를 들어, 특정 태그(tag)에서만 허용하려면 ref:refs/tags/v*와 같이 설정할 수 있습니다.
    1. 정책 검토 후 다음 단계로 넘어갑니다.
    2. 이 역할에 필요한 Permissions (권한)을 부여합니다. 예를 들어 S3 버킷에 파일을 업로드해야 한다면 AmazonS3FullAccess 대신 필요한 최소한의 권한(Least Privilege)을 부여하는 것이 좋습니다. 저는 테스트를 위해 AmazonS3ReadOnlyAccess를 잠시 붙여봤습니다.
    3. 역할 이름을 지정하고(예: GitHubActionsOIDC-S3AccessRole) 역할을 생성합니다.

    3단계: GitHub Actions Workflow 설정하기

    마지막으로 GitHub Actions 워크플로우 YAML 파일에 OIDC를 통한 AWS 인증 로직을 추가합니다. configure-aws-credentials 액션을 사용하면 아주 쉽게 설정할 수 있습니다.

    name: Deploy to AWS S3 via OIDC
    
    on:
      push:
        branches:
          - main
    
    permissions:
      id-token: write # OIDC ID Token을 요청하기 위해 필수
      contents: read # 레포지토리 코드 읽기 권한
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Configure AWS Credentials with OIDC
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/GitHubActionsOIDC-S3AccessRole # 2단계에서 생성한 역할 ARN
              aws-region: ap-northeast-2 # 여러분의 AWS 리전
              # role-session-name: optional-session-name # (선택 사항)
    
          - name: List S3 buckets (for verification)
            run: aws s3 ls

    ⚠️ 여기서 중요한 포인트! permissions: id-token: write 설정은 반드시 필요합니다. 이 권한이 없으면 GitHub Actions가 OIDC ID Token을 생성할 수 없어 AWS 인증이 실패합니다. 처음엔 이걸 몰라서 삽질 좀 했습니다 ㅎㅎ

    삽질 경험: “AccessDenied” 에러의 늪에서 벗어나기 ⚠️

    제가 처음 GitHub Actions OIDC를 설정하면서 가장 많이 겪었던 문제는 다름 아닌 “AccessDenied” 에러였습니다. 워크플로우 로그를 보면 An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation: Not authorized to perform sts:AssumeRoleWithWebIdentity 이런 메시지가 뜨더라고요. 진짜 답답했죠.

    주요 원인은 대부분 IAM Role의 Trust Policy (신뢰 정책) 조건 문제였습니다. 제가 겪었던 실수와 해결 방법은 다음과 같습니다.

    • Audience 불일치: IAM Identity Provider를 생성할 때 Audience를 sts.amazonaws.com으로 설정했는데, IAM Role Trust Policy의 "token.actions.githubusercontent.com:aud" 조건에도 정확히 "sts.amazonaws.com"이 들어가야 합니다. 한 글자라도 다르면 인증이 실패합니다.
    • sub 조건 오류: 가장 흔한 실수입니다. "token.actions.githubusercontent.com:sub" 조건에 지정된 GitHub 레포지토리, 브랜치 정보가 정확해야 합니다. 예를 들어, repo:YOUR_GITHUB_ORG/YOUR_GITHUB_REPO:ref:refs/heads/main 에서 오타가 있거나, 워크플로우가 실행되는 브랜치와 다르면 에러가 발생합니다. 저는 처음에 main 브랜치로 설정해놓고 develop 브랜치에서 테스트하다가 한참을 헤맸습니다.
    • permissions: id-token: write 누락: GitHub Actions 워크플로우 자체에 OIDC 토큰을 생성할 권한이 없어서 생기는 문제입니다. 이 한 줄 때문에 몇 시간을 날린 적도 있어요. 꼭 확인하세요!
    • AWS 계정 ID 또는 역할 ARN 오타: 기본적인 실수지만, 의외로 자주 발생합니다. ARN을 복사 붙여넣기 할 때 다시 한번 확인하는 습관을 들이세요.

    문제가 발생하면 AWS CloudTrail 로그를 확인하는 것이 가장 좋습니다. AssumeRoleWithWebIdentity 호출이 실패한 이유가 자세히 기록되어 있으니 꼭 살펴보세요. 삽질 끝에 드디어 성공했을 때의 그 쾌감이란! 진짜 이 맛에 엔지니어 하는 것 같아요.

    드디어 성공! OIDC로 안전하게 AWS 리소스 접근하기 🎉

    위에 설명드린 대로 모든 설정을 마치고 GitHub Actions 워크플로우를 실행하면, 아래와 같이 성공적으로 AWS 리소스에 접근하는 모습을 볼 수 있습니다.

    GitHub Actions 워크플로우가 성공적으로 AWS CLI 명령어를 실행하여 S3 버킷 목록을 가져오는 로그입니다. OIDC 기반 인증이 정상적으로 작동했음을 보여줍니다.

    워크플로우 로그를 보면 aws s3 ls 명령어가 문제없이 실행되고, S3 버킷 목록이 출력되는 것을 확인할 수 있습니다. 이제 더 이상 GitHub Secrets에 Access Key를 저장할 필요가 없어졌습니다! 🎉

    이 방식의 가장 큰 장점은 바로 단기 자격 증명(Short-lived Credentials)이라는 점입니다. GitHub Actions 워크플로우가 실행될 때만 임시 자격 증명을 발급받아 사용하고, 워크플로우가 끝나면 만료되죠. 덕분에 키 탈취로 인한 보안 위험이 현저히 줄어들고, 키 교체 주기를 관리할 필요도 없어졌습니다. 관리적인 측면에서도 엄청난 이득이라고 생각해요.

    OIDC, CI/CD 보안의 새로운 표준이 되다

    오늘은 GitHub Actions OIDC를 이용해 AWS에 안전하게 인증하는 방법을 자세히 알아봤습니다. 제가 직접 경험하며 얻은 삽질 경험과 해결 과정도 솔직하게 공유해 드렸는데요. 이 방식은 단순히 편리함을 넘어 CI/CD 파이프라인의 보안을 근본적으로 강화하는 핵심 기술이라고 생각합니다.

    기존의 Access Key 방식과 OIDC를 활용한 AWS 인증 방식의 주요 특징을 비교한 표입니다. 보안성, 관리 용이성 등 여러 측면에서 OIDC의 장점을 한눈에 보여줍니다.

    핵심 장점을 다시 정리해볼까요?

    • 보안 강화: 영구적인 Access Key 없이 임시 자격 증명 사용으로 탈취 위험 최소화.
    • 관리 용이성: Access Key 생성, 배포, 교체 등의 관리 부담 해소.
    • 정교한 권한 제어: 특정 레포지토리, 특정 브랜치, 특정 태그에서만 AWS 리소스 접근 허용 가능.
    • 감사 추적 용이: CloudTrail을 통해 어떤 GitHub Actions 워크플로우가 어떤 역할로 AWS에 접근했는지 명확하게 추적 가능.

    저도 홈랩에서 다양한 CI/CD 환경을 구성하면서 OIDC의 강력함을 몸소 체험하고 있습니다. 앞으로는 OIDC 기반의 워크로드 아이덴티티가 CI/CD 보안의 표준으로 자리매김할 것이라고 확신합니다. 혹시 아직 Access Key를 사용하고 계시다면, 이번 기회에 OIDC로 전환해 보시는 것을 강력히 추천합니다! 처음엔 조금 낯설 수 있지만, 한번 설정해두면 정말 든든하거든요.

    다음 글에서는 AWS S3에 정적 웹사이트를 배포하는 과정을 GitHub Actions OIDC와 함께 자동화하는 방법에 대해 다뤄볼까 합니다. 기대해주세요! 그럼 다음 글에서 만나요! 👋

  • [Cloud] GitHub Actions 자체 호스팅 러너 보안 강화: 백도어 방지 및 2026년 최신 가이드

    [Cloud] GitHub Actions 자체 호스팅 러너 보안 강화: 백도어 방지 및 2026년 최신 가이드

    CI/CD 파이프라인의 약한 고리, 자체 호스팅 러너

    몇 달 전에 팀 내에서 꽤 아찔한 일이 있었습니다. 동료 엔지니어가 운영하던 GitHub Actions 자체 호스팅 러너(Self-hosted Runner)가 공급망 공격(Supply Chain Attack)의 타깃이 될 뻔했거든요. 다행히 조기에 발견했지만, 그때 이후로 저도 홈랩이랑 실무 환경 전체를 다시 들여다보게 됐습니다.

    솔직히 말씀드리면, 저도 처음에는 “GitHub에서 제공하는 거니까 그냥 쓰면 되겠지”라고 생각했었어요. 근데 자체 호스팅 러너는 얘기가 완전히 다릅니다. 여러분 서버 위에 올라가는 순간, 그 보안 책임은 100% 여러분 몫이거든요.

    이번 글에서는 GitHub Actions 자체 호스팅 러너 보안을 실질적으로 강화하는 방법을 단계별로 정리해 드릴게요. 2025~2026년 기준으로 GitHub에서 권고하는 최신 가이드라인과 제가 직접 적용해본 경험을 섞어서 써봤습니다.

    GitHub Actions 자체 호스팅 러너 보안 아키텍처 전체 개요 다이어그램

    ▲ GitHub Actions 자체 호스팅 러너의 전체 보안 아키텍처 개요 — 러너, 리포지토리, 네트워크, 시크릿 관리 레이어별 보안 포인트를 한눈에 보여줍니다.


    자체 호스팅 러너(Self-hosted Runner)란 무엇인가요?

    GitHub Actions에는 두 가지 러너 타입이 있습니다.

    • GitHub 호스팅 러너(GitHub-hosted Runner): GitHub이 관리하는 클라우드 VM. 워크플로우 실행 후 바로 폐기됩니다.
    • 자체 호스팅 러너(Self-hosted Runner): 여러분이 직접 서버를 준비하고, 그 위에 러너 에이전트를 설치해서 운영하는 방식입니다.

    쉽게 말해, 자체 호스팅 러너는 “내 서버를 GitHub CI/CD 파이프라인에 연결하는 것”입니다. 비용 절감, 내부망 접근, 특수 하드웨어 활용 등 장점이 많죠. 근데 바로 그 유연성 때문에 보안 리스크도 같이 따라옵니다.

    왜 위험한가요?

    핵심 문제는 워크플로우 파일(.yml)이 코드 저장소에 존재한다는 것입니다. 누군가 PR(Pull Request)을 통해 악성 워크플로우를 심으면, 그게 여러분 서버 위에서 실행될 수 있어요. GitHub 호스팅 러너라면 그 VM은 바로 폐기되지만, 자체 호스팅 러너는 여러분 서버가 그대로 남아있고, 내부 네트워크에도 접근 가능하죠.

    구분 GitHub 호스팅 러너 자체 호스팅 러너
    인프라 관리 GitHub 책임 사용자 책임
    보안 패치 자동 직접 관리
    실행 후 환경 VM 폐기 (격리) 서버 유지 (잔류 위험)
    내부망 접근 불가 가능 (위험 요소)
    비용 분당 과금 자체 서버 비용
    공급망 공격 위험 낮음 높음

    GitHub Actions 자체 호스팅 러너 보안 강화 — 단계별 실전 가이드

    1단계: 러너 접근 범위를 최소화하세요

    제가 처음 자체 호스팅 러너를 설정할 때 Organization 레벨로 등록했었어요. 편하긴 한데, 나중에 생각해보니 이게 꽤 위험한 설정이더라고요. 모든 리포지토리의 워크플로우가 그 러너에 접근할 수 있으니까요.

    권장 설정: 러너를 특정 리포지토리 레벨에만 등록하거나, Runner Group으로 접근을 제한하세요.

    GitHub Enterprise나 Organization을 쓰신다면 Runner Group(러너 그룹) 기능을 꼭 활용하세요.

    1. GitHub Organization → Settings → Actions → Runner groups
    2. 새 그룹 생성 후, 접근 허용할 리포지토리만 선택
    3. “Allow public repositories” 옵션은 반드시 비활성화
    # 러너 등록 시 특정 리포지토리 레벨로 등록하는 예시
    # Organization 레벨 대신 리포지토리 레벨 토큰 사용
    ./config.sh \
      --url https://github.com/your-org/specific-repo \
      --token YOUR_REPO_LEVEL_TOKEN \
      --name "secure-runner-01" \
      --labels "self-hosted,linux,secure"

    2단계: 에페머럴 러너(Ephemeral Runner) 도입 — 이게 진짜 핵심입니다

    이거 처음 알았을 때 “왜 진작 이걸 안 썼지?” 싶었어요. 에페머럴(Ephemeral, 일회성)이라는 단어처럼, 하나의 잡(Job)을 처리하고 나면 러너가 자동으로 폐기되는 방식입니다. GitHub 호스팅 러너처럼요.

    이렇게 하면 이전 잡에서 심어진 악성 코드나 환경 오염이 다음 잡으로 이어지지 않습니다. 공급망 공격 방어에 가장 효과적인 방법 중 하나거든요.

    # 에페머럴 모드로 러너 실행
    ./config.sh \
      --url https://github.com/your-org/your-repo \
      --token YOUR_TOKEN \
      --ephemeral  # 이 플래그 하나로 일회성 러너가 됩니다
    
    ./run.sh

    컨테이너 환경이라면 Actions Runner Controller(ARC)를 활용하면 Kubernetes 위에서 에페머럴 러너를 자동으로 스케일링할 수 있어요. 저도 홈랩 k3s 클러스터에 ARC 올려서 쓰고 있는데, 진짜 편하더라고요.

    # Helm으로 Actions Runner Controller 설치
    helm install arc \
      --namespace "arc-systems" \
      --create-namespace \
      oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
    
    # 에페머럴 러너 스케일셋 설정 (values.yaml)
    githubConfigUrl: "https://github.com/your-org/your-repo"
    githubConfigSecret: "arc-github-secret"
    
    containerMode:
      type: "dind"  # Docker-in-Docker로 격리 강화
    
    template:
      spec:
        containers:
          - name: runner
            image: ghcr.io/actions/actions-runner:latest
            resources:
              limits:
                cpu: "2"
                memory: "4Gi"
    GitHub Actions 에페머럴 러너 라이프사이클 다이어그램 - 잡 실행 후 자동 폐기 과정

    ▲ 에페머럴 러너의 라이프사이클 — 잡 수신 → 실행 → 자동 폐기 과정을 통해 환경 오염을 원천 차단합니다.

    3단계: 워크플로우 권한(Permissions)을 최소화하세요

    이게 은근히 놓치기 쉬운 부분이에요. GitHub Actions 워크플로우는 기본적으로 꽤 넓은 권한을 가질 수 있거든요. 최소 권한 원칙(Principle of Least Privilege)을 워크플로우에도 적용해야 합니다.

    # .github/workflows/ci.yml
    name: CI Pipeline
    
    on:
      push:
        branches: [main]
      pull_request:
        branches: [main]
    
    # 워크플로우 전체 기본 권한을 최소화
    permissions:
      contents: read  # 코드 읽기만 허용
    
    jobs:
      build:
        runs-on: [self-hosted, linux, secure]
        
        # 잡별로 필요한 권한만 추가
        permissions:
          contents: read
          packages: write  # 패키지 빌드가 필요한 경우에만
        
        steps:
          - name: Checkout
            uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
            with:
              persist-credentials: false  # 크리덴셜 잔류 방지!
          
          - name: Build
            run: make build

    특히 persist-credentials: false 옵션, 저도 처음엔 그냥 넘겼다가 나중에 알고 깜짝 놀랐어요. 이게 없으면 체크아웃 후 GitHub 토큰이 git 설정에 남아있을 수 있거든요.

    4단계: 외부 액션(Third-party Actions) SHA 핀닝(Pinning)

    공급망 공격(Supply Chain Attack)에서 가장 많이 노출되는 지점이 바로 여기입니다. uses: some-action/checkout@main처럼 브랜치 태그를 쓰면, 그 액션 저장소가 해킹당했을 때 여러분 파이프라인도 같이 당하게 돼요.

    해결책: 액션을 커밋 SHA 해시로 고정(Pin)하세요.

    # ❌ 이렇게 쓰면 위험합니다
    - uses: actions/checkout@v4
    - uses: actions/setup-node@main
    
    # ✅ 커밋 SHA로 고정하는 것이 안전합니다
    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
    - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0

    SHA 해시를 일일이 관리하기 귀찮으시다면 Dependabot을 활용하세요. .github/dependabot.yml에 설정하면 액션 버전 업데이트를 자동으로 PR로 올려줍니다.

    # .github/dependabot.yml
    version: 2
    updates:
      - package-ecosystem: "github-actions"
        directory: "/"
        schedule:
          interval: "weekly"
        # SHA 핀닝된 액션도 자동으로 업데이트 PR 생성
        groups:
          actions:
            patterns:
              - "*"

    5단계: 시크릿(Secrets) 관리와 환경 보호 규칙

    시크릿 관리도 허술하면 다 무너져요. 몇 가지 중요한 포인트를 짚어드릴게요.

    • 환경(Environment) 보호 규칙 설정: Production 환경에는 반드시 Reviewer 승인 단계를 추가하세요.
    • 시크릿 범위 최소화: Organization 시크릿보다는 리포지토리 시크릿, 그것보다는 환경 시크릿이 더 안전합니다.
    • OIDC(OpenID Connect) 활용: 장기 자격증명(Long-lived Credentials) 대신 단기 토큰을 사용하세요.
    # OIDC로 AWS 인증하는 예시 — 장기 Access Key 불필요!
    jobs:
      deploy:
        runs-on: [self-hosted, linux]
        environment: production  # 환경 보호 규칙 적용
        
        permissions:
          id-token: write  # OIDC 토큰 발급에 필요
          contents: read
        
        steps:
          - name: Configure AWS Credentials via OIDC
            uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502  # v4
            with:
              role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
              aws-region: ap-northeast-2
              # Access Key/Secret Key가 전혀 필요 없습니다!

    6단계: 러너 서버 자체 하드닝(Hardening)

    러너가 올라가는 서버 자체도 단단하게 만들어야 하거든요. 이건 일반적인 리눅스 서버 보안과 겹치는 부분도 있는데, 자체 호스팅 러너 특화 포인트만 짚어드릴게요.

    # 러너를 전용 비권한 사용자로 실행
    sudo useradd -m -s /bin/bash github-runner
    sudo usermod -aG docker github-runner  # Docker 사용이 필요한 경우만
    
    # 러너 디렉토리 권한 설정
    sudo chown -R github-runner:github-runner /opt/actions-runner
    
    # systemd 서비스로 등록 (루트 실행 방지)
    cd /opt/actions-runner
    sudo ./svc.sh install github-runner  # 전용 유저로 서비스 설치
    sudo ./svc.sh start
    # /etc/systemd/system/actions.runner.*.service 에서 확인
    # User=github-runner 로 설정되어 있어야 합니다
    
    # Docker 소켓 접근 제한 (필요한 경우 rootless Docker 고려)
    # /etc/docker/daemon.json
    {
      "userns-remap": "github-runner",  # 사용자 네임스페이스 리매핑
      "no-new-privileges": true,
      "log-driver": "json-file",
      "log-opts": {
        "max-size": "10m",
        "max-file": "3"
      }
    }

    그리고 네트워크 측면에서는, 러너 서버가 외부 인터넷에 직접 노출되지 않도록 해야 해요. GitHub Actions 자체 호스팅 러너는 아웃바운드 연결만 사용합니다. 인바운드 포트를 열 필요가 없거든요. 방화벽 규칙을 아웃바운드 443(HTTPS)만 허용하는 식으로 좁힐 수 있습니다.

    GitHub Actions 자체 호스팅 러너 보안 강화 설정 체크리스트 대시보드

    ▲ 자체 호스팅 러너 보안 강화 체크리스트 — 네트워크, OS, 워크플로우, 시크릿 관리 등 레이어별 적용 현황을 확인하세요.


    ⚠️ 실제로 삽질했던 문제들과 해결법

    문제 1: pull_request 이벤트와 포크 리포지토리

    이거 진짜 주의하셔야 해요. 퍼블릭 리포지토리에서 자체 호스팅 러너를 쓰면 포크(Fork)된 리포지토리의 PR도 트리거될 수 있거든요. 외부 기여자가 악성 워크플로우를 PR에 담아서 보내면… 생각만 해도 아찔하죠.

    해결책: 퍼블릭 리포지토리에는 자체 호스팅 러너를 절대 사용하지 마세요. GitHub도 공식적으로 이를 권고하지 않아요. 꼭 써야 한다면 pull_request_target 이벤트 대신 pull_request를 쓰고, 환경 보호 규칙으로 외부 기여자 PR은 수동 승인 후 실행되도록 설정하세요.

    # 외부 기여자 PR 실행 제한 예시
    on:
      pull_request:
        branches: [main]
    
    jobs:
      build:
        # 포크 PR의 경우 시크릿 접근 불가 (GitHub 기본 동작)
        # 자체 호스팅 러너는 내부 PR만 처리하도록 조건 추가
        if: github.event.pull_request.head.repo.full_name == github.repository
        runs-on: [self-hosted, linux]

    문제 2: 러너 업데이트 누락

    홈랩 운영하다 보면 러너 에이전트 업데이트를 깜빡하기 쉬워요. 저도 몇 달 방치했다가 나중에 보니 꽤 여러 버전이 밀려있더라고요. 자동 업데이트 설정을 켜두는 게 좋습니다.

    # 러너 자동 업데이트 설정 확인
    # .runner 파일에서 disableUpdate 옵션 확인
    cat /opt/actions-runner/.runner
    
    # 자동 업데이트가 비활성화되어 있다면
    # 주기적으로 업데이트 스크립트를 cron으로 실행
    # 에페머럴 러너라면 이미지 자체를 최신으로 유지하는 것이 더 좋습니다

    문제 3: 워크플로우 로그에 시크릿 노출

    워크플로우 실행 로그가 공개되는 경우, 시크릿이 실수로 echo 되거나 에러 메시지에 포함되는 경우가 있어요. GitHub은 등록된 시크릿 값을 자동으로 마스킹해주지만, 파생된 값(예: base64 인코딩된 시크릿)은 마스킹이 안 될 수 있거든요.

    # 파생 값도 마스킹하려면 add-mask 명령을 사용하세요
    - name: Mask derived secret
      run: |
        DERIVED_SECRET=$(echo "${{ secrets.MY_SECRET }}" | base64)
        echo "::add-mask::$DERIVED_SECRET"  # 이 값도 로그에서 마스킹됨
        echo "DERIVED_SECRET=$DERIVED_SECRET" >> $GITHUB_ENV

    보안 검증 — 설정이 제대로 됐는지 확인하기

    설정을 다 했다면 실제로 제대로 동작하는지 확인해봐야 하거든요. 제가 주기적으로 체크하는 항목들입니다.

    GitHub의 보안 권고 확인

    리포지토리 → Security → Code scanning alerts에서 워크플로우 파일의 보안 문제를 스캔할 수 있어요. CodeQL이나 actionlint를 CI에 넣어두면 자동으로 체크됩니다.

    # actionlint로 워크플로우 파일 정적 분석
    # 로컬에서 실행
    docker run --rm -v $(pwd):/repo rhysd/actionlint:latest -color /repo/.github/workflows/
    
    # CI에 통합
    - name: Lint GitHub Actions workflows
      uses: raven-actions/actionlint@v2
      with:
        files: .github/workflows/*.yml

    보안 강화 체크리스트 최종 점검

    • ✅ 러너가 특정 리포지토리/그룹에만 등록되어 있는가?
    • ✅ 에페머럴 모드로 실행되는가?
    • ✅ 워크플로우 기본 권한이 read-all 또는 그 이하인가?
    • ✅ 외부 액션이 SHA 해시로 핀닝되어 있는가?
    • ✅ Dependabot으로 액션 업데이트를 추적하는가?
    • ✅ 프로덕션 환경에 보호 규칙이 설정되어 있는가?
    • ✅ 장기 자격증명 대신 OIDC를 사용하는가?
    • ✅ 러너가 비권한 사용자로 실행되는가?
    • ✅ 러너 서버의 인바운드 포트가 닫혀 있는가?
    • ✅ 퍼블릭 리포지토리에 자체 호스팅 러너를 사용하지 않는가?
    GitHub Actions 자체 호스팅 러너 보안 강화 10가지 핵심 체크리스트 인포그래픽

    ▲ GitHub Actions 자체 호스팅 러너 보안 강화 요약 인포그래픽 — 10가지 핵심 체크리스트를 한눈에 확인하세요.


    마무리 — CI/CD 보안은 한 번이 아닙니다

    긴 글 읽어주셔서 감사합니다. 정리하자면, GitHub Actions 자체 호스팅 러너 보안의 핵심은 이렇습니다.

    1. 접근 최소화: 러너 범위를 필요한 리포지토리로만 제한
    2. 에페머럴 러너: 잡 실행 후 환경을 폐기해서 오염 방지
    3. 최소 권한: 워크플로우 권한을 필요한 것만 허용
    4. 공급망 공격 방어: 외부 액션을 SHA로 핀닝하고 Dependabot으로 관리
    5. 시크릿 보호: OIDC 활용, 환경 보호 규칙 설정
    6. 서버 하드닝: 비권한 사용자 실행, 네트워크 제한

    CI/CD 보안은 “한 번 설정하면 끝”이 아니거든요. 공급망 공격 기법은 계속 진화하고 있고, GitHub도 꾸준히 새로운 보안 기능을 추가하고 있어요. 저도 이 글 쓰면서 다시 한 번 제 홈랩 설정을 점검했는데, 고쳐야 할 부분이 몇 개 보이더라고요.

    💡 다음 글에서는 Kubernetes 기반의 Actions Runner Controller(ARC)를 활용한 에페머럴 러너 자동 스케일링 설정을 더 자세히 다룰 예정이에요. 홈랩에 k3s 올려서 직접 구성해본 경험을 공유해드릴 거니까 기대해주세요.

    궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있으면 댓글로 남겨주세요. 제 경험이 여러분의 파이프라인을 조금 더 안전하게 만드는 데 도움이 됐으면 좋겠습니다. 🎉