MCP Security Checklist: Letting AI Agents Touch Your Infrastructure
Giving an agent MCP tools for AWS, Kubernetes or Terraform is useful right up to the moment it runs the wrong command. This checklist covers scoped tools, dry-run-first changes, approvals, audit logs and secrets.
Short answer
Expose narrow tools instead of a shell, give each MCP server its own least-privilege identity, make every change produce a dry run that a human approves before it applies, log every call with who asked and who approved, and keep credentials out of the model's context entirely.
Assume the model will eventually do the wrong thing
The useful version of an infrastructure agent can read logs, inspect a cluster, propose a fix and apply it. The dangerous version is the same agent after a confusing prompt, a hallucinated resource name or a malicious string in a log line it was asked to summarise.
OWASP calls this excessive agency and traces it to three root causes: excessive functionality, excessive permissions and excessive autonomy. Everything in this checklist is about shrinking one of those three, so that when the model is wrong, the damage is small, visible and reversible.
1. Scope the tools, not just the prompt
A run_shell or call_any_api tool turns every other control into a suggestion. OWASP specifically recommends avoiding open-ended extensions such as running shell commands or fetching arbitrary URLs, in favour of tools that do one specific thing.
In practice I split tools by risk. Read tools such as get_pod_logs, describe_deployment or list_security_groups live on one server. Mutating tools such as scale_deployment or plan_terraform_change live on another, with tighter approval rules. Each tool takes typed, validated arguments: a namespace from an allowlist, not a free-text kubectl command.
The MCP tools specification backs this up from the server side: servers must validate all tool inputs, implement access controls, rate limit tool invocations and sanitise outputs.
2. Give each server its own least-privilege identity
The tool boundary only helps if the credentials behind it match. A read-only tool backed by an admin role is still an admin tool with extra steps. OWASP's example is a read extension connecting to a database with an identity that can also update and delete.
Give each MCP server its own cloud role or service account, scoped to the resources and verbs it needs: a read-only IAM role for the inspection server, a Kubernetes service account bound to one namespace for the deploy server. Where an action is on behalf of a specific person, run it in that person's context rather than a shared high-privilege identity.
3. Make every change dry-run first
For infrastructure, the safest pattern I know is to split every mutation into two calls. The first produces a plan: terraform plan output, a kubectl server-side dry run, or a diff of the config that would change. The second applies that exact plan, identified by a hash, and nothing else.
This gives the human approver something concrete to read, and it stops the model from quietly changing its mind between "here is what I will do" and doing it. If the plan is stale or the hash does not match, the apply fails.
// Mutations are two tools: plan, then apply the exact plan that was approved
server.tool('plan_scale_deployment', {
namespace: z.enum(['web', 'workers']),
deployment: z.string().regex(/^[a-z0-9-]+$/),
replicas: z.number().int().min(1).max(20),
}, async (args) => {
const diff = await k8s.dryRunScale(args); // server-side dry run, no changes
const planId = await plans.save({ args, diff, ttlMinutes: 15 });
return { content: [{ type: 'text', text: JSON.stringify({ planId, diff }) }] };
});
server.tool('apply_plan', { planId: z.string().uuid() }, async ({ planId }) => {
const plan = await plans.requireApproved(planId); // throws unless a human approved it
return k8s.applyScale(plan.args);
});4. Put approvals outside the model
The MCP specification says there should always be a human in the loop with the ability to deny tool invocations, and that clients should prompt for confirmation on sensitive operations and show tool inputs to the user before calling the server.
The important design choice is where that approval lives. It should be enforced by the system that executes the change, not by an instruction to the model. OWASP calls this complete mediation: authorisation is checked in the downstream system on every request, rather than trusting the LLM to decide whether an action is allowed.
Not every call needs a person. Reads can usually run freely. Low-risk changes in a dev namespace might be auto-approved within limits. Anything that touches production, deletes data, opens network access or changes IAM should wait for a named human.
5. Log every call like it will be read in an incident review
The MCP tools specification recommends that clients log tool usage for audit purposes. For infrastructure I treat that as mandatory and log on the server side as well, because the server is the part you control.
Each entry should answer the questions you will be asked later: which user started the session, which agent and model made the call, which tool with which arguments, what the dry run showed, who approved it and when, and what actually happened. Tie them together with a correlation ID and ship them somewhere the agent's own credentials cannot edit.
6. Keep secrets out of the context window
Credentials should be held by the MCP server and used on the model's behalf. They should never appear in tool arguments, tool output or the prompt, because anything in the context can end up in a log, a trace or a response.
The MCP security best practices are explicit on two points. Token passthrough is forbidden: an MCP server must not accept tokens that were not issued for that server and pass them straight to a downstream API. And scopes should be minimal and progressive, starting with low-risk read scopes and elevating only when a privileged operation is first attempted, rather than requesting wildcard scopes up front.
Prefer short-lived credentials, such as federated cloud roles or tokens that expire in minutes, over long-lived keys in environment variables. If a token does leak, the window is small.
7. Treat third-party servers and their metadata as untrusted
Tool annotations that describe a tool as read-only or non-destructive are hints, not guarantees. The specification says clients must consider tool annotations untrusted unless they come from trusted servers.
Local MCP servers run with the privileges of whatever launched them, and the security guidance recommends sandboxing them with restricted file system and network access. For servers that make outbound requests, block private ranges and cloud metadata endpoints such as 169.254.169.254, which the same guidance calls out as a route to exfiltrating cloud credentials.
The checklist
Before an agent gets access to anything that matters, I want a yes to each of these:
- No open-ended shell or arbitrary HTTP tools in production.
- Read and write tools are on separate servers with separate identities.
- Every identity is least-privilege and scoped to specific resources.
- Mutations are plan then apply, and apply only accepts an approved plan.
- Approval is enforced by the executing system, not by the prompt.
- Every call is logged with user, agent, arguments, approval and result.
- No credentials in prompts, tool arguments or tool output; no token passthrough.
- Scopes start minimal and elevate per operation.
- Third-party servers are sandboxed and their annotations are not trusted blindly.
- Rate limits and timeouts are set on every tool.
Getting this built
You can retrofit most of this onto an existing agent, but it is much easier to design in from the start. My Agent Mesh package does exactly this: MCP-based agents with scoped access to AWS, Azure, Kubernetes, Terraform and internal APIs, dry-run-first mutating workflows, approval gates and a clean audit trail, routed across whichever models fit your environment.