Open MCP servers are powerful — and dangerous. Learn how to lock down your MCP server with API keys, OAuth 2.0, RBAC, and AWS Secrets Manager so only the right people call the right tools.
An MCP server without authentication is like an AWS Lambda function with a public URL and no IAM policy — technically functional, practically a liability. Every production MCP server needs a clear answer to: "Who can call this tool, and what are they allowed to do?"
When you expose an MCP server — whether locally via stdio or over the network via SSE/HTTP — you're exposing a set of callable functions to any client that can reach it. Without authentication, an attacker who discovers your endpoint can invoke your tools: delete database records, read private files, exfiltrate secrets, or rack up cloud bills.
The MCP spec itself is auth-agnostic — it defines the protocol, not the security layer. That means you own the security. Here's the standard threat model you must design for:
Threat
Impact
Mitigation
Unauthenticated tool calls
Full tool access for anyone
API key / OAuth guard
Over-privileged client
Can call destructive tools
RBAC — scope per role
Stolen API key
Impersonation
Key rotation + Secrets Manager
No audit trail
Can't detect abuse
CloudWatch structured logging
Prompt injection via tool result
LLM manipulated by data
Output sanitization (Day 18)
🚨
Especially critical for SSE transport
stdio MCP servers run locally and inherit the user's OS permissions. SSE/HTTP MCP servers are network-accessible — they MUST have authentication before receiving any real traffic.
🔑 API Keys
API Key Authentication — Bearer Tokens
The simplest production-ready auth pattern. The client sends a secret key in the Authorization header with every request. Your MCP server validates it before processing any message. This works for both SSE and HTTP transports.
1
Generate and store API keys securely
Never hardcode keys. Use AWS Secrets Manager or environment variables loaded at startup.
# Generate a cryptographically secure keyimportsecretsapi_key = secrets.token_urlsafe(32)
# Store in AWS Secrets Manager (see Section 5)# Output: "xK7mP2qN9vL4wR8jT1cA5dF6yH3bE0uI..."
2
Add auth middleware to your FastMCP server
Validate the Bearer token on every incoming request before it reaches any tool handler.
Direct string comparison (key == provided) is vulnerable to timing attacks — an attacker can measure nanosecond differences to guess the key character by character. hmac.compare_digest() always takes the same time regardless of where strings differ.
🔐 OAuth 2.0
OAuth 2.0 with MCP Servers
For multi-tenant MCP servers (many users, each with their own identity), OAuth 2.0 is the industry standard. The MCP spec defines an OAuth 2.0 flow specifically for MCP authorization — the client obtains an access token from an Authorization Server, then presents it to the MCP server.
The recommended flow for MCP is OAuth 2.0 with PKCE (Proof Key for Code Exchange) — which is secure even for public clients like CLI tools and desktop apps.
🏨
The Hotel Key Card Analogy
OAuth 2.0 is like a hotel. You show your ID at the front desk (Authorization Server) and prove who you are. They give you a key card (access token) with specific permissions — room 302 only, no pool access after 10pm. When you use the elevator or pool (MCP tools), it checks your key card, not your ID. The key card expires, and the hotel can revoke it instantly without changing the locks on every door.
Cognito gives you a fully managed OAuth 2.0 Authorization Server with user pools, social login (Google/GitHub), and JWT token validation in under 30 minutes. Free tier: 50,000 MAUs. Perfect for MCP servers with external users.
🎭 RBAC
Role-Based Access Control for Tools
Authentication answers "who are you?" — Authorization answers "what are you allowed to do?" RBAC assigns users to roles (admin, analyst, readonly) and tools to permission groups. A tool call is only processed if the caller's role includes that tool's permission.
# RBAC implementation for MCP toolsfromfunctoolsimportwrapsfromfastmcpimportFastMCP, Contextmcp = FastMCP("RBACServer")
# Define permissions per roleROLE_PERMISSIONS = {
"admin": {"read_data", "write_data", "delete_data", "run_reports"},
"analyst": {"read_data", "run_reports"},
"readonly": {"read_data"},
}
defrequire_permission(permission: str):
defdecorator(func):
@wraps(func)
async defwrapper(ctx: Context, *args, **kwargs):
role = ctx.meta.get("role", "readonly")
ifpermissionnot inROLE_PERMISSIONS.get(role, set()):
raisePermissionError(f"Role '{role}' cannot use '{permission}'")
return awaitfunc(ctx, *args, **kwargs)
returnwrapperreturndecorator@mcp.tool()
@require_permission("delete_data")
async defdelete_record(ctx: Context, record_id: str) -> str:
# Only admin role can reach this linereturnf"Deleted {record_id}"@mcp.tool()
@require_permission("read_data")
async defget_record(ctx: Context, record_id: str) -> str:
# All roles can reach this linereturnf"Record: {record_id}"
🔒 Secrets Manager
AWS Secrets Manager Integration
Never store API keys in environment variables that get committed to source control. AWS Secrets Manager is the right place — it encrypts secrets at rest (KMS), enables automatic rotation, and provides fine-grained IAM access control. Your MCP server fetches the secret at startup (or caches it with a TTL).
importboto3, jsonfromfunctoolsimportlru_cachefromdatetimeimportdatetime, timedelta_secrets_client = boto3.client("secretsmanager", region_name="us-east-1")
_secret_cache: dict = {}
defget_secret(secret_name: str, ttl_minutes: int = 15) -> dict:
"""Fetch secret with local TTL cache to avoid API throttling."""cached = _secret_cache.get(secret_name)
ifcachedanddatetime.now() < cached["expires"]:
returncached["value"]
response = _secrets_client.get_secret_value(SecretId=secret_name)
value = json.loads(response["SecretString"])
_secret_cache[secret_name] = {
"value": value,
"expires": datetime.now() + timedelta(minutes=ttl_minutes)
}
returnvalue# Usage at server startupsecrets = get_secret("mcp/production/api-keys")
MCP_API_KEY = secrets["mcp_api_key"]
DB_PASSWORD = secrets["db_password"]
EXTERNAL_KEY = secrets["external_service_key"]
🏗️
IAM policy for your MCP server
Grant your ECS task role or Lambda execution role only the specific secrets it needs: secretsmanager:GetSecretValue on arn:aws:secretsmanager:region:account:secret:mcp/*. Never use * on resource.
📋 Audit Logging
Audit Logging Every Tool Call
Security without visibility is incomplete. Every tool call should emit a structured log to CloudWatch Logs — who called it, what arguments were passed, what was returned, and how long it took. This enables intrusion detection, debugging, and compliance audits.
A financial services company runs an MCP server with a query_customer_data tool. With audit logging in CloudWatch, their security team sets a CloudWatch Alarm: "alert if any single user_id calls query_customer_data more than 100 times in 5 minutes." This automatically catches both runaway automation bugs and deliberate data exfiltration attempts.
🧠 Knowledge Check — Day 17
4 questions on MCP authentication & authorization
QUESTION 01 / 04
Why should you use hmac.compare_digest() instead of == when comparing API keys?
A hmac.compare_digest() is faster for long strings
B It prevents timing attacks by always taking the same time regardless of where strings differ
C It automatically hashes the key before comparing
D It works only in Python 3.11+
✅ Correct answer: B. Timing attacks measure how long a comparison takes — early exit on first mismatch leaks information about how many characters matched. hmac.compare_digest() always iterates the full string, making timing attacks infeasible.
QUESTION 02 / 04
Which AWS service is the recommended way to store MCP server API keys in production?
A Environment variables in a .env file checked into source control
B AWS Systems Manager Parameter Store (free tier)
C AWS Secrets Manager with TTL caching in the server
D S3 bucket with server-side encryption
✅ Correct answer: C. AWS Secrets Manager provides encryption at rest (KMS), automatic rotation, fine-grained IAM access control, and versioning. TTL caching avoids API throttling. Parameter Store is also acceptable for lower-sensitivity configs, but Secrets Manager is preferred for credentials.
QUESTION 03 / 04
In RBAC for MCP tools, an "analyst" role has permissions: {read_data, run_reports}. What happens when an analyst calls the delete_record tool (which requires "delete_data" permission)?
A The call succeeds because analysts can see all tools
B A PermissionError is raised before the tool function executes
C The tool is hidden from the analyst's tool list
D It falls back to read-only mode
✅ Correct answer: B. The @require_permission decorator raises a PermissionError before the actual function body runs — the analyst gets a clear error. Optionally you can also filter the tools list per role so analysts never see destructive tools at all (defense in depth).
QUESTION 04 / 04
OAuth 2.0 with PKCE is preferred over basic OAuth 2.0 for MCP because:
A It eliminates the need for HTTPS
B It is secure for public clients (CLI, desktop apps) that cannot safely store a client secret
C It issues tokens that never expire
D It bypasses the authorization server
✅ Correct answer: B. PKCE (Proof Key for Code Exchange) was designed for clients that can't keep a client_secret secure — like native desktop apps or CLI tools. It uses a dynamically generated code verifier/challenge pair instead, preventing authorization code interception attacks.
Up Next — Day 18
Rate Limiting, Security & Input Validation
Stop prompt injection, tool abuse, and DDoS attacks before they reach your business logic.