From 0d02d96c2510115f7054de6144e1d457132e1e3c Mon Sep 17 00:00:00 2001 From: Lili Xu Date: Fri, 7 Nov 2025 15:22:34 -0800 Subject: [PATCH] Support private endpoints --- deployment/infra/azure-deployment.bicep | 72 +++++++++++++++ deployment/infra/azure-deployment.json | 92 ++++++++++++++++++- deployment/k8s/cloud-deployment-template.yml | 14 +++ .../AdapterReverseProxyController.cs | 10 +- 4 files changed, 181 insertions(+), 7 deletions(-) diff --git a/deployment/infra/azure-deployment.bicep b/deployment/infra/azure-deployment.bicep index eefe9e1..81ed4ba 100644 --- a/deployment/infra/azure-deployment.bicep +++ b/deployment/infra/azure-deployment.bicep @@ -10,6 +10,9 @@ param resourceLabel string = resourceGroup().name @description('The Azure region for resource deployment. Defaults to the resource group location.') param location string = resourceGroup().location +@description('Enable private endpoints for Azure resources (ACR, Cosmos DB). When enabled, resources will only be accessible within the VNet.') +param enablePrivateEndpoints bool = false + var resourceLabelLower = toLower(resourceLabel) var aksNameBase = 'mg-aks-${resourceLabelLower}' @@ -36,6 +39,9 @@ var aksSubnetName = substring(aksSubnetNameBase, 0, min(length(aksSubnetNameBase var appGwSubnetNameBase = 'mg-aag-subnet-${resourceLabelLower}' var appGwSubnetName = substring(appGwSubnetNameBase, 0, min(length(appGwSubnetNameBase), 80)) +var peSubnetNameBase = 'mg-pe-subnet-${resourceLabelLower}' +var peSubnetName = substring(peSubnetNameBase, 0, min(length(peSubnetNameBase), 80)) + var appGwNameBase = 'mg-aag-${resourceLabelLower}' var appGwName = substring(appGwNameBase, 0, min(length(appGwNameBase), 80)) @@ -73,6 +79,13 @@ resource vnet 'Microsoft.Network/virtualNetworks@2022-07-01' = { addressPrefix: '10.0.2.0/24' } } + { + name: peSubnetName + properties: { + addressPrefix: '10.0.3.0/24' + privateEndpointNetworkPolicies: 'Disabled' + } + } ] } } @@ -155,6 +168,7 @@ resource aks 'Microsoft.ContainerService/managedClusters@2023-04-01' = { dependsOn: [vnet] } + // Attach ACR to AKS resource acrRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(aks.id, acr.id, 'acrpull') @@ -384,6 +398,7 @@ resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { defaultConsistencyLevel: 'Session' } enableFreeTier: false + publicNetworkAccess: enablePrivateEndpoints ? 'Disabled' : 'Enabled' } } @@ -452,6 +467,63 @@ resource cosmosDbRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAs } } +// Private DNS Zone for Cosmos DB +resource cosmosPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (enablePrivateEndpoints) { + name: 'privatelink.documents.azure.com' + location: 'global' +} + +// Link Private DNS Zone to VNet +resource cosmosDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (enablePrivateEndpoints) { + parent: cosmosPrivateDnsZone + name: '${vnetName}-cosmos-link' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnet.id + } + } +} + +// Private Endpoint for Cosmos DB +resource cosmosPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = if (enablePrivateEndpoints) { + name: 'pe-${cosmosDbAccountName}' + location: location + properties: { + subnet: { + id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, peSubnetName) + } + privateLinkServiceConnections: [ + { + name: 'pe-${cosmosDbAccountName}-connection' + properties: { + privateLinkServiceId: cosmosDb.id + groupIds: [ + 'Sql' + ] + } + } + ] + } + dependsOn: [vnet] +} + +resource cosmosPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-04-01' = if (enablePrivateEndpoints) { + parent: cosmosPrivateEndpoint + name: 'cosmos-dns-zone-group' + properties: { + privateDnsZoneConfigs: [ + { + name: 'privatelink-documents-azure-com' + properties: { + privateDnsZoneId: cosmosPrivateDnsZone.id + } + } + ] + } +} + // Application Insights resource appInsights 'Microsoft.Insights/components@2020-02-02' = { name: appInsightsName diff --git a/deployment/infra/azure-deployment.json b/deployment/infra/azure-deployment.json index d42ef28..6076177 100644 --- a/deployment/infra/azure-deployment.json +++ b/deployment/infra/azure-deployment.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.38.33.27573", - "templateHash": "12874722858654947563" + "templateHash": "5648627402420051357" } }, "parameters": { @@ -30,6 +30,13 @@ "metadata": { "description": "The Azure region for resource deployment. Defaults to the resource group location." } + }, + "enablePrivateEndpoints": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable private endpoints for Azure resources (ACR, Cosmos DB). When enabled, resources will only be accessible within the VNet." + } } }, "variables": { @@ -50,6 +57,8 @@ "aksSubnetName": "[substring(variables('aksSubnetNameBase'), 0, min(length(variables('aksSubnetNameBase')), 80))]", "appGwSubnetNameBase": "[format('mg-aag-subnet-{0}', variables('resourceLabelLower'))]", "appGwSubnetName": "[substring(variables('appGwSubnetNameBase'), 0, min(length(variables('appGwSubnetNameBase')), 80))]", + "peSubnetNameBase": "[format('mg-pe-subnet-{0}', variables('resourceLabelLower'))]", + "peSubnetName": "[substring(variables('peSubnetNameBase'), 0, min(length(variables('peSubnetNameBase')), 80))]", "appGwNameBase": "[format('mg-aag-{0}', variables('resourceLabelLower'))]", "appGwName": "[substring(variables('appGwNameBase'), 0, min(length(variables('appGwNameBase')), 80))]", "publicIpNameBase": "[format('mg-pip-{0}', variables('resourceLabelLower'))]", @@ -84,6 +93,13 @@ "properties": { "addressPrefix": "10.0.2.0/24" } + }, + { + "name": "[variables('peSubnetName')]", + "properties": { + "addressPrefix": "10.0.3.0/24", + "privateEndpointNetworkPolicies": "Disabled" + } } ] } @@ -424,7 +440,8 @@ "consistencyPolicy": { "defaultConsistencyLevel": "Session" }, - "enableFreeTier": false + "enableFreeTier": false, + "publicNetworkAccess": "[if(parameters('enablePrivateEndpoints'), 'Disabled', 'Enabled')]" } }, { @@ -511,6 +528,77 @@ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('userAssignedIdentityName'))]" ] }, + { + "condition": "[parameters('enablePrivateEndpoints')]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "privatelink.documents.azure.com", + "location": "global" + }, + { + "condition": "[parameters('enablePrivateEndpoints')]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', 'privatelink.documents.azure.com', format('{0}-cosmos-link', variables('vnetName')))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.documents.azure.com')]", + "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" + ] + }, + { + "condition": "[parameters('enablePrivateEndpoints')]", + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[format('pe-{0}', variables('cosmosDbAccountName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('peSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('pe-{0}-connection', variables('cosmosDbAccountName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbAccountName'))]", + "groupIds": [ + "Sql" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbAccountName'))]", + "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" + ] + }, + { + "condition": "[parameters('enablePrivateEndpoints')]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', format('pe-{0}', variables('cosmosDbAccountName')), 'cosmos-dns-zone-group')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "privatelink-documents-azure-com", + "properties": { + "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.documents.azure.com')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.documents.azure.com')]", + "[resourceId('Microsoft.Network/privateEndpoints', format('pe-{0}', variables('cosmosDbAccountName')))]" + ] + }, { "type": "Microsoft.Insights/components", "apiVersion": "2020-02-02", diff --git a/deployment/k8s/cloud-deployment-template.yml b/deployment/k8s/cloud-deployment-template.yml index 3b36600..610f9c6 100644 --- a/deployment/k8s/cloud-deployment-template.yml +++ b/deployment/k8s/cloud-deployment-template.yml @@ -28,6 +28,13 @@ spec: env: - name: ASPNETCORE_ENVIRONMENT value: "Production" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" envFrom: - configMapRef: name: app-config @@ -145,6 +152,13 @@ spec: env: - name: ASPNETCORE_ENVIRONMENT value: "Production" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" envFrom: - configMapRef: name: app-config diff --git a/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs b/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs index 7d72537..8eed8f9 100644 --- a/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs +++ b/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.McpGateway.Service.Session; @@ -9,10 +8,10 @@ namespace Microsoft.McpGateway.Service.Controllers { [ApiController] - [Route("adapters")] [Authorize] public class AdapterReverseProxyController(IHttpClientFactory httpClientFactory, IAdapterSessionStore sessionStore, ISessionRoutingHandler sessionRoutingHandler) : ControllerBase { + private const string ToolGateway = "toolgateway"; private readonly IHttpClientFactory httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); private readonly IAdapterSessionStore sessionStore = sessionStore ?? throw new ArgumentNullException(nameof(sessionStore)); private readonly ISessionRoutingHandler sessionRoutingHandler = sessionRoutingHandler ?? throw new ArgumentNullException(nameof(sessionRoutingHandler)); @@ -20,13 +19,14 @@ public class AdapterReverseProxyController(IHttpClientFactory httpClientFactory, /// /// Support for MCP streamable HTTP connection. /// - [HttpPost("{name}/mcp")] - public async Task ForwardStreamableHttpRequest(string name, CancellationToken cancellationToken) + [HttpPost("mcp")] + [HttpPost("adapters/{name}/mcp")] + public async Task ForwardStreamableHttpRequest(string? name, CancellationToken cancellationToken) { var sessionId = AdapterSessionRoutingHandler.GetSessionId(HttpContext); string? targetAddress; if (string.IsNullOrEmpty(sessionId)) - targetAddress = await sessionRoutingHandler.GetNewSessionTargetAsync(name, HttpContext, cancellationToken).ConfigureAwait(false); + targetAddress = await sessionRoutingHandler.GetNewSessionTargetAsync(name ?? ToolGateway, HttpContext, cancellationToken).ConfigureAwait(false); else targetAddress = await sessionRoutingHandler.GetExistingSessionTargetAsync(HttpContext, cancellationToken).ConfigureAwait(false);