Securely Deploying MCP Servers with SSO on AWS

This tutorial walks through a simplified pattern for deploying an MCP Server on AWS, enforcing
SSO authentication
and per-user role assumption. It assumes you already run other
services in your VPC with SSO and IAM roles.


0) Prerequisites


1) Front door: API Gateway HTTP API

Purpose: require tokens on every call; keep MCP server “auth-dumb”.

  • Create HTTP API (not REST API).
  • Route: ANY /{proxy+}VPC Link
    (to NLB).
  • Add a Lambda Authorizer:
    • Identity source: Authorization header (Bearer).
    • Cache disabled initially (0s).

Return 401 with WWW-Authenticate when token missing/invalid.


2) Lambda Authorizer

Purpose: validate tokens and decide user role.

Steps: 1. Validate OIDC token (iss/aud/exp/sig).\

  1. Extract claims: sub, email, org/tenant, optional
    OIDC-A claims.\
  2. DB lookup: sub → { roleArn, tags[], limits }.\
  3. Return Allow and inject headers upstream.

// Node/TS sketch export async function handler(event) { const token = parseBearer(event.headers.authorization); const claims = await verifyJwt(token, { iss, jwks, aud }); const { roleArn, tags } = await dbLookup(claims.sub); return { isAuthorized: true, context: { "x-decided-role": roleArn, "x-source-identity": claims.sub, "x-session-tags": JSON.stringify(tags) } }; }


3) Private networking to MCP

  • Deploy MCP server behind
    NLB
    in private subnets.
  • Configure API Gateway VPC Link → NLB.
  • Forward headers to MCP.

4) IAM wiring

4.1 ECS Task Role

Minimal permissions, only sts:AssumeRole.{ "Effect":"Allow", "Action":["sts:AssumeRole","sts:TagSession"], "Resource":["arn:aws:iam::<acct>:role/quilt-mcp-target-*"] }

4.2 Target Role Trust Policy

Restrict to ECS task role; enforce SourceIdentity and tag patterns.{ "Effect":"Allow", "Principal":{"AWS":"arn:aws:iam::<acct>:role/<ecsTaskRole>"}, "Action":["sts:AssumeRole","sts:TagSession"], "Condition":{ "StringLike":{"sts:SourceIdentity":"usr-*"}, "ForAllValues:StringEquals":{"sts:TagKeys":["tenant"]} } }

4.3 Resource Policy Example (S3 ABAC)

{ "Effect":"Allow", "Principal":{"AWS":"*"}, "Action":["s3:GetObject","s3:PutObject"], "Resource":"arn:aws:s3:::quilt-tenant-${aws:PrincipalTag/tenant}/*", "Condition":{"StringEquals":{"aws:PrincipalTag/tenant":"${aws:PrincipalTag/tenant}"}} }


5) MCP Server code

Read headers → Assume role per request.import boto3, json def creds_from_headers(headers): role_arn = headers["x-decided-role"] source_id = headers["x-source-identity"] tags = json.loads(headers["x-session-tags"]) sts = boto3.client("sts") resp = sts.assume_role( RoleArn=role_arn, RoleSessionName=source_id[:32], SourceIdentity=source_id, Tags=tags, DurationSeconds=1800 ) return resp["Credentials"]


6) Token acquisition (“force SSO”)

  • Agents/clients must present a Bearer token
    (aud = api://quilt-mcp).
  • Supported flows:

401 response example:WWW-Authenticate: Bearer realm="quilt-mcp", authorization_uri="https://login.example.com/device", token_service="https://login.example.com/token"


7) Observability

  • CloudTrail:
    all calls show sourceIdentity=<user_sub>.
  • Logs: (sub, tenant, roleArn, tags, decision, requestId).
  • Metrics: deny spikes, STS errors.

8) Hardening options


Checklist

  • [ ] API Gateway + VPC Link → private NLB/ALB\
  • [ ] Lambda Authorizer: JWT verify → DB map → inject headers\
  • [ ] ECS task role with only sts:AssumeRole\
  • [ ] Target role trust: enforce SourceIdentity + tags\
  • [ ] ABAC resource policies (e.g., S3)\
  • [ ] MCP server: AssumeRole per request\
  • [ ] 401 with device flow bootstrap\
  • [ ] Logging, metrics, alarms

Further Reading

Leave a comment

Blog at WordPress.com.

Up ↑