I tried generating and validating an intentionally vulnerable EC2 environment CloudFormation template with Kiro's AI-DLC

I tried generating and validating an intentionally vulnerable EC2 environment CloudFormation template with Kiro's AI-DLC

# Kiro CLI AI-DLC × Security Hub CFn テンプレート生成 実験レポート ## 試みた開発サイクルの概要 ``` Security Hub Controls ↓ Kiro AI-DLC で生成 ↓ 脆弱版 CFn テンプレート ←→ 修正版 CFn テンプレート ↓ cfn-guard で静的検証 ↓ AWS CLI で E2E 確認 ↓ 差分を requirements.md へ書き戻し ↓ (フィードバックループ) ``` --- ## 各フェーズの詳細 ### 1. Kiro AI-DLC によるテンプレート生成 **生成対象の Security Hub コントロール例** | コントロール ID | 内容 | |---|---| | EC2.1 | EBS スナップショットのパブリック公開禁止 | | EC2.2 | VPC デフォルト SG でインバウンド/アウトバウンド禁止 | | EC2.6 | VPC Flow Logs の有効化 | | EC2.7 | EBS 暗号化のデフォルト有効化 | **脆弱版テンプレート(生成例)** ```yaml # vulnerable-ec2.yaml AWSTemplateFormatVersion: "2010-09-09" Description: Vulnerable EC2 Template (for testing) Resources: VulnerableInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0abcdef1234567890 InstanceType: t3.micro # ❌ IMDSv2 未強制 MetadataOptions: HttpTokens: optional # ❌ 詳細モニタリング無効 Monitoring: false # ❌ パブリック IP 自動割り当て NetworkInterfaces: - AssociatePublicIpAddress: true DeviceIndex: "0" VulnerableSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Vulnerable SG # ❌ 全ポート全開放 SecurityGroupIngress: - IpProtocol: "-1" CidrIp: 0.0.0.0/0 VulnerableVolume: Type: AWS::EC2::Volume Properties: AvailabilityZone: ap-northeast-1a # ❌ 暗号化なし Encrypted: false Size: 20 ``` **修正版テンプレート(生成例)** ```yaml # secure-ec2.yaml AWSTemplateFormatVersion: "2010-09-09" Description: Secure EC2 Template aligned with Security Hub Resources: SecureInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0abcdef1234567890 InstanceType: t3.micro # ✅ IMDSv2 強制 MetadataOptions: HttpTokens: required HttpPutResponseHopLimit: 1 # ✅ 詳細モニタリング有効 Monitoring: true # ✅ パブリック IP 割り当て無効 NetworkInterfaces: - AssociatePublicIpAddress: false DeviceIndex: "0" SubnetId: !Ref PrivateSubnet GroupSet: - !Ref SecureSG # ✅ IAM ロール付与 IamInstanceProfile: !Ref InstanceProfile SecureSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Secure SG - no unrestricted access # ✅ インバウンドルールなし(明示的に空) SecurityGroupIngress: [] VpcId: !Ref VPC SecureVolume: Type: AWS::EC2::Volume Properties: AvailabilityZone: ap-northeast-1a # ✅ 暗号化有効 Encrypted: true KmsKeyId: !Ref EBSKmsKey Size: 20 # ✅ EBS デフォルト暗号化 EBSEncryptionByDefault: Type: AWS::EC2::EncryptionByDefault Properties: EnableEncryptionByDefault: true ``` --- ### 2. cfn-guard による静的検証 **ルールファイル(security-hub-ec2.guard)** ```ruby # EC2.8: IMDSv2 強制チェック rule ec2_imdsv2_required { AWS::EC2::Instance { Properties.MetadataOptions.HttpTokens == "required" << EC2.8: IMDSv2 (HttpTokens=required) を設定してください >> } } # EC2.2: SG 全開放チェック rule sg_no_unrestricted_ingress { AWS::EC2::SecurityGroup { Properties.SecurityGroupIngress[*] { CidrIp != "0.0.0.0/0" or IpProtocol != "-1" << EC2.2: 全ポート全開放 (0.0.0.0/0) は禁止です >> } } } # EC2.3: 暗号化チェック rule ebs_encryption_required { AWS::EC2::Volume { Properties.Encrypted == true << EC2.3: EBS ボリュームの暗号化を有効にしてください >> } } # EC2.15: パブリック IP チェック rule no_public_ip { AWS::EC2::Instance { Properties.NetworkInterfaces[*] { AssociatePublicIpAddress == false << EC2.15: パブリック IP の自動割り当てを無効にしてください >> } } } ``` **検証コマンドと結果** ```bash # 脆弱版の検証 $ cfn-guard validate \ --data vulnerable-ec2.yaml \ --rules security-hub-ec2.guard # 出力例 vulnerable-ec2.yaml Status = FAIL FAILED rules ec2_imdsv2_required FAIL sg_no_unrestricted_ingress FAIL ebs_encryption_required FAIL no_public_ip FAIL # 修正版の検証 $ cfn-guard validate \ --data secure-ec2.yaml \ --rules security-hub-ec2.guard # 出力例 secure-ec2.yaml Status = PASS ``` --- ### 3. AWS CLI による E2E 確認 **デプロイスクリプト(e2e-verify.sh)** ```bash #!/bin/bash set -euo pipefail STACK_NAME="security-hub-e2e-test" TEMPLATE="secure-ec2.yaml" REGION="ap-northeast-1" echo "=== Stack デプロイ ===" aws cloudformation deploy \ --template-file "$TEMPLATE" \ --stack-name "$STACK_NAME" \ --capabilities CAPABILITY_IAM \ --region "$REGION" INSTANCE_ID=$(aws cloudformation describe-stack-resource \ --stack-name "$STACK_NAME" \ --logical-resource-id SecureInstance \ --query "StackResourceDetail.PhysicalResourceId" \ --output text) echo "=== IMDSv2 確認 ===" IMDS_STATUS=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query "Reservations[0].Instances[0].MetadataOptions.HttpTokens" \ --output text) if [ "$IMDS_STATUS" = "required" ]; then echo "✅ IMDSv2: PASS ($IMDS_STATUS)" else echo "❌ IMDSv2: FAIL ($IMDS_STATUS)" FAIL=true fi echo "=== EBS 暗号化確認 ===" VOLUME_ID=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ --output text) ENCRYPTED=$(aws ec2 describe-volumes \ --volume-ids "$VOLUME_ID" \ --query "Volumes[0].Encrypted" \ --output text) if [ "$ENCRYPTED" = "True" ]; then echo "✅ EBS 暗号化: PASS" else echo "❌ EBS 暗号化: FAIL" FAIL=true fi echo "=== パブリック IP 確認 ===" PUBLIC_IP=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query "Reservations[0].Instances[0].PublicIpAddress" \ --output text) if [ "$PUBLIC_IP" = "None" ] || [ -z "$PUBLIC_IP" ]; then echo "✅ パブリック IP なし: PASS" else echo "❌ パブリック IP あり: FAIL ($PUBLIC_IP)" FAIL=true fi # Security Hub 確認(デプロイ後 5 分待機) echo "=== Security Hub Findings 確認 ===" sleep 300 aws securityhub get-findings \ --filters '{ "ResourceId": [{"Value": "'"$INSTANCE_ID"'", "Comparison": "EQUALS"}], "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}], "WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}] }' \ --query "Findings[].{Title:Title,Severity:Severity.Label}" \ --output table if [ "${FAIL:-false}" = "true" ]; then echo "❌ E2E 検証: 一部失敗" exit 1 else echo "✅ E2E 検証: 全項目 PASS" fi ``` --- ### 4. 差分の requirements.md への書き戻し **差分検出スクリプト(update-requirements.sh)** ```bash #!/bin/bash # デプロイ中に発見した差分を requirements.md へ追記 REQUIREMENTS_FILE="requirements.md" TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") # Security Hub Findings を取得して差分として記録 FINDINGS=$(aws securityhub get-findings \ --filters '{ "ResourceType": [{"Value": "AwsEc2Instance", "Comparison": "EQUALS"}], "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}] }' \ --query "Findings[].{ ControlId:ProductFields.\"aws/securityhub/FindingId\", Title:Title, Severity:Severity.Label, Description:Description }" \ --output json) # requirements.md への書き戻し cat >> "$REQUIREMENTS_FILE" << EOF ## 発見された差分 (${TIMESTAMP}) ### E2E 検証で新たに発見されたコントロール違反 $(echo "$FINDINGS" | jq -r '.[] | "- **\(.Severity)**: \(.Title)\n - \(.Description)\n"') ### 次サイクルで対応すべき要件 $(echo "$FINDINGS" | jq -r '.[] | "- [ ] \(.Title) に対応するテンプレート修正"') EOF echo "requirements.md を更新しました" ``` **requirements.md の更新例** ```markdown # EC2 Security Requirements ## 初期要件 (2025-01-01) - [ ] IMDSv2 強制 (EC2.8) - [ ] EBS 暗号化 (EC2.3) - [ ] SG 全開放禁止 (EC2.2) ## 発見された差分 (2025-01-15 14:30:00) ### E2E 検証で新たに発見されたコントロール違反 - **MEDIUM**: EC2 instances should use IMDSv2 - インスタンス xxx で HttpTokens が optional のまま - **HIGH**: EBS volumes should be encrypted at rest - スナップショットから復元したボリュームが未暗号化 ### 次サイクルで対応すべき要件 - [ ] IMDSv2 に対応するテンプレート修正 - [ ] スナップショット復元時の暗号化継承確認 - [ ] Launch Template での MetadataOptions 統一 ``` --- ## 開発サイクル全体のまとめ ``` ┌─────────────────────────────────────────────┐ │ 仕様開発サイクル │ │ │ │ requirements.md │ │ ↓ Kiro AI-DLC が読み込み │ │ 脆弱版 / 修正版 CFn 生成 │ │ ↓ │ │ cfn-guard 静的検証 ──→ FAIL なら修正 │ │ ↓ PASS │ │ AWS CLI デプロイ & E2E 確認 │ │ ↓ │ │ Security Hub Findings 取得 │ │ ↓ │ │ 差分を requirements.md へ書き戻し ←────── │ │ ↓ │ │ 次サイクルへ(継続的改善) │ └─────────────────────────────────────────────┘ ``` ### 気づいた点・課題 | 観点 | 内容 | |---|---| | **AI-DLC の強み** | Security Hub コントロール ID を指定するだけで対応する CFn 差分を生成できる | | **cfn-guard の役割** | デプロイ前に静的で弾けるため、AWS リソース消費を抑えられる | | **E2E との乖離** | cfn-guard では検出できない実行時の設定(スナップショット復元など)は E2E が必要 | | **書き戻しの価値** | 発見した差分を requirements.md に蓄積することで、次世代テンプレートの品質が向上する | | **改善点** | cfn-guard ルールと Security Hub コントロール ID のマッピング自動化が次の課題 | このサイクルは **「生成 → 検証 → デプロイ → 発見 → 仕様更新」** というループを Kiro が支援することで、セキュリティ要件の継続的な改善を自動化できる点が有効でした。
2026.06.14

