Everyone’s connecting AI agents to cloud infrastructure right now. Many of you could be doing it better.
If your setup looks something like this — create an IAM user, generate access keys, paste them into the agent’s config, give it broad permissions “because it needs to do stuff,” and hope nothing goes sideways — but you want to do it better, keep reading.
I run five AWS accounts across two businesses. My AI agent has access to all of them. And its standing permissions are essentially zero — it can read a Secrets Manager secret. That’s it. And without my approval, the secret is a placeholder — absolutely useless.
Everything else — deployments, restarts, config changes — requires me to explicitly approve it, with credentials that expire automatically.
Here’s the full setup.
The Problem
AI agents need cloud access to be useful. Mine monitors infrastructure, deploys services, checks logs, starts and stops instances, deploys new resources, destroys resources — even handles security. But traditional access models weren’t designed for autonomous systems that run 24/7 and make their own decisions about what to do.
The usual approaches:
- Static access keys with broad permissions — the “yolo” method. If the machine is compromised, the attacker gets everything the agent had.
- Assume role with MFA — better, but agents can’t type MFA codes. So people skip MFA. Back to yolo.
- AWS SSO / Identity Center — great for humans. Terrible for agents. Requires interactive browser login.
- IAM Roles Anywhere — solid, but still gives the agent permanent permissions. Just with better credential hygiene.
- Humans still do the scary stuff — IAM users, roles, policies, security groups. So the agent gets broad access “except for the important things,” which usually means broad access to everything else.
None of these solve the core question: how do you give an agent the access it needs, only when it needs it, and take it away automatically?
This is where the principle of least privilege actually means something. Not “give it read-only plus a few extras.” Truly minimal — the agent gets almost nothing by default, and everything operational requires explicit, time-boxed human approval.
The Model: Bare Minimum Permissions + On-Demand Elevation
Here’s the architecture:
Agent needs to do something operational
↓
Notifies you: "🔐 Need elevated access to prod for: restart ECS service"
↓
You approve — from your phone, laptop, CloudShell, anywhere
↓
Small script generates time-boxed credentials (5-60 min)
↓
Stores them in Secrets Manager → notifies the agent it's ready
↓
Agent reads the secret, uses the creds
↓
Credentials expire automatically — no cleanup needed
The agent’s permanent role can only do three things:
- Read only the specific secrets it’s been given access to — where short-lived temporary credentials will be stored, that only it can use
- Query CloudTrail (for post-session reports)
- Check its own identity (
sts:GetCallerIdentity)
That’s the entire attack surface if someone compromises the agent’s credentials. They can read placeholder secrets and look at audit logs. Congratulations.
Step 1: The Agent’s Base IAM Role
This is all the agent gets permanently. Read-only. Near-zero blast radius. True least privilege — every secret is explicitly named, no wildcards.
// agent-base-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadElevatedCredentials",
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": [
"arn:aws:secretsmanager:ap-southeast-2:111111111111:secret:agent/elevated/111111111111-??????",
"arn:aws:secretsmanager:ap-southeast-2:111111111111:secret:agent/elevated/222222222222-??????",
"arn:aws:secretsmanager:ap-southeast-2:111111111111:secret:agent/elevated/333333333333-??????"
]
},
{
"Sid": "AuditAccess",
"Effect": "Allow",
"Action": [
"cloudtrail:LookupEvents",
"sts:GetCallerIdentity"
],
"Resource": "*"
}
]
}
Each secret is listed explicitly by ARN — no wildcards. The ?????? suffix is how AWS Secrets Manager generates the unique ID portion, so the policy matches exactly one secret per account. Anyone reviewing this policy can see immediately: the agent can read these three secrets, query audit logs, and check its own identity. Nothing else.
You can use regular IAM access keys for this role. Yes, long-lived. It doesn’t matter — these keys can’t do anything dangerous. The worst case scenario if they’re leaked is someone reads your placeholder secrets.
💡 The key insight: The agent’s permanent credentials are deliberately worthless. All the real permissions come through time-boxed elevation that only a human can grant. This is least privilege taken seriously — not “read-only plus some extras,” but genuinely close to zero.
Step 2: Secrets Manager — The Credential Handoff
Create one secret per AWS account your agent needs access to. These start as placeholders and only contain credentials when you’ve approved an elevation request.
# create-secrets.sh
ACCOUNTS=("111111111111" "222222222222" "333333333333")
REGION="ap-southeast-2"
for ACCOUNT_ID in "${ACCOUNTS[@]}"; do
aws secretsmanager create-secret \
--name "agent/elevated/${ACCOUNT_ID}" \
--description "Elevated agent credentials for account ${ACCOUNT_ID}" \
--secret-string '{"status": "inactive"}' \
--region "$REGION"
echo "✓ Created secret for account ${ACCOUNT_ID}"
done
When you approve an elevation request, the secret gets populated with temporary STS credentials:
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"SessionToken": "...",
"Expiration": "2026-03-25T13:30:00Z",
"GrantedFor": "restart ECS service",
"GrantedAt": "2026-03-25T12:30:00Z",
"RoleName": "AgentElevated-Ops"
}
After the credentials expire, the secret goes back to being a useless placeholder. The agent checks Expiration before using anything — if it’s in the past, the creds are dead.
Step 3: The Elevated Permission Sets
This is where it gets interesting. You don’t just have one elevated role — you create multiple roles for different risk levels. The approval script lets you pick which role to grant, and for how long.
// elevated-ops.json — standard operational access
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "OpsAccess",
"Effect": "Allow",
"Action": [
"ec2:Describe*", "ec2:StartInstances", "ec2:StopInstances", "ec2:RebootInstances",
"ecs:DescribeServices", "ecs:UpdateService", "ecs:DescribeTaskDefinition",
"ecs:RegisterTaskDefinition", "ecs:ListTasks", "ecs:DescribeTasks",
"s3:GetObject", "s3:PutObject", "s3:ListBucket",
"logs:GetLogEvents", "logs:DescribeLogGroups", "logs:FilterLogEvents",
"cloudwatch:GetMetricData", "cloudwatch:DescribeAlarms",
"ssm:SendCommand", "ssm:GetCommandInvocation"
],
"Resource": "*"
}
]
}
// elevated-deploy.json — deployment access (broader, longer duration OK)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeployAccess",
"Effect": "Allow",
"Action": [
"ecs:*", "ecr:*",
"s3:*",
"route53:*",
"elasticloadbalancing:*",
"logs:*", "cloudwatch:*"
],
"Resource": "*"
}
]
}
// elevated-security.json — IAM/security access (scary, short duration)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SecurityAccess",
"Effect": "Allow",
"Action": [
"iam:ListUsers", "iam:ListRoles", "iam:ListPolicies",
"iam:GetUser", "iam:GetRole", "iam:GetPolicy",
"iam:CreateUser", "iam:CreateRole", "iam:AttachRolePolicy",
"cognito-idp:*",
"ec2:AuthorizeSecurityGroupIngress", "ec2:RevokeSecurityGroupIngress",
"ec2:DescribeSecurityGroups"
],
"Resource": "*"
}
]
}
Every elevated role also gets an explicit deny policy attached — the hard ceiling that can’t be bypassed:
// deny-always.json — attached to ALL elevated roles
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "NeverEvenElevated",
"Effect": "Deny",
"Action": [
"ec2:TerminateInstances",
"ec2:DeleteVpc",
"ec2:DeleteSubnet",
"rds:DeleteDBInstance",
"rds:DeleteDBCluster",
"organizations:*",
"account:*"
],
"Resource": "*"
}
]
}
💡 The beauty is granularity. It’s not all-or-nothing. You pick the role and duration based on what’s needed:
Role What It’s For Typical Duration AgentElevated-OpsRestart services, check logs, run commands 15-30 min AgentElevated-DeployShip new versions, update infrastructure 30-60 min AgentElevated-SecurityIAM changes, security groups — the scary stuff 5 min Security access for 5 minutes. Deployment access for an hour. You control the blast radius and the time window.
Step 4: The Approval Script (You Run This)
This is the only thing that requires your action. When the agent requests elevation, you run this script from anywhere — laptop, phone, CloudShell:
#!/bin/bash
# grant-elevated.sh <account-id> [duration-minutes] [role] [reason]
ACCOUNT_ID="${1:?Usage: $0 <account-id> [duration-minutes] [role] [reason]}"
DURATION_MIN="${2:-15}"
ROLE="${3:-AgentElevated-Ops}"
REASON="${4:-manual elevation}"
REGION="ap-southeast-2"
echo "Granting ${ROLE} to account ${ACCOUNT_ID} for ${DURATION_MIN} min..."
echo "Reason: ${REASON}"
# Generate temporary credentials via STS assume-role
CREDS=$(aws sts assume-role \
--role-arn "arn:aws:iam::${ACCOUNT_ID}:role/${ROLE}" \
--role-session-name "agent-elevated-$(date +%s)" \
--duration-seconds $((DURATION_MIN * 60)) \
--query 'Credentials' \
--output json)
# Add metadata for audit trail
ENRICHED=$(echo "$CREDS" | jq \
--arg reason "$REASON" \
--arg granted "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg role "$ROLE" \
'. + {GrantedFor: $reason, GrantedAt: $granted, RoleName: $role}')
# Store in Secrets Manager
aws secretsmanager put-secret-value \
--secret-id "agent/elevated/${ACCOUNT_ID}" \
--secret-string "$ENRICHED" \
--region "$REGION"
EXPIRATION=$(echo "$CREDS" | jq -r '.Expiration')
echo ""
echo "✅ Elevated access granted"
echo " Role: ${ROLE}"
echo " Expires: ${EXPIRATION} (${DURATION_MIN} min)"
echo " Account: ${ACCOUNT_ID}"
# Notify the agent that credentials are ready
# (webhook, Slack message, SNS topic — whatever your setup uses)
echo "Notifying agent..."
A few examples:
# Standard ops — restart a service, 15 minutes
./grant-elevated.sh 111111111111 15 AgentElevated-Ops "restart ECS service"
# Big deployment — shipping a release, 60 minutes
./grant-elevated.sh 222222222222 60 AgentElevated-Deploy "deploy v4.14.1"
# Security change — scary stuff, 5 minutes max
./grant-elevated.sh 333333333333 5 AgentElevated-Security "add cognito user"
That’s it. One command, one approval, one time window. Different roles for different risk levels. Works from your phone.
If you’re using IAM Identity Center (SSO), the flow is even cleaner — use aws sso get-role-credentials instead of sts assume-role. Same concept, but you authenticate through your SSO portal with MFA.
Step 5: The Agent Side
The agent doesn’t poll. When you run the approval script, the last step notifies the agent that credentials are ready — via webhook, Slack message, SNS, whatever fits your setup. The agent then reads and uses them:
# agent-use-elevated.sh — called when notified that credentials are ready
ACCOUNT_ID="$1"
REGION="ap-southeast-2"
# Read the credentials
SECRET=$(aws secretsmanager get-secret-value \
--secret-id "agent/elevated/${ACCOUNT_ID}" \
--region "$REGION" \
--query 'SecretString' --output text)
# Verify they're valid and not expired
EXPIRATION=$(echo "$SECRET" | jq -r '.Expiration // empty')
if [ -z "$EXPIRATION" ]; then
echo "❌ No valid credentials found"
exit 1
fi
EXPIRY_EPOCH=$(date -d "$EXPIRATION" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
if [ "$EXPIRY_EPOCH" -le "$NOW_EPOCH" ]; then
echo "❌ Credentials expired at ${EXPIRATION}"
exit 1
fi
ROLE=$(echo "$SECRET" | jq -r '.RoleName // "unknown"')
REMAINING=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 60 ))
echo "✅ Credentials valid. Role: ${ROLE}. ${REMAINING} min remaining."
# Use the credentials
export AWS_ACCESS_KEY_ID=$(echo "$SECRET" | jq -r '.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$SECRET" | jq -r '.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$SECRET" | jq -r '.SessionToken')
# Do the work...
# (whatever was requested)
# Clear credentials when done
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
echo "🔒 Credentials cleared. Session complete."
No polling loop. The agent gets notified, reads the secret, verifies it’s valid, does the work, and clears up. If the credentials expire mid-operation, the AWS API calls simply start failing — no lingering access.
Step 6: Post-Session Audit Report
After every elevated session, the agent queries CloudTrail and generates a report of everything it did:
# audit-report.sh — run after elevated session completes
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=agent-elevated \
--start-time "$SESSION_START" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--max-results 50 \
--query 'Events[].{Time:EventTime,Action:EventName,Resource:Resources[0].ResourceName}' \
--output table
This gives you a complete list of every API call the agent made with its elevated credentials. CloudTrail is the source of truth — not the agent’s self-report. If the agent says it did 4 things but CloudTrail shows 12, that’s your signal to investigate.
A typical report looks like:
📋 Elevated Session Report
━━━━━━━━━━━━━━━━━━━━━━━━━
Account: production (222222222222)
Role: AgentElevated-Ops
Requested: 2026-03-25 12:00 AEDT
Reason: Restart ECS service after deployment
Duration: 8 minutes (of 15 min grant)
Actions performed:
1. ecs:DescribeServices — prod-cluster/api ✅
2. ecs:UpdateService (forceNewDeployment) ✅
3. ecs:DescribeServices — confirmed new task ✅
4. logs:GetLogEvents — verified no errors ✅
Errors: None
Resources modified: 1 (ECS service redeployed)
What You Get
Let’s take stock:
- ✅ True least privilege — agent can only read specific named secrets and audit logs
- ✅ Human-approved elevation — you control what role, what account, and for how long
- ✅ Granular role selection — ops, deploy, security — different roles for different risk levels
- ✅ Auto-expiring credentials — 5 minutes for scary stuff, 60 for big deployments
- ✅ Explicit deny boundaries — destructive actions blocked even with elevation
- ✅ Full audit trail — CloudTrail logs every API call with session name
- ✅ Multi-account — same pattern, separate secrets per account
- ✅ Instant revocation — delete the secret value, access dies immediately (even before expiry)
- ✅ Works without AWS Organizations — standalone accounts, no management account
- ✅ Approve from anywhere — phone, laptop, CloudShell, terminal
- ✅ Event-driven, not polling — agent gets notified when credentials are ready
The agent literally cannot self-approve. It can’t escalate its own permissions. It can’t suppress the audit trail. And even when elevated, the deny policy sets a hard ceiling on what’s possible.
But What About Compliance?
Everything above is solid for most teams. But if you’re in a regulated industry — finance, healthcare, government — auditors will ask harder questions:
- Can the person who approved the access tamper with the audit trail?
- Are the logs stored in a way that’s cryptographically verifiable?
- Is there separation of duties between the operator and the record keeper?
That’s where this pattern evolves into something much more serious:
- IAM Roles Anywhere — eliminate even the long-lived CLI keys entirely. Certificate-based identity, no secrets on disk at all.
- Org-level CloudTrail — logs shipped to a dedicated Log Archive account that nobody in the workload accounts can touch.
- S3 Object Lock — WORM storage. Even the Log Archive admin can’t delete logs.
- Digest chain validation — AWS signs every log file with SHA-256. Tamper with one, the chain breaks.
- Independent report pipeline — Lambda in the Log Archive account emails reports. The agent never touches this pipeline.
The trust chain becomes: Agent actions → CloudTrail (can’t be disabled) → S3 in Log Archive (can’t be modified) → Object Lock (can’t be deleted) → Digest validation (can’t be tampered) → Report Lambda (agent has zero access).
To falsify that audit, you’d need to compromise AWS itself.
Need compliance-grade agent access control?
We build tamper-proof JIT approval systems for APRA CPS 234, HIPAA, SOC 2, and ISO 27001. Full audit trail. Cryptographic proof. Zero trust. From solo operator to enterprise.
Talk to us →Try It This Afternoon
Here’s the path:
- Create an IAM user with only
secretsmanager:GetSecretValueon your specific named secrets - Create the secrets — one per account, initially placeholders
- Create the elevated roles — ops, deploy, security (or whatever levels make sense for you)
- Attach the deny policy to every elevated role — the hard ceiling
- Write the approval script — adapt
grant-elevated.shto your setup - Test it — request elevation, approve with a specific role and duration, use, let it expire
The whole thing took me an afternoon across five accounts. Now every operational action goes through a human approval gate with a specific role and time window, credentials expire automatically, and I have a CloudTrail record of everything.
If you’re giving your AI agent static access keys right now, this is your nudge. The alternative takes 2 hours to set up and costs nothing.
Go build something secure.
Grab the scripts: github.com/eumeco-ai/aws-jit-access — everything in this post, ready to run.