Checkov で Terraform の脆弱性を検知する: insecure vs secure の比較デモ
Terraform の terraform apply を実行した後に「この S3 バケット、暗号化されてなかった」と気づくのは避けたい状況です。IaC のセキュリティは、コードを書いた時点で検証できるのが理想です。
Checkov は Prisma Cloud(旧 Bridgecrew)が開発するオープンソースの静的解析ツールです。Terraform、CloudFormation、Kubernetes、Dockerfile など多くのフレームワークに対応し、セキュリティとコンプライアンスの問題を plan や apply なしで検知できます。
本記事では、意図的に脆弱な Terraform コードと修正済みコードを並べたデモリポジトリを作成し、Checkov がどのように脆弱性を検知するかを実際に確認します。
デモリポジトリ: codenote-net/checkov-terraform-demo
デモリポジトリの構成
リポジトリは insecure/ と secure/ の 2 つのディレクトリで構成されています。
checkov-terraform-demo/
├── insecure/ # 意図的に脆弱な Terraform コード
│ ├── provider.tf
│ ├── s3.tf
│ ├── sg.tf
│ ├── rds.tf
│ ├── iam.tf
│ └── cloudtrail.tf
├── secure/ # 修正済みの Terraform コード
│ ├── provider.tf
│ ├── s3.tf
│ ├── sg.tf
│ ├── rds.tf
│ ├── iam.tf
│ └── cloudtrail.tf
└── .github/workflows/
└── checkov.yml対象リソースは AWS の 5 種類です。
- S3: バケットの暗号化、パブリックアクセス、バージョニング
- Security Group: ポート開放範囲、SSH アクセス制限
- RDS: ストレージ暗号化、パブリックアクセス、Multi-AZ
- IAM: ポリシーの権限範囲
- CloudTrail: ログの暗号化、バリデーション
Checkov は静的解析ツールなので、terraform init も terraform plan も不要です。.tf ファイルさえあれば、即座にスキャンできます。
脆弱なコードを書く
まず、insecure/ ディレクトリに意図的に脆弱なコードを配置します。実装の詳細は PR #1 で確認できます。
S3: 暗号化なし、パブリックアクセス許可
resource "aws_s3_bucket" "data" {
bucket = "demo-insecure-bucket"
}
resource "aws_s3_bucket_acl" "data" {
bucket = aws_s3_bucket.data.id
acl = "public-read"
}暗号化の設定がなく、ACL が public-read になっています。バージョニング、アクセスログ、パブリックアクセスブロックもありません。
Security Group: 全ポートを全 IP に開放
resource "aws_security_group" "web" {
name = "demo-insecure-sg"
description = "Insecure security group for demo"
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}TCP の全ポート(0-65535)を 0.0.0.0/0 に開放し、SSH も全 IP からアクセスできる状態です。
RDS: 暗号化なし、パブリックアクセス有効
resource "aws_db_instance" "main" {
identifier = "demo-insecure-db"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "admin"
password = "password123"
publicly_accessible = true
storage_encrypted = false
skip_final_snapshot = true
}ストレージが暗号化されておらず、パブリックアクセスが有効です。パスワードもハードコードされています。
IAM: ワイルドカードポリシー
resource "aws_iam_policy" "admin" {
name = "demo-insecure-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "*"
Resource = "*"
}
]
})
}Action と Resource の両方が * になっており、全リソースに対する全操作を許可しています。
CloudTrail: 暗号化なし、バリデーション無効
resource "aws_cloudtrail" "main" {
name = "demo-insecure-trail"
s3_bucket_name = aws_s3_bucket.data.id
enable_log_file_validation = false
}KMS 暗号化がなく、ログファイルのバリデーションも無効です。
Checkov でスキャンする
Checkov のインストールは pip で行います。
pip install checkovinsecure/ ディレクトリに対してスキャンを実行します。
checkov -d insecure/ --framework terraform結果は Passed: 13 / Failed: 37 でした。
検知された 37 個の脆弱性
リソース別に、検知されたチェック ID と内容をまとめます。
S3(aws_s3_bucket.data): 8 件
| チェック ID | 内容 |
|---|---|
| CKV_AWS_18 | アクセスログが無効 |
| CKV_AWS_20 | パブリック READ アクセスを許可 |
| CKV_AWS_21 | バージョニングが無効 |
| CKV_AWS_144 | クロスリージョンレプリケーションが未設定 |
| CKV_AWS_145 | KMS による暗号化が未設定 |
| CKV2_AWS_6 | パブリックアクセスブロックが未設定 |
| CKV2_AWS_61 | ライフサイクル設定が未設定 |
| CKV2_AWS_62 | イベント通知が無効 |
Security Group(aws_security_group.web): 6 件
| チェック ID | 内容 |
|---|---|
| CKV_AWS_23 | ルールに description がない |
| CKV_AWS_24 | SSH(22)が 0.0.0.0/0 に開放 |
| CKV_AWS_25 | RDP(3389)が 0.0.0.0/0 に開放 |
| CKV_AWS_260 | HTTP(80)が 0.0.0.0/0 に開放 |
| CKV_AWS_382 | 全ポートで Egress が 0.0.0.0/0 に開放 |
| CKV2_AWS_5 | リソースにアタッチされていない |
RDS(aws_db_instance.main): 9 件
| チェック ID | 内容 |
|---|---|
| CKV_AWS_16 | ストレージ暗号化が無効 |
| CKV_AWS_17 | パブリックアクセスが有効 |
| CKV_AWS_118 | Enhanced Monitoring が無効 |
| CKV_AWS_129 | ログ出力が無効 |
| CKV_AWS_157 | Multi-AZ が無効 |
| CKV_AWS_161 | IAM 認証が無効 |
| CKV_AWS_226 | マイナーバージョン自動アップグレードが無効 |
| CKV_AWS_293 | 削除保護が無効 |
| CKV2_AWS_60 | スナップショットへのタグコピーが無効 |
IAM(aws_iam_policy.admin): 9 件
| チェック ID | 内容 |
|---|---|
| CKV_AWS_62 | 完全な管理者権限を付与 |
| CKV_AWS_63 | Action に * を許可 |
| CKV_AWS_286 | 権限昇格が可能 |
| CKV_AWS_287 | 認証情報の露出が可能 |
| CKV_AWS_288 | データの持ち出しが可能 |
| CKV_AWS_289 | リソースが無制限に公開される |
| CKV_AWS_290 | 無制限な書き込みアクセス |
| CKV_AWS_355 | 制限可能なアクションに * リソースを許可 |
| CKV2_AWS_40 | IAM の完全な権限を付与 |
CloudTrail(aws_cloudtrail.main): 5 件
| チェック ID | 内容 |
|---|---|
| CKV_AWS_35 | KMS による暗号化が未設定 |
| CKV_AWS_36 | ログファイルのバリデーションが無効 |
| CKV_AWS_67 | 全リージョンで有効化されていない |
| CKV_AWS_252 | SNS トピックが未設定 |
| CKV2_AWS_10 | CloudWatch Logs と統合されていない |
1 つの IAM ポリシーに Action: "*" を書いただけで 9 件の FAILED が出ます。Checkov は単純なパターンマッチではなく、権限昇格やデータ持ち出しの可能性まで分析しています。
修正済みコードで PASSED を目指す
secure/ ディレクトリに修正済みコードを配置します。実装の詳細は PR #2 で確認できます。
修正のポイントをリソース別に紹介します。
S3: 暗号化、アクセス制御、ライフサイクル管理
resource "aws_s3_bucket" "data" {
bucket = "demo-secure-bucket"
}
resource "aws_s3_bucket_versioning" "data" {
bucket = aws_s3_bucket.data.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "data" {
bucket = aws_s3_bucket.data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}KMS 暗号化、バージョニング、パブリックアクセスブロックを有効化しています。これに加えて、アクセスログ、ライフサイクル設定、クロスリージョンレプリケーションも設定しています(全コードは secure/s3.tf を参照)。
Security Group: ポートとアクセス元の制限
variable "allowed_ssh_cidr" {
description = "CIDR block allowed to SSH"
type = string
default = "203.0.113.0/24"
}
resource "aws_security_group" "web" {
name = "demo-secure-sg"
description = "Secure security group for demo - restricted access"
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH from allowed CIDR only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.allowed_ssh_cidr]
}
egress {
description = "Allow HTTPS outbound"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}全ポート開放を廃止し、HTTPS(443)のみ許可しています。SSH は変数で指定した特定の CIDR ブロックに制限し、各ルールに description を追加しています。CKV2_AWS_5(リソースにアタッチされていない)を解消するため、EC2 インスタンスも定義しています。
RDS: 暗号化と可用性の強化
variable "db_password" {
description = "Database master password"
type = string
sensitive = true
}
resource "aws_db_instance" "main" {
identifier = "demo-secure-db"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "admin"
password = var.db_password
publicly_accessible = false
storage_encrypted = true
copy_tags_to_snapshot = true
auto_minor_version_upgrade = true
deletion_protection = true
iam_database_authentication_enabled = true
multi_az = true
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
enabled_cloudwatch_logs_exports = ["audit", "error", "general", "slowquery"]
}パスワードを sensitive 変数に切り出し、ストレージ暗号化、Multi-AZ、IAM 認証、Enhanced Monitoring、CloudWatch Logs へのログ出力を有効化しています。
IAM: 最小権限の原則
resource "aws_iam_policy" "app" {
name = "demo-secure-policy"
description = "Least privilege policy for application"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::demo-secure-bucket",
"arn:aws:s3:::demo-secure-bucket/*"
]
}
]
})
}* を廃止し、必要な S3 操作だけを特定のバケットに対して許可しています。
CloudTrail: 暗号化と監視の統合
resource "aws_cloudtrail" "main" {
name = "demo-secure-trail"
s3_bucket_name = aws_s3_bucket.data.id
is_multi_region_trail = true
enable_log_file_validation = true
kms_key_id = aws_kms_key.cloudtrail.arn
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cloudwatch.arn
sns_topic_name = aws_sns_topic.cloudtrail.arn
}KMS 暗号化、ログバリデーション、全リージョン対応、CloudWatch Logs 統合、SNS 通知を設定しています。
修正後のスキャン結果
checkov -d secure/ --framework terraform結果は Passed: 111 / Failed: 0 でした。37 個の FAILED がすべて解消され、チェック項目自体も 50 件から 111 件に増えています。修正にあたってリソース(ログバケット、KMS キー、IAM ロールなど)を追加したため、チェック対象が増えたことが要因です。
GitHub Actions で CI に組み込む
PR ごとに Checkov を自動実行し、結果を PR コメントに投稿する GitHub Actions ワークフローを設定しています。
name: Checkov
on:
push:
branches: [main]
paths:
- "insecure/**"
- "secure/**"
- ".github/workflows/checkov.yml"
pull_request:
branches: [main]
paths:
- "insecure/**"
- "secure/**"
- ".github/workflows/checkov.yml"
permissions:
contents: read
pull-requests: write
jobs:
scan-insecure:
name: "Scan insecure/"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov on insecure/
id: checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: insecure/
framework: terraform
soft_fail: true
output_format: cli
scan-secure:
name: "Scan secure/"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check if secure/ exists
id: check
run: |
if [ -d "secure" ] && [ "$(ls -A secure/)" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Run Checkov on secure/
if: steps.check.outputs.exists == 'true'
uses: bridgecrewio/checkov-action@v12
with:
directory: secure/
framework: terraform
soft_fail: false
output_format: cliポイントは 2 つあります。
insecure/はsoft_fail: trueで、FAILED があっても CI を通す(脆弱なコードは意図的なため)secure/はsoft_fail: falseで、FAILED があれば CI を落とす
paths フィルターにより、README の更新など .tf ファイルに関係ない変更ではスキャンをスキップします。
まとめ
Checkov を使うことで、Terraform コードの脆弱性を apply の前に検知できます。
今回のデモでは、5 つの AWS リソースに対して 37 個の脆弱性を検知し、修正後に 111 個のチェックをすべて PASSED にしました。Checkov は terraform init すら不要で、.tf ファイルに対して即座にスキャンを実行できるため、開発フローへの導入コストが低いのが魅力です。
GitHub Actions に組み込めば、PR のたびに自動でセキュリティチェックが走ります。「脆弱な設定をうっかりマージしてしまった」というリスクを、コードレビューだけに頼らず仕組みで防げるようになります。
デモリポジトリのコードはすべて公開しています。手元で checkov -d insecure/ を実行して、検知結果を確認してみてください。
- デモリポジトリ: codenote-net/checkov-terraform-demo
- Checkov: bridgecrewio/checkov
- Checkov ドキュメント: checkov.io
以上、Terraform の脆弱性は apply する前に潰していきたい、現場からお送りしました。