This page has been translated by machine translation. View original

Introduction

AI-DLC is a specification-driven development flow available in Kiro CLI. It progresses through the stages of requirements definition → code generation → review, with human approval at each stage.

This time, I used AI-DLC to intentionally generate an EC2 environment with vulnerable configurations using CloudFormation. This is a subject where the resource logical IDs of the vulnerable version and the fixed version, cfn-guard rules, and E2E confirmation script verification targets need to be aligned. Using AI-DLC's supervised mode, I created each artifact starting from a requirements document, and confirmed the process up to writing back differences found during deployment verification to requirements.md.

The verification scope is not detection/resolution confirmation of Security Hub Findings themselves, but static verification using cfn-guard and E2E confirmation using AWS CLI.

Verification Environment

Item Details
Kiro CLI 2.7.0 / On EC2 (t4g.small, AL2023 ARM64)
Model claude-sonnet-4.6
AI-DLC v2 branch (obtained 2026-06-14)
Controls used as subject EC2.8, EC2.3, EC2.18, EC2.19, IAM.1
Target resources EC2, EBS, SecurityGroup, IAM Role

Kiro CLI was deployed on EC2 using this template. The necessary permissions for CloudFormation deployment are granted via an IAM role. As long as Kiro CLI and the necessary credentials are available, the AI-DLC specification development flow itself can also be executed locally.

