ai-ops-agent-zero-standing-creds
by JSR-codes
README.md
# AI Ops Agent with Zero Standing AWS Credentials
An AI SRE agent (Amazon Bedrock + Claude) that investigates operational incidents by
querying a DynamoDB table — **without ever holding a long-lived AWS access key**.
Instead of a static credential, the agent assumes a short-lived IAM role via **STS**,
scoped to **read-only access on exactly one table**, with every call auditable in
**CloudTrail**.
> This project is inspired by a similar "no static credential" security pattern I came
> across, adapted here to AWS-native tooling: IAM roles + STS instead of a third-party
> identity provider.
---
## The big idea
| The "wrong way" ❌ | This project's way ✅ |
|---|---|
| App holds a **static AWS access key** | App holds **no key** — assumes a role via STS |
| Credential never expires | Session **expires in 15 minutes**, auto-renewed |
| Often over-permissioned | **Least privilege**: read-only, one table, nothing else |
| No record of what ran | **Every call auditable** in CloudTrail |
Two things you can watch happen live in the UI:
1. **No static credential** — the agent process never has an `AWS_ACCESS_KEY_ID` for
itself; it calls `sts:AssumeRole` and gets a token that expires on its own.
2. **Least privilege enforced** — clicking "Try write" attempts a `PutItem` with the
agent's credentials and is **denied** by IAM before it ever reaches DynamoDB.
---
## How it works (request flow)
```
You ask a question in the chat UI (app.py / Streamlit)
│
▼
agent_core.py ──► Amazon Bedrock (Claude) "run this query" (tool_use)
│ ◄──────────────────────────────────
▼
mcp_server.py (MCP server, one tool: query_incidents_readonly)
│
▼
aws_client.py ──► sts:AssumeRole ──► short-lived creds (15 min, auto-refreshed)
│ │
│ IAM policy: read-only, one table
│ write attempt → DENIED
▼ ▼
DynamoDB (Query/Scan only) ──► results ──► rendered in the chat UI
│
▼
CloudTrail logs the AssumeRole call and every DynamoDB read
```
---
## What's in the box
| File | What it does |
|---|---|
| `app.py` | Streamlit chat UI + live security panel (session countdown, "Try write → DENIED"). |
| `agent_core.py` | The agent loop: Bedrock **Converse API** + **MCP client** over stdio. |
| `mcp_server.py` | MCP server exposing the single tool `query_incidents_readonly` — the only seam into AWS. |
| `aws_client.py` | STS AssumeRole logic — the core of the "zero standing credentials" story. |
| `seed.py` | One-time: creates the table and loads sample incident data (run with your own admin credentials). |
| `iam/readonly-incidents-policy.json` | The least-privilege policy: read-only, one table. |
| `iam/role-trust-policy.json` | Who is allowed to assume the role. |
| `terraform/` | Provisions the DynamoDB table + IAM role + policy as code. |
---
## Prerequisites
- **Python 3.11+**
- An **AWS account** with:
- **Amazon Bedrock** access to a Claude model ([request access here](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) if you haven't already)
- Permission to create IAM roles and DynamoDB tables (for initial setup)
- AWS credentials configured locally (`aws configure`)
---
## Setup
```bash
git clone https://github.com/JSR-codes/ai-ops-agent-zero-standing-creds.git
cd ai-ops-agent-zero-standing-creds
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
### 1. Provision the AWS infrastructure with Terraform
```bash
cd terraform
terraform init
terraform apply -var="assuming_principal_arn=arn:aws:iam::<your-account-id>:user/<your-iam-username>"
```
Copy the `readonly_role_arn` output — you'll need it next.
### 2. Configure the app
```bash
cd ..
cp .env.example .env
# edit .env: paste in READONLY_ROLE_ARN from the terraform output
```
### 3. Seed sample data
```bash
python seed.py
```
(This runs with **your own** admin credentials, not the agent's read-only role — the
agent's role isn't allowed to write, by design.)
### 4. Run the app
```bash
streamlit run app.py
```
---
## Try it
- Ask *"what open incidents do we have?"* and watch the agent query DynamoDB through
the read-only role.
- Watch the **session countdown** in the security panel — it's counting down a real
STS session, not a fake timer.
- Click **"Try write (should be denied)"** to see the IAM policy block a write attempt
live.
- Check **CloudTrail** in the AWS console afterward — you'll see the `AssumeRole` call
and the `Query`/`Scan` calls, all attributable to the short-lived session.
---
## Security notes for anyone reusing this
- This is a **teaching demo**, not production-hardened — review IAM policies and
network setup before adapting it further.
- `.env` and all Terraform state/var files are git-ignored — never commit real values.
- The whole point is that the agent runs with **short-lived, least-privilege, audited**
credentials — keep it that way if you extend this.
## Possible extensions
- Add a second MCP tool with its own narrower role, to show per-tool credential scoping
- Add an EventBridge rule that alerts if `AccessDenied` shows up in CloudTrail unexpectedly
- Swap DynamoDB for RDS and compare the IAM-auth flow to this table-scoped one
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues