배포하고 나서 "아 저 S3 버킷 퍼블릭이었네" 하고 알아차리는 순간만큼 등골 서늘한 게 없다. 나도 예전에 스테이징 환경 버킷 하나를 실수로 public-read로 열어놨다가, 다행히 내부 리뷰에서 걸려서 넘어간 적이 있다. 문제는 이런 게 사람 눈에만 의존하면 언젠가는 반드시 새어나간다는 거다. 이번 글은 Terraform 코드가 머지되기 전에 설정 오류를 자동으로 걸러내는 파이프라인을 Checkov와 GitHub Actions로 짜는 이야기다.
1. 왜 지금 IaC 보안 자동화인가
Terraform, Pulumi, OpenTofu 같은 도구로 인프라를 코드로 정의하는 게 표준이 된 지 오래다. 버전 관리 되고, 반복 가능하고, 리뷰도 가능하다. 좋은 얘기다. 그런데 코드로 정의된다는 건 오류도 코드로 복제된다는 뜻이다. 모듈 하나 잘못 만들어놓고 열 개 프로젝트에서 갖다 쓰면, 취약점이 열 배로 퍼진다.
실무에서 자주 보는 사고 패턴은 대충 이렇다.
- S3 버킷을 급하게 만들면서 암호화 설정을 빼먹음
- 보안 그룹에
0.0.0.0/0으로 22번 포트를 열어둠 (테스트한다고 열었다가 안 닫음) - IAM 정책에
"Action": "*"를 박아놓고 "나중에 좁혀야지" 하고 잊어버림
이런 건 리뷰어가 terraform plan diff를 눈으로 훑어서 잡기엔 한계가 있다. diff가 100줄 넘어가면 사람은 그냥 "LGTM" 누른다. 그래서 정적 분석 도구(SAST)로 코드 자체를 스캔해서, 배포 파이프라인 왼쪽 끝(개발 단계)에서 막아버리는 "shift-left" 접근이 필요하다. Checkov가 바로 그 역할을 한다.
2. Checkov는 어떻게 동작하나
Checkov는 Bridgecrew(현재 Prisma Cloud 소속)가 만든 오픈소스 정적 분석 도구다. Terraform 파일뿐 아니라 CloudFormation, Kubernetes 매니페스트, Dockerfile, Serverless 프레임워크 등을 스캔한다. AWS/Azure/GCP에 대한 수백 개의 내장 정책을 가지고 있다.
동작 원리를 비유하자면, 맞춤법 검사기랑 비슷하다. 문서를 실제로 "실행"하지 않고 텍스트 구조만 읽어서 "여기 틀렸어요"를 지적한다. Checkov도 Terraform을 실제로 apply하지 않는다. HCL 코드를 파싱해서 리소스 블록을 추출하고, 각 리소스마다 미리 정의된 정책(check)을 하나씩 대입해본다.
정책 하나하나에는 CKV_AWS_20 같은 ID가 붙어 있다. 예를 들면:
CKV_AWS_20: S3 버킷 ACL이 퍼블릭 READ 접근을 허용하는지CKV_AWS_19: S3 버킷 데이터가 저장 시 암호화되는지CKV_AWS_21: S3 버킷 버저닝이 활성화됐는지
각 정책은 "이 리소스 타입에서 이 속성이 이 값이어야 통과" 같은 규칙으로 구성된다. 코드를 실행하는 게 아니라 정적으로 속성 값만 검사하기 때문에 빠르고, CI에서 돌리기 부담이 없다. 대신 런타임 상태(실제 배포된 리소스가 지금 어떤지)는 모른다는 한계도 여기서 나온다. 이건 뒤에서 다시 얘기한다.
3. 로컬에서 직접 돌려보기
일단 손으로 해봐야 감이 온다. 원문 예제를 그대로 따라가되, 실제 출력까지 확인하자.
취약한 Terraform 만들기
main.tf를 이렇게 작성한다. 일부러 퍼블릭 버킷을 만든다.
# main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "my_vulnerable_bucket" {
bucket = "my-company-public-data-bucket-12345"
}
# 일부러 넣은 취약점: 퍼블릭 READ 접근
resource "aws_s3_bucket_acl" "example" {
bucket = aws_s3_bucket.my_vulnerable_bucket.id
acl = "public-read"
}
Checkov 설치 및 실행
Python의 pip로 설치한다. (파이썬 3.7 이상 환경 권장. 정확한 최소 버전은 공식 문서 확인 필요)
pip install checkov
checkov -d .
디렉터리를 스캔하면 이런 리포트가 나온다.
_ _
___| |__ ___ ___| | _______ __
/ __| '_ \ / _ \/ __| |/ / _ \ \ / /
| (__| | | | __/ (__| < (_) \ V /
\___|_| |_|\___|\___|_|\_\___/ \_/
By Prisma Cloud | version: 3.1.0
terraform scan results:
Passed checks: 0, Failed checks: 3, Skipped checks: 0
Failed checks:
1. Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
FAILED for resource: aws_s3_bucket_acl.example
File: /main.tf:10-13
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-1-acl-read-access
2. Check: CKV_AWS_19: "Ensure all data stored in the S3 bucket is securely encrypted at rest"
FAILED for resource: aws_s3_bucket.my_vulnerable_bucket
File: /main.tf:6-8
3. Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
FAILED for resource: aws_s3_bucket.my_vulnerable_bucket
File: /main.tf:6-8
여기서 눈여겨볼 점: 나는 퍼블릭 ACL 하나만 일부러 넣었는데, Checkov는 암호화 미설정(CKV_AWS_19)과 버저닝 미설정(CKV_AWS_21)까지 세 개를 잡아냈다. 즉 "내가 인식조차 못 한 문제"를 알려준다. 이게 정적 분석의 진짜 가치다.
고쳐서 통과시키기
세 가지를 다 해결한 버전은 이렇다.
# main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "my_secure_bucket" {
bucket = "my-company-secure-data-bucket-12345"
}
# Fix 1: private ACL
resource "aws_s3_bucket_acl" "example" {
bucket = aws_s3_bucket.my_secure_bucket.id
acl = "private"
}
# Fix 2: 버저닝 활성화
resource "aws_s3_bucket_versioning" "versioning_example" {
bucket = aws_s3_bucket.my_secure_bucket.id
versioning_configuration {
status = "Enabled"
}
}
# Fix 3: 서버사이드 암호화
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
bucket = aws_s3_bucket.my_secure_bucket.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
이제 다시 checkov -d .를 돌리면 Passed checks 쪽으로 넘어간다.
4. GitHub Actions 파이프라인에 붙이기
로컬에서 돌리는 건 개인의 성실함에 의존한다. 성실함은 배포 압박 앞에서 쉽게 무너진다. 그래서 PR마다 자동으로 돌게 만들어야 한다. .github/workflows/checkov.yml을 만든다.
name: Checkov IaC Scan
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run Checkov action
uses: bridgecrewio/checkov-action@master
with:
directory: .
framework: terraform
output_format: cli
soft_fail: false # true로 하면 취약점 있어도 빌드 통과
단계별로 보면:
- 트리거: main 브랜치로의 push, PR에서 실행. 실무에선 PR 트리거가 핵심이다. 머지되기 전에 막아야 의미가 있으니까.
- Checkout: 코드를 러너로 가져온다.
- Checkov Action: 공식
bridgecrewio/checkov-action으로 현재 디렉터리를 스캔한다. - soft_fail: false: 취약점 발견 시 잡을 실패시킨다. 브랜치 보호 규칙에서 이 잡을 필수 체크로 걸어두면 취약한 코드는 머지 자체가 안 된다.
한 가지 실무 팁: bridgecrewio/checkov-action@master처럼 브랜치를 그대로 참조하면 어느 날 액션이 바뀌면서 파이프라인이 갑자기 깨질 수 있다. 재현 가능성 측면에서 특정 릴리스 태그나 커밋 SHA로 핀 고정하는 걸 권한다. 이건 Checkov만의 문제가 아니라 서드파티 액션 전반의 공급망 위생 문제다.
5. 실무 관점 — 트레이드오프와 흔한 함정
오탐과 SAST 노이즈, 그리고 예외 처리
Checkov를 실제 레거시 저장소에 처음 돌리면 십중팔구 이런 광경을 본다. 실패 체크 수백 개. 이 중 상당수는 "우리 맥락에선 문제 없는데?" 하는 것들이다. 예를 들어 사내망 전용 로그 버킷에 KMS 대신 AES256을 쓴다든가, 특정 정책이 우리 아키텍처엔 아예 해당 안 되는 경우다.
이럴 때 쓰는 게 인라인 억제(suppression)다. 특정 리소스에서 특정 체크만 건너뛴다.
resource "aws_s3_bucket" "internal_logs" {
bucket = "internal-logs-bucket-12345"
# checkov:skip=CKV_AWS_21:내부 로그 버킷, 버저닝 불필요 (보안팀 승인 2024-01, 티켓 SEC-123)
}
여기서 흔한 함정. skip 주석의 형식이 조금이라도 틀리면 조용히 무시된다. Checkov 버전에 따라 이런 경고를 뱉기도 한다.
WARNING: Failed to parse suppression comment
혹은 아예 아무 로그 없이 스킵이 안 먹어서, "분명 skip 넣었는데 왜 계속 FAILED로 뜨지?" 하고 한참 헤매게 된다. 규칙은 간단하다. # 뒤에 공백 하나, 그다음 checkov:skip=<체크ID>:<사유>. 체크 ID를 CKV_AWS_21이 아니라 대충 AWS_21로 쓰거나, 콜론 위치를 틀리면 안 먹는다.
그리고 억제는 양날의 검이다. 개발 속도를 위해선 필요하지만, 관리 안 하면 어느새 코드베이스가 checkov:skip 천지가 된다. 그러면 도구를 도입한 의미가 사라진다. 그래서 나는 이렇게 운영한다.
- skip 주석에는 반드시 사유와 승인 근거(티켓 번호 등)를 남긴다.
- skip 라인을 CODEOWNERS로 보안팀 리뷰 대상에 걸거나, 정기적으로
grep -rn "checkov:skip" .로 집계해서 리뷰한다. - 조직 전체 공통 예외는 인라인이 아니라 중앙 설정 파일(
.checkov.yaml)이나 커스텀 정책으로 관리한다. 그래야 개발자가 임의로 핵심 통제를 우회 못 한다.
도입 초기 현실적인 전략
레거시에 처음 붙일 때 soft_fail: false로 시작하면 팀 전체가 폭동을 일으킨다. 모든 PR이 빨간불이 되니까. 현실적인 순서는:
- 1단계:
soft_fail: true로 시작. 리포트만 뜨고 빌드는 통과. 현황 파악. - 2단계: 명백히 심각한 것부터 고치거나 예외 처리.
- 3단계: 어느 정도 정리되면
soft_fail: false로 전환해서 새 위반은 못 들어오게 막는다.
SAST의 근본적 한계
Checkov는 코드만 본다. 이미 배포돼서 콘솔에서 손으로 바꿔놓은 리소스(drift)는 모른다. 또 정적 분석이라 "이 변수가 런타임에 어떤 값이 될지"를 완벽히 추적하진 못한다. 그래서 Checkov는 첫 번째 방어선이지, 유일한 방어선이 아니다. 런타임 상태 점검은 별도 도구(클라우드 네이티브 보안 스캐너 등)로 보완해야 한다.
대안과 확장
원문은 tfsec을 일부러 피하고 Checkov를 다뤘다. 실무 선택지를 정리하면:
- tfsec / Trivy: Trivy는 이제 IaC 스캔도 하고 컨테이너 이미지, 의존성까지 통합해서 본다. 툴 개수 줄이고 싶으면 고려할 만하다.
- OPA (Open Policy Agent) + Conftest: 내장 정책으로 부족하고 조직 고유의 정책을 Rego로 직접 정의하고 싶을 때. 예: "모든 리소스에
CostCenter태그 필수" 같은 사내 규칙. - Terraform Cloud/Enterprise의 Sentinel: HashiCorp 상용 제품 라인을 쓴다면 plan 결과에 대한 policy-as-code를 붙일 수 있다.
참고로 Checkov 자체도 커스텀 정책(파이썬 또는 YAML 기반)을 지원한다. 사내 표준을 강제하고 싶으면 굳이 OPA까지 안 가고 Checkov 커스텀 정책으로 해결되는 경우도 많다.
6. 정리
한 줄 요약: Checkov + GitHub Actions는 Terraform 설정 오류를 배포 전에 막는 가장 가성비 좋은 첫 방어선이다.
누가 언제 쓰면 좋은가:
- Terraform으로 클라우드 인프라를 관리하는 모든 팀 — 사실상 안 쓸 이유가 없다. 설치와 CI 연동이 30분이면 끝난다.
- 다만 레거시가 크다면
soft_fail: true로 부드럽게 시작하고, 예외 처리에 대한 거버넌스(누가 skip을 승인하는가)를 반드시 함께 정해라. 이걸 안 정하면 6개월 뒤 skip 주석 무덤을 마주하게 된다. - 사내 고유 정책이 많다면 처음부터 커스텀 정책이나 OPA 연계를 염두에 두고 설계하는 게 낫다.
결국 핵심은 도구가 아니라 습관이다. 보안을 파이프라인 왼쪽으로 당겨서, 개발자가 프로덕션 사고를 내기 전에 스스로 고치게 만드는 것. Checkov는 그 습관을 자동화해주는 도구일 뿐이다.
참고 자료
'Tech_News' 카테고리의 다른 글
| htop/top의 숫자, 진짜로 읽을 줄 아나요? — CPU·메모리·Load Average 완전 분석 (0) | 2026.07.06 |
|---|---|
| 스위스는 25Gbit 대칭형 광인터넷이 되는데 왜 미국은 안 되나: 오픈액세스 망 아키텍처 뜯어보기 (0) | 2026.07.05 |
| Linux 6.9 LUKS suspend 회귀 버그: 메모리에 암호화 키가 남는다 (0) | 2026.07.04 |
| Google Copybara 실전 가이드: 내부 저장소와 공개 저장소를 동기화하는 법 (0) | 2026.07.03 |
| Claude Code가 요청에 심는 보이지 않는 마커: ANTHROPIC_BASE_URL 스테가노그래피 분석 (0) | 2026.07.02 |