AI-DLC Workflow Execution

I placed the AI-DLC skill in the project, launched the orchestration skill (/skill aidlc-orchestration), and executed each stage sequentially in supervised mode.

# Clone AI-DLC v2
git clone -b v2 https://github.com/awslabs/aidlc-workflows.git

# Symlink skills to the project directory
mkdir -p weak-ec2-securityhub-lab/.kiro/skills
for dir in aidlc-workflows/kiro/src/skills/*/; do
  ln -s "$(realpath "$dir")" "weak-ec2-securityhub-lab/.kiro/skills/$(basename "$dir")"
done

In the following article structure, the 6 stages proposed by Kiro are organized and explained in 4 Steps.

Step 1: Kickoff — Agreeing on the Stage Configuration

I invoked the skill and entered the following prompt.

Kiro CLI with AI-DLC launched, showing Stage 1: Clarification beginning

/skill aidlc-orchestration

I want to build an EC2 environment with intentionally vulnerable configurations detectable by SecurityHub using CloudFormation.
The goal is to experience the learning cycle of SecurityHub finding detection → IaC remediation → finding resolution.

Target vulnerabilities: EC2.8, EC2.3, EC2.18, EC2.19, IAM.1 (5 total)
Target resources: EC2, EBS, SecurityGroup, IAM Role

In supervised mode, present clarification and artifacts for each stage.
Obtain approval before proceeding to the next stage.
Generate the requirements document as a file.

Kiro presented 4 clarification questions while simultaneously proposing the stage configuration.

# Question Content
Q1 Deployment target region? Is SecurityHub enabled?
Q2 Confirmation of violation details for each control (proposed in table format)
Q3 Is deletion expected after learning? Any instance type preference?
Q4 Single file or split CFn template? Format for requirements document?

Proposed stage configuration:

  1. clarification (questions → answers)
  2. Requirements document creation (file generation)
  3. CFn template creation (weak/fixed)
  4. cfn-guard rules creation
  5. E2E test script creation
  6. Deployment

By specifying "generate the requirements document as a file," the requirements document was configured to be output as an independent file. This becomes the foundation for carrying forward policies to subsequent code generation.

Proposed violation details presented in Q2:

Control Violation Details (Draft)
EC2.8 IMDSv2 disabled (HttpTokens=optional)
EC2.3 EBS volume without encryption
EC2.18 Security group allows 0.0.0.0/0 inbound (SSH:22)
EC2.19 Security group allows 0.0.0.0/0 inbound (RDP:3389)
IAM.1 Full access policy attached to IAM role

Step 2: Requirements Analysis — Finalizing the Specifications

Clarification Answers

Q1. ap-northeast-1. SecurityHub is enabled.

Q2. All OK. However, IAM.1 should be implemented with an inline policy rather than a ManagedPolicy (no iam:CreatePolicy permission).
Action:"*", Resource:"*" inline policy.

Q3. Will delete after learning. Use t4g.nano (ARM/Graviton). Connect via SSM Session Manager, no SSH key.

Q4. Single file. Markdown is fine for the requirements document.

The key point was explicitly stating the IAM.1 implementation constraint (implement with inline policy). Since iam:CreatePolicy is not available in the execution environment, communicating this constraint makes it easier to prevent deployment failures in subsequent stages.

requirements.md Generation and Additions

Since the requirements.md generated by Kiro lacked descriptions of the fixed template, cfn-guard, and E2E test method, I requested the following additions.

Add the following to the requirements document:

1. Include the fixed template in the deliverables. Use the same logical IDs as the vulnerable version, changing or deleting only the properties related to vulnerable configurations. Configure for overwrite deployment with the same stack name.
2. Enable static verification with cfn-guard rules (expected results: weak → all rules FAIL, fixed → PASS)
3. E2E test script that directly confirms actual configuration values with AWS CLI after deployment (not SecurityHub polling. Immediately executable)

Additionally, since the AMI ID was hardcoded, I also requested a change to SSM parameter reference.

Here is the final version of requirements.md after incorporating the additions. Note that requirements.md originally described the Security Hub detection/resolution cycle as the learning objective, but the verification conducted in this article extends only to cfn-guard and AWS CLI E2E.

requirements.md (excerpt from the final version after specification feedback incorporation)
# Requirements Document: SecurityHub Learning Vulnerable EC2 Environment

## 1. Purpose

Build an EC2 environment with intentional vulnerabilities detectable by AWS SecurityHub using CloudFormation,
to experience the learning cycle of "SecurityHub finding detection → IaC remediation → finding resolution."

## 2. Prerequisites

| Item | Value |
|---|---|
| AWS Region | ap-northeast-1 (Tokyo) |
| SecurityHub | Enabled |
| Deployment Method | CloudFormation (single template) |
| Lifecycle | Stack deletion after learning (temporary use) |
| AMI | Dynamically retrieved from SSM public parameters |

## 3. Target Vulnerabilities and Violation Specifications

### 3.1 EC2.8 — IMDSv2 Disabled
- Implementation: `MetadataOptions.HttpTokens = optional`

### 3.2 EC2.3 — EBS Encryption Disabled
- Implementation: BlockDeviceMappings `Encrypted = false`

### 3.3 EC2.18 — Security Group Inbound SSH Unrestricted
- Implementation: Ingress: Port 22, CidrIp = 0.0.0.0/0

### 3.4 EC2.19 — Security Group Inbound RDP Unrestricted
- Implementation: Ingress: Port 3389, CidrIp = 0.0.0.0/0

### 3.5 IAM.1 — IAM Inline Policy Full Access
- Implementation: Inline policy Action="*", Resource="*"
- Constraint: Implemented with inline policy due to lack of `iam:CreatePolicy` permission

## 4. Resource Configuration

CloudFormation Stack: weak-ec2-securityhub-lab
├── SecurityGroup (EC2.18, EC2.19 violation)
│   ├── VpcId: Parameter reference
│   ├── Ingress: TCP 22, 0.0.0.0/0
│   └── Ingress: TCP 3389, 0.0.0.0/0
├── IAM Role (IAM.1 violation)
│   └── Inline Policy: Action=*, Resource=*
├── IAM Instance Profile
└── EC2 Instance (EC2.8, EC2.3 violation)
    ├── InstanceType: t4g.nano (ARM/Graviton)
    ├── MetadataOptions.HttpTokens: optional
    ├── BlockDeviceMapping: Encrypted=false
    └── KeyName: None (connect via SSM Session Manager)

(omitted)

## 6. Deliverables

| Deliverable | Filename |
|---|---|
| Requirements document | requirements.md |
| Vulnerable CFn template | template-weak.yaml |
| Fixed CFn template | template-fixed.yaml |
| cfn-guard rules file | rules.guard |
| E2E test script | e2e-test.sh |

### Template Design Policy
- Vulnerable and fixed versions use the same logical IDs
- Only properties related to vulnerable configurations are changed or deleted
- Can be overwrite-deployed with the same stack name

### E2E Test Script Specifications

| Verification Item | Expected Value (weak) | Expected Value (fixed) |
|---|---|---|
| EC2.8 HttpTokens | optional | required |
| EC2.3 Encrypted | false | true |
| EC2.18 Ingress 22 | 0.0.0.0/0 present | absent |
| EC2.19 Ingress 3389 | 0.0.0.0/0 present | absent |
| IAM.1 Inline policy | Action:* present | absent |

Step 3: Code Generation — Translating Specifications into CFn and Verification Means

Kiro generated 4 files based on the requirements document.

File Role
template-weak.yaml CFn template containing 5 vulnerable configurations
template-fixed.yaml Template maintaining the same logical IDs as the vulnerable version with 5 verification items remediated
rules.guard cfn-guard rules (5 items)
e2e-test.sh Script for confirming actual configurations after deployment

Logical ID Unification

By clearly stating in requirements.md "use the same logical IDs as the vulnerable version, changing or deleting only the properties related to vulnerable configurations," the logical IDs of both templates were unified.

Resource weak fixed
SG LabSecurityGroup LabSecurityGroup ✅
Role LabRole LabRole ✅
Profile LabInstanceProfile LabInstanceProfile ✅
Instance LabInstance LabInstance ✅

When overwrite-deploying with the same stack name, target resources are treated as CloudFormation differential updates. Depending on the property, it may be treated as Replace rather than Update.

Vulnerable Template

template-weak.yaml (excerpt of vulnerable configurations)
# Security Group — EC2.18, EC2.19
SecurityGroupIngress:
  - IpProtocol: tcp
    FromPort: 22
    ToPort: 22
    CidrIp: 0.0.0.0/0              # EC2.18
  - IpProtocol: tcp
    FromPort: 3389
    ToPort: 3389
    CidrIp: 0.0.0.0/0              # EC2.19

# IAM Role — IAM.1
Policies:
  - PolicyName: OverlyPermissive
    PolicyDocument:
      Statement:
        - Effect: Allow
          Action: "*"
          Resource: "*"

# EC2 Instance — EC2.8, EC2.3
MetadataOptions:
  HttpTokens: optional              # EC2.8
BlockDeviceMappings:
  - DeviceName: /dev/xvda
    Ebs:
      Encrypted: false              # EC2.3

Fixed Template Changes

Control Remediation
EC2.8 HttpTokens: required
EC2.3 Encrypted: true
EC2.18 / EC2.19 Set SecurityGroupIngress: [] and remove inbound rules for 22/tcp and 3389/tcp
IAM.1 Remove inline policy → AmazonSSMManagedInstanceCore only

cfn-guard Verification

A FAIL in cfn-guard means "a rule violation was detected." Having all items FAIL on the vulnerable version confirms that the vulnerable configurations are included as intended.

rules.guard
# EC2.8 equivalent: IMDSv2 enforcement (HttpTokens=required)
rule EC2_IMDSV2_REQUIRED {
  AWS::EC2::Instance {
    Properties.MetadataOptions.HttpTokens == "required"
  }
}

# EC2.3 equivalent: EBS root volume encryption
rule EBS_ENCRYPTED {
  AWS::EC2::Instance {
    Properties.BlockDeviceMappings[*].Ebs.Encrypted == true
  }
}

# EC2.18 equivalent: Security group inbound 0.0.0.0/0 prohibited
rule NO_UNRESTRICTED_INGRESS {
  AWS::EC2::SecurityGroup {
    Properties.SecurityGroupIngress not exists
      or Properties.SecurityGroupIngress == []
      or Properties.SecurityGroupIngress[*].CidrIp != "0.0.0.0/0"
  }
}

# EC2.19 equivalent: Unrestricted inbound to high-risk port (3389) prohibited
rule NO_HIGH_RISK_PORT_INGRESS {
  AWS::EC2::SecurityGroup {
    Properties.SecurityGroupIngress not exists
      or Properties.SecurityGroupIngress == []
      or Properties.SecurityGroupIngress[*] {
        FromPort != 3389
        or CidrIp != "0.0.0.0/0"
      }
  }
}

# IAM.1 equivalent: In this template, the design deletes the overly permissive inline policy,
# so a simple judgment is made based on the presence or absence of inline policies
rule NO_IAM_INLINE_POLICY {
  AWS::IAM::Role {
    Properties.Policies not exists
      or Properties.Policies == []
  }
}

NO_UNRESTRICTED_INGRESS is a simple rule that detects any inbound 0.0.0.0/0. In this weak template, both the EC2.18-equivalent and EC2.19-equivalent configurations contain 0.0.0.0/0, but the rules are separated to explicitly show the correspondence with verification items.

The initially generated rules only checked Properties.SecurityGroupIngress not exists. It failed because it couldn't pass the fixed version's SecurityGroupIngress: [], so I resolved this by adding or Properties.SecurityGroupIngress == [].

Final verification results:

Rule weak fixed
EC2_IMDSV2_REQUIRED FAIL ✅ PASS ✅
EBS_ENCRYPTED FAIL ✅ PASS ✅
NO_UNRESTRICTED_INGRESS FAIL ✅ PASS ✅
NO_HIGH_RISK_PORT_INGRESS FAIL ✅ PASS ✅
NO_IAM_INLINE_POLICY FAIL ✅ PASS ✅

E2E Test Script

./e2e-test.sh weak|fixed retrieves configuration values of deployed resources using AWS CLI and compares them against expected values.

e2e-test.sh (overview)
#!/bin/bash
set -euo pipefail

# Usage: ./e2e-test.sh [weak|fixed]
MODE="${1:-}"
STACK_NAME="weak-ec2-securityhub-lab"
REGION="ap-northeast-1"

# Retrieve resource information from CFn Outputs
get_output() {
  aws cloudformation describe-stacks \
    --stack-name "$STACK_NAME" --region "$REGION" \
    --query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" \
    --output text
}

INSTANCE_ID=$(get_output InstanceId)
SG_ID=$(get_output SecurityGroupId)
ROLE_NAME=$(get_output RoleName)

echo "=== E2E Test: $STACK_NAME ==="
echo "  InstanceId:  $INSTANCE_ID"
echo "  SgId:        $SG_ID"
echo "  RoleName:    $ROLE_NAME"

FAIL=0

# EC2.8: IMDSv2
HTTP_TOKENS=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" \
  --region "$REGION" \
  --query 'Reservations[0].Instances[0].MetadataOptions.HttpTokens' \
  --output text)

# EC2.3: EBS encryption
ENCRYPTED=$(aws ec2 describe-volumes --region "$REGION" \
  --filters "Name=attachment.instance-id,Values=$INSTANCE_ID" \
  --query 'Volumes[0].Encrypted' --output text)

# EC2.18: Ingress 0.0.0.0/0:22
INGRESS_SSH=$(aws ec2 describe-security-groups --group-ids "$SG_ID" \
  --region "$REGION" \
  --query 'SecurityGroups[0].IpPermissions[?FromPort==`22`].IpRanges[].CidrIp' \
  --output text)

# EC2.19: Ingress 0.0.0.0/0:3389
INGRESS_RDP=$(aws ec2 describe-security-groups --group-ids "$SG_ID" \
  --region "$REGION" \
  --query 'SecurityGroups[0].IpPermissions[?FromPort==`3389`].IpRanges[].CidrIp' \
  --output text)

# IAM.1: Inline policy
INLINE_POLICIES=$(aws iam list-role-policies --role-name "$ROLE_NAME" \
  --query 'PolicyNames' --output text)

# ... weak/fixed judgment logic (omitted)

Step 4: Review + Deployment Verification

I told AI-DLC "approved" and executed the deployment.

Deployment Failure → Fix: VpcId Not Specified

The initial deployment failed. CloudFormation failed to create the resource because VpcId was not specified for the security group.

Kiro presented a fix, which I reviewed and applied:

  • Added VpcId parameter to both templates (Type: AWS::EC2::VPC::Id)
  • Added VpcId: !Ref VpcId to the security group
  • Deleted the failed stack → Redeployed specifying the VPC ID as a parameter

The redeployment succeeded with CREATE_COMPLETE.

E2E Test Failure → Fix: JMESPath Query

The initial E2E test execution failed for EC2.18/EC2.19 verification.

FAIL: EC2.18 expected 0.0.0.0/0, got ''
FAIL: EC2.19 expected 0.0.0.0/0, got ''

There were two causes:

  1. JMESPath backticks conflicted with shell interpretation, preventing the expected query from being passed correctly
  2. Nested filter [?condition].IpRanges[?condition].CidrIp did not expand as expected

Kiro confirmed the actual data with --output json and fixed it to use single quotes with flat expansion.

# After fix
--query 'SecurityGroups[0].IpPermissions[?FromPort==`22`].IpRanges[].CidrIp'
--query 'SecurityGroups[0].IpPermissions[?FromPort==`3389`].IpRanges[].CidrIp'

weak ALL PASS

Confirmed all 5 items PASS in the E2E script's weak mode.

=== E2E Test: weak-ec2-securityhub-lab ===
  InstanceId:  i-xxxxxxxxxxxxxxxxx
  SgId:        sg-xxxxxxxxxxxxxxxxx
  RoleName:    weak-ec2-securityhub-lab-role

[EC2.8]  HttpTokens = optional        → PASS
[EC2.3]  Encrypted = False            → PASS
[EC2.18] Ingress 0.0.0.0/0:22 present → PASS
[EC2.19] Ingress 0.0.0.0/0:3389 present → PASS
[IAM.1]  Action:* inline policy       → PASS
=== Result: ALL PASS ===

Fixed Version Overwrite Deployment → fixed ALL PASS

Overwrote the same stack weak-ec2-securityhub-lab with the fixed version. The instance was Replaced due to the EBS encryption change (as expected).

[EC2.8]  HttpTokens = required           PASS
[EC2.3]  Encrypted = True                PASS
[EC2.18] Ingress 0.0.0.0/0:22 absent     PASS
[EC2.19] Ingress 0.0.0.0/0:3389 absent   PASS
[IAM.1]  No inline policy                PASS
=== Result: ALL PASS ===

Running the E2E script's weak mode against the fixed stack as a reverse check also resulted in all 5 items FAIL, confirming that the judgment works as expected. The stack was deleted after verification.

Step 4 Impressions

For both the deployment failure (VpcId) and E2E test failure (JMESPath), Kiro was able to proceed from confirming actual data → identifying the cause → generating a fix proposal, without requiring humans to provide additional explanations.

For reference, in this verification environment, the total time from Kickoff to deployment verification completion was approximately 16 minutes, consuming 8.40 credits.

Specification Feedback — Reverse Reflection from Implementation to Specification

After deployment and E2E testing were complete, I checked the consistency between the requirements document and the actual files.

Are there any discrepancies between requirements.md and the actual files? Look for them.

Kiro detected 2 discrepancies and presented proposed fixes to requirements.md. I reviewed and applied them.

Discrepancy Fix Content
VpcId parameter Added VpcId to the configuration diagram in Section 4, created a parameter table, added a CFn constraint note
EC2.19 Ingress rule Explicitly stated Ingress specifications for both weak (SSH+RDP open) and fixed (all rules deleted)

By simply requesting "find the discrepancies," Kiro was able to compare the specification and implementation, and proceed to discrepancy detection and fix proposal presentation.

Supplement: Also IaC-ify Existing Vulnerable EC2s and Place Them in the Improvement Cycle

AI-DLC has a reverse-engineering skill that can generate IaC scaffolding and specification documents based on existing resources. This is introduced in a preceding article.

https://dev.classmethod.jp/articles/ai-dlc-v2-reverse-engineering-improvement-cycle/

This time, I executed reverse engineering against the verification EC2 with the vulnerable version redeployed. Starting from a single instance ID, it retrieves related resources in a chain: describe-instances → SG → EBS → IAM → inline policies. In the flow of generating CFn templates and specification document artifacts, in this verification, even resources outside of CloudFormation management could have IaC scaffolding generated starting from an instance ID.

In this reverse engineering, no instructions related to Security Hub were given in the prompt. However, the generated component-inventory.md included a Security Hub detection mapping. The following is that content (some IDs are masked).

component-inventory.md
# Component Inventory

## IAM Resources

### LabRole (AWS::IAM::Role)
- **Logical ID**: LabRole
- **Trust Policy**: ec2.amazonaws.com
- **Managed Policy**: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- **Inline Policies**:
  - Name: `OverlyPermissive`
  - Effect: Allow / Action: `*` / Resource: `*`
- **⚠️ Risk**: Full access to all AWS services (Security Hub IAM.1 target)

### LabInstanceProfile (AWS::IAM::InstanceProfile)
- **Logical ID**: LabInstanceProfile
- **Role**: !Ref LabRole

## Network Resources

### LabSecurityGroup (AWS::EC2::SecurityGroup)
- **Logical ID**: LabSecurityGroup
- **Description**: "Lab SG - intentionally weak"
- **VPC**: !Ref VpcId
- **Inbound**: TCP:22 ← 0.0.0.0/0 (⚠️ Security Hub EC2.13/EC2.19 target)
- **Outbound**: All ← 0.0.0.0/0

## Compute Resources

### LabInstance (AWS::EC2::Instance)
- **Logical ID**: LabInstance
- **AMI**: ami-xxxxxxxxxxxxxxxxx (arm64, Amazon Linux 2023)
- **Type**: t4g.nano (2vCPU, arm64)
- **Subnet**: !Ref SubnetId
- **Monitoring**: Disabled
- **EBS**:
  - Device: /dev/xvda
  - Size: 8GB / Type: gp3 / IOPS: 3000 / Throughput: 125MB/s
  - Encrypted: false (⚠️ Security Hub EC2.3 target)
  - DeleteOnTermination: true
- **Metadata**:
  - HttpTokens: optional (⚠️ IMDSv1 enabled / Security Hub EC2.8 target)
  - HttpPutResponseHopLimit: 2
  - HttpEndpoint: enabled
- **Tags**: Name=weak-ec2-lab

## Security Hub Detection Mapping

| Control | Target Resource | Vulnerable Configuration |
|---|---|---|
| EC2.8 | LabInstance | IMDSv2 not enforced (HttpTokens: optional) |
| EC2.3 | EBS (/dev/xvda) | No encryption |
| EC2.13 | LabSecurityGroup | SSH 0.0.0.0/0 |
| EC2.19 | LabSecurityGroup | SSH 0.0.0.0/0 |
| IAM.1 | LabRole | OverlyPermissive policy |

Among the intentionally configured vulnerable items, IMDSv2 not enforced, EBS without encryption, SSH inbound open, and overly permissive IAM inline policy were also reported as problems in the reverse engineering.

Note that while the main text targets EC2.18 (unrestricted SSH), the mapping returned by reverse engineering uses EC2.13. EC2.13 is a control that "does not allow SSH inbound from 0.0.0.0/0 in security groups," which overlaps with EC2.18 in detection targets but is a different control. Since no control ID was specified in the prompt for reverse engineering, EC2.13 was selected at Kiro's discretion.

Summary

In this verification, I used AI-DLC's specification-definition-first process to handle a subject requiring consistency between multiple artifacts, such as a "vulnerable version and fixed version pair." CloudFormation templates, cfn-guard rules, E2E test scripts, and the fixed template were all generated from the same specification as the starting point. By proceeding while adding missing content, it became easier to maintain the unification of logical IDs and the correspondence between verification targets.

The key point is not treating the requirements document as a one-time creation. If the initial requirements.md is lacking, add to it, and write back differences found during deployment verification to the specification. By reflecting execution-time constraints discovered like the missing VpcId, it becomes easier to reduce rework next time. Through this cycle, artifacts can be developed while maintaining consistency.

The reverse engineering tried in the supplement can also connect to this specification development cycle. As in this case, if existing EC2s can be analyzed starting from an instance ID and IaC scaffolding can be extracted, an entry point can be created to place existing environments into the same improvement flow.

Share this article

AWSのお困り事はクラスメソッドへ