LESSON 40分

ストーリー

佐藤CTO
アプリケーションコードのセキュリティは押さえた。でもインフラのコードはどうだ?

佐藤CTOがTerraformのコードを指差しました。

佐藤CTO
S3バケットがパブリックアクセス許可になっている。セキュリティグループが0.0.0.0/0で全開放。こういうミスがIaCコードに潜んでいる
あなた
IaCもスキャンする必要があるんですね
佐藤CTO
その通り。tfsec、Checkov、OPA — ツールは揃っている。さらにPolicy as Codeで組織のセキュリティポリシーをコードとして強制する方法も学ぼう

IaCセキュリティの重要性

IaCに潜む典型的なセキュリティ問題

問題影響検出ツール
S3バケットのパブリックアクセスデータ漏洩tfsec, Checkov
セキュリティグループの全開放不正アクセスtfsec, Checkov
暗号化未設定のRDSデータ漏洩tfsec, Checkov
IAMポリシーのワイルドカード過剰な権限IAM Access Analyzer
CloudTrail無効化監査不能AWS Config Rules
VPCフローログ未設定ネットワーク監視不能Checkov

tfsec / trivy config

tfsecはTerraformのセキュリティスキャンツールです(現在はTrivyに統合)。

# BAD: セキュリティ問題のあるTerraformコード
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
  # tfsec:ignore[aws-s3-bucket-logging]  ← 個別に無視する場合

  # 問題1: バージョニング未設定
  # 問題2: 暗号化未設定
  # 問題3: パブリックアクセス制御未設定
  # 問題4: ログ未設定
}

resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]  # 問題: 全ポート全開放
  }
}
# GOOD: セキュリティベストプラクティスに準拠
resource "aws_s3_bucket" "data" {
  bucket = "my-data-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"
      kms_master_key_id = aws_kms_key.s3.arn
    }
    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
}

resource "aws_s3_bucket_logging" "data" {
  bucket        = aws_s3_bucket.data.id
  target_bucket = aws_s3_bucket.access_logs.id
  target_prefix = "s3-access-logs/"
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web server security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]  # VPC内のみ
    description = "HTTPS from VPC"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound"
  }

  tags = {
    Name = "web-sg"
  }
}

Checkov

Checkovは、Terraform、CloudFormation、Kubernetes、Dockerfileなど多数のIaCフレームワークに対応したスキャンツールです。

# GitHub Actions での Checkov 実行
- name: Checkov IaC Scan
  uses: bridgecrewio/checkov-action@master
  with:
    directory: ./infrastructure
    framework: terraform
    output_format: sarif
    output_file_path: checkov-results.sarif
    soft_fail: false
    skip_check: CKV_AWS_18  # 特定チェックのスキップ(理由をコメントで記載)

Checkov カスタムポリシー

# custom_policies/s3_naming_convention.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories

class S3NamingConvention(BaseResourceCheck):
    def __init__(self):
        name = "Ensure S3 bucket follows naming convention"
        id = "CUSTOM_S3_001"
        supported_resources = ['aws_s3_bucket']
        categories = [CheckCategories.CONVENTION]
        super().__init__(name=name, id=id, categories=categories,
                        supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        bucket_name = conf.get("bucket", [""])[0]
        # 命名規則: {env}-{project}-{purpose}
        import re
        pattern = r'^(dev|stg|prod)-[a-z]+-[a-z]+$'
        if re.match(pattern, bucket_name):
            return CheckResult.PASSED
        return CheckResult.FAILED

check = S3NamingConvention()

Policy as Code(OPA / Rego)

OPA(Open Policy Agent)

OPAは汎用的なポリシーエンジンで、Rego言語でポリシーを記述します。

# policy/terraform.rego
package terraform.security

# S3バケットは暗号化必須
deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  not has_encryption(resource)
  msg := sprintf("S3 bucket '%s' must have server-side encryption enabled", [resource.name])
}

has_encryption(resource) {
  resource.change.after.server_side_encryption_configuration[_].rule[_].apply_server_side_encryption_by_default[_].sse_algorithm
}

# セキュリティグループは0.0.0.0/0の全ポート開放を禁止
deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_security_group"
  ingress := resource.change.after.ingress[_]
  ingress.cidr_blocks[_] == "0.0.0.0/0"
  ingress.from_port == 0
  ingress.to_port == 0
  msg := sprintf("Security group '%s' must not allow unrestricted access on all ports", [resource.name])
}

# RDSは暗号化と自動バックアップ必須
deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_db_instance"
  not resource.change.after.storage_encrypted
  msg := sprintf("RDS instance '%s' must have storage encryption enabled", [resource.name])
}

# タグの必須チェック
required_tags := {"Environment", "Team", "CostCenter"}

deny[msg] {
  resource := input.resource_changes[_]
  tags := object.get(resource.change.after, "tags", {})
  missing := required_tags - {key | tags[key]}
  count(missing) > 0
  msg := sprintf("Resource '%s' is missing required tags: %v", [resource.name, missing])
}

OPAをCI/CDに統合

# Terraform plan → OPA 検証 → Apply の流れ
- name: Terraform Plan
  run: |
    cd infrastructure
    terraform plan -out=tfplan
    terraform show -json tfplan > tfplan.json

- name: OPA Policy Check
  run: |
    opa eval \
      --input infrastructure/tfplan.json \
      --data policy/ \
      --format pretty \
      'data.terraform.security.deny'

    # deny が1つでもあればビルドを失敗
    VIOLATIONS=$(opa eval \
      --input infrastructure/tfplan.json \
      --data policy/ \
      --format json \
      'count(data.terraform.security.deny)')

    if [ "$VIOLATIONS" != "0" ]; then
      echo "Policy violations found!"
      exit 1
    fi

Sentinel(HashiCorp)

HashiCorp Sentinel は Terraform Enterprise/Cloud 向けの Policy as Code フレームワークです。

# sentinel/require-encryption.sentinel
import "tfplan/v2" as tfplan

# 全S3バケットにサーバーサイド暗号化を要求
s3_buckets = filter tfplan.resource_changes as _, rc {
  rc.type is "aws_s3_bucket" and
  rc.mode is "managed" and
  (rc.change.actions contains "create" or rc.change.actions contains "update")
}

encryption_check = rule {
  all s3_buckets as _, bucket {
    bucket.change.after.server_side_encryption_configuration is not null
  }
}

main = rule {
  encryption_check
}

AWS Config Rules によるドリフト検知

// CDK で AWS Config Rules を定義
import * as config from 'aws-cdk-lib/aws-config';

// S3バケットのパブリックアクセスチェック
new config.ManagedRule(this, 'S3PublicReadRule', {
  identifier: 'S3_BUCKET_PUBLIC_READ_PROHIBITED',
  configRuleName: 's3-bucket-public-read-prohibited',
});

// セキュリティグループの開放チェック
new config.ManagedRule(this, 'SgOpenRule', {
  identifier: 'RESTRICTED_INCOMING_TRAFFIC',
  configRuleName: 'restricted-incoming-traffic',
  inputParameters: {
    blockedPort1: '22',
    blockedPort2: '3389',
  },
});

// RDS暗号化チェック
new config.ManagedRule(this, 'RdsEncryptionRule', {
  identifier: 'RDS_STORAGE_ENCRYPTED',
  configRuleName: 'rds-storage-encrypted',
});

// CloudTrail有効チェック
new config.ManagedRule(this, 'CloudTrailRule', {
  identifier: 'CLOUD_TRAIL_ENABLED',
  configRuleName: 'cloudtrail-enabled',
});

// 自動修復アクション
new config.CfnRemediationConfiguration(this, 'S3Remediation', {
  configRuleName: 's3-bucket-public-read-prohibited',
  targetId: 'AWS-DisableS3BucketPublicReadWrite',
  targetType: 'SSM_DOCUMENT',
  automatic: true,
  maximumAutomaticAttempts: 3,
  retryAttemptSeconds: 60,
  parameters: {
    S3BucketName: {
      ResourceValue: { Value: 'RESOURCE_ID' },
    },
  },
});

まとめ

ポイント内容
IaCセキュリティスキャンtfsec/Trivy、Checkovでインフラコードの脆弱性を検出
Policy as CodeOPA/Regoで組織のセキュリティポリシーをコードとして強制
SentinelTerraform Enterprise/Cloud向けのポリシーフレームワーク
AWS Config Rulesデプロイ後のコンプライアンスドリフトを継続的に検知
自動修復ポリシー違反の自動修復で人的ミスを排除

チェックリスト

  • IaCに潜むセキュリティ問題の主なパターンを理解した
  • tfsec/Checkovでterraformコードをスキャンできる
  • OPA/Regoでカスタムセキュリティポリシーを記述できる
  • Policy as CodeをCI/CDパイプラインに統合できる
  • AWS Config Rulesでドリフト検知を設定できる

次のステップへ

次は「演習:セキュリティパイプラインを構築しよう」です。Step 3で学んだSAST/SCA/DAST/コンテナスキャン/IaCスキャンを統合した、包括的なセキュリティパイプラインを構築しましょう。


推定読了時間: 40分