I Tried Using Kiro's AI-DLC to Deliberately Generate and Validate a CloudFormation Template for a Vulnerable EC2 Environment

I Tried Using Kiro's AI-DLC to Deliberately Generate and Validate a CloudFormation Template for a Vulnerable EC2 Environment

Using Kiro CLI's AI-DLC, I generated vulnerable and fixed EC2 CloudFormation templates based on Security Hub controls, verified them end to end with cfn-guard and the AWS CLI, and wrote deployment discrepancies back to requirements.md.
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 moves through the stages of requirements definition → code generation → review, with human approval at each step.

This time, I used AI-DLC to generate an EC2 environment with deliberately vulnerable settings using CloudFormation. This is a subject where the resource logical IDs of the vulnerable and fixed versions, the cfn-guard rules, and the verification targets of the E2E check script must all be aligned. In AI-DLC's supervised mode, I created each artifact starting from the requirements document, and verified the process through to writing differences found during deployment verification back into requirements.md.

The verification scope is not detection or resolution of Security Hub Findings themselves, but static verification with cfn-guard and E2E checks with the 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 the subject EC2.8, EC2.3, EC2.18, EC2.19, IAM.1
Target resources EC2, EBS, SecurityGroup, IAM Role

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

Running the AI-DLC Workflow

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

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

# Symlink the skills into 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 rest of this article, I organize the six stages proposed by Kiro into four Steps.

Step 1: Kickoff — Agreeing on the Stage Structure

I invoked the skill and submitted the following prompt.

Kiro CLI launching AI-DLC and starting Stage 1: Clarification

/skill aidlc-orchestration

SecurityHubで検出される脆弱性を意図的に持たせたEC2環境をCloudFormationで構築したい。
目的はSecurityHub指摘の検出→IaC修正→指摘解消の学習サイクル体験。

対象脆弱性: EC2.8, EC2.3, EC2.18, EC2.19, IAM.1(計5件)
対象リソース: EC2, EBS, SecurityGroup, IAMロール

supervisedモードで各ステージのclarificationと成果物を毎回提示して。
次のステージに進む前に承認を得て。
要件定義書はファイルとして生成して。

Kiro presented four clarification questions and, at the same time, proposed a stage structure.

# Question
Q1 Deployment region? Is SecurityHub already enabled?
Q2 Confirmation of the violation details for each control (proposed in table form)
Q3 Is deletion planned after the exercise? Any preference for the instance type?
Q4 Should the CFn template be a single file or split? What format for the requirements document?

Proposed stage structure:

  1. Clarification (questions → answers)
  2. Create the requirements document (file generation)
  3. Create the CFn templates (weak/fixed)
  4. Create the cfn-guard rules
  5. Create the E2E test script
  6. Deploy

By specifying "generate the requirements document as a file," the structure was set up so that the requirements document is output as an independent file. This becomes the foundation for carrying the policy over to later code generation.

Proposed violation details presented in Q2:

Control Violation (proposal)
EC2.8 IMDSv2 disabled (HttpTokens=optional)
EC2.3 EBS volume not encrypted
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 the IAM role

Step 2: Requirements Analysis — Firming Up the Specification

Clarification Answers

Q1. ap-northeast-1。SecurityHub有効化済み。

Q2. 全てOK。ただしIAM.1はManagedPolicy付与ではなくインラインポリシーで実装して(iam:CreatePolicy権限がないため)。Action:"*", Resource:"*" のインラインポリシー。

Q3. 学習後に削除する。t4g.nano(ARM/Graviton)で。SSM Session Managerで接続、SSH鍵なし。

Q4. 単一ファイル。要件定義書はMarkdownでOK。

The key point is that I explicitly stated the implementation constraint for IAM.1 (implement it with an inline policy). Because the execution environment does not have iam:CreatePolicy, communicating this constraint makes it easier to prevent later deployment failures.

requirements.md Generation and Additions

The requirements.md generated by Kiro lacked descriptions of the fixed template, cfn-guard, and the E2E test approach, so I asked for the following additions.

要件定義書に以下を追記して:

1. 修正版テンプレートも成果物に含める。脆弱版と同一論理IDで脆弱設定に関わるプロパティのみ変更または削除。同一スタック名で上書きデプロイする構成
2. cfn-guardルールで静的検証できること(期待結果: weak→全ルールFAIL、fixed→PASS)
3. デプロイ後にAWS CLIで実設定値を直接確認するE2Eテストスクリプト(SecurityHubポーリングではない。即時実行可能)

Because the AMI ID was hardcoded, I also asked for it to be changed to an SSM parameter reference.

Here is the final version of requirements.md after the additions. Note that requirements.md describes the Security Hub detection and resolution cycle as the original learning goal, but the verification performed in this article goes only as far as cfn-guard and the AWS CLI E2E.

requirements.md (excerpt from the final version after reflecting specification feedback)
# 要件定義書: SecurityHub 学習用 脆弱性EC2環境

## 1. 目的

AWS SecurityHub で検出される脆弱性を意図的に持つ EC2 環境を CloudFormation で構築し、
「SecurityHub 指摘の検出 → IaC 修正 → 指摘解消」の学習サイクルを体験する。

## 2. 前提条件

| 項目 | 値 |
|---|---|
| AWSリージョン | ap-northeast-1(東京) |
| SecurityHub | 有効化済み |
| デプロイ方式 | CloudFormation(単一テンプレート) |
| ライフサイクル | 学習後にスタック削除(一時利用) |
| AMI | SSM 公開パラメータから動的取得 |

## 3. 対象脆弱性と違反仕様

### 3.1 EC2.8 — IMDSv2 の無効化
- 実装方法: `MetadataOptions.HttpTokens = optional`

### 3.2 EC2.3 — EBS 暗号化の無効化
- 実装方法: BlockDeviceMappings の `Encrypted = false`

### 3.3 EC2.18 — セキュリティグループ インバウンド SSH 全開放
- 実装方法: Ingress: Port 22, CidrIp = 0.0.0.0/0

### 3.4 EC2.19 — セキュリティグループ インバウンド RDP 全開放
- 実装方法: Ingress: Port 3389, CidrIp = 0.0.0.0/0

### 3.5 IAM.1 — IAM インラインポリシー フルアクセス
- 実装方法: インラインポリシー Action="*", Resource="*"
- 制約: `iam:CreatePolicy` 権限がないためインラインポリシーで実装

## 4. リソース構成

CloudFormation Stack: weak-ec2-securityhub-lab
├── SecurityGroup (EC2.18, EC2.19 違反)
│   ├── VpcId: パラメータ参照
│   ├── Ingress: TCP 22, 0.0.0.0/0
│   └── Ingress: TCP 3389, 0.0.0.0/0
├── IAM Role (IAM.1 違反)
│   └── Inline Policy: Action=*, Resource=*
├── IAM Instance Profile
└── EC2 Instance (EC2.8, EC2.3 違反)
    ├── InstanceType: t4g.nano (ARM/Graviton)
    ├── MetadataOptions.HttpTokens: optional
    ├── BlockDeviceMapping: Encrypted=false
    └── KeyName: なし(SSM Session Manager で接続)

(中略)

## 6. 成果物

| 成果物 | ファイル名 |
|---|---|
| 要件定義書 | requirements.md |
| 脆弱版 CFn テンプレート | template-weak.yaml |
| 修正版 CFn テンプレート | template-fixed.yaml |
| cfn-guard ルールファイル | rules.guard |
| E2E テストスクリプト | e2e-test.sh |

### テンプレート設計方針
- 脆弱版・修正版は同一の論理 ID を使用
- 脆弱設定に関わるプロパティのみを変更または削除
- 同一スタック名で上書きデプロイ可能

### E2E テストスクリプト仕様

| 検証項目 | 期待値(weak) | 期待値(fixed) |
|---|---|---|
| EC2.8 HttpTokens | optional | required |
| EC2.3 Encrypted | false | true |
| EC2.18 Ingress 22 | 0.0.0.0/0 あり | なし |
| EC2.19 Ingress 3389 | 0.0.0.0/0 あり | なし |
| IAM.1 インラインポリシー | Action:* あり | なし |

Step 3: Code Generation — Turning the Specification into CFn and Verification Means

Kiro generated four files based on the requirements document.

File Role
template-weak.yaml CFn template containing the 5 vulnerable settings
template-fixed.yaml Template that keeps the same logical IDs as the vulnerable version and fixes the 5 verification items
rules.guard cfn-guard rules (5 rules)
e2e-test.sh Script to check actual settings after deployment

Unifying Logical IDs

By stating in requirements.md that "the logical IDs are the same as the vulnerable version, and only properties related to vulnerable settings are changed or removed," 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, the target resources are handled as CloudFormation differential updates. Depending on the property, this may result in a Replace rather than an Update.

Vulnerable Template

template-weak.yaml (excerpt of vulnerable settings)
# 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

Changes in the Fixed Template

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

cfn-guard Verification

A FAIL in cfn-guard means "a rule violation was detected." All rules failing on the vulnerable version confirms that the vulnerable settings are included as intended.

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

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

# Equivalent to EC2.18: prohibit 0.0.0.0/0 inbound on security groups
rule NO_UNRESTRICTED_INGRESS {
  AWS::EC2::SecurityGroup {
    Properties.SecurityGroupIngress not exists
      or Properties.SecurityGroupIngress == []
      or Properties.SecurityGroupIngress[*].CidrIp != "0.0.0.0/0"
  }
}

# Equivalent to EC2.19: prohibit unrestricted inbound to a high-risk port (3389)
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"
      }
  }
}

# Equivalent to IAM.1: since this template is designed to remove the overly permissive inline policy,
# a simple check based on the presence 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 0.0.0.0/0 inbound in general. In this weak template, both the EC2.18-equivalent and EC2.19-equivalent settings include 0.0.0.0/0, but the rules are separated to make the correspondence with the verification items explicit.

The initially generated rule only looked at Properties.SecurityGroupIngress not exists. It could not pass the fixed version's SecurityGroupIngress: [] and resulted in FAIL, so I resolved it 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

With ./e2e-test.sh weak|fixed, the configuration values of the deployed resources are retrieved with the AWS CLI and compared against the 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"

# 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暗号化
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_POLICIES=$(aws iam list-role-policies --role-name "$ROLE_NAME" \
  --query 'PolicyNames' --output text)

# ... weak/fixed判定ロジック(省略)

Step 4: Review + Deployment Verification

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

Deployment Failure → Fix: VpcId Not Specified

The first deployment failed. Because VpcId was not specified for the security group, CloudFormation failed to create the resource.

Kiro presented a fix, and after reviewing the diff it was applied:

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

The redeployment succeeded with CREATE_COMPLETE.

E2E Test Failure → Fix: JMESPath Query

On the first E2E test run, the EC2.18/EC2.19 checks failed.

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. The backticks in JMESPath collided with shell interpretation, so the query was not passed as expected
  2. The nested filter [?condition].IpRanges[?condition].CidrIp did not expand as expected

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

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

weak ALL PASS

I confirmed that 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 あり   → PASS
[EC2.19] Ingress 0.0.0.0/0:3389 あり → PASS
[IAM.1]  Action:* インラインポリシー  → PASS
=== Result: ALL PASS ===

Overwrite Deployment of the Fixed Version → fixed ALL PASS

I overwrite-deployed the fixed version to the same stack weak-ec2-securityhub-lab. 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 なし      PASS
[EC2.19] Ingress 0.0.0.0/0:3389 なし    PASS
[IAM.1]  インラインポリシーなし          PASS
=== Result: ALL PASS ===

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

Step 4 Impressions

For both the deployment failure (VpcId) and the E2E test failure (JMESPath), Kiro proceeded from checking actual data → identifying the cause → generating a fix, without a human needing to explain the cause.

As a reference, in this environment the time from Kickoff to completion of deployment verification was about 16 minutes, and the credits consumed were 8.40.

Specification Feedback — Reverse-Reflecting Implementation into the Specification

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

requirements.md と実ファイルに差分がでていない?探して。

Kiro detected two differences and presented proposed corrections to requirements.md. They were applied after confirmation.

Difference Fix
VpcId parameter Added VpcId to the configuration diagram in section 4, added a new parameter table, and noted the CFn constraint
EC2.19 Ingress rules Made the Ingress specification explicit for weak (SSH+RDP open) and fixed (all rules removed) respectively

Just by asking it to "find the differences," Kiro was able to compare the specification and implementation, detect differences, and present proposed corrections.

Supplement: Bringing an Existing Vulnerable EC2 into IaC and Onto the Improvement Cycle

AI-DLC has a reverse-engineering skill that can generate a starting point for IaC and specification documents from existing resources. I introduced it in a previous article.

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

This time, I ran reverse engineering against the verification EC2 on which I had redeployed the vulnerable version. From a single instance ID, it retrieves related resources one after another: describe-instances → SG → EBS → IAM → inline policy. In the flow of generating a CFn template and specification artifacts, in this verification it was able to generate a starting point for IaC from the instance ID even for resources not managed by CloudFormation.

In this reverse-engineering run, I gave no instructions about Security Hub in the prompt. However, the generated component-inventory.md included a Security Hub detection mapping. The following is its content (some IDs are masked).

component-inventory.md
# Component Inventory

## IAM Resources

### LabRole (AWS::IAM::Role)
- **論理ID**: LabRole
- **信頼ポリシー**: ec2.amazonaws.com
- **マネージドポリシー**: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- **インラインポリシー**:
  - 名前: `OverlyPermissive`
  - Effect: Allow / Action: `*` / Resource: `*`
- **⚠️ リスク**: 全 AWS サービスへの完全アクセス(Security Hub IAM.1 対象)

### LabInstanceProfile (AWS::IAM::InstanceProfile)
- **論理ID**: LabInstanceProfile
- **ロール**: !Ref LabRole

## Network Resources

### LabSecurityGroup (AWS::EC2::SecurityGroup)
- **論理ID**: LabSecurityGroup
- **説明**: "Lab SG - intentionally weak"
- **VPC**: !Ref VpcId
- **インバウンド**: TCP:22 ← 0.0.0.0/0 (⚠️ Security Hub EC2.13/EC2.19 対象)
- **アウトバウンド**: All ← 0.0.0.0/0

## Compute Resources

### LabInstance (AWS::EC2::Instance)
- **論理ID**: LabInstance
- **AMI**: ami-xxxxxxxxxxxxxxxxx (arm64, Amazon Linux 2023)
- **タイプ**: t4g.nano (2vCPU, arm64)
- **サブネット**: !Ref SubnetId
- **モニタリング**: 無効
- **EBS**:
  - デバイス: /dev/xvda
  - サイズ: 8GB / タイプ: gp3 / IOPS: 3000 / スループット: 125MB/s
  - 暗号化: false(⚠️ Security Hub EC2.3 対象)
  - DeleteOnTermination: true
- **メタデータ**:
  - HttpTokens: optional(⚠️ IMDSv1 有効 / Security Hub EC2.8 対象)
  - HttpPutResponseHopLimit: 2
  - HttpEndpoint: enabled
- **タグ**: Name=weak-ec2-lab

## Security Hub 検出マッピング

| コントロール | 対象リソース | 脆弱設定 |
|---|---|---|
| EC2.8 | LabInstance | IMDSv2 未強制 (HttpTokens: optional) |
| EC2.3 | EBS (/dev/xvda) | 暗号化なし |
| EC2.13 | LabSecurityGroup | SSH 0.0.0.0/0 |
| EC2.19 | LabSecurityGroup | SSH 0.0.0.0/0 |
| IAM.1 | LabRole | OverlyPermissive ポリシー |

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

Note that this article targets EC2.18 (SSH fully open), but the mapping returned by the reverse engineering uses EC2.13. EC2.13 is the control "security groups should not allow ingress from 0.0.0.0/0 to SSH," which overlaps with EC2.18 in what it detects but is a separate control. Since the reverse engineering was not given control IDs in the prompt, EC2.13 was chosen at Kiro's discretion.

Summary

In this verification, I handled a subject requiring consistency across multiple artifacts, namely a "pair of vulnerable and fixed versions," using AI-DLC's specification-first process. The CloudFormation templates, cfn-guard rules, E2E test script, and fixed template were all generated starting from the same specification. By adding what was missing as I went, it became easier to keep the logical IDs unified and the correspondence between verification targets intact.

The key point is not to create the requirements document once and be done with it. If the initial requirements.md is lacking, add to it, and write differences found during deployment verification back into the specification as well. By reflecting constraints discovered at runtime, such as the unspecified VpcId, you can more easily reduce rework next time. This cycle lets you grow the artifacts while maintaining consistency.

The reverse engineering I tried in the supplement can also be connected to this specification development cycle. If, as here, an existing EC2 can be analyzed starting from the instance ID to extract a starting point for IaC, it creates an entry point for putting existing environments onto the same improvement flow.

Share this article

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