
AIによるAWS操作を安全に。Kiro CLIで作るsudo的なIAM権限昇格
はじめに
Kiro CLI は AWS 操作に複数の認証方法を利用できます。環境変数、EC2 インスタンスプロファイル、~/.aws/credentials や ~/.aws/config のプロファイルなど、AWS CLI/SDK と同じ認証チェーンがそのまま使えます。
便利な反面、強い IAM 権限を Kiro に渡すことへの不安もあります。AI エージェントが意図しないリソースの変更や削除を行うリスクは、特に本番環境では許容できないケースもあるかと思います。
Kiro にはエージェント設定で利用可能なツールや操作を制御する方法もありますが、本記事ではより根本的なアプローチとして、「Kiro が利用する AWS プロファイル自体を使い分ける方式」を採用しました。AWS プロファイルとは、~/.aws/config に定義する名前付きの設定で、それぞれに異なる IAM ロール(権限セット)を紐づけられます。プロファイルを切り替えるだけで、Kiro が使える AWS の権限が変わります。IAM ポリシーによる権限制御なので、LLM の挙動に依存しない確実なセキュリティ境界を実現できます。
具体的には、Kiro CLI のカスタムエージェント機能と AWS CLI のネイティブ AssumeRole を組み合わせて、「普段は読み取り専用、必要なときだけ変更権限を昇格」という sudo 的な運用を実現します。
何を作るか
三層の権限分離を実現します。
| 層 | ロール | 権限 | セキュリティ境界 |
|---|---|---|---|
| EC2 インスタンス | kiro-ec2-* |
SSMCore + AssumeRole のみ | IAM ポリシー |
| ReadOnly | kiro-readonly-* |
ReadOnlyAccess | IAM ポリシー |
| Elevated | kiro-elevated-* |
PowerUserAccess | IAM ポリシー |
セキュリティ境界は IAM ポリシーのみです。エージェントのプロンプト(「変更操作を実行しない」等)は UX 上のガードレール(暴発防止の補助)であり、防御メカニズムではありません。LLM はプロンプトの指示を無視する可能性があるため、IAM で確実にブロックすることが重要です。
仕組み
カスタムエージェント
Kiro CLI では ~/.kiro/agents/ にエージェント定義の JSON を配置することで、カスタムエージェントを作成できます。/agent swap <name> で切り替えると、そのエージェント固有のプロンプトやツール制限が適用されます。
エージェントを切り替えても会話のコンテキスト(履歴)は保持されます。例えば、以下のようなやり取りが可能です。
[readonly] 3% > 合言葉は「バナナ」です。覚えてください。
> 了解しました。合言葉「バナナ」を覚えました。
[readonly] 5% > /agent swap elevated
[elevated] 5% > 合言葉は何ですか?
> 合言葉は「バナナ」です。
readonly で調査した内容を覚えたまま elevated に切り替えて変更操作を行い、また readonly に戻れます。権限管理のために別の Kiro セッションを使い分ける場合と異なり、調査結果や作業の文脈を引き継いだままシームレスに権限昇格できるのがこの方式の利点です。
なお、/agent swap はユーザーが CLI に直接入力するスラッシュコマンドであり、LLM が実行できるツールではありません。LLM がレスポンスに /agent swap elevated と書いても、それはただのテキスト出力です。つまり、readonly エージェントが自発的に elevated に切り替えることはできません。権限昇格は常にユーザーの明示的な操作を必要とします。
AWS CLI のネイティブ AssumeRole
シェルスクリプトで aws sts assume-role を叩いて認証情報をファイルに書き込む方法もありますが、~/.aws/config のネイティブ機能を使えば、はるかにシンプルかつ安全です。
[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
この設定だけで以下が実現します。
kiro-readonlyプロファイル使用時 → EC2 インスタンスメタデータから自動で ReadOnly ロールに AssumeRolekiro-elevatedプロファイル使用時 → まず ReadOnly に AssumeRole し、そこから Elevated にチェーン
AWS SDK/CLI がバックグラウンドで AssumeRole とトークン更新を自動処理します。認証情報がディスクに平文保存されることもありません。
| 観点 | スクリプト方式 | config ネイティブ方式 |
|---|---|---|
| 実装の複雑さ | bash + jq + aws configure set | ~/.aws/config の静的定義のみ |
| トークン更新 | 期限切れで操作不能 | SDK が自動更新 |
| セキュリティ | 一時認証情報がファイルに保存 | メモリ上で処理 |
| agentSpawn hooks | 必要(スクリプト実行) | 不要 |
AssumeRole チェーン
EC2 インスタンスメタデータ
→ AssumeRole → ReadOnly ロール(最大12時間)
→ AssumeRole → Elevated ロール(チェーンのため最大1時間)
EC2 のインスタンスプロファイルは SSM 接続と AssumeRole のみ許可しており、直接 AWS リソースを操作する権限はありません。AssumeRole で取得するトークンの有効期限は SDK が管理し、期限切れ時には自動で再取得するため、トークン管理を意識する必要はありません。
実装
エージェント定義
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": []
}
hooks もスクリプトも不要です。エージェントのプロンプトで使用するプロファイルを指定するだけです。
AWS CLI config
「仕組み」で説明した通り、~/.aws/config にプロファイルを定義します。CloudFormation テンプレートの UserData で自動配置されるため、手動作成は不要です。
credential_source = Ec2InstanceMetadata により、EC2 インスタンスプロファイルの認証情報を起点に AssumeRole します。source_profile = kiro-readonly により、elevated は readonly を経由してチェーンします。
CloudFormation テンプレート
IAM ロール3つ + EC2 + UserData(Kiro CLI インストール・エージェント設定・AWS config 配置)を一括構築します。
技術的なポイントを2つ紹介します。
「IAM ロールの循環参照回避」
信頼ポリシーで Principal にロール ARN を直接指定すると、ロールが存在しない段階でエラーになります。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 の構成」
UserData で行うのは以下の4つだけです。
- Kiro CLI のインストール(
--no-confirmで非対話実行) ~/.aws/configの配置(3プロファイル定義)- エージェント JSON の配置(2ファイル)
- swap 512MB の設定(t4g.small のメモリ不足対策)
スクリプトや hooks は不要です。テンプレート全体は記事末尾の「参考情報」に掲載しています。
動作確認
SSM 接続と Kiro CLI のセットアップ
スタック作成完了後、SSM で接続します。
aws ssm start-session --target <INSTANCE_ID> --region us-west-2
接続後、ec2-user に切り替えてから Kiro CLI にログインします。SSM セッションはデフォルトで ssm-user になるため、ec2-user への切り替えが必要です(Kiro CLI や ~/.aws/config は ec2-user のホームに配置されています)。
sudo su - ec2-user
kiro-cli login --use-device-flow
--use-device-flow を指定すると、ブラウザでデバイスコードを入力する認証フローになります。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
表示された URL をローカル PC のブラウザで開き、画面に表示されたコードが一致していることを確認して承認すれば認証完了です。
Kiro CLI の EC2(Amazon Linux 2023)へのインストール手順の詳細は kiro-cli を EC2(Amazon Linux 2023)にインストールする を参照してください。
認証後、Kiro CLI を起動します。
kiro-cli
readonly エージェント
/agent swap readonly で切り替え、sts get-caller-identity を実行させました。
[readonly] 5% > 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 が自動実行されました。
次に、変更操作を依頼しました。
[readonly] 6% > S3バケットを作成して(バケット名: 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
エージェントのプロンプトがガードレールとして機能し、IAM に到達する前にブロックされました。仮にプロンプトを無視して実行しても、IAM の ReadOnlyAccess ポリシーで AccessDenied になります。
elevated から readonly に戻した場合も同様でした。
[elevated] 6% > /agent swap readonly
[readonly] 6% > S3バケットを作成して(バケット名: 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.
config 方式では認証情報のクリアは不要です。プロファイルを切り替えるだけで、使用される IAM ロールが変わります。
elevated エージェント
/agent swap elevated で切り替え、同様に sts get-caller-identity を実行させました。
[elevated] 5% > 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"
}
AssumeRole チェーン(EC2 → ReadOnly → Elevated)が自動で処理され、PowerUserAccess の権限で操作できることを確認しました。
監査
/chat save で会話ログを JSON 形式で保存できます。保存データには以下が含まれます。
transcript: エージェント切替(/agent swap)を含む全操作の時系列記録context_manager.current_profile: 保存時点のエージェント名history: 各リクエストの詳細(ツール呼び出し、パラメータ、profile_name、結果、タイムスタンプ)
どのエージェントで、いつ、どの AWS 操作を、どのプロファイルで行ったかが追跡可能です。
agentSpawn hooks を使う場合
本記事では ~/.aws/config のネイティブ AssumeRole を推奨していますが、Kiro CLI の agentSpawn hooks を使って追加の自動化を行うこともできます。例えば、elevated への切り替え時に Slack 通知を送る、特定の条件チェック(営業時間外は elevated を拒否する等)といった用途です。
{
"hooks": {
"agentSpawn": [
{"command": "bash /path/to/notify-slack.sh 'elevated mode activated'", "timeout_ms": 5000}
]
}
}
ただし、hooks がエラーを返してもエージェントの切り替え自体は完了します。hooks の失敗をセキュリティ境界として扱わないでください。
運用の流れ
普段は readonly エージェントを指定して起動します。
kiro-cli --agent readonly
これにより、最初から ReadOnly 権限で安全に作業を開始できます。変更が必要になったら、チャット内で昇格します。
[readonly] > /agent swap elevated
[elevated] > (変更操作を実行)
[elevated] > /agent swap readonly
変更が終わったら readonly に戻す。この繰り返しが、sudo 的な運用の基本サイクルです。
まとめ
Kiro CLI のカスタムエージェント機能を使うことで、sudo 的な IAM 権限昇格運用を実現できました。
/agent swap elevated=sudo(権限昇格)/agent swap readonly=exit(権限降格)
設計のポイントをまとめます。
- 「セキュリティ境界は IAM のみ」 — プロンプトは UX 上のガードレール。LLM が指示を無視しても IAM で確実にブロックされる
- 「
~/.aws/configのネイティブ AssumeRole」 — スクリプト不要、トークン自動更新、認証情報はメモリ上で処理される - 「三層権限分離」 — EC2 ロール(最小権限)→ ReadOnly → Elevated の AssumeRole チェーン
- 「IaC 管理」 — CloudFormation で環境構築を自動化、クリーンアップはスタック削除のみ
- 「監査可能」 —
/chat saveで操作ログを JSON 保存。エージェント名・プロファイル名・タイムスタンプ付き
CloudFormation テンプレート付きですぐに試せる構成となっているので、ぜひお試しください。
参考情報: CloudFormation テンプレート
検証に使用した CloudFormation テンプレートを掲載します。aws cloudformation create-stack でデプロイし、SSM で接続、kiro-cli login --use-device-flow でライセンス認証後すぐに検証を開始できます。クリーンアップは 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 - ネイティブ 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
# エージェント設定: 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
# エージェント設定: 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}
デプロイ、クリーンアップ
デプロイコマンド:
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
クリーンアップ:
aws cloudformation delete-stack --stack-name kiro-agent-test --region us-west-2
サンプルテンプレートには以下のセキュリティ対策を含めています。
IMDSv2 の強制(SSRF 対策)
AI エージェントにコードを書かせて EC2 上で実行する環境では、IMDSv2 の強制が不可欠です。もし AI がハルシネーションや外部の悪意あるコードにより SSRF 脆弱性を引き起こした場合、IMDSv1 のままだと curl http://169.254.169.254/latest/meta-data/iam/security-credentials/... で一時認証情報を取得されてしまいます。テンプレートでは MetadataOptions.HttpTokens: required を設定し、IMDS へのアクセスにセッショントークンを必須にしています。
SSM 経由の接続(ポート開放ゼロ)
本構成では EC2 にパブリック IP もキーペアも不要で、セキュリティグループのインバウンドポートも完全に閉じたまま運用できます。SSM Session Manager 経由で接続するためです。
VS Code の Remote SSH で接続したい場合は、EC2 Instance Connect Endpoint を利用する方法があります。詳細は下記の記事を参照ください。
ローカル PC(aws sso login)から利用する場合
本記事では EC2 インスタンスメタデータを起点とした構成を紹介しましたが、この仕組みは「認証(どうやって AWS に入るか)」と「認可(Kiro CLI 内でどう権限を切り替えるか)」が疎結合になっています。ローカル PC から aws sso login で AWS にログインしている環境でも、~/.aws/config を書き換えるだけで同じ sudo アーキテクチャを流用できます。
[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
credential_source = Ec2InstanceMetadata が SSO セッションに変わるだけで、kiro-elevated が source_profile = kiro-readonly でチェーンする構造は同じです。作業開始前に aws sso login --profile kiro-readonly を1回実行すれば、あとは /agent swap でシームレスに権限昇格・降格できます。
なお、Elevated ロールの信頼ポリシーでは、AssumeRole の呼び出し元が EC2 ロールから SSO ロールに変わるため、Condition の aws:PrincipalArn を SSO が生成するロール ARN(arn:aws:iam::<ACCOUNT_ID>:role/aws-reserved/sso.amazonaws.com/*/AWSReservedSSO_ReadOnlyAccess_*)に変更する必要があります。








