← Back to Blog
📅 25 March 2026 ⏱️ 12 min read ✍️ Eugene Concepcion

I Gave My AI Agent Access to 5 AWS Accounts. Here's How I Sleep at Night.

Zero standing permissions. Human-approved elevation. Expiring credentials. A practical guide to secure agent access on AWS — no management account required.

AWSSecurityAI AgentsIAMDevOps

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:

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:

  1. Read only the specific secrets it’s been given access to — where short-lived temporary credentials will be stored, that only it can use
  2. Query CloudTrail (for post-session reports)
  3. 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:

RoleWhat It’s ForTypical Duration
AgentElevated-OpsRestart services, check logs, run commands15-30 min
AgentElevated-DeployShip new versions, update infrastructure30-60 min
AgentElevated-SecurityIAM changes, security groups — the scary stuff5 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:

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:

That’s where this pattern evolves into something much more serious:

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:

  1. Create an IAM user with only secretsmanager:GetSecretValue on your specific named secrets
  2. Create the secrets — one per account, initially placeholders
  3. Create the elevated roles — ops, deploy, security (or whatever levels make sense for you)
  4. Attach the deny policy to every elevated role — the hard ceiling
  5. Write the approval script — adapt grant-elevated.sh to your setup
  6. 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.

EC

Eugene Concepcion

Founder of eUmeco ai — building digital platforms for small business. I make AI simple.