Skip to content

Move WAF to a CloudFront front door; add optional Cognito inbound auth - #16

Open
arditti wants to merge 5 commits into
aws-samples:mainfrom
arditti:feat/cloudfront-waf-front-door
Open

Move WAF to a CloudFront front door; add optional Cognito inbound auth#16
arditti wants to merge 5 commits into
aws-samples:mainfrom
arditti:feat/cloudfront-waf-front-door

Conversation

@arditti

@arditti arditti commented Aug 26, 2026

Copy link
Copy Markdown

What this changes

This PR moves edge protection off the AgentCore Gateway and onto a CloudFront distribution in front of it, following the Gateway custom-domains guide — but with a CloudFront Function instead of Lambda@Edge — and adds an optional Cognito JWT inbound authorizer.

CloudFront + WAF front door

  • The REGIONAL Web ACL and its Gateway association are removed. The same policy (ChatGPT/Claude IP allowlist, AWS managed rule sets, rate limiting, default-deny) moves to a new EdgeWafStack at CLOUDFRONT scope, pinned to us-east-1 with CDK cross-region references, so the app still deploys to any AgentCore region.
  • A CloudFront distribution reverse-proxies the Gateway (CACHING_DISABLED, ALL_VIEWER_EXCEPT_HOST_HEADER, HTTPS-only, HTTP/2+3) with the Web ACL attached. GatewayResourceUrl now returns the CloudFront URL; the raw Gateway URL is exposed separately as GatewayDirectUrl with a warning.
  • Blocking now happens at the edge instead of in-region, and the sample is custom-domain-ready (add domainNames + certificate).

CloudFront Function instead of Lambda@Edge

The custom-domains guide suggests a Lambda@Edge ORIGIN_RESPONSE function to fix /.well-known/oauth-protected-resource advertising the Gateway's own domain. A viewer-request CloudFront Function generates the discovery document directly at the edge from the Host header — correct for both the default cloudfront.net domain and any custom domain, cheaper and simpler than Lambda@Edge, and the request never reaches the Gateway.

Optional Cognito inbound auth (-c auth=cognito)

  • Creates a machine-to-machine Cognito User Pool (resource server with an mcp-gateway/invoke scope, hosted domain, client_credentials app client) and switches the Gateway to GatewayAuthorizer.usingCognito.
  • This closes the gap where the raw Gateway URL bypasses the WAF: tokenless requests are rejected by the Gateway itself (401).
  • The CloudFront Function advertises the Cognito issuer in authorization_servers.
  • The auth mode is baked into the gateway's logical ID/name because the service rejects in-place authorizer-type updates; switching modes replaces the Gateway while the public CloudFront URL stays stable.
  • Default remains No Auth: ChatGPT/Claude connectors negotiate OAuth via dynamic client registration, which Cognito does not offer.

Protocol + testing

  • The Gateway now also advertises MCP 2025-06-18 (modern stateless streamable HTTP) alongside 2025-03-26, so current clients no longer fall back to legacy mode.
  • verify.sh covers the new architecture end to end: asserts WAF default-deny (403), allowlists the caller's IPv4, runs initializetools/listtools/call list_unicorns, checks the discovery document, asserts 401 without a token in Cognito mode, and cleans up after itself. All calls force IPv4 — CloudFront is dual-stack while the allowlist IP sets are IPv4-only, which silently 403s IPv6 clients otherwise.
  • New get-token.sh (mints a Cognito client_credentials token from stack outputs) and inspect.sh (launches MCP Inspector with IPv4-first DNS and the right endpoint/header).
  • AGENTS.md documents the deploy/test fast path and the gotchas (IPv6, CLOUDFRONT-scope WAF API calls, edge propagation, <target>___<tool> naming).
  • Architecture diagram (drawio + PNG) updated to match.

Testing performed

Deployed to a personal account in us-east-1 in both modes and ran ./verify.sh end to end. Cognito mode, all checks green:

PASS WAF default-deny (got 403 before allowlisting)
PASS gateway rejects requests without a Cognito token (HTTP 401)
PASS initialize
PASS tools/list (found unicorn-mcp-runtime-target___list_unicorns)
PASS tools/call list_unicorns — returned 6 unicorns
PASS oauth-protected-resource discovery returns the front-door domain

Also verified interactively with MCP Inspector (streamable HTTP, modern 2025-06-18 protocol) and confirmed the direct Gateway URL returns 401 without a token in Cognito mode.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT-0 license.

- Remove the REGIONAL WAF Web ACL and its Gateway association
- Add EdgeWafStack (us-east-1) with the same policy at CLOUDFRONT scope:
  ChatGPT/Claude IP allowlist, AWS managed rules, rate limiting
- Add a CloudFront distribution in front of the Gateway (caching disabled,
  all-viewer-except-host origin request policy, HTTPS only)
- Serve /.well-known/oauth-protected-resource from a CloudFront Function
  instead of the Lambda@Edge approach in the custom-domains guide
- deploy.sh bootstraps us-east-1 when needed and deploys both stacks
- verify.sh targets the CLOUDFRONT-scope IP set and also checks the
  default-deny behavior and the OAuth discovery rewrite
…e verify

- Cognito machine-to-machine pool + resource server (mcp-gateway/invoke) +
  hosted domain + client_credentials app client; gateway inbound switches to
  GatewayAuthorizer.usingCognito, closing the direct-gateway WAF bypass
- Auth mode is baked into the gateway logical ID/name: the service forbids
  in-place authorizer-type updates, so switching modes replaces the gateway
  (the CloudFront front-door URL stays stable)
- CloudFront Function advertises the Cognito issuer in authorization_servers
  (placeholder substitution now replaces all occurrences, not just the first)
- verify.sh: force IPv4 (CloudFront is dual-stack, allowlist is IPv4-only),
  fetch a Cognito token when present, assert 401 without a token, and check
  the discovery document carries the issuer
- AGENTS.md: deploy/test runbook for agents, including the IPv6 and
  CLOUDFRONT-scope gotchas
…25-06-18 support

- Gateway now advertises MCP protocol 2025-06-18 (modern stateless
  streamable HTTP) alongside 2025-03-26 — modern clients no longer have to
  fall back to legacy mode
- get-token.sh: mint a Cognito client_credentials token from stack outputs
- inspect.sh: launch MCP Inspector with IPv4-first DNS and the right
  endpoint/header printed (and on the clipboard on macOS)
- README/AGENTS.md: document the interactive test flow
…Function, optional Cognito

- AI hosts now enter through the CloudFront front door (/mcp); WAF protects
  the distribution instead of the Gateway
- CloudFront Function note for the /.well-known OAuth discovery rewrite
- Amazon Cognito shown as the optional -c auth=cognito JWT authorizer
- Existing CloudFront relabeled as the Images CDN; drawio source updated
  and PNG re-exported
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant