
I tried generating and validating an intentionally vulnerable EC2 environment CloudFormation template with Kiro's AI-DLC
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.

/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:
- clarification (questions → answers)
- Requirements document creation (file generation)
- CFn template creation (weak/fixed)
- cfn-guard rules creation
- E2E test script creation
- 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
VpcIdparameter to both templates (Type: AWS::EC2::VPC::Id) - Added
VpcId: !Ref VpcIdto 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:
- JMESPath backticks conflicted with shell interpretation, preventing the expected query from being passed correctly
- Nested filter
[?condition].IpRanges[?condition].CidrIpdid 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.
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
## 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.
