AWSでのDevSecOps:シークレット管理、脆弱性スキャン、コンプライアンス自動化

Shunku

DevSecOpsは、開発プロセスにセキュリティを組み込む手法です。本記事では、セキュリティ記事で触れなかったCI/CDへのセキュリティ統合を解説します。

DevSecOps概要

flowchart LR
    subgraph ShiftLeft["シフトレフト"]
        Plan["計画"]
        Code["コード"]
        Build["Build"]
        Test["Testing"]
        Deploy["Deployment"]
        Monitor["監視"]
    end

    subgraph Security["セキュリティ統合"]
        ThreatModel["脅威モデリング"]
        SAST["SAST"]
        SCA["SCA"]
        DAST["DAST"]
        Compliance["Compliance"]
    end

    Plan --> ThreatModel
    Code --> SAST
    Build --> SCA
    Test --> DAST
    Deploy --> Compliance

    style ShiftLeft fill:#3b82f6,color:#fff
    style Security fill:#ef4444,color:#fff

シークレット管理

Secrets Managerパターン

# シークレット定義
DatabaseSecret:
  Type: AWS::SecretsManager::Secret
  Properties:
    Name: !Sub "${AWS::StackName}/database"
    Description: Database credentials
    GenerateSecretString:
      SecretStringTemplate: '{"username": "admin"}'
      GenerateStringKey: password
      PasswordLength: 32
      ExcludeCharacters: '"@/\'
    KmsKeyId: !Ref KMSKey
    Tags:
      - Key: Application
        Value: !Ref ApplicationName

# シークレットローテーション
SecretRotationSchedule:
  Type: AWS::SecretsManager::RotationSchedule
  Properties:
    SecretId: !Ref DatabaseSecret
    RotationLambdaARN: !GetAtt RotationFunction.Arn
    RotationRules:
      AutomaticallyAfterDays: 30
      ScheduleExpression: "cron(0 4 ? * SUN *)"

# Lambda関数でのシークレット取得
SecretAccessPolicy:
  Type: AWS::IAM::ManagedPolicy
  Properties:
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Action:
            - secretsmanager:GetSecretValue
          Resource: !Ref DatabaseSecret
          Condition:
            StringEquals:
              secretsmanager:VersionStage: AWSCURRENT

Lambda Extensionでのシークレット取得

import json
import urllib.request
import os

def get_secret_from_extension(secret_name):
    """Lambda Extensionを使用したシークレット取得(キャッシュ対応)"""

    headers = {
        "X-Aws-Parameters-Secrets-Token": os.environ.get('AWS_SESSION_TOKEN')
    }

    # Parameters and Secrets Lambda Extension
    url = f"http://localhost:2773/secretsmanager/get?secretId={secret_name}"

    req = urllib.request.Request(url, headers=headers)
    response = urllib.request.urlopen(req)
    secret = json.loads(response.read().decode())

    return json.loads(secret['SecretString'])

def lambda_handler(event, context):
    # シークレット取得(Extensionによりキャッシュされる)
    db_credentials = get_secret_from_extension('my-app/database')

    connection = connect_to_database(
        host=db_credentials['host'],
        user=db_credentials['username'],
        password=db_credentials['password']
    )

    return {"statusCode": 200}

Parameter Storeパターン

# 機密パラメータ
SecureParameter:
  Type: AWS::SSM::Parameter
  Properties:
    Name: /app/production/api-key
    Type: SecureString
    Value: !Sub "{{resolve:secretsmanager:api-key}}"
    KeyId: !Ref KMSKey
    Tier: Standard
    Tags:
      Environment: Production

# ECSタスクでの使用
TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    ContainerDefinitions:
      - Name: app
        Secrets:
          - Name: DB_PASSWORD
            ValueFrom: !Ref DatabaseSecret
          - Name: API_KEY
            ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/app/production/api-key"

静的アプリケーションセキュリティテスト (SAST)

CodeBuild統合

# buildspec.yml with SAST
version: 0.2

phases:
  install:
    commands:
      - pip install bandit safety semgrep

  pre_build:
    commands:
      # Python SAST (Bandit)
      - echo "Running Bandit security scan..."
      - bandit -r src/ -f json -o bandit-report.json || true

      # Semgrep (multi-language)
      - echo "Running Semgrep..."
      - semgrep --config=auto src/ --json --output=semgrep-report.json || true

      # 依存関係の脆弱性チェック
      - echo "Checking dependencies..."
      - safety check -r requirements.txt --json > safety-report.json || true

  build:
    commands:
      # セキュリティスキャン結果の評価
      - |
        python << 'EOF'
        import json
        import sys

        # Bandit結果チェック
        with open('bandit-report.json') as f:
            bandit = json.load(f)
            high_severity = [r for r in bandit.get('results', [])
                           if r.get('issue_severity') == 'HIGH']
            if high_severity:
                print(f"Found {len(high_severity)} HIGH severity issues in Bandit")
                sys.exit(1)

        # Safety結果チェック
        with open('safety-report.json') as f:
            safety = json.load(f)
            if safety:
                print(f"Found {len(safety)} vulnerable dependencies")
                sys.exit(1)

        print("Security scans passed")
        EOF

reports:
  bandit-report:
    files:
      - bandit-report.json
    file-format: BANDITJSON

Amazon CodeGuru Reviewer

CodeGuruReviewerAssociation:
  Type: AWS::CodeGuruReviewer::RepositoryAssociation
  Properties:
    Name: my-repo
    Type: CodeCommit

# CodePipelineでの統合
SecurityScanStage:
  Type: AWS::CodePipeline::Pipeline
  Properties:
    Stages:
      - Name: SecurityScan
        Actions:
          - Name: CodeGuruReview
            ActionTypeId:
              Category: Test
              Owner: AWS
              Provider: CodeGuruReviewer
              Version: "1"
            Configuration:
              RepositoryName: !Ref CodeCommitRepo
            InputArtifacts:
              - Name: SourceOutput

ソフトウェア構成分析 (SCA)

Amazon Inspector

# ECRスキャン設定
ECRRepository:
  Type: AWS::ECR::Repository
  Properties:
    RepositoryName: my-app
    ImageScanningConfiguration:
      ScanOnPush: true
    ImageTagMutability: IMMUTABLE

# Inspector設定
InspectorResourceGroup:
  Type: AWS::Inspector2::Filter
  Properties:
    FilterAction: NONE
    FilterCriteria:
      EcrImageTags:
        - Comparison: EQUALS
          Value: latest
      FindingStatus:
        - Comparison: EQUALS
          Value: ACTIVE
      Severity:
        - Comparison: EQUALS
          Value: CRITICAL

CodeBuildでのスキャン統合

version: 0.2

phases:
  build:
    commands:
      - echo "Building Docker image..."
      - docker build -t $ECR_REPO:$CODEBUILD_RESOLVED_SOURCE_VERSION .

      # Trivy でコンテナスキャン
      - echo "Scanning container image..."
      - |
        trivy image --exit-code 1 --severity CRITICAL,HIGH \
          --format json --output trivy-report.json \
          $ECR_REPO:$CODEBUILD_RESOLVED_SOURCE_VERSION

  post_build:
    commands:
      - echo "Pushing image to ECR..."
      - docker push $ECR_REPO:$CODEBUILD_RESOLVED_SOURCE_VERSION

      # Inspector スキャン完了を待機
      - |
        aws ecr wait image-scan-complete \
          --repository-name my-app \
          --image-id imageTag=$CODEBUILD_RESOLVED_SOURCE_VERSION

      # スキャン結果確認
      - |
        FINDINGS=$(aws ecr describe-image-scan-findings \
          --repository-name my-app \
          --image-id imageTag=$CODEBUILD_RESOLVED_SOURCE_VERSION \
          --query 'imageScanFindings.findingSeverityCounts')

        CRITICAL=$(echo $FINDINGS | jq -r '.CRITICAL // 0')
        if [ "$CRITICAL" -gt "0" ]; then
          echo "Critical vulnerabilities found!"
          exit 1
        fi

IaCセキュリティスキャン

CloudFormation Guard

# cfn-guard-rules.guard

# S3バケットは暗号化必須
let s3_buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]

rule s3_encryption_required when %s3_buckets !empty {
    %s3_buckets.Properties.BucketEncryption exists
    %s3_buckets.Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm in ['AES256', 'aws:kms']
}

# S3バケットはパブリックアクセスブロック必須
rule s3_public_access_blocked when %s3_buckets !empty {
    %s3_buckets.Properties.PublicAccessBlockConfiguration exists
    %s3_buckets.Properties.PublicAccessBlockConfiguration.BlockPublicAcls == true
    %s3_buckets.Properties.PublicAccessBlockConfiguration.BlockPublicPolicy == true
}

# RDSは暗号化必須
let rds_instances = Resources.*[ Type == 'AWS::RDS::DBInstance' ]

rule rds_encryption_required when %rds_instances !empty {
    %rds_instances.Properties.StorageEncrypted == true
}

# Lambda関数はVPC内に配置
let lambda_functions = Resources.*[ Type == 'AWS::Lambda::Function' ]

rule lambda_in_vpc when %lambda_functions !empty {
    %lambda_functions.Properties.VpcConfig exists
    %lambda_functions.Properties.VpcConfig.SubnetIds !empty
}

CodeBuildでのIaCスキャン

version: 0.2

phases:
  install:
    commands:
      - pip install cfn-lint checkov

  pre_build:
    commands:
      # CloudFormation Lint
      - echo "Running cfn-lint..."
      - cfn-lint templates/*.yaml --format json > cfn-lint-report.json || true

      # CloudFormation Guard
      - echo "Running cfn-guard..."
      - |
        cfn-guard validate \
          --data templates/*.yaml \
          --rules rules/security-rules.guard \
          --output-format json > cfn-guard-report.json || true

      # Checkov (包括的IaCスキャン)
      - echo "Running Checkov..."
      - |
        checkov --directory templates/ \
          --framework cloudformation \
          --output json > checkov-report.json || true

  build:
    commands:
      # スキャン結果評価
      - |
        python << 'EOF'
        import json
        import sys

        # Checkov結果チェック
        with open('checkov-report.json') as f:
            checkov = json.load(f)
            failed = checkov.get('results', {}).get('failed_checks', [])
            critical = [c for c in failed if c.get('check_result', {}).get('result') == 'FAILED']

            if len(critical) > 0:
                print(f"Found {len(critical)} failed security checks")
                for check in critical[:5]:
                    print(f"  - {check.get('check_id')}: {check.get('check_name')}")
                sys.exit(1)

        print("IaC security scans passed")
        EOF

動的アプリケーションセキュリティテスト (DAST)

OWASP ZAP統合

version: 0.2

phases:
  build:
    commands:
      # アプリケーションをステージング環境にデプロイ
      - aws cloudformation deploy --stack-name staging --template-file template.yaml

      # デプロイ完了を待機
      - sleep 60

      # ZAPスキャン実行
      - |
        docker run --rm -v $(pwd):/zap/wrk:rw \
          owasp/zap2docker-stable zap-baseline.py \
          -t https://staging.example.com \
          -g gen.conf \
          -r zap-report.html \
          -J zap-report.json \
          -I

      # 結果評価
      - |
        ALERTS=$(cat zap-report.json | jq '.site[0].alerts | length')
        HIGH_ALERTS=$(cat zap-report.json | jq '[.site[0].alerts[] | select(.riskcode == "3")] | length')

        if [ "$HIGH_ALERTS" -gt "0" ]; then
          echo "High risk vulnerabilities found!"
          exit 1
        fi

artifacts:
  files:
    - zap-report.html
    - zap-report.json

コンプライアンス自動化

Security Hub統合

SecurityHubConfiguration:
  Type: AWS::SecurityHub::Hub
  Properties:
    EnableDefaultStandards: true

# カスタム標準
CustomStandard:
  Type: Custom::SecurityHubStandard
  Properties:
    Standards:
      - AWS Foundational Security Best Practices
      - CIS AWS Foundations Benchmark
      - PCI DSS

# 自動修復
SecurityHubAutoRemediation:
  Type: AWS::Events::Rule
  Properties:
    EventPattern:
      source:
        - aws.securityhub
      detail-type:
        - Security Hub Findings - Imported
      detail:
        findings:
          Compliance:
            Status:
              - FAILED
          Severity:
            Label:
              - CRITICAL
              - HIGH
    Targets:
      - Id: RemediationLambda
        Arn: !GetAtt RemediationFunction.Arn

コンプライアンスダッシュボード

import boto3
from datetime import datetime

securityhub = boto3.client('securityhub')

def get_compliance_summary():
    """コンプライアンスサマリーを取得"""

    response = securityhub.get_findings(
        Filters={
            'ComplianceStatus': [
                {'Value': 'FAILED', 'Comparison': 'EQUALS'}
            ],
            'RecordState': [
                {'Value': 'ACTIVE', 'Comparison': 'EQUALS'}
            ]
        },
        MaxResults=100
    )

    findings_by_standard = {}
    findings_by_severity = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}

    for finding in response.get('Findings', []):
        # 標準別集計
        for standard in finding.get('Compliance', {}).get('RelatedRequirements', []):
            if standard not in findings_by_standard:
                findings_by_standard[standard] = 0
            findings_by_standard[standard] += 1

        # 重大度別集計
        severity = finding.get('Severity', {}).get('Label', 'LOW')
        findings_by_severity[severity] += 1

    return {
        'timestamp': datetime.utcnow().isoformat(),
        'by_standard': findings_by_standard,
        'by_severity': findings_by_severity,
        'total_findings': len(response.get('Findings', []))
    }

CI/CDパイプライン統合

flowchart TB
    subgraph Pipeline["セキュアCI/CDパイプライン"]
        Source["ソース"]
        SAST["SAST<br/>Bandit/Semgrep"]
        SCA["SCA<br/>Safety/Trivy"]
        IaC["IaCスキャン<br/>cfn-guard/Checkov"]
        Build["Build"]
        ContainerScan["コンテナスキャン<br/>Inspector/Trivy"]
        DAST["DAST<br/>ZAP"]
        Deploy["Deployment"]
    end

    Source --> SAST
    SAST --> SCA
    SCA --> IaC
    IaC --> Build
    Build --> ContainerScan
    ContainerScan --> DAST
    DAST --> Deploy

    style Pipeline fill:#3b82f6,color:#fff

ベストプラクティス

flowchart TB
    subgraph BestPractices["Best Practices"]
        ShiftLeft["シフトレフト<br/>早期発見"]
        Automate["スキャン自動化"]
        Secrets["シークレット管理"]
        Baseline["セキュリティベースライン"]
    end

    style BestPractices fill:#22c55e,color:#fff
カテゴリ 項目
設計 シフトレフトで早期に問題発見
自動化 全てのセキュリティチェックを自動化
シークレット コードにシークレットを含めない
継続的 コンプライアンスの継続的な監視

まとめ

領域 ツール
SAST CodeGuru Reviewer, Bandit, Semgrep
SCA Inspector, Safety, Trivy
IaC cfn-guard, Checkov
コンプライアンス Security Hub, Config

DevSecOpsを実践することで、セキュリティをビルトインした開発プロセスを実現できます。

参考資料