10 views
Your coding agent needs a GitHub token, a SaaS API key, or a per-tenant webhook secret to run a tool. The shortcut is stuffing GITHUB_TOKEN=ghp_... into the Lambda environment and giving the execution role kms:Decrypt on *. That shortcut is how one prompt-injected tool call exfiltrates every secret in the account. The unfair advantage is Secrets Manager + KMS grants with encryption context, short-lived grant tokens, and CloudTrail-auditable decrypts scoped to tenantId and toolName — not env plaintext.
⚡ TL;DR: Store tool secrets in Secrets Manager encrypted under CMKs. Issue KMS grants (or role policies) constrained by encryption context (tenantId,toolName). Pass grant tokens into the agent runtime only for the active tool turn. Refusekms:Decrypton*. Pair with IAM Condition Keys for Agent Runtimes, Secure AI Sandboxes (ECS), and AI Coding Agent Tool Schemas.
KMS Decrypt Grants for Agent Tools: production guidance
Lambda environment variables are visible to anyone withlambda:GetFunction / console access, often land in CI dumps, and are process-wide: every tool in the runtime can read every secret. Agents multiply blast radius — one compromised tool path sees all env keys.
typescript
✅ Fetch from Secrets Manager at tool time, decrypt under a constrained grant, never cache across tenants in global memory without a keyed cache and TTL.
// ❌ Plaintext in env — every tool can read it
const githubToken = process.env.GITHUB_TOKEN!;
await fetch("https://api.github.com/repos/acme/app/pulls", {
headers: { Authorization: `Bearer ${githubToken}` },
});
Secrets Manager + CMK with encryption context
Encrypt secrets with a customer managed key. Put encryption context on the secret’s KMS usage so decrypt requires matching context:tenantId— which customer / workspacetoolName— which agent tool is allowed to see this secret- optionally
env=prod|staging
typescript
On the CMK key policy, require encryption context for decrypt:
import {
SecretsManagerClient,
CreateSecretCommand,
} from "@aws-sdk/client-secrets-manager";
const sm = new SecretsManagerClient({});
export async function putToolSecret(input: {
tenantId: string;
toolName: string;
secretString: string;
kmsKeyId: string;
}) {
const name = `agent/${input.tenantId}/${input.toolName}`;
await sm.send(
new CreateSecretCommand({
Name: name,
SecretString: input.secretString,
KmsKeyId: input.kmsKeyId,
// Secrets Manager attaches context on encrypt; enforce on key policy / grants
Tags: [
{ Key: "tenantId", Value: input.tenantId },
{ Key: "toolName", Value: input.toolName },
{ Key: "agentSecret", Value: "true" },
],
})
);
return name;
}
json
Tighten further: prefer {
"Sid": "AllowDecryptOnlyWithAgentContext",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/AgentRuntime" },
"Action": ["kms:Decrypt", "kms:DescribeKey"],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:toolName": "github_pr_read",
"kms:ViaService": "secretsmanager.ap-south-1.amazonaws.com"
},
"StringLike": {
"kms:EncryptionContext:tenantId": "*"
}
}
}
StringEquals on tenantId via grants per tenant instead of StringLike on the role policy.
Grants and grant tokens with constraints
KMS grants let you mint a narrow decrypt capability without rewriting the key policy for every tenant. Create a grant that allowsDecrypt only when encryption context matches, then pass the grant token into the hot path so eventual consistency on the grant does not race.
typescript
For high-QPS tools, long-lived grants per import {
KMSClient,
CreateGrantCommand,
RetireGrantCommand,
} from "@aws-sdk/client-kms";
import {
GetSecretValueCommand,
SecretsManagerClient,
} from "@aws-sdk/client-secrets-manager";
const kms = new KMSClient({});
const sm = new SecretsManagerClient({});
export async function withToolSecretGrant<T>(input: {
keyId: string;
granteePrincipal: string;
tenantId: string;
toolName: string;
secretId: string;
fn: (secret: string) => Promise<T>;
}): Promise<T> {
const grant = await kms.send(
new CreateGrantCommand({
KeyId: input.keyId,
GranteePrincipal: input.granteePrincipal,
Operations: ["Decrypt"],
Constraints: {
EncryptionContextSubset: {
tenantId: input.tenantId,
toolName: input.toolName,
},
},
// ✅ Name grants for audit; retire after the tool turn when possible
Name: `agent-${input.tenantId}-${input.toolName}`.slice(0, 256),
RetiringPrincipal: input.granteePrincipal,
})
);
try {
const secret = await sm.send(
new GetSecretValueCommand({
SecretId: input.secretId,
// Grant tokens help immediately after CreateGrant
})
);
// For direct KMS decrypt paths, pass GrantTokens: [grant.GrantToken!]
const value = secret.SecretString;
if (!value) throw new Error("empty_secret");
return await input.fn(value);
} finally {
if (grant.GrantId) {
await kms.send(
new RetireGrantCommand({
KeyId: input.keyId,
GrantId: grant.GrantId,
})
).catch(() => undefined);
}
}
}
tenantId+toolName with a rotator job may be better than create/retire every call — but still never put the plaintext in env.
Wire into the agent tool handler
typescript
Combine with tag-based IAM so the runtime role can only read secrets tagged export async function githubPrReadTool(ctx: {
tenantId: string;
runtimeRoleArn: string;
kmsKeyId: string;
}, args: { prNumber: number }) {
// ✅ Schema already validated prNumber; now least-privilege secret fetch
return withToolSecretGrant({
keyId: ctx.kmsKeyId,
granteePrincipal: ctx.runtimeRoleArn,
tenantId: ctx.tenantId,
toolName: "github_pr_read",
secretId: `agent/${ctx.tenantId}/github_pr_read`,
fn: async (token) => {
const res = await fetch(
`https://api.github.com/repos/acme/app/pulls/${args.prNumber}`,
{ headers: { Authorization: `Bearer ${token}`, "User-Agent": "cheatcoders-agent" } }
);
if (!res.ok) throw new Error(`github_${res.status}`);
return res.json();
},
});
}
agentSecret=true for its tenant — see IAM Condition Keys for Agent Runtimes.
❌ kms:Decrypt on * and other anti-patterns
| Anti-pattern | Risk | Fix |
|---|---|---|
kms:Decrypt Resource * |
Any CMK ciphertext in account | Resource = specific key ARNs |
| No encryption context | Any secret under the key | Require tenantId + toolName |
| Plaintext in env / SSM unencrypted | Universal tool access | Secrets Manager + CMK |
| Logging secret values | Prompt + log exfil | Redact; never EMF the value |
| Grant without retire / expiry process | Privilege pile-up | Retire or scheduled revoke |
| Shared “god secret” for all tools | One tool = all SaaS | One secret per toolName |
Audit with CloudTrail
Enable CloudTrail data events for KMS (and Secrets Manager). Alert on:Decryptmissing expected encryption context keysCreateGrantfrom principals that are not your grant brokerGetSecretValuespikes for a tenant outside normal agent QPS
typescript
Ship agent tool failures without echoing secret material. Meter decrypt counts per tenant with CloudWatch EMF — count decrypts, not secret bytes.
// Conceptual: metric filter on CloudTrail → alarm
// eventName=Decrypt && !.requestParameters.encryptionContext.toolName
Checklist
- [ ] No tool API keys in Lambda environment variables
- [ ] Secrets Manager secrets under CMK; tagged with
tenantId,toolName - [ ] Key policy / grants require encryption context on decrypt
- [ ] No
kms:Decrypton*— explicit key ARNs only - [ ] Grant broker issues constrained grants; retire or rotate
- [ ] Tool handler fetches secret only inside the tool call scope
- [ ] CloudTrail alerts on decrypt without context / anomalous GetSecretValue
- [ ] Sandboxes get task-role credentials, not copied env blobs
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- n8n vs AI Agent: I Gave Both the Same Job (Local, Ollama)
- Local AI Video Generator: Build Your Own App (Free, Python, No GPU)
- AI Wrote Raw Binary Instead of Code. Here’s What Happened (WebAssembly)
- I Built a Browser That Only Runs Encrypted Binary (Ep 1)
- RAG From Scratch: Chunking, Embeddings, Reranking and Evaluation (Real Numbers)
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.