Copy & Run

Checkov로 Terraform 설정 오류를 찾고 고치기

연습용 main.tf를 apply 없이 Checkov로 점검해 실패 항목 표를 읽고, 설정을 고쳐 통과시킵니다. 사유를 남기는 skip 주석, soft-fail과 종료 코드로 만드는 CI 게이트, json과 sarif 출력, Trivy config와의 결과 비교까지 다룹니다.

실습 노트 · 2026-10-16 발행 · 16분 · 2026-10 확인

Terraform 파일을 apply 하지 않고 파일 내용만 읽어서 설정 오류를 찾는 정적 점검을 Checkov로 해 봅니다. 열린 S3 버킷, 전 세계에 열린 SSH, 암호화하지 않은 EBS 볼륨을 일부러 넣은 연습용 main.tf를 점검하고 고친 뒤, 같은 파일을 Trivy로도 점검해 결과를 비교합니다. 이 글의 취약한 설정은 직접 만든 연습용이며 실제 서비스에 쓰면 안 됩니다. 이 파일은 점검용이며 terraform init, plan, apply를 실행하지 않습니다. AWS 계정이나 자격 증명은 필요하지 않고 클라우드 호출도 하지 않습니다.

준비물

항목 내용
Checkov 3.3.29 (pip install checkov==3.3.29로 고정)
Python 3.12.10 (가상환경에 pip로 설치)
Docker Docker Desktop (Trivy 실행용)
Trivy 0.76.0 (이미지 aquasec/trivy:0.76.0)
실행 환경 Windows Git Bash
예상 소요 시간 약 25분

명령은 Windows의 Git Bash 기준입니다. 가상환경 활성화는 Linux와 macOS에서 source .venv/bin/activate입니다. 가상환경 크기는 약 260MB입니다. 이 글의 건수(12/20, 36/8 등)는 Checkov 3.3.29 기준이며 버전이 다르면 달라질 수 있습니다. 한글이 든 파일을 읽을 때 생기는 인코딩 문제는 4절과 아래 자주 막히는 곳에 적었습니다.

mkdir cr-checkov-lab && cd cr-checkov-lab
python -m venv .venv
source .venv/Scripts/activate
pip install checkov==3.3.29
checkov --version
mkdir bad fixed

기대 출력의 마지막 줄입니다.

3.3.29

1. 설정 오류가 든 main.tf 만들기

퍼블릭 읽기 ACL이 있는 버킷, 암호화와 버전 관리가 없는 버킷, 0.0.0.0/0에 SSH가 열린 보안 그룹, 암호화하지 않은 EBS 볼륨을 한 파일에 넣습니다. 이 파일은 점검용이며 terraform init, plan, apply를 실행하지 않습니다.

cat > bad/main.tf <<'EOF'
resource "aws_s3_bucket" "assets" {
  bucket = "cr-checkov-demo-assets"
  acl    = "public-read"
}

resource "aws_s3_bucket" "logs" {
  bucket = "cr-checkov-demo-logs"
}

resource "aws_security_group" "ssh" {
  name        = "cr-checkov-demo-ssh"
  description = "ssh access"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_ebs_volume" "data" {
  availability_zone = "ap-northeast-2a"
  size              = 20
}
EOF

2. 점검하고 실패 항목 읽기

-d는 점검할 폴더입니다. --compact는 코드 블록을 빼고, --quiet는 실패 항목만 보여 줍니다. 출력이 길어 앞부분만 봅니다.

checkov -d bad --compact --quiet | head -28

기대 출력입니다. 항목 순서는 실행마다 다를 수 있습니다.

terraform scan results:

Passed checks: 12, Failed checks: 20, Skipped checks: 0

Check: CKV_AWS_23: "Ensure every security group and rule has a description"
	FAILED for resource: aws_security_group.ssh
	File: \main.tf:10-20
	Guide: https://docs.prismacloud.io/...

한 건은 네 줄로 읽습니다.

줄 읽는 법
Check: CKV_AWS_23 검사 ID와 설명입니다. skip 주석과 --check 옵션에 이 ID를 씁니다.
FAILED for resource 문제가 있는 리소스 종류.이름입니다.
File: \main.tf:10-20 파일과 리소스가 차지한 줄 범위입니다. Windows에서는 역슬래시로 나옵니다.
Guide 이유와 수정 방법을 적은 문서 주소입니다.

같은 검사가 버킷마다 한 번씩 반복되므로 건수(20)는 문제 종류보다 많습니다. 실패한 검사 ID가 몇 가지인지는 5절의 json 출력으로 세어 보며, 13가지입니다.

Passed 12, Failed 20 요약 아래에 검사 ID, 리소스, 파일과 줄 범위, 가이드 주소가 건마다 나오는 것을 확인합니다.
Passed 12, Failed 20 요약 아래에 검사 ID, 리소스, 파일과 줄 범위, 가이드 주소가 건마다 나오는 것을 확인합니다.

3. 설정 고치기

핵심 오류는 CKV_AWS_20(퍼블릭 읽기 ACL), CKV_AWS_24(SSH 전체 개방), CKV_AWS_3(EBS 암호화 없음)입니다. 이들과 함께 버전 관리, 암호화, 퍼블릭 액세스 차단, 접근 로그까지 고친 파일입니다. S3 설정은 별도 리소스로 분리해 적었고 SSH 대역은 연습용 사설 대역 10.0.0.0/24입니다.

cat > fixed/main.tf <<'EOF'
resource "aws_kms_key" "data" {
  description         = "cr-checkov-demo data key"
  enable_key_rotation = true
}

resource "aws_s3_bucket" "assets" {
  bucket = "cr-checkov-demo-assets"
}

resource "aws_s3_bucket_public_access_block" "assets" {
  bucket                  = aws_s3_bucket.assets.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.data.arn
    }
  }
}

resource "aws_s3_bucket_logging" "assets" {
  bucket        = aws_s3_bucket.assets.id
  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "assets/"
}

resource "aws_s3_bucket" "logs" {
  bucket = "cr-checkov-demo-logs"
}

resource "aws_s3_bucket_public_access_block" "logs" {
  bucket                  = aws_s3_bucket.logs.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "logs" {
  bucket = aws_s3_bucket.logs.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "logs" {
  bucket = aws_s3_bucket.logs.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.data.arn
    }
  }
}

resource "aws_security_group" "ssh" {
  name        = "cr-checkov-demo-ssh"
  description = "ssh access from the office network only"

  ingress {
    description = "ssh from office"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/24"]
  }
}

resource "aws_ebs_volume" "data" {
  availability_zone = "ap-northeast-2a"
  size              = 20
  encrypted         = true
  kms_key_id        = aws_kms_key.data.arn
}
EOF

checkov -d fixed --compact --quiet | grep -E "Passed checks|Check:"

기대 출력입니다.

Passed checks: 36, Failed checks: 8, Skipped checks: 0
Check: CKV2_AWS_64: "Ensure KMS key Policy is defined"
Check: CKV2_AWS_62: "Ensure S3 buckets should have event notifications enabled"
Check: CKV2_AWS_62: "Ensure S3 buckets should have event notifications enabled"
Check: CKV2_AWS_61: "Ensure that an S3 bucket has a lifecycle configuration"
...

20건이 8건으로 줄었습니다. 남은 것은 KMS 키 정책, 이벤트 알림, 수명 주기, 교차 리전 복제, 보안 그룹 연결 여부입니다. 이 연습 환경에는 해당하지 않는 항목이라 다음 절에서 사유를 남기고 넘깁니다.

고친 뒤 Passed 36, Failed 8로 줄고 남은 검사 ID가 나열되는 것을 확인합니다.
고친 뒤 Passed 36, Failed 8로 줄고 남은 검사 ID가 나열되는 것을 확인합니다.

4. 사유를 남기고 검사 예외 처리하기

리소스 블록 안에 #checkov:skip=<검사 ID>:<사유> 형식으로 적습니다. 공식 문서의 Suppressing and Skipping Policies 형식이며 사유는 선택이지만 출력에 그대로 남으므로 적어 둡니다. 사유에 한글을 쓰므로 먼저 PYTHONUTF8=1을 지정합니다. 지정하지 않으면 Windows에서 Checkov가 UnicodeDecodeError로 종료합니다(아래 자주 막히는 곳). 아래 awk는 리소스 시작 줄 바로 아래에 주석을 넣으며 GNU sed와 달리 macOS에서도 같게 동작합니다. 두 번째 보안 그룹 사유는 연습용 예시 문구입니다. 실제로는 근거가 있을 때만 사유를 적습니다.

export PYTHONUTF8=1
awk '
{ print }
/^resource "aws_kms_key" "data"/ { print "  #checkov:skip=CKV2_AWS_64:연습용 키라 기본 키 정책을 사용합니다" }
/^resource "aws_s3_bucket" "/ {
  print "  #checkov:skip=CKV2_AWS_62:연습용 버킷이라 이벤트 알림을 쓰지 않습니다"
  print "  #checkov:skip=CKV_AWS_144:단일 리전 연습용이라 교차 리전 복제를 쓰지 않습니다"
  print "  #checkov:skip=CKV2_AWS_61:연습용 버킷이라 수명 주기 규칙을 두지 않습니다" }
/^resource "aws_security_group" "ssh"/ { print "  #checkov:skip=CKV2_AWS_5:연습용 예시 사유입니다. 실제로는 근거가 있을 때만 적습니다" }
' fixed/main.tf > fixed/main.tf.new && mv fixed/main.tf.new fixed/main.tf

checkov -d fixed --compact --quiet; echo "exit=$?"
checkov -d fixed --compact | grep -A3 SKIPPED | head -9

기대 출력입니다.

Passed checks: 36, Failed checks: 0, Skipped checks: 8

exit=0
	SKIPPED for resource: aws_s3_bucket.assets
	Suppress comment: 연습용 버킷이라 이벤트 알림을 쓰지 않습니다
	File: \main.tf:7-12

실패 0건에 종료 코드 0입니다. 예외 처리한 항목은 사라지지 않고 SKIPPED와 Suppress comment로 남아서 나중에 사유를 검토할 수 있습니다. 반대로 --skip-check는 해당 검사를 실행하지 않아 출력에도 남지 않으므로, 리소스 단위의 예외는 주석으로 적는 편이 기록에 유리합니다.

Skipped 8, 종료 코드 0이고 SKIPPED 항목마다 Suppress comment에 사유가 나오는 것을 확인합니다.
Skipped 8, 종료 코드 0이고 SKIPPED 항목마다 Suppress comment에 사유가 나오는 것을 확인합니다.

5. 종료 코드와 출력 형식으로 CI 게이트 만들기

Checkov는 실패가 있으면 종료 코드 1로 끝납니다. 이 값이 CI의 통과와 실패가 됩니다. 아래는 bad 폴더에 옵션별 동작을 확인한 것입니다.

checkov -d bad --quiet --compact --soft-fail > /dev/null; echo "soft-fail exit=$?"
checkov -d bad --check CKV_AWS_20,CKV_AWS_24,CKV_AWS_3 --quiet --compact; echo "gate exit=$?"
checkov -d bad -o json --quiet > report.json; echo "json exit=$?"
python -c "import json; d=json.load(open('report.json')); print(d['summary'])"
python -c "import json; d=json.load(open('report.json')); print(len({c['check_id'] for c in d['results']['failed_checks']}))"
checkov -d bad -o sarif --output-file-path . --quiet > /dev/null; ls -l results_sarif.sarif

마지막 줄 python -c는 실패한 검사 ID의 종류를 세며 13이 출력됩니다. 앞의 summary 줄은 아래 그림에서 확인합니다.

옵션 동작
--soft-fail 점검은 하되 종료 코드를 0으로 합니다. 도입 초기에 보고만 할 때 씁니다.
--check ID,ID 지정한 검사만 실행하고 나머지는 건너뜁니다. 꼭 막을 항목만 게이트로 거는 데 씁니다. 위 결과는 3건 실패에 종료 코드 1이었습니다.
-o json / -o sarif 결과를 json 또는 sarif로 냅니다. json의 summary에는 passed, failed, 버전이 들어 있고, sarif는 --output-file-path로 지정한 폴더에 results_sarif.sarif로 저장됩니다.
--hard-fail-on ID 공식 문서는 "Exits with a non-zero exit code for specified checks"라고 설명합니다. --soft-fail과 함께 쓰면 지정한 검사만 실패로 처리하며, 이 글에서는 CKV_AWS_20으로 종료 코드 1이 나오는 것을 확인했습니다.

CI에는 다음처럼 옮깁니다. 알려진 항목을 한꺼번에 막지 않고 핵심 오류가 새로 들어오지 않게 하려면 --check로 시작해 범위를 넓혀 갑니다. 공식 문서(https://www.checkov.io/2.Basics/CLI%20Command%20Reference.html)는 --check를 "Checks to run; any other checks will be skipped"라고 설명합니다. 즉 --check로 고른 검사 외의 나머지 검사는 실행되지 않습니다.

checkov -d . --framework terraform --quiet --compact --check CKV_AWS_20,CKV_AWS_24,CKV_AWS_3

이 게이트는 연습 파일의 모양만 잡습니다. 다른 형태로 쓴 리소스나 다른 포트는 지나갈 수 있어서 게이트 통과가 안전하다는 증거는 아닙니다. 아래 weak 폴더는 22번이 아닌 2222번 포트를 0.0.0.0/0에 연 보안 그룹 규칙과 설정이 빈 버킷입니다.

mkdir weak && cat > weak/main.tf <<'EOF'
resource "aws_s3_bucket" "open" {
  bucket = "cr-checkov-demo-open"
}

resource "aws_security_group_rule" "ssh_alt" {
  type              = "ingress"
  security_group_id = "sg-0123456789abcdef0"
  from_port         = 2222
  to_port           = 2222
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}
EOF
checkov -d weak --check CKV_AWS_20,CKV_AWS_24,CKV_AWS_3 --quiet --compact; echo "gate exit=$?"
checkov -d weak --quiet --compact | grep -c "FAILED for"
terraform scan results:

Passed checks: 2, Failed checks: 0, Skipped checks: 0

gate exit=0
8

세 항목만 거는 게이트는 종료 코드 0으로 통과하지만, 같은 폴더를 모든 검사로 돌리면 8건이 실패합니다.

soft-fail은 종료 코드 0, check로 고른 세 항목은 3건 실패와 종료 코드 1, json 요약과 sarif 파일 생성이 나오는 것을 확인합니다.
soft-fail은 종료 코드 0, check로 고른 세 항목은 3건 실패와 종료 코드 1, json 요약과 sarif 파일 생성이 나오는 것을 확인합니다.

6. Trivy config로 같은 파일 교차 점검하기

Trivy도 trivy config로 Terraform을 정적 점검합니다. 이미지 스캔과 빌드 게이트는 Trivy로 컨테이너 이미지 취약점을 스캔하고 빌드 게이트 만들기에서 다뤘으므로 여기서는 설정 점검만 봅니다. 현재 폴더를 컨테이너에 연결하는 함수를 먼저 등록합니다. 점검 규칙은 실행할 때 내려받아 캐시 볼륨에 보관합니다.

trivy() { MSYS_NO_PATHCONV=1 docker run --rm -v "$(pwd -W):/work" -w /work -v cr-checkov-trivy-cache:/root/.cache/ aquasec/trivy:0.76.0 "$@"; }
trivy config -q bad | head -16

Linux와 macOS에서는 MSYS_NO_PATHCONV=1을 빼고 $(pwd -W) 대신 $PWD를 씁니다. 첫 줄 요약에서 main.tf에 21건이 나옵니다.

Report Summary에서 main.tf 한 파일에 21건의 설정 오류가 나오는 것을 확인합니다.
Report Summary에서 main.tf 한 파일에 21건의 설정 오류가 나오는 것을 확인합니다.

trivy config -q --exit-code 1 bad > trivy-bad.txt; echo "bad exit=$?"
trivy config -q --exit-code 1 fixed > trivy-fixed.txt; echo "fixed exit=$?"
grep -E "^AWS-|Failures" trivy-bad.txt | cut -c1-90 | sort | uniq -c
grep -E "^AWS-|Failures" trivy-fixed.txt

Trivy는 --exit-code를 주지 않으면 실패가 있어도 종료 코드 0입니다. Checkov는 기본값이 1이라 이 점이 다릅니다. 고친 파일은 LOW 한 건(AWS-0089, 로그 버킷에 접근 로그 없음)만 남았습니다. --exit-code의 공식 설명은 "specify exit code when any security issues are found"이고, --severity로 거른 결과만 판정에 쓰입니다. --severity HIGH,CRITICAL을 더해 실제로 실행한 결과입니다.

trivy config -q --exit-code 1 --severity HIGH,CRITICAL fixed > /dev/null; echo "severity exit=$?"
severity exit=0

LOW 한 건만 남은 고친 파일이 HIGH, CRITICAL 기준에서는 통과합니다. 같은 파일을 --severity 없이 --exit-code 1로 돌리면 종료 코드 1입니다(위 fixed exit=1).

bad는 21건과 종료 코드 1, fixed는 AWS-0089 LOW 한 건이 남는 것을 확인합니다.
bad는 21건과 종료 코드 1, fixed는 AWS-0089 LOW 한 건이 남는 것을 확인합니다.

같은 bad에 대한 두 도구의 결과를 비교하면 다음과 같습니다.

항목 Checkov 3.3.29 Trivy 0.76.0
건수 20건 (검사 ID 13가지) 21건 (ID 13가지)
퍼블릭 ACL CKV_AWS_20 AWS-0092
SSH 전체 개방 CKV_AWS_24 AWS-0107
EBS 암호화 없음 CKV_AWS_3 AWS-0026
퍼블릭 액세스 차단 없음 CKV2_AWS_6 한 건 AWS-0086, 0087, 0091, 0093, 0094로 나뉨
이벤트 알림, 수명 주기, 교차 리전 복제 지적함 지적하지 않음
로그 버킷의 접근 로그 (고친 뒤) 통과 AWS-0089 지적
예외 처리 주석 #checkov:skip=ID:사유 #trivy:ignore:AWS-0089
실패 시 기본 종료 코드 1 0 (--exit-code 1 필요)

핵심 세 오류는 둘 다 잡았습니다. 갈리는 부분은 도구마다 가진 검사 목록의 폭과 묶는 방식입니다. Trivy 공식 문서(https://trivy.dev/docs/latest/guide/coverage/iac/terraform/)는 인라인 # trivy:ignore 주석으로 예외 처리할 수 있다고만 안내하고 상세 문법은 싣지 않습니다. 그래서 #trivy:ignore:<ID> 형식은 직접 실행으로 확인했습니다. 로그 버킷 리소스 바로 윗줄에 #trivy:ignore:AWS-0089를 적었더니 --exit-code 1에서도 종료 코드 0이 되었습니다. 어느 쪽이 맞는지는 팀이 정한 기준으로 판단하고, 한 도구의 검사 목록이 닿지 않는 곳을 다른 도구가 메우는 용도로 함께 쓸 수 있습니다. 시크릿 점검은 gitleaks 글에서 따로 다룹니다.

자주 막히는 곳

한글이 든 skip 주석을 넣었더니 Checkov가 UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d로 종료됩니다. Windows의 기본 인코딩(cp1252)으로 .tf 파일을 읽다가 생깁니다. 이 환경(Windows Git Bash, Python 3.12, Checkov 3.3.29)에서 종료 코드는 2였고 환경에 따라 다를 수 있습니다. Checkov 공식 문서의 종료 코드 2는 --no-fail-on-crash 설명에 나오는 플랫폼 연동 실패이며 이 오류와는 다른 것입니다. 4절처럼 export PYTHONUTF8=1을 지정하면 정상 점검되며(종료 코드 0), 영문 사유만 쓰는 방법도 있습니다. 점검 실패(종료 코드 1)와 구분해서 봅니다.

File: \main.tf:10-20처럼 경로가 역슬래시로 나오고 폴더 이름이 보이지 않습니다. -d에 지정한 폴더를 기준으로 한 상대 경로이고 Windows에서는 구분자가 역슬래시입니다. 폴더가 여럿이면 어느 폴더인지 구분이 어려우니 폴더마다 따로 실행합니다.

항목 순서가 실행할 때마다 달라서 결과를 비교하기 어렵습니다. 같은 파일을 반복해 실행하면 순서가 바뀌는 것을 확인했습니다. 비교는 순서 대신 검사 ID로 하고, 자동화에는 -o json을 씁니다.

Trivy config가 처음 실행할 때 느립니다. 규칙을 내려받으므로 네트워크가 필요하고, 캐시 볼륨을 같이 마운트하면 다음부터는 받지 않습니다.

이어서 해 볼 것

  • Docker 이미지로 실행하기: 가상환경 대신 Checkov 공식 이미지로 같은 점검을 돌려 CI 러너에 맞는 방식을 고릅니다. 이미지 이름과 태그는 공식 문서에서 확인합니다.
  • 새 리소스 추가해 보기: aws_instance에 보안 그룹을 연결하면 CKV2_AWS_5 예외 처리가 필요 없어지는지 확인합니다.
  • sarif를 code scanning에 올리기: results_sarif.sarif를 github/codeql-action/upload-sarif 단계로 GitHub에 올려 PR에 표시하는 구성을 시도해 봅니다.

확인 범위

직접 실행한 부분은 1절부터 6절까지입니다. 환경은 Windows의 Git Bash, Python 3.12.10 가상환경, Checkov 3.3.29, Docker Desktop과 aquasec/trivy:0.76.0입니다. 한글 skip 주석에서 PYTHONUTF8=1을 지정하지 않았을 때의 인코딩 오류도 재현했습니다. 모두 파일을 읽기만 했고 terraform init, plan, apply는 하지 않았으며 AWS를 호출하지 않았습니다.

설치는 pip만 확인했습니다. Checkov의 Docker 이미지와 Linux, macOS 명령(source .venv/bin/activate, awk의 macOS 동작), GitHub Actions 연동은 실행하지 못했습니다. Checkov 옵션은 공식 문서 https://www.checkov.io/2.Basics/CLI%20Command%20Reference.html 과 https://www.checkov.io/2.Basics/Suppressing%20and%20Skipping%20Policies.html 을 읽고 checkov --help로 대조했습니다. --hard-fail-on은 검사 ID 지정의 종료 코드 확인까지만 했고, 심각도로 지정하는 방식은 API 키가 필요하다고 안내되어 실행하지 않았습니다. Trivy는 https://trivy.dev/docs/latest/guide/coverage/iac/terraform/ 에서 인라인 # trivy:ignore 언급을, https://trivy.dev/docs/latest/references/configuration/cli/trivy_config/ 에서 --exit-code 설명을, https://trivy.dev/docs/latest/guide/configuration/filtering/ 에서 --severity 설명을 확인했고 #trivy:ignore:<ID> 상세 문법은 공식 문서에서 찾지 못해 실행 결과만 적었습니다. 검사 목록과 건수는 도구 버전에 따라 달라집니다.

이 글의 다음 단계

DevSecOps 실무 과정의 일부입니다

이 실습은 교육에서 참가자마다 독립된 환경으로 진행합니다. 원하는 일정을 고르면 문의 양식에 미리 채워집니다.

과정 보기

교육 진행 순서와 자주 묻는 질문 보기

같은 카테고리의 글