DEV Community

Dinesh_gowtham
Dinesh_gowtham

Posted on

How IAM Roles Anywhere Lets Your AI Agents Run Securely On‑Prem and In ECS, Explained Simply

AI agents need credentials, but stuffing API keys into containers is a ticking time bomb. IAM Roles Anywhere flips the script, giving you short‑lived, auditable permissions without ever touching a secret file. Learn the exact steps to wire it up for your Node.js/TypeScript AI workloads.


Why IAM Roles Anywhere Matters for AI Agents

When an AI model talks to other AWS services (S3, Bedrock, DynamoDB…) it must prove “who it is”. The most common pattern is to drop an access key‑ID and secret‑access‑key into a secret store, mount that file into the container, and hope nobody steals it.

In plain English: Hard‑coded keys are like leaving your house keys under the doormat. Anyone who finds the container can walk straight in.

IAM Roles Anywhere (RA) replaces those static keys with short‑lived credentials that are created on demand. The flow is:

  1. You create a trust anchor – a public key that tells AWS “I trust certificates signed by this CA”.
  2. Your AI agent gets an X.509 certificate (the private half stays on the machine).
  3. The agent sends the certificate to AWS STS (Security Token Service) and asks for a role.
  4. STS returns temporary credentials (access key, secret key, session token) that are valid for a few minutes.

Because the credentials expire quickly and are logged, you gain zero‑trust behavior: the moment a certificate is revoked or expires, the agent can no longer call AWS.

Analogy

Think of IAM Roles Anywhere like a single‑use backstage pass at a concert. The pass is printed just before you need it, works for a short window, and the security staff can instantly see who used it and when. If the pass is lost, it’s already worthless after its time window.

Key takeaway: RA gives AI agents a dynamic, auditable identity instead of a permanent password.


Creating a Trust Anchor and Issuing a Certificate

The “why”

Before any agent can ask AWS for credentials, AWS must know which certificate authorities (CAs) it trusts. This is the trust anchor. If the anchor is missing or malformed, every request fails with AccessDeniedException – often with no obvious clue that the PEM order is wrong.

Step‑by‑step

  1. Generate a private key and a self‑signed CA certificate (or use an internal PKI).
  2. Upload the CA certificate to IAM as a trust anchor.
  3. Issue a leaf certificate for the AI agent, signed by the CA.

Below is a minimal Node.js script that uses the IAM SDK to create the trust anchor. (You could also do it from the console; the code shows the API shape.)

// trust-anchor.ts
import {
  IAMClient,
  CreateOpenIDConnectProviderCommand,
  CreateOpenIDConnectProviderCommandInput,
} from "@aws-sdk/client-iam";
import { readFileSync } from "node:fs";

// 1️⃣ Load the CA certificate in PEM format.
//    The file must contain the full chain: leaf → intermediate → root, each block separated by a newline.
const caPem = readFileSync("./ca-cert.pem", "utf-8");

// 2️⃣ Build the request object.
//    IAM expects the certificate string exactly as‑is; any extra whitespace or wrong order breaks the call.
const input: CreateOpenIDConnectProviderCommandInput = {
  // The URL is a placeholder; IAM uses it only as an identifier.
  Url: "https://my-company-iam-roles-anywhere",
  // The PEM string must be a single entry – no extra line breaks at start/end.
  ThumbprintList: [], // Not needed for RA; keep empty.
  // Provide the full certificate chain in the correct order.
  CertificateBody: caPem,
};

async function registerTrustAnchor() {
  const iam = new IAMClient({});
  try {
    const cmd = new CreateOpenIDConnectProviderCommand(input);
    const response = await iam.send(cmd);
    console.log("Trust anchor created, ARN:", response.OpenIdConnectProviderArn);
  } catch (err) {
    console.error("Failed to create trust anchor:", err);
  }
}

registerTrustAnchor();
Enter fullscreen mode Exit fullscreen mode

Important PEM tip

The certificate file must list the leaf certificate first, then any intermediates, and finally the root. If you accidentally put the root at the top, IAM silently rejects the request with AccessDeniedException even though the ARN you pass looks correct.

Tip: Open the PEM in a text editor and verify the order: -----BEGIN CERTIFICATE----- (leaf) → -----BEGIN CERTIFICATE----- (intermediate) → -----BEGIN CERTIFICATE----- (root).

Issuing a leaf certificate

You can use OpenSSL or your corporate PKI. Here’s a one‑liner that creates a 30‑day certificate for the agent:

openssl req -new -newkey rsa:2048 -nodes -keyout agent.key.pem \
  -subj "/CN=ai-agent.example.com/O=MyCompany" \
  -out agent.csr.pem

openssl x509 -req -in agent.csr.pem -CA ca-cert.pem -CAkey ca-key.pem \
  -CAcreateserial -days 30 -sha256 -out agent-cert.pem
Enter fullscreen mode Exit fullscreen mode

Keep agent.key.pem private on the host; agent-cert.pem will be mounted read‑only into the container.


Configuring an ECS Task to Assume the Role

The “why”

An ECS task runs inside a container. To let that container call AWS, you attach an IAM role to the task definition. With RA, the task’s execution role stays static (it only needs sts:AssumeRoleWithWebIdentity permission), while the application role is fetched at runtime using the certificate.

If you mistakenly assign the permission to the task role instead of the execution role, the container will fail to retrieve credentials, and the error will appear only in CloudWatch logs.

Minimal task definition (JSON)

{
  "family": "ai-agent-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "2048",
  "executionRoleArn": "arn:aws:iam::123456789012:role/EcsExecutionRole",   // <-- needs sts:AssumeRoleWithWebIdentity
  "taskRoleArn": "arn:aws:iam::123456789012:role/AIApplicationRole",      // <-- optional, not used for RA
  "containerDefinitions": [
    {
      "name": "ai-agent",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/ai-agent:latest",
      "essential": true,
      "environment": [
        { "name": "AWS_REGION", "value": "us-east-1" },
        { "name": "CERT_PATH", "value": "/certs/agent-cert.pem" },
        { "name": "KEY_PATH", "value": "/certs/agent.key.pem" }
      ],
      "mountPoints": [
        {
          "sourceVolume": "certs",
          "containerPath": "/certs",
          "readOnly": true
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/ai-agent",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "volumes": [
    {
      "name": "certs",
      "host": {
        "sourcePath": "/opt/ai-agent/certs"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

What each part does

  • executionRoleArn – the role that ECS itself uses to pull images and to call STS on behalf of the container. It must contain the permission sts:AssumeRoleWithWebIdentity.
  • taskRoleArn – not required for RA; you could still use it for other AWS calls that don’t need the dynamic certificate.
  • mountPoints – maps the host directory that holds agent-cert.pem and agent.key.pem into the container at /certs.

In plain English: The container gets a static role that only lets it ask AWS for a temporary role, and the certificate is the secret that proves who it is.


Node.js Code: Using the IAM SDK with TypeScript’s satisfies

The “why”

Now that the container has its certificate, it needs to ask STS for temporary credentials. The call we make is AssumeRoleWithWebIdentity, where the web identity token is the PEM‑encoded X.509 certificate. The response contains three strings (AccessKeyId, SecretAccessKey, SessionToken) that we feed to any other AWS SDK client, such as the Bedrock client for Claude.

Using TypeScript’s satisfies operator lets us declare the shape of the request object once and get compile‑time safety without locking us into a specific type alias.

Full example (agent.ts)

// agent.ts
import {
  IAMClient,
  AssumeRoleWithWebIdentityCommand,
  AssumeRoleWithWebIdentityCommandInput,
} from "@aws-sdk/client-iam";
import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
import { readFileSync } from "node:fs";
import { fromTemporaryCredentials } from "@aws-sdk/credential-providers";

// ---------------------------------------------------------------------------
// 1️⃣ Load the PEM‑encoded certificate (the "web identity token").
//    The file must contain the leaf certificate *only*.
// ---------------------------------------------------------------------------
const certPath = process.env.CERT_PATH ?? "/certs/agent-cert.pem";
const certPem = readFileSync(certPath, "utf-8");

// ---------------------------------------------------------------------------
// 2️⃣ Build the AssumeRole request.
//    We use `satisfies` so the object is checked against the SDK’s input type
//    but the inferred type stays as a plain literal (useful for later spreads).
// ---------------------------------------------------------------------------
const assumeInput = {
  RoleArn: "arn:aws:iam::123456789012:role/AIApplicationRole", // the role we want temporary creds for
  RoleSessionName: "ai-agent-session",                       // an identifier that appears in CloudTrail
  WebIdentityToken: certPem,                                 // our X.509 cert becomes the token
  DurationSeconds: 900,                                      // 15 minutes – short enough for security
} satisfies AssumeRoleWithWebIdentityCommandInput;

// ---------------------------------------------------------------------------
// 3️⃣ Call STS via IAM client to get temporary credentials.
// ---------------------------------------------------------------------------
async function getTemporaryCredentials() {
  const iam = new IAMClient({}); // region comes from AWS_REGION env var
  const cmd = new AssumeRoleWithWebIdentityCommand(assumeInput);
  try {
    const resp = await iam.send(cmd);
    // The response contains AccessKeyId, SecretAccessKey, SessionToken.
    if (!resp.Credentials) {
      throw new Error("No credentials returned from AssumeRole");
    }
    return {
      accessKeyId: resp.Credentials.AccessKeyId!,
      secretAccessKey: resp.Credentials.SecretAccessKey!,
      sessionToken: resp.Credentials.SessionToken!,
    };
  } catch (err) {
    // Specific handling for expired or malformed certificates
    if ((err as any).name === "ExpiredTokenException") {
      console.error("⚠️ Certificate has expired – rotate it and redeploy.");
    } else {
      console.error("Failed to assume role:", err);
    }
    throw err; // re‑throw so the caller knows we failed
  }
}

// ---------------------------------------------------------------------------
// 4️⃣ Use the temporary credentials to call Claude on Bedrock.
// ---------------------------------------------------------------------------
async function invokeClaude(prompt: string) {
  const tempCreds = await getTemporaryCredentials();

  // Build a credential provider that returns the temporary creds.
  const credProvider = fromTemporaryCredentials({
    // The provider expects a function that returns a promise of credentials.
    masterCredentials: {
      accessKeyId: tempCreds.accessKeyId,
      secretAccessKey: tempCreds.secretAccessKey,
      sessionToken: tempCreds.sessionToken,
    },
  });

  const bedrock = new BedrockRuntimeClient({
    region: process.env.AWS_REGION,
    credentials: credProvider,
  });

  const invokeCmd = new InvokeModelCommand({
    modelId: "anthropic.claude-v2", // Example model
    contentType: "application/json",
    accept: "application/json",
    body: JSON.stringify({
      prompt,
      max_tokens_to_sample: 200,
      temperature: 0.7,
    }),
  });

  const response = await bedrock.send(invokeCmd);
  const result = JSON.parse(Buffer.from(response.body as Uint8Array).toString());
  console.log("Claude response:", result);
}

// ---------------------------------------------------------------------------
// 5️⃣ Run a simple test when the container starts.
// ---------------------------------------------------------------------------
(async () => {
  try {
    await invokeClaude("Explain why a short‑lived credential is safer than an API key.");
  } catch (e) {
    console.error("Agent failed:", e);
    process.exit(1);
  }
})();
Enter fullscreen mode Exit fullscreen mode

What each block does

  • Load certificate – reads the PEM file that proves the container’s identity.
  • assumeInput – declares the request shape; satisfies guarantees we didn’t miss a required field.
  • getTemporaryCredentials – contacts STS, handles ExpiredTokenException (common when the 30‑day cert rolls over).
  • invokeClaude – builds a Bedrock client using the temporary credentials and sends a prompt.

Key takeaway: The whole credential dance lives inside the container; no secret file ever leaves the host, and the temporary keys disappear after 15 minutes.


Testing, Rotating, and Auditing the Credential Flow

The “why”

Even a perfect setup can go sideways if you don’t verify it regularly. Testing proves the certificate chain works, rotation prevents surprise expirations, and audit logs let you answer “who called Bedrock at 3 am?”.

Step‑by‑step checklist

  1. Local dry‑run – Run node agent.ts on your laptop with a copy of the certificate. The script should print the Claude response or a clear error.
  2. ECS integration test – Deploy the task definition with aws ecs run-task. Tail the logs (aws logs tail /ecs/ai-agent) and watch for the success line.
  3. Certificate rotation – Schedule a CI job that generates a new leaf cert 5 days before the old one expires, places it in the host directory, and restarts the service.
  4. CloudTrail audit – Enable CloudTrail for AssumeRoleWithWebIdentity. In the console, filter by eventName=AssumeRoleWithWebIdentity and verify the userIdentity.sessionContext.sessionIssuer.arn matches your AIApplicationRole.

Gotcha: PEM order again

When you replace the certificate, double‑check the file still contains only the leaf cert. Including the intermediate or root in the same file confuses the token parser and results in an AccessDeniedException with no hint in the container logs.

Tip: Keep two separate files: agent-cert.pem (leaf only) and ca-chain.pem (full chain) – the latter is only needed when you register the trust anchor.

Automating rotation with a simple script

#!/usr/bin/env bash
# rotate-cert.sh – run on the host that stores /opt/ai-agent/certs
set -euo pipefail

# 1️⃣ Generate a new leaf cert (30‑day validity)
openssl req -new -newkey rsa:2048 -nodes -keyout agent.key.pem \
  -subj "/CN=ai-agent.example.com/O=MyCompany" -out agent.csr.pem

openssl x509 -req -in agent.csr.pem -CA ca-cert.pem -CAkey ca-key.pem \
  -CAcreateserial -days 30 -sha256 -out new-agent-cert.pem

# 2️⃣ Swap the files atomically
mv new-agent-cert.pem /opt/ai-agent/certs/agent-cert.pem
mv agent.key.pem   /opt/ai-agent/certs/agent.key.pem

# 3️⃣ Restart the ECS service (or task) so the new cert is picked up
aws ecs update-service --cluster my-cluster --service ai-agent-service --force-new-deployment
echo "✅ Certificate rotated and service redeployed."
Enter fullscreen mode Exit fullscreen mode

Running this script as a daily cron ensures you never see a surprise ExpiredTokenException.


The Takeaway

What you should remember after reading this guide

  • IAM Roles Anywhere lets an AI agent prove its identity with a short‑lived X.509 certificate instead of a static API key.
  • The trust anchor (CA certificate) must be uploaded to IAM with the correct PEM order; otherwise AWS silently denies the request.
  • In ECS, give the execution role permission sts:AssumeRoleWithWebIdentity; the task role is not used for RA.
  • The TypeScript agent loads the certificate, calls AssumeRoleWithWebIdentity, and uses the returned temporary credentials to talk to Bedrock (or any other AWS service).
  • Handle ExpiredTokenException and rotate certificates before they expire; automate the rotation to avoid downtime.
  • CloudTrail logs every AssumeRole call, giving you a full audit trail of which AI agent performed which action.

By following the steps above, you can run powerful AI workloads on‑premises or in ECS without ever storing long‑lived secrets, keeping your system both secure and observable. Happy building!


Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-09-02 · Primary focus: IAM

All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.

Find an error? Drop a comment — corrections are always welcome.

Top comments (0)