
Safe AWS operations with AI. IAM privilege escalation like sudo with Kiro CLI
This page has been translated by machine translation. View original
Introduction
Kiro CLI can use multiple authentication methods for AWS operations. The same authentication chain as AWS CLI/SDK can be used, such as environment variables, EC2 instance profiles, and profiles in ~/.aws/credentials or ~/.aws/config.
While convenient, there may be concerns about passing strong IAM permissions to Kiro. The risk of AI agents making unintended changes or deletions to resources might be unacceptable, especially in production environments.
Although Kiro has methods to control available tools and operations in agent settings, this article adopts a more fundamental approach: "switching AWS profiles used by Kiro." AWS profiles are named configurations defined in ~/.aws/config that can be linked to different IAM roles (permission sets). By switching profiles, you can change the AWS permissions available to Kiro. Permission control through IAM policies provides a reliable security boundary that doesn't depend on LLM behavior.
Specifically, we'll combine Kiro CLI's custom agent feature with AWS CLI's native AssumeRole to implement a sudo-like operation where "normally read-only, elevated permissions only when needed."
What We're Building
We'll implement three layers of permission separation:
| Layer | Role | Permissions | Security Boundary |
|---|---|---|---|
| EC2 Instance | kiro-ec2-* |
SSMCore + AssumeRole only | IAM Policy |
| ReadOnly | kiro-readonly-* |
ReadOnlyAccess | IAM Policy |
| Elevated | kiro-elevated-* |
PowerUserAccess | IAM Policy |
The security boundary is solely IAM policies. Agent prompts (like "do not perform change operations") serve as UX guardrails (helping prevent mistakes) but are not defense mechanisms. Since LLMs may ignore prompt instructions, it's important to block operations definitively with IAM.
How It Works
Custom Agents
In Kiro CLI, you can create custom agents by placing agent definition JSONs in ~/.kiro/agents/. When you switch with /agent swap <name>, that agent's specific prompts and tool restrictions are applied.
Even when switching agents, the conversation context (history) is preserved. For example, the following exchange is possible:
[readonly] 3% > The password is "banana". Please remember it.
> I understand. I've remembered the password "banana".
[readonly] 5% > /agent swap elevated
[elevated] 5% > What is the password?
> The password is "banana".
You can switch to elevated to perform change operations with the context from readonly investigations, then switch back. The benefit of this method compared to using separate Kiro sessions for permission management is that you can seamlessly escalate privileges while maintaining the context of investigation results and work.
Note that /agent swap is a slash command that users directly input into the CLI, not a tool that the LLM can execute. If the LLM writes /agent swap elevated in a response, it's just text output. In other words, the readonly agent cannot switch to elevated on its own. Permission escalation always requires explicit user action.
AWS CLI's Native AssumeRole
While you could use shell scripts to call aws sts assume-role and write credentials to a file, using the native features of ~/.aws/config is much simpler and safer.
[profile kiro-readonly]
role_arn = arn:aws:iam::<ACCOUNT_ID>:role/kiro-readonly-<STACK_NAME>
credential_source = Ec2InstanceMetadata
role_session_name = kiro-readonly-session
[profile kiro-elevated]
role_arn = arn:aws:iam::<ACCOUNT_ID>:role/kiro-elevated-<STACK_NAME>
source_profile = kiro-readonly
role_session_name = kiro-elevated-session
This configuration accomplishes:
- When using
kiro-readonlyprofile → Automatically AssumeRole to ReadOnly role from EC2 instance metadata - When using
kiro-elevatedprofile → First AssumeRole to ReadOnly, then chain to Elevated
AWS SDK/CLI automatically handles AssumeRole and token renewal in the background. Credentials are never stored in plaintext on disk.
| Aspect | Script Method | Config Native Method |
|---|---|---|
| Implementation complexity | bash + jq + aws configure set | Static definition in ~/.aws/config only |
| Token renewal | Operations fail when expired | SDK auto-refreshes |
| Security | Temporary credentials stored in file | Processed in memory |
| agentSpawn hooks | Required (script execution) | Not needed |
AssumeRole Chain
EC2 instance metadata
→ AssumeRole → ReadOnly role (max 12 hours)
→ AssumeRole → Elevated role (max 1 hour due to chaining)
The EC2 instance profile only permits SSM connection and AssumeRole, with no direct permissions to operate AWS resources. The SDK manages the expiration of tokens obtained via AssumeRole and automatically refreshes them when expired, eliminating the need to manually manage tokens.
Implementation
Agent Definitions
readonly.json
{
"name": "readonly",
"description": "ReadOnly agent - reference only",
"prompt": "You are a read-only agent. Always use the kiro-readonly profile for all AWS operations via use_aws. Never perform create, update, delete, or any mutating operations.",
"tools": ["@builtin"],
"resources": []
}
elevated.json
{
"name": "elevated",
"description": "Elevated agent for AWS changes",
"prompt": "You are an elevated agent with change permissions. Always use the kiro-elevated profile for all AWS operations via use_aws. Confirm with the user before any mutating operation.",
"tools": ["@builtin"],
"resources": []
}
No hooks or scripts are needed. Simply specifying the profile to use in the agent's prompt is sufficient.
AWS CLI config
As explained in "How It Works," profiles are defined in ~/.aws/config. These are automatically placed by the CloudFormation template's UserData, so manual creation is unnecessary.
credential_source = Ec2InstanceMetadata uses EC2 instance profile credentials as the starting point for AssumeRole. With source_profile = kiro-readonly, elevated chains through readonly.
CloudFormation Template
The template builds three IAM roles + EC2 + UserData (Kiro CLI installation, agent configuration, AWS config placement).
Two technical points worth highlighting:
"Avoiding circular IAM role references"
Directly specifying role ARN as Principal in the trust policy would cause an error when the role doesn't exist yet. This is avoided using Principal: root + Condition: ArnEquals.
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: sts:AssumeRole
Condition:
ArnEquals:
aws:PrincipalArn: !Sub arn:aws:iam::${AWS::AccountId}:role/kiro-ec2-${AWS::StackName}
"UserData configuration"
UserData performs just four tasks:
- Installing Kiro CLI (non-interactive with
--no-confirm) - Placing
~/.aws/config(defining 3 profiles) - Placing agent JSONs (2 files)
- Setting up 512MB swap (addressing memory shortage on t4g.small)
No scripts or hooks are needed. The complete template is provided in the "Reference Information" section at the end of the article.
Testing
SSM Connection and Kiro CLI Setup
After stack creation, connect via SSM:
aws ssm start-session --target <INSTANCE_ID> --region us-west-2
After connecting, switch to ec2-user and log in to Kiro CLI. SSM sessions default to ssm-user, so switching to ec2-user is necessary (Kiro CLI and ~/.aws/config are in ec2-user's home):
sudo su - ec2-user
kiro-cli login --use-device-flow
Specifying --use-device-flow initiates authentication where you enter a device code in a browser. This works even in environments where a browser cannot be directly opened, like via SSM.
✔ Select login method · Use for Free with Builder ID
Confirm the following code in the browser
Code: XXXX-XXXX
Open this URL: https://view.awsapps.com/start/#/device?user_code=XXXX-XXXX
Device authorized
Logged in successfully
Open the displayed URL in a browser on your local PC, verify the code matches, and approve to complete authentication.
For detailed instructions on installing Kiro CLI on EC2 (Amazon Linux 2023), refer to Installing kiro-cli on EC2 (Amazon Linux 2023).
After authentication, launch Kiro CLI:
kiro-cli
readonly agent
Switch with /agent swap readonly and have it run sts get-caller-identity:
[readonly] 5% > run aws sts get-caller-identity
> I'll run aws sts get-caller-identity using the kiro-readonly profile.
{
"UserId": "AROA1EXAMPLE1READONLY:kiro-readonly-session",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/kiro-readonly-kiro-agent-test/kiro-readonly-session"
}
AssumeRole was automatically executed just by specifying the profile.
Next, I requested a change operation:
[readonly] 6% > Create an S3 bucket (bucket name: kiro-temp-20260223)
> I cannot create an S3 bucket. As a read-only agent, I'm restricted from
performing any create, update, delete, or mutating operations. I can only
perform read-only AWS operations using the kiro-readonly profile.
To create the S3 bucket kiro-temp-20260223, you need to switch to
the elevated agent:
/agent swap elevated
The agent's prompt functioned as a guardrail, blocking before reaching IAM. Even if the prompt were ignored and execution attempted, the ReadOnlyAccess IAM policy would result in AccessDenied.
The same applies when returning from elevated to readonly:
[elevated] 6% > /agent swap readonly
[readonly] 6% > Create an S3 bucket (bucket name: kiro-temp-20260223)
> I cannot create an S3 bucket. As a read-only agent, I'm restricted from
performing any create, update, delete, or mutating operations.
With the config method, clearing credentials is unnecessary. Switching profiles alone changes the IAM role used.
elevated agent
Switch with /agent swap elevated and similarly run sts get-caller-identity:
[elevated] 5% > run aws sts get-caller-identity
> I'll run aws sts get-caller-identity using the kiro-elevated profile.
{
"UserId": "AROA1EXAMPLE1ELEVATED:kiro-elevated-session",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/kiro-elevated-kiro-agent-test/kiro-elevated-session"
}
The AssumeRole chain (EC2 → ReadOnly → Elevated) is automatically processed, confirming operations with PowerUserAccess permissions.
Auditing
You can save conversation logs in JSON format with /chat save. Saved data includes:
transcript: Chronological record of all operations including agent switches (/agent swap)context_manager.current_profile: Agent name at the time of savinghistory: Details of each request (tool calls, parameters,profile_name, results, timestamps)
This enables tracking which agent performed which AWS operation with which profile and when.
Using agentSpawn hooks
While this article recommends native AssumeRole in ~/.aws/config, you can also use Kiro CLI's agentSpawn hooks for additional automation. For example, sending Slack notifications when switching to elevated, or checking specific conditions (like denying elevated outside business hours).
{
"hooks": {
"agentSpawn": [
{"command": "bash /path/to/notify-slack.sh 'elevated mode activated'", "timeout_ms": 5000}
]
}
}
Note that even if hooks return errors, the agent switch itself completes. Do not treat hook failures as security boundaries.
Operational Flow
In normal operation, start with the readonly agent:
kiro-cli --agent readonly
This allows safely starting work with ReadOnly permissions. When changes are needed, escalate within the chat:
[readonly] > /agent swap elevated
[elevated] > (execute change operations)
[elevated] > /agent swap readonly
Return to readonly when changes are complete. This repetition forms the basic cycle of sudo-like operation.
Summary
Using Kiro CLI's custom agent feature, we've implemented sudo-like IAM permission escalation:
/agent swap elevated=sudo(permission escalation)/agent swap readonly=exit(permission de-escalation)
Key design points:
- "Security boundary is IAM only" — Prompts are UX guardrails. Even if the LLM ignores instructions, IAM reliably blocks operations
- "Native AssumeRole in
~/.aws/config" — No scripts needed, automatic token renewal, credentials processed in memory - "Three-layer permission separation" — AssumeRole chain from EC2 role (minimal permissions) → ReadOnly → Elevated
- "IaC management" — Automated environment setup with CloudFormation, cleanup requires only stack deletion
- "Auditable" — Save operation logs in JSON with
/chat save. Includes agent name, profile name, timestamps
The configuration is ready to try with the included CloudFormation template.
Reference Information: CloudFormation Template
Here's the CloudFormation template used for verification. Deploy with aws cloudformation create-stack, connect via SSM, authenticate with kiro-cli login --use-device-flow, and begin verification immediately. Cleanup requires only aws cloudformation delete-stack.
cfn-kiro-test.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Kiro agent role switching test - EC2 with minimal permissions and AssumeRole chain
Parameters:
Subnet:
Type: AWS::EC2::Subnet::Id
Description: Subnet ID for the EC2 instance
SecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
Description: Security Group ID for the EC2 instance (Must allow SSM)
AmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64
Resources:
ReadOnlyRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub kiro-readonly-${AWS::StackName}
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: sts:AssumeRole
Condition:
ArnEquals:
aws:PrincipalArn: !Sub arn:aws:iam::${AWS::AccountId}:role/kiro-ec2-${AWS::StackName}
ManagedPolicyArns:
- arn:aws:iam::aws:policy/ReadOnlyAccess
Policies:
- PolicyName: AllowAssumeElevated
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/kiro-elevated-${AWS::StackName}
ElevatedRole:
Type: AWS::IAM::Role
DependsOn: ReadOnlyRole
Properties:
RoleName: !Sub kiro-elevated-${AWS::StackName}
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: sts:AssumeRole
Condition:
ArnEquals:
aws:PrincipalArn: !Sub arn:aws:iam::${AWS::AccountId}:role/kiro-readonly-${AWS::StackName}
ManagedPolicyArns:
- arn:aws:iam::aws:policy/PowerUserAccess
Ec2Role:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub kiro-ec2-${AWS::StackName}
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: AllowAssumeReadOnly
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/kiro-readonly-${AWS::StackName}
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub kiro-ec2-${AWS::StackName}
Roles:
- !Ref Ec2Role
Ec2Instance:
Type: AWS::EC2::Instance
DependsOn: InstanceProfile
Properties:
InstanceType: t4g.small
ImageId: !Ref AmiId
SubnetId: !Ref Subnet
SecurityGroupIds:
- !Ref SecurityGroup
IamInstanceProfile: !Ref InstanceProfile
MetadataOptions:
HttpTokens: required
HttpEndpoint: enabled
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: 10
VolumeType: gp3
Tags:
- Key: Name
Value: !Sub kiro-${AWS::StackName}
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -eux
dnf install -y unzip
# swap 512MB
dd if=/dev/zero of=/swapfile bs=1M count=512
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile swap swap defaults 0 0' >> /etc/fstab
mkdir -p /home/ec2-user/.kiro/agents
mkdir -p /home/ec2-user/.aws
su - ec2-user -c '
curl --proto "=https" --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-aarch64-linux.zip" -o "/tmp/kirocli.zip"
cd /tmp && unzip -o kirocli.zip
/tmp/kirocli/install.sh --no-confirm
'
# AWS CLI config - Native AssumeRole
printf '[default]\nregion = ${AWS::Region}\n\n[profile kiro-readonly]\nrole_arn = ${ReadOnlyRole.Arn}\ncredential_source = Ec2InstanceMetadata\nrole_session_name = kiro-readonly-session\nregion = ${AWS::Region}\n\n[profile kiro-elevated]\nrole_arn = ${ElevatedRole.Arn}\nsource_profile = kiro-readonly\nrole_session_name = kiro-elevated-session\nregion = ${AWS::Region}\n' > /home/ec2-user/.aws/config
# Agent config: readonly
printf '{\n "name": "readonly",\n "description": "ReadOnly agent - reference only",\n "prompt": "You are a read-only agent. Always use the kiro-readonly profile for all AWS operations via use_aws. Never perform create, update, delete, or any mutating operations.",\n "tools": ["@builtin"],\n "resources": []\n}\n' > /home/ec2-user/.kiro/agents/readonly.json
# Agent config: elevated
printf '{\n "name": "elevated",\n "description": "Elevated agent for AWS changes",\n "prompt": "You are an elevated agent with change permissions. Always use the kiro-elevated profile for all AWS operations via use_aws. Confirm with the user before any mutating operation.",\n "tools": ["@builtin"],\n "resources": []\n}\n' > /home/ec2-user/.kiro/agents/elevated.json
chown -R ec2-user:ec2-user /home/ec2-user/.kiro /home/ec2-user/.aws
Outputs:
InstanceId:
Value: !Ref Ec2Instance
Ec2RoleArn:
Value: !GetAtt Ec2Role.Arn
ReadOnlyRoleArn:
Value: !GetAtt ReadOnlyRole.Arn
ElevatedRoleArn:
Value: !GetAtt ElevatedRole.Arn
SsmConnectCommand:
Value: !Sub aws ssm start-session --target ${Ec2Instance} --region ${AWS::Region}
Deployment and Cleanup
Deployment command:
aws cloudformation create-stack \
--stack-name kiro-agent-test \
--template-body file://cfn-kiro-test.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=Subnet,ParameterValue=<YOUR_SUBNET_ID> \
ParameterKey=SecurityGroup,ParameterValue=<YOUR_SG_ID> \
--region us-west-2
Cleanup:
aws cloudformation delete-stack --stack-name kiro-agent-test --region us-west-2
The sample template includes these security measures:
Enforcing IMDSv2 (SSRF protection)
In environments where AI agents write and execute code on EC2, enforcing IMDSv2 is essential. If AI causes an SSRF vulnerability through hallucination or malicious external code, keeping IMDSv1 would allow curl http://169.254.169.254/latest/meta-data/iam/security-credentials/... to retrieve temporary credentials. The template sets MetadataOptions.HttpTokens: required to mandate session tokens for IMDS access.
Connection via SSM (zero port exposure)
This configuration requires no public IP, key pair, or open inbound ports on the security group, as connection is via SSM Session Manager.
To connect with VS Code's Remote SSH, you can use EC2 Instance Connect Endpoint. For details, refer to:
Using from a local PC (aws sso login)
While this article introduces a configuration based on EC2 instance metadata, this mechanism has loose coupling between "authentication (how to access AWS)" and "authorization (how to switch permissions within Kiro CLI)." Even in environments logging into AWS with aws sso login from a local PC, you can reuse the same sudo architecture just by modifying ~/.aws/config.
[sso-session my-sso]
sso_start_url = https://d-xxxxxxxxxx.awsapps.com/start
sso_region = us-west-2
sso_registration_scopes = sso:account:access
[profile kiro-readonly]
sso_session = my-sso
sso_account_id = 123456789012
sso_role_name = AWSReservedSSO_ReadOnlyAccess_xxxxxxxx
region = us-west-2
[profile kiro-elevated]
role_arn = arn:aws:iam::123456789012:role/kiro-elevated-role
source_profile = kiro-readonly
role_session_name = kiro-elevated-session
region = us-west-2
Only credential_source = Ec2InstanceMetadata changes to an SSO session, while the structure of kiro-elevated chaining with source_profile = kiro-readonly remains the same. Run aws sso login --profile kiro-readonly once before starting work, then seamlessly escalate and de-escalate permissions with /agent swap.
Note that in the Elevated role's trust policy, the AssumeRole caller changes from the EC2 role to the SSO role, so you need to change Condition's aws:PrincipalArn to the role ARN generated by SSO (arn:aws:iam::<ACCOUNT_ID>:role/aws-reserved/sso.amazonaws.com/*/AWSReservedSSO_ReadOnlyAccess_*).