はじめに
こんにちは、最近実家の近くにラ・ムーができた株式会社 日立製作所の三本康貴です。
近年、AI Agent、Model Context Protocol(MCP)の普及に伴い、MCP Serverの開発、公開も増える一方で、既存のMCP Serverを統合したり、REST APIをMCP Serverとして管理するゲートウェイ機能の需要や認証認可の重要性が高まっています。
本記事では、MCPゲートウェイサービスである「Amazon Bedrock AgentCore Gateway」と認証認可サーバの「Keycloak」を組み合わせて、最新(2026年6月現在)のMCP 2025-11-25 認可仕様に準拠したMCP認可を検証していきます。
Amazon Bedrock AgentCore Gatewayとは
Amazon Bedrock AgentCore Gateway(以下、AgentCore Gateway) は、AWSが提供するAI Agent が外部ツールや既存システムへ安全に接続するためのマネージドなゲートウェイサービスです。
本検証では、次の機能を利用します。
- ツール統合
REST API、MCP Server、Lambda、API Gateway などの既存のサービスを単一のMCPエンドポイントに統合 - 包括的な認可
AI AgentやMCP Clientの身元確認や外部ツールへの接続を管理
AgentCore Gatewayの認可機能によって、AI AgentやMCP ClientなどがAgentCore Gatewayを呼び出す際に付与されるJWT、IAM identityを利用して呼び出し元が正当な利用者またはアプリケーションであることを検証することができます。JWT検証では、クレーム、スコープを検証します。AgentCore Gatewayと連携する認証認可サーバは、Amazon Cognito等の特定の認証認可サーバに依存せず、OAuth対応であれば連携可能です。
参考:
- Amazon Bedrock AgentCore Gateway: Securely connect tools and other resources to your Gateway
- Set up inbound authorization for your gateway
KeycloakとMCP認可
Keycloakは、OAuth 2.1 準拠の認証認可サーバのOSSであり、MCP認可サーバとして使用することが可能です。
MCP仕様では、認可サーバに対して複数のOAuth関連標準への対応が要求されています。Keycloakの対応バージョンと対応状況は、次の通りです。
| MCPバージョン | Keycloak対応状況 |
|---|---|
| 2025-03-26 | 対応 |
| 2025-06-18 | RFC 8707 Resource Indicators未対応のため部分対応 ※ |
| 2025-11-25 | RFC 8707 Resource Indicators未対応のため部分対応 ※ |
| 標準仕様 | MCP側の要求(2025-11-25) | Keycloak対応 |
|---|---|---|
| OAuth 2.1 Authorization Framework | MUST | 対応 |
| OAuth 2.0 Authorization Server Metadata / RFC 8414 | MUST | 対応 |
| OAuth 2.0 Resource Indicators / RFC 8707 | MUST | 未対応 |
| OAuth 2.0 Dynamic Client Registration / RFC 7591 | MAY | 対応 |
| OAuth Client ID Metadata Document / CIMD | SHOULD | 対応(ただし実験的) |
Keycloakは、OAuth 2.1 に準拠するほか、最新バージョン26.6(2026年6月現在)ではCIMDを実験的にサポートしています。
※ 公式リリースでは未対応になっているRFC 8707 Resource Indicatorsについても、OAuth 2.0の scope を利用したワークアラウンドが示されています。また、初期実装のPR(Initial experimental support for Resource Indicators#46763)は既に取り込まれており、まもなく対応されることが期待されます。
参考:
- Authorization - Model Context Protocol
- Integrating with Model Context Protocol (MCP)
- Keycloak 26.6.0 release
OAuth 2.1と認可コードフロー
OAuth 2.1については、弊社の黒坂さんが執筆した「OAuth 2.1」の認可コードフローを「Keycloak」で実装しようの説明が分かりやすいので引用します。
(引用開始)---
「OAuth 2.1」は、業界標準の認可プロトコルである「OAuth 2.0」の後継として提案されている仕様です。OAuth 2.0は柔軟性が高い反面、不適切な実装が生じる可能性がありました。OAuth 2.1は、10年以上に及ぶOAuth 2.0の運用から得たセキュリティのベストプラクティスを統合し、開発者が安全な実装をしやすくする目的で提案されています。
OAuth 2.1における主な変更点を以下にまとめます。
- 認可コードフローにおけるPKCE(Proof Key for Code Exchange)の必須化
- リダイレクトURIの完全一致の必須化
- Implicit Grantの廃止
- Resource Owner Password Credentials Grantの廃止
- URIのクエリでのBearerトークンの送信の禁止
- パブリッククライアントに向けたリフレッシュトークンは、送信者制約またはワンタイム使用にすることを必須化
OAuth 2.1はOAuth 2.0のセキュリティを強化したサブセットであり、これにより開発者は安全な実装が可能になります。
(引用終了)---
認可コードフローは、次の流れでAccess Tokenを取得します。
- クライアントが
code_verifierを生成し、そこからcode_challengeを作ります。 - クライアントはユーザエージェント、通常はブラウザを認可エンドポイントへリダイレクトします。
- 認可リクエストには
response_type=code、client_id、redirect_uri、scope、state、code_challenge、code_challenge_methodを含めます。 - 認可サーバはユーザを認証し、同意・認可判断を行います。
- 認可されると、認可サーバは
redirect_uriにcodeとstateを付けてリダイレクトします。 - クライアントはトークンエンドポイントに
grant_type=authorization_code、code、code_verifierを送ります。 - 認可サーバは認可コード、クライアント、
code_verifierを検証し、問題なければAccess Tokenを返します。
参考:
CIMDとは
CIMDは、OAuthのクライアント登録方法の1つで、クライアントとサーバの間に事前の関係がない、つまりAI AgentなどのクライアントをMCP認可サーバに事前登録していない場合でも、OAuthのフローを開始できるようになります。
MCPは、OAuthのクライアント登録方法について次の3つをサポートします。
- CIMD
クライアントとサーバの間に事前の関係がない場合。 - Pre-registration / 事前登録
クライアントとサーバの間に既存の関係がある場合。 - DCR(Dynamic Client Registration)/ 動的クライアント登録
後方互換性、または特定要件のために使用します。
CIMDは client_id 自体を HTTPS URL にして、そのURLで公開されるJSONメタデータを認可サーバが取得してクライアント情報として扱う方式です。
インフォメーション
本記事では、CIMDについてdraft-ietf-oauth-client-id-metadata-document-01 を前提に説明します。CIMDはInternet-Draft段階の仕様であり、今後の改訂により内容が変更される可能性があります。
クライアント登録を踏まえたMCPの認可コードフローのシーケンス図を示します。
※ シーケンス図はAuthorization Flow Stepsより引用
参考:Authorization - Model Context Protocol
改めて、本記事では、AgentCore GatewayとKeycloakを組み合わせて、
- CIMDを利用してクライアントを事前登録することなくOAuth 2.1 準拠の認可コードフローでKeycloakからAccess Tokenを取得できること
- 取得したAccess TokenでAgentCore Gatewayにアクセスできること
を検証します。
検証構成
AWS構成
今回の検証の主なAWS構成を示します。
※1 Keycloak、CIMD metadata、MCP Serverを同一EC2にホストし、同一internal ALBでルーティングします。可読性を考慮して構成図は分けて図示しています。
※2 Public subnet, Elastic IP, Nat GatewayはEC2のセットアップ時に利用しますが、検証の通信には利用しないため図を省略しています。
AWSサービス一覧
| 分類 | AWSサービス | 用途 |
|---|---|---|
| DNS | Route 53 Public Hosted Zone | ACM public certificateのDNS検証 |
| DNS | Route 53 Private Hosted Zone | VPC内でKeycloak、cimd、mcp-serverのドメイン名解決 |
| TLS | AWS Certificate Manager | internal ALBにpublic certificateを設定 |
| Network | VPC / Private Subnet | EC2、ALB、VPC Endpointを配置 |
| Network | Internal Application Load Balancer | Keycloak、CIMD、MCP ServerのHTTPS入口 |
| Network | SSM用VPC Endpoint | EC2にSSM経由でログイン、ポートフォワードでRDP接続 |
| Network | AgentCore Gateway用VPC Endpoint | VPC内のMCP InspectorからAgentCore Gatewayにアクセス |
| Compute | EC2 Linux | Keycloak、CIMD nginx、MCP Serverをホスト |
| Compute | EC2 Windows | MCP Inspectorを実行する踏み台 |
| Access | Systems Manager Session Manager | EC2への接続、RDPポートフォワード |
| MCP Gateway | AgentCore Gateway | MCP Client向け入口、JWT検証、Target集約 |
ドメイン設定
今回は次のドメインを登録して使用しました。別のドメインで検証する場合は、置き換えて読み進めてください。
agentcore-keycloak-example.com
サブドメインは次のように使います。
auth.agentcore-keycloak-example.com(Keycloak用)
cimd.agentcore-keycloak-example.com(CIMD用)
mcp-server.agentcore-keycloak-example.com(MCPサーバ用)
ACM public certificateは、次のようなワイルドカード証明書を利用します。
*.agentcore-keycloak-example.com
KeycloakはVPC内private resourceですが、AgentCore IdentityがOIDC Discovery URLをHTTPSで検証する必要があるため、internal ALBにpublic ACM certificateを付与します。
参考: AgentCore Identity private IdP
登場人物
| OAuth / MCPのロール | 今回の実体 | 説明 |
|---|---|---|
| Resource Owner | エンドユーザ | Keycloakにログインし、MCP Clientにアクセスを許可する |
| OAuth Client / MCP Client | MCP Inspector | KeycloakからAccess Tokenを取得し、AgentCore Gatewayを呼び出す |
| Authorization Server | Keycloak | 認可コードフローを担当する |
| Protected MCP Server / Resource Server | AgentCore Gateway | Keycloak JWTを検証し、MCPエンドポイントを提供する |
| Gateway Target | ALB + EC2上のMCP Server | 実際のMCP toolsを提供する |
検証
前提条件
ローカルPCに次のソフトウェアをインストールします。
- AWS CLI
- Session Manager Plugin
IAMの権限は必要に応じて付与してください。筆者はAdministratorAccess相当の権限で検証しています。
参考:
事前準備
次の事前準備を行います。自分で検証される方は必要に応じて参考にしてください。
- ドメイン登録とパブリックホストゾーン作成
- Windows踏み台EC2のkey-pair作成
- CloudFormationで検証環境を作成(AgentCore Gatewayを除く)
- EC2上でKeycloak、CIMD nginx、MCP Serverを起動
- Windows踏み台EC2にNode.jsをインストール
事前準備
1. ドメイン登録とパブリックホストゾーン作成
パブリックACM証明書をDNS検証するため、Route53を利用してドメインagentcore-keycloak-example.com(任意のドメイン名)の登録とパブリックホストゾーンを作成します。ドメインをRoute 53に登録する際に、そのドメインのホストゾーンが自動的に作成されます。既存のドメインとホストゾーンを利用する場合は、この手順スキップしてください。
参考:新しいドメインの登録
2. Windows踏み台EC2のkey-pair作成
Windows踏み台EC2のAdministratorパスワード復号のためのkey-pairを作成します。既存のkey-pairを利用する場合は、この手順をスキップしてください。
# コマンド例
# Windows踏み台EC2で使うEC2 Key Pairを作成し、秘密鍵をローカルファイルに保存
aws ec2 create-key-pair \
--region ap-northeast-1 \
--key-name mcp-keycloak-agentcore-poc-key \
--key-type rsa \
--key-format pem \
--query 'KeyMaterial' \
--output text > mcp-keycloak-agentcore-poc-key.pem
参考:Amazon EC2 インスタンスのキーペアを作成する
3. CloudFormationで検証環境を作成(AgentCore Gatewayを除く)
CloudFormationでAgentCore Gatewayを除く検証環境を作成します。CloudFormationテンプレートとデプロイコマンド例を示します。
CloudFormationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: >
AgentCore Gateway foundation for Keycloak + CIMD nginx + EC2-hosted MCP server + Windows bastion MCP client.
Assumes a public domain and an existing Route 53 public hosted zone for ACM DNS validation.
Creates a public ACM certificate for the internal ALB, VPC, NAT, SSM endpoints, an AgentCore Gateway VPC endpoint,
Route 53 private hosted zone, internal ALB routes for auth/cimd/mcp-server, EC2 instances, EC2 IAM role/instance profile, and security groups.
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Project
Parameters:
- ProjectName
- EnvironmentName
- Label:
default: Network
Parameters:
- VpcCidr
- PublicSubnet1Cidr
- PrivateSubnet1Cidr
- PrivateSubnet2Cidr
- Label:
default: DNS and TLS
Parameters:
- HostedZoneName
- PublicHostedZoneId
- AuthHostname
- CimdHostname
- McpServerHostname
- Label:
default: EC2 access
Parameters:
- KeyPairName
- Label:
default: AgentCore PrivateLink
Parameters:
- AgentCoreGatewayEndpointPolicyResourceArn
ParameterLabels:
PublicHostedZoneId:
default: Existing Route 53 public hosted zone ID for ACM DNS validation
KeyPairName:
default: Existing EC2 key pair used to decrypt the Windows Administrator password
AgentCoreGatewayEndpointPolicyResourceArn:
default: AgentCore Gateway ARN allowed by the VPC endpoint policy
Parameters:
ProjectName:
Type: String
Default: mcp-keycloak-agentcore
AllowedPattern: '^[a-zA-Z0-9-]+$'
EnvironmentName:
Type: String
Default: poc
AllowedPattern: '^[a-zA-Z0-9-]+$'
VpcCidr:
Type: String
Default: 10.0.0.0/16
PublicSubnet1Cidr:
Type: String
Default: 10.0.0.0/24
PrivateSubnet1Cidr:
Type: String
Default: 10.0.10.0/24
PrivateSubnet2Cidr:
Type: String
Default: 10.0.11.0/24
HostedZoneName:
Type: String
Default: agentcore-keycloak-example.com
Description: DNS zone name used for the VPC private hosted zone and ACM wildcard certificate. Do not include a trailing dot.
PublicHostedZoneId:
Type: AWS::Route53::HostedZone::Id
Description: Existing Route 53 public hosted zone ID used by ACM DNS validation. The zone must cover HostedZoneName.
AuthHostname:
Type: String
Default: auth.agentcore-keycloak-example.com
CimdHostname:
Type: String
Default: cimd.agentcore-keycloak-example.com
McpServerHostname:
Type: String
Default: mcp-server.agentcore-keycloak-example.com
KeyPairName:
Type: AWS::EC2::KeyPair::KeyName
Description: Required to retrieve/decrypt the default Windows Administrator password for RDP.
AgentCoreGatewayEndpointPolicyResourceArn:
Type: String
Default: '*'
Description: >-
AgentCore Gateway ARN allowed by the AgentCore Gateway interface VPC endpoint policy.
Keep '*' until the gateway ARN is known, then narrow it to arn:aws:bedrock-agentcore:region:account-id:gateway/gateway-id.
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-vpc'
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-igw'
VpcGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref InternetGateway
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PublicSubnet1Cidr
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-public-a'
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PrivateSubnet1Cidr
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-private-a-primary'
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PrivateSubnet2Cidr
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-private-b-alb-only'
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-public-rt'
PublicDefaultRoute:
Type: AWS::EC2::Route
DependsOn: VpcGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
NatEip:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-nat-eip'
NatGateway:
Type: AWS::EC2::NatGateway
DependsOn: VpcGatewayAttachment
Properties:
AllocationId: !GetAtt NatEip.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-nat'
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-private-rt'
PrivateDefaultRouteViaNat:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
AlbSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Internal ALB security group
VpcId: !Ref Vpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-alb-sg'
KeycloakCimdMcpServerEc2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EC2 security group for Keycloak, CIMD nginx, and lightweight MCP server
VpcId: !Ref Vpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-keycloak-cimd-mcp-server-ec2-sg'
WinBastionMcpClientEc2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Windows bastion MCP client EC2 security group; no inbound required for SSM port forwarding
VpcId: !Ref Vpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2-sg'
AgentCorePrivateIdpSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Attach this SG to AgentCore Identity private IdP managed VPC resource. ALB allows HTTPS from this SG.
VpcId: !Ref Vpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-agentcore-private-idp-sg'
AgentCoreGatewayMcpServerEgressSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Attach this SG to AgentCore Gateway VPC egress private endpoint for EC2-hosted MCP server target.
VpcId: !Ref Vpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-agentcore-gateway-egress-sg'
AlbIngressFromWinBastionMcpClient:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref AlbSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref WinBastionMcpClientEc2SecurityGroup
Description: HTTPS from Windows bastion MCP client
AlbIngressFromKeycloakCimdMcpServerEc2:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref AlbSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
Description: HTTPS from keycloak-cimd-mcp-server EC2 for local validation
AlbIngressFromAgentCorePrivateIdp:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref AlbSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref AgentCorePrivateIdpSecurityGroup
Description: HTTPS from AgentCore Identity private IdP managed VPC resource
AlbIngressFromAgentCoreGatewayMcpServerEgress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref AlbSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref AgentCoreGatewayMcpServerEgressSecurityGroup
Description: HTTPS from AgentCore Gateway VPC egress private endpoint to EC2-hosted MCP target
KeycloakCimdMcpServerEc2IngressKeycloakFromAlb:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref AlbSecurityGroup
Description: Keycloak HTTP from internal ALB
KeycloakCimdMcpServerEc2IngressCimdFromAlb:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
IpProtocol: tcp
FromPort: 4000
ToPort: 4000
SourceSecurityGroupId: !Ref AlbSecurityGroup
Description: CIMD nginx from internal ALB
KeycloakCimdMcpServerEc2IngressMcpServerFromAlb:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
IpProtocol: tcp
FromPort: 8000
ToPort: 8000
SourceSecurityGroupId: !Ref AlbSecurityGroup
Description: Lightweight MCP server from internal ALB
VpcEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: VPC endpoint security group for private Systems Manager connectivity
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref WinBastionMcpClientEc2SecurityGroup
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-sg'
AgentCoreGatewayVpcEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: VPC endpoint security group for Amazon Bedrock AgentCore Gateway PrivateLink access
VpcId: !Ref Vpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-agentcore-gateway-vpce-sg'
AgentCoreGatewayVpceIngressFromWinBastionMcpClient:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref AgentCoreGatewayVpcEndpointSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref WinBastionMcpClientEc2SecurityGroup
Description: HTTPS from Windows bastion MCP client to AgentCore Gateway interface VPC endpoint
SsmEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref Vpc
VpcEndpointType: Interface
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssm'
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnet1
SecurityGroupIds:
- !Ref VpcEndpointSecurityGroup
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-ssm'
SsmMessagesEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref Vpc
VpcEndpointType: Interface
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssmmessages'
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnet1
SecurityGroupIds:
- !Ref VpcEndpointSecurityGroup
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-ssmmessages'
Ec2MessagesEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref Vpc
VpcEndpointType: Interface
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ec2messages'
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnet1
SecurityGroupIds:
- !Ref VpcEndpointSecurityGroup
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-ec2messages'
AgentCoreGatewayEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref Vpc
VpcEndpointType: Interface
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.bedrock-agentcore.gateway'
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnet1
SecurityGroupIds:
- !Ref AgentCoreGatewayVpcEndpointSecurityGroup
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowInvokeGatewayThroughThisEndpoint
Effect: Allow
Principal: '*'
Action:
- bedrock-agentcore:InvokeGateway
Resource: !Ref AgentCoreGatewayEndpointPolicyResourceArn
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-agentcore-gateway'
PublicAlbCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Sub '*.${HostedZoneName}'
ValidationMethod: DNS
DomainValidationOptions:
- DomainName: !Sub '*.${HostedZoneName}'
HostedZoneId: !Ref PublicHostedZoneId
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-alb-public-cert'
PrivateHostedZone:
Type: AWS::Route53::HostedZone
Properties:
Name: !Ref HostedZoneName
VPCs:
- VPCId: !Ref Vpc
VPCRegion: !Ref AWS::Region
HostedZoneConfig:
Comment: !Sub '${ProjectName}-${EnvironmentName} private hosted zone'
HostedZoneTags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-private-zone'
InternalAlb:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internal
Type: application
IpAddressType: ipv4
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroups:
- !Ref AlbSecurityGroup
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-internal-alb'
KeycloakTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref Vpc
Protocol: HTTP
Port: 8080
TargetType: instance
HealthCheckProtocol: HTTP
HealthCheckPath: /
Matcher:
HttpCode: '200-399'
Targets:
- Id: !Ref KeycloakCimdMcpServerEc2
Port: 8080
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-tg-keycloak'
CimdTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref Vpc
Protocol: HTTP
Port: 4000
TargetType: instance
HealthCheckProtocol: HTTP
HealthCheckPath: /oauth/client-metadata.json
Matcher:
HttpCode: '200'
Targets:
- Id: !Ref KeycloakCimdMcpServerEc2
Port: 4000
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-tg-cimd'
McpServerTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref Vpc
Protocol: HTTP
Port: 8000
TargetType: instance
HealthCheckProtocol: HTTP
HealthCheckPath: /
Matcher:
HttpCode: '200-499'
Targets:
- Id: !Ref KeycloakCimdMcpServerEc2
Port: 8000
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-tg-mcp-server'
HttpsListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref InternalAlb
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref PublicAlbCertificate
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
StatusCode: '404'
ContentType: text/plain
MessageBody: Not found
KeycloakListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref HttpsListener
Priority: 10
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- !Ref AuthHostname
Actions:
- Type: forward
TargetGroupArn: !Ref KeycloakTargetGroup
CimdListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref HttpsListener
Priority: 20
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- !Ref CimdHostname
Actions:
- Type: forward
TargetGroupArn: !Ref CimdTargetGroup
McpServerListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref HttpsListener
Priority: 30
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- !Ref McpServerHostname
Actions:
- Type: forward
TargetGroupArn: !Ref McpServerTargetGroup
AuthRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref PrivateHostedZone
Name: !Ref AuthHostname
Type: A
AliasTarget:
DNSName: !GetAtt InternalAlb.DNSName
HostedZoneId: !GetAtt InternalAlb.CanonicalHostedZoneID
CimdRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref PrivateHostedZone
Name: !Ref CimdHostname
Type: A
AliasTarget:
DNSName: !GetAtt InternalAlb.DNSName
HostedZoneId: !GetAtt InternalAlb.CanonicalHostedZoneID
McpServerRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref PrivateHostedZone
Name: !Ref McpServerHostname
Type: A
AliasTarget:
DNSName: !GetAtt InternalAlb.DNSName
HostedZoneId: !GetAtt InternalAlb.CanonicalHostedZoneID
Ec2InstanceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ProjectName}-${EnvironmentName}-ec2-ssm-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-ec2-ssm-role'
Ec2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub '${ProjectName}-${EnvironmentName}-ec2-ssm-profile'
Roles:
- !Ref Ec2InstanceRole
KeycloakCimdMcpServerEc2:
Type: AWS::EC2::Instance
Properties:
ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
InstanceType: t3.medium
IamInstanceProfile: !Ref Ec2InstanceProfile
SubnetId: !Ref PrivateSubnet1
SecurityGroupIds:
- !Ref KeycloakCimdMcpServerEc2SecurityGroup
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-keycloak-cimd-mcp-server-ec2'
WinBastionMcpClientEc2:
Type: AWS::EC2::Instance
Properties:
ImageId: '{{resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base}}'
InstanceType: t3.medium
IamInstanceProfile: !Ref Ec2InstanceProfile
KeyName: !Ref KeyPairName
SubnetId: !Ref PrivateSubnet1
SecurityGroupIds:
- !Ref WinBastionMcpClientEc2SecurityGroup
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2'
Outputs:
VpcId:
Description: VPC ID used for AgentCore private connectivity settings.
Value: !Ref Vpc
PrivateSubnet1Id:
Description: Primary private subnet for EC2, VPC endpoints, and AgentCore private connectivity settings.
Value: !Ref PrivateSubnet1
PrivateSubnet2Id:
Description: Secondary private subnet used by the internal ALB and available for multi-AZ private connectivity settings.
Value: !Ref PrivateSubnet2
KeycloakCimdMcpServerEc2Id:
Description: EC2 instance ID hosting Keycloak, CIMD metadata nginx, and the lightweight MCP server.
Value: !Ref KeycloakCimdMcpServerEc2
WinBastionMcpClientEc2Id:
Description: Windows bastion EC2 instance ID used as the MCP client host.
Value: !Ref WinBastionMcpClientEc2
AgentCorePrivateIdpSecurityGroupId:
Description: Use this SG in the AgentCore Identity private IdP managed VPC resource so it can reach the internal ALB over HTTPS.
Value: !Ref AgentCorePrivateIdpSecurityGroup
AgentCoreGatewayMcpServerEgressSecurityGroupId:
Description: Use this SG in the AgentCore Gateway target VPC egress managed VPC resource for the EC2-hosted MCP server target.
Value: !Ref AgentCoreGatewayMcpServerEgressSecurityGroup
AgentCoreGatewayVpcEndpointId:
Description: Interface VPC endpoint ID for Amazon Bedrock AgentCore Gateway PrivateLink. Use this as aws:SourceVpce in the Gateway resource policy.
Value: !Ref AgentCoreGatewayEndpoint
AuthUrl:
Description: Private URL for Keycloak through the internal ALB.
Value: !Sub 'https://${AuthHostname}'
KeycloakIssuerUrl:
Description: Keycloak issuer URL for the mcp-demo realm.
Value: !Sub 'https://${AuthHostname}/realms/mcp-demo'
CimdMetadataUrl:
Description: CIMD client metadata URL served through the internal ALB.
Value: !Sub 'https://${CimdHostname}/oauth/client-metadata.json'
McpServerUrl:
Description: EC2-hosted MCP server URL served through the internal ALB.
Value: !Sub 'https://${McpServerHostname}/mcp'
SsmKeycloakCimdMcpServerEc2SessionCommand:
Description: Run locally to open a Session Manager shell on the keycloak-cimd-mcp-server EC2 instance.
Value: !Sub >-
aws ssm start-session --target ${KeycloakCimdMcpServerEc2}
SsmRdpPortForwardCommand:
Description: Run locally, then connect an RDP client to localhost:13389.
Value: !Sub >-
aws ssm start-session --target ${WinBastionMcpClientEc2} --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["3389"],"localPortNumber":["13389"]}'
| パラメータ | 設定値 | 説明 |
|---|---|---|
ProjectName |
mcp-keycloak-agentcore |
リソース名のPrefixに使うプロジェクト名 ※1 |
EnvironmentName |
poc |
リソース名のPrefixに使う環境名 ※1 |
HostedZoneName |
agentcore-keycloak-example.com |
Route53HostedZoneのドメイン名 ※2 |
PublicHostedZoneId |
ZXXXXXXXXXXXXX |
ACM証明書のDNS検証に使うRoute53 Public Hosted Zone ID |
AuthHostname |
auth.agentcore-keycloak-example.com |
Keycloakへアクセスするためのホスト名 |
CimdHostname |
cimd.agentcore-keycloak-example.com |
CIMD metadataを配信するためのホスト名 |
McpServerHostname |
mcp-server.agentcore-keycloak-example.com |
MCP Server用のホスト名 |
KeyPairName |
mcp-keycloak-agentcore-poc-key |
Windows踏み台EC2に設定する既存のEC2 Key Pair名 |
AgentCoreGatewayEndpointPolicyResourceArn |
* |
AgentCore Gateway用Interface VPC Endpoint policyで許可するGateway ARN ※3 |
※1 ${ProjectName}-${EnvironmentName}-リソース名になります。
※2 Keycloak、CIMD metadata、MCP Server用のDNS名の親ドメインになります。
※3 AgentCore Gateway構築前でARNが未確定のため"*"にします。
# コマンド例
aws cloudformation deploy \
--region ap-northeast-1 \
--stack-name mcp-keycloak-agentcore-poc-foundation \
--template-file ./aws-mcp-keycloak-foundation.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
ProjectName=mcp-keycloak-agentcore \
EnvironmentName=poc \
HostedZoneName=agentcore-keycloak-example.com \
PublicHostedZoneId=ZXXXXXXXXXXXXX \
AuthHostname=auth.agentcore-keycloak-example.com \
CimdHostname=cimd.agentcore-keycloak-example.com \
McpServerHostname=mcp-server.agentcore-keycloak-example.com \
KeyPairName=mcp-keycloak-agentcore-poc-key \
AgentCoreGatewayEndpointPolicyResourceArn="*"
4. EC2上でKeycloak、CIMD nginx、MCP Serverを起動
${ProjectName}-${EnvironmentName}-keycloak-cimd-mcp-server-ec2にSSMで接続し、以下を起動します。
- Keycloak container:
8080 - CIMD nginx container:
4000 - MCP server:
8000
4.1 Dockerをインストール
# コマンド例
# Amazon Linux 2023のパッケージを更新
sudo dnf update -y
# Dockerをインストール
sudo dnf install -y docker
# Dockerデーモンを起動し、自動起動有効化
sudo systemctl enable --now docker
# バージョン確認
sudo docker version
4.2 Keycloakを起動
# コマンド例
# イメージをpull
sudo docker pull quay.io/keycloak/keycloak:26.6.1
# 永続化用volume作成
sudo docker volume create keycloak-data
# Keycloak起動(CIMD機能有効化)
# 検証のためHTTP有効化
# CIMDのキャッシュは最小1分、最大60分
sudo docker run -d --name keycloak \
--restart unless-stopped \
-p 8080:8080 \
-v keycloak-data:/opt/keycloak/data \
-e KC_BOOTSTRAP_ADMIN_USERNAME="admin" \
-e KC_BOOTSTRAP_ADMIN_PASSWORD="admin" \
-e KC_HOSTNAME="https://auth.agentcore-keycloak-example.com" \
-e KC_PROXY_HEADERS=xforwarded \
-e KC_HTTP_ENABLED=true \
quay.io/keycloak/keycloak:26.6.1 \
start-dev --features=cimd \
--spi-client-policy-executor--client-id-metadata-document--min-cache-time=1 \
--spi-client-policy-executor--client-id-metadata-document--max-cache-time=60
#応答確認
curl -i http://127.0.0.1:8080/realms/master/.well-known/openid-configuration
4.3 CIMD nginxを起動
CIMDのメタデータを配信するためにJSONを用意します。Example Metadata Documentをベースに値を設定します。
{
"client_id": "https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json",
"client_name": "MCP Inspector AWS CIMD Client",
"grant_types": [
"authorization_code"
],
"response_types": [
"code"
],
"token_endpoint_auth_method": "none",
"redirect_uris": [
"http://localhost:6274/oauth/callback"
]
}
| 項目 | 設定値 | 説明 |
|---|---|---|
client_id |
https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json |
OAuthクライアントの識別子。CIMDではJSONメタデータをホストするHTTPS URLを指定する。 |
client_name |
MCP Inspector AWS CIMD Client |
エンドユーザに表示するクライアント名 |
grant_types |
["authorization_code"] |
クライアントが利用するグラント種別。Authorization Code Grantを利用する場合は["authorization_code"]を指定する。 |
response_types |
["code"] |
認可エンドポイントで使用するレスポンスタイプ。Authorization Code Grantを利用する場合はcodeを指定する。 |
token_endpoint_auth_method |
none |
トークンエンドポイントでのクライアント認証方式。CIMDでは、クライアントシークレットを持たないpublic clientを使用するため、token_endpoint_auth_method に none を指定する。 |
redirect_uris |
["http://localhost:6274/oauth/callback"] |
認可サーバが認可結果を返すリダイレクトURIの一覧。MCP Inspectorがローカルで受け取るコールバックURLを指定する。 |
※CIMDドラフトでは、CIMDドキュメントのメタデータ値は OAuth Dynamic Client Registration Metadata registry に定義された値を使う、と説明されています。
参考:
- https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
- RFC 7591 - OAuth 2.0 Dynamic Client Registration Protocol
nginxを起動します。
# コマンド例
# CIMD metadataとnginx設定ファイルの配置先を作成
sudo mkdir -p /opt/cimd/public/oauth /opt/cimd/nginx
# CIMD client metadata JSONを作成
sudo tee /opt/cimd/public/oauth/client-metadata.json >/dev/null <<__CIMD_JSON__
{
"client_id": "https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json",
"client_name": "MCP Inspector AWS CIMD Client",
"grant_types": [
"authorization_code"
],
"response_types": [
"code"
],
"token_endpoint_auth_method": "none",
"redirect_uris": [
"http://localhost:6274/oauth/callback"
]
}
__CIMD_JSON__
# nginx設定を作成
sudo tee /opt/cimd/nginx/default.conf >/dev/null <<'__NGINX_CONF__'
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
location = /oauth/client-metadata.json {
default_type application/json;
add_header Cache-Control "no-store";
try_files /oauth/client-metadata.json =404;
}
location / {
return 404;
}
}
__NGINX_CONF__
# イメージをpull
sudo docker pull nginx:1.30.0-alpine3.23
# CIMD metadata配信用nginxを起動
sudo docker run -d --name cimd-nginx \
--restart unless-stopped \
-p 4000:80 \
-v /opt/cimd/public:/usr/share/nginx/html:ro \
-v /opt/cimd/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro \
nginx:1.30.0-alpine3.23
# 応答確認
curl -i http://127.0.0.1:4000/oauth/client-metadata.json
4.4 MCPサーバを起動
認証認可不要の軽量のMCP Serverとしてmy_mcp_server.pyを用意します。
from mcp.server.fastmcp import FastMCP
from starlette.responses import JSONResponse
mcp = FastMCP(host="0.0.0.0", port=8000, stateless_http=True)
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
@mcp.tool()
def multiply_numbers(a: int, b: int) -> int:
"""Multiply two numbers together"""
return a * b
@mcp.tool()
def greet_user(name: str) -> str:
"""Greet a user by name"""
return f"Hello, {name}! Nice to meet you."
if __name__ == "__main__":
mcp.run(transport="streamable-http")
uvをインストールして、MCP Serverを起動します。
# コマンド例
# uvを現在のOSユーザのホームディレクトリ配下にインストール
curl -LsSf https://astral.sh/uv/install.sh | sh
# このシェルでuvコマンドを使えるようにPATHを追加
export PATH="$HOME/.local/bin:$PATH"
# uvのバージョン確認
uv --version
# MCP Serverの配置先を作成
sudo mkdir -p /opt/mcp-server
# MCP ServerのPythonファイルを作成
sudo tee /opt/mcp-server/my_mcp_server.py >/dev/null <<'__MCP_PY__'
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(host="0.0.0.0", port=8000, stateless_http=True)
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
@mcp.tool()
def multiply_numbers(a: int, b: int) -> int:
"""Multiply two numbers together"""
return a * b
@mcp.tool()
def greet_user(name: str) -> str:
"""Greet a user by name"""
return f"Hello, {name}! Nice to meet you."
if __name__ == "__main__":
mcp.run(transport="streamable-http")
__MCP_PY__
# systemdサービス定義を作成
sudo tee /etc/systemd/system/mcp-server.service >/dev/null <<'__SYSTEMD__'
[Unit]
Description=FastMCP Server
After=network-online.target
Wants=network-online.target
[Service]
WorkingDirectory=/opt/mcp-server
ExecStart=/usr/local/bin/uv run --with mcp /opt/mcp-server/my_mcp_server.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
__SYSTEMD__
# systemd設定を再読み込みし、MCP Serverを起動
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server
# MCP Serverの状態を確認
sudo systemctl status mcp-server
#応答確認
curl -i http://127.0.0.1:8000/mcp
# 応答
#HTTP/1.1 406 Not Acceptable
#date: Wed, 20 May 2026 05:13:19 GMT
#server: uvicorn
#content-type: application/json
#content-length: 126
#{"jsonrpc":"2.0","id":"server-error","error":{"code":-32600,"message":"Not Acceptable: Client must accept text/event-stream"}}
5. Windows踏み台EC2にNode.jsをインストール
${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2にSSM経由でRDP接続します。
参考:踏み台ホストを使用せずに、Systems Manager Session Manager のポート転送を使用して RDP 経由で EC2 インスタンスに接続する方法を教えてください
MCP Inspectorを使用するには、Node.jsがWindows踏み台EC2にインストールされている必要があります。powershellでwinget経由でインストールするか、公式サイトからNode.js MSIをインストールします。
# コマンド例
winget install OpenJS.NodeJS
Keycloak設定
${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2にSSM経由でRDP接続します。
1. Realm作成
ブラウザからhttps://auth.agentcore-keycloak-example.com/adminでKeycloak管理コンソールにアクセスします。
User name:
admin
Password:
admin
Realmを作成します。
Realm name:
mcp-demo
Issuer URLは次です。
https://auth.agentcore-keycloak-example.com/realms/mcp-demo
Discovery URLは次です。
https://auth.agentcore-keycloak-example.com/realms/mcp-demo/.well-known/openid-configuration
このURLをAgentCore GatewayのInbound Identity detailsに設定します。
2. 検証ユーザ作成
「Users」から、Userを作成します。
Username:
alice
ユーザ作成後に上タブの「Credentials」からパスワードを設定します。
Password:
alice
Temporary:
OFF
3. Client Scope mcp:tools 作成
「Client scopes」からclient scopeを作成します。
Name:
mcp:tools
Protocol:
OpenID Connect
Client scope type:
Optional
作成後、Include in token scopeをONにします。これをONにしないと、Access Tokenのscope claimにmcp:toolsが入りません。
4. Audience mapper作成
MCP仕様では、Access Tokenが特定のMCPサーバ向けに発行されたものであることを保証するため、Audience Binding が求められます。
MCP側の要求は次の通りです。
- MCPクライアントは、認可リクエストおよびトークンリクエストで RFC 8707 の
resourceパラメータ を含める必要がある -
resourceの値は、そのトークンを使う対象のMCP Serverを識別する必要がある - MCP Serverは、提示されたトークンが自分自身のために発行されたものか検証する必要がある
ただし、Keycloakは現時点で resource パラメータを認識できません。そのため、Keycloakで同等の効果を得る暫定策として、resource の代わりにOAuth 2.0の scope を利用し、スコープに紐づくAudience MapperでAccess Tokenの aud クレームを設定します。
参考:Token Audience Binding and Validation
「Client scopes」→「mcp:tools」→「Mappers」から「Configure a new mapper」を押下します。

設定例:
Name:
aud-agentcore-gateway
Included Custom Audience:
mcp-gateway-demo
Add to access token:
ON
Add to token introspection:
ON
「mcp:tools」スコープが要求された時に、MCP Serverの識別子としてaudクレーム「mcp-gateway-demo」を付与します。
Included Custom Audienceの値は、AgentCore Gateway側のAllowed audiencesの設定と一致させます。
5. CIMD Client Policy / Profile作成
Keycloakを--features=cimd付きで起動している前提です。
「Realm settings」→「Client Policies」→「Profiles」からCIMD用のClient Profileを作成します。この Client Policy / Profileを作成することでCIMDを利用することができます。
Client Profileを作成後、client-id-metadata-document executorを追加します。
Executor:
client-id-metadata-document
設定項目の内容は次の通りです。
| 設定 | 意味 |
|---|---|
| Allow http scheme | ONの場合、Client ID URLやメタデータ中のURLでHTTPを許可する。開発環境のみONにすべきで、本番ではOFF |
| Trusted domains | Client ID URLやメタデータURLとして許可するドメインパターン |
| Restrict same domain | ONの場合、Client ID URL、Redirect URI、メタデータ内URLが同じ信頼ドメイン配下にあることを確認する |
| Required properties | Client ID Metadata Documentに必須とするメタデータ項目 |
| Only Allow Confidential Client | ONの場合、confidential clientのみ許可する。その場合、jwks または jwks_uri が必要で、認証方式は private_key_jwt または tls_client_auth が必要 |
設定例:
Allow http scheme:
OFF
Trusted domains:
cimd.agentcore-keycloak-example.com
localhost
Restrict same domain:
OFF
Only Allow Confidential Client:
OFF
Required properties:
client_id
client_name
redirect_uris
「Realm settings」→「Client Policies」→「Policies」からClient Policyを作成します。
Client Policyを作成し、client-id-uri conditionを追加します。このconditionでは、認可リクエストの client_id が、指定したURIスキームや信頼ドメインに一致するかを判定します。
| 設定 | 意味 |
|---|---|
| URI scheme |
client_id に許可するURIスキーム。本番では通常 https のみ |
| Trusted domains |
client_id URLのホスト部として許可するドメイン |
設定例:
Condition:
client-id-uri
URI scheme:
https
Trusted domains:
cimd.agentcore-keycloak-example.com
Client Policyを作成後、Client Profileに追加します。
6. OAuth 2.1 Client Policy / Profile作成
「Realm settings」→「Client Policies」→「Profiles」から OAuth 2.1 用のClient Profileを作成します。このClient Policy / Profileを作成することで OAuth 2.1 準拠の認可コードフローを強制します。
Client Profileを作成後、各Executorを追加します。
ビルトインのoauth-2-1-for-public-clientプロファイルをベースにoauth-2-1-for-public-client-no-dpopを作成します。OAuth 2.1 ではDPoP(Demonstration of Proof-of-Possession)は推奨ですが、本検証では利用しないため削除します。
Executor:
pkce-enforcer
reject-implicit-grant
reject-ropc-grant
secure-redirect-uris-enforcer
secure-redirect-uris-enforcerExecutorは、検証のためloopback addressやhttpを許可します。
他のExecutorの設定は、oauth-2-1-for-public-clientプロファイルと同様なので省略します。
設定例:
Allow IPv4 loopback address:
ON
Allow IPv6 loopback address:
ON
Allow private use URI:
ON
Allow http scheme:
ON
Allow wildcard in context-path:
OFF
OAuth 2.1 Compliant:
ON
Allow open redirect:
OFF
続いて、Client Policyを作成し、client-access-type conditionを追加します。このconditionでは、クライアントのアクセス種別がpublic clientかどうかを判定します。
設定例:
Condition:
client-access-type
Client Access Type:
public
Client Profileにoauth-2-1-for-public-client-no-dpopを追加します。
インフォメーション
OAuth 2.1 用のClient Profileで全てのExecutorが有効になることが正常な動きですが、検証してみたところsecure-redirect-uris-enforcerが有効化されないようです(2026年6月時点)。回避策として暫定的にCIMD用のClient Policyに OAuth 2.1用のClient Profileを追加することで、secure-redirect-uris-enforcerが有効化され、CIMDの無効なリダイレクトURIがバリデーションエラーになることが確認できたので、この対応方法で検証を進めています。本件については、KeycloakのGithub Issue作成含めkeycloakコミュニティーへのフィードバックを実施済みです。
[CIMD] secure-redirect-uris-enforcer is not enforced for CIMD public clients during authorization requests
#49456
Bedrock AgentCore Gateway設定
AWSマネジメントコンソールからBedrock AgentCore Gatewayの作成、設定を行います。
1. Gatewayを作成
AgentCore Gatewayを作成します。
Define gateway details:
Gateway name:
mcp-gateway-keycloak-demo
IAM permissions:
Create default role
JWT Authorization Configurationでaudとscopeで検証するよう設定します。
Inbound Identity details:
Inbound Auth type:
Use JSON Web Tokens (JWT)
JWT schema configuration:
Use existing Identity provider configurations
Discovery URL:
https://auth.agentcore-keycloak-example.com/realms/mcp-demo/.well-known/openid-configuration
JWT Authorization Configuration:
Allowed audiences:
mcp-gateway-demo
Allowed scopes:
mcp:tools
Allowed clients:
none
Custom claims:
none
VPC SecurityでAgentCore GatewayがKeycloak(Private Idp)が配置されるVPCにアクセスできるよう設定します。
ここで指定するSecurity GroupはKeycloakへのアクセスが許可されているものとします(CloudFormationで既に作成済み)。
VPC Security:
Managed
VPC:
${ProjectName}-${EnvironmentName}-vpc
Subnets:
${ProjectName}-${EnvironmentName}-private-b-alb-only
Security Group:
${ProjectName}-${EnvironmentName}-AgentCorePrivateIdpSecurityGroup
2. MCP Server Targetを作成
Select a target protocol:
MCP target
Target name:
my-mcp-server
Target type:
MCP server
MCP endpoint:
https://mcp-server.agentcore-keycloak-example.com/mcp
MCP listing mode:
Default
Outbound Auth configurations:
No auth
VPC SecurityでAgentCore GatewayがMCP Serverが配置されるVPCにアクセスできるよう設定します。
ここで指定するSecurity GroupはMCP Serverへのアクセスが許可されているものとします(CloudFormationで既に作成済み)。
Additional configurations:
VPC Security:
Managed
VPC:
${ProjectName}-${EnvironmentName}-vpc
Subnets:
${ProjectName}-${EnvironmentName}-private-b-alb-only
Security Group:
${ProjectName}-${EnvironmentName}-AgentCoreGatewayMcpServerEgressSecurityGroup
Endpoint IP address type:
IPv4
マネジメントコンソールでは、Gateway作成時にTargetも指定する画面フローになります。Gateway本体がまだCreatingの間にTarget作成が走り、次のようなエラーになることがあります。
Gateway ... is in Creating state, there was an error in creating the target
その場合、GatewayのステータスがReadyになった後、Targetを再作成してください。
Resource-based policyの設定(Option)
指定したVPC Endpoint以外からInvokeGatewayを拒否するResource-based policyの設定をします。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowInvokeGatewayOnlyFromVpce",
"Effect": "Allow",
"Principal": "*",
"Action": "bedrock-agentcore:InvokeGateway",
"Resource": "arn:aws:bedrock-agentcore:ap-northeast-1:<account-id>:gateway/<gateway-id>",
"Condition": {
"StringEquals": {
"aws:SourceVpce": "<vpce-id>"
}
}
},
{
"Sid": "DenyInvokeGatewayNotFromVpce",
"Effect": "Deny",
"Principal": "*",
"Action": "bedrock-agentcore:InvokeGateway",
"Resource": "arn:aws:bedrock-agentcore:ap-northeast-1:<account-id>:gateway/<gateway-id>",
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "<vpce-id>"
}
}
}
]
}
動作確認
${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2にSSM経由でRDP接続します。
参考:踏み台ホストを使用せずに、Systems Manager Session Manager のポート転送を使用して RDP 経由で EC2 インスタンスに接続する方法を教えてください
1. MCP Inspectorを起動
事前準備でNode.jsがインストールされている前提です。
powershellでホストと許可オリジンを指定してMCP Inspectorを起動します。
ホストを指定するのは Keycloak の OAuth 2.1 Client Policy / Profile によってlocalhostが拒否されるためです。
# コマンド例
$env:HOST = "127.0.0.1"
$env:ALLOWED_ORIGINS = "http://127.0.0.1:6274"
npx -y @modelcontextprotocol/inspector@0.21.2
参考:
2. MCP InspectorのOAuth設定
MCP Inspectorの接続先をAgentCore Gatewayにします。接続先はGateway resource URLを指定します。
Transport Type:
Streamable HTTP
URL:
https://<gateway-endpoint>/mcp
MCP InspectorのOAuth設定で、CIMD URLをClient IDとして指定します。
Client ID:
https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json
Scope:
mcp:tools
Connectionを押下して、MCP Serverへ接続を試みると、Keycloakのログイン画面に遷移します。
UsernameとPasswordを入力してログインすると、CIMDによってクライアント登録することを確認されます。
Keycloakログイン後、MCP Inspectorの上タブのAuthからAccess Tokenを取得、デコードして以下が含まれることを確認します。
{
"iss": "https://auth.agentcore-keycloak-example.com/realms/mcp-demo",
"aud": [
"mcp-gateway-demo"
],
"scope": "openid profile email mcp:tools"
}
3. MCP InspectorからAgentCore Gatewayへ接続(正常系)
OAuthで取得したAccess Tokenが付与された状態で接続します。
期待するtools:
add_numbers
multiply_numbers
greet_user
tools/callで次を実行します。
{
"a": 2,
"b": 3
}
add_numbersなら5、multiply_numbersなら6が返れば成功です。
4. MCP InspectorからAgentCore Gatewayへ接続(異常系)
誤ったスコープを指定して、再度MCP Serverに接続します。
Client ID:
https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json
Scope:
mcp:fail
予期しないスコープのためアクセスが拒否されることを確認します。
まとめ
本記事では、AgentCore GatewayとKeycloakを組み合わせて、
- CIMDを利用してクライアントを事前登録することなくOAuth 2.1 準拠の認可コードフローでKeycloakからAccess Tokenを取得できること
- 取得したAccess TokenでAgentCore Gatewayにアクセスできること
を検証しました。また機会があれば、AgentCore Gatewayから外部ツールへの接続(Outbound Authorization)や詳細なアクセス制御の検証を行っていきたいと思います。






























