Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ All notable changes to DevBrain are tracked in this file. Versions follow [Seman

- Added an opt-in `COSMOS_SERVERLESS` azd value (`cosmosServerless` Bicep parameter) that creates the Cosmos DB account in serverless mode. It defaults to `false`, so existing and new provisioned deployments are unchanged.
- Added an optional `CUSTOM_DOMAIN_NAME` that sets the OAuth issuer, protected-resource URL, redirect URI, and `AllowedHosts` from a public host name, replacing the manual `main.bicep` edits the README previously described.
- Added optional publishing through an existing Azure Front Door Standard/Premium profile (`FRONT_DOOR_PROFILE_NAME`, `FRONT_DOOR_RESOURCE_GROUP`, `FRONT_DOOR_SKU`), with a dedicated endpoint, route, managed-certificate custom domain, and custom-rule WAF policy. The WAF includes a path allow list, per-IP rate limits for the OAuth endpoints and overall traffic, and an optional country allow list (`FRONT_DOOR_ALLOWED_COUNTRIES`). With `DNS_ZONE_NAME`, provisioning also writes the CNAME and domain-validation TXT records into an existing Azure DNS zone.
- Added optional publishing through an existing Azure Front Door Standard/Premium profile (`FRONT_DOOR_PROFILE_NAME`, `FRONT_DOOR_RESOURCE_GROUP`, `FRONT_DOOR_SKU`), with a dedicated endpoint, route, managed-certificate custom domain, and custom-rule WAF policy. The WAF includes a path allow list, per-IP rate limits for the OAuth endpoints and overall traffic, and an optional country allow list (`FRONT_DOOR_ALLOWED_COUNTRIES`). With `DNS_ZONE_NAME`, provisioning also writes the CNAME and domain-validation TXT records into an existing Azure DNS zone. The profile and the zone can each live in another subscription of the same tenant (`FRONT_DOOR_SUBSCRIPTION_ID`, `DNS_ZONE_SUBSCRIPTION_ID`).
- Added an opt-in `FrontDoor:Id` origin check. When it's set, the server rejects requests that lack the matching `X-Azure-FDID` header, except `/healthz`, so the Container Apps host name can't bypass the Front Door WAF. Provisioning sets it automatically when Front Door is enabled.
- Added `DEVBRAIN_PUBLIC_URL` and `DEVBRAIN_MCP_URL` provisioning outputs.

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,21 @@ DevBrain can publish through an Azure Front Door Standard or Premium profile you
azd env set CUSTOM_DOMAIN_NAME devbrain.contoso.com
azd env set FRONT_DOOR_PROFILE_NAME <front-door-profile>
azd env set FRONT_DOOR_RESOURCE_GROUP <front-door-resource-group>
azd env set FRONT_DOOR_SUBSCRIPTION_ID <subscription-id> # only when the profile is in another subscription
azd env set DNS_ZONE_NAME contoso.com # optional: Azure DNS zone that hosts the domain
azd env set DNS_ZONE_RESOURCE_GROUP <dns-zone-resource-group>
azd env set DNS_ZONE_SUBSCRIPTION_ID <subscription-id> # only when the zone is in another subscription
azd env set FRONT_DOOR_ALLOWED_COUNTRIES US # optional: comma-separated country allow list
azd env set FRONT_DOOR_SKU Premium_AzureFrontDoor # only for a Premium profile
azd provision
```

The Front Door profile and DNS zone can be in other subscriptions of the same tenant. The Front Door resource group and subscription default to DevBrain's own, and the DNS zone's default to the Front Door profile's. The identity that runs `azd provision` needs to be able to deploy into each of those resource groups.

`CUSTOM_DOMAIN_NAME` becomes the OAuth issuer, the protected-resource URL, and the redirect URI, so register the new `OAUTH_REDIRECT_URI` on the Entra app (step 4). With `DNS_ZONE_NAME` set, provisioning creates the CNAME and the `_dnsauth` validation TXT record. Without it, create both records yourself, using the `FRONT_DOOR_ENDPOINT_HOST_NAME` and `FRONT_DOOR_DOMAIN_VALIDATION_TOKEN` outputs. Front Door issues the managed certificate after it validates the domain, which can take several minutes.

To move an existing deployment behind Front Door, add the new redirect URI to the Entra app before you provision, and keep the old one until every client has moved. As soon as provisioning finishes, the Container Apps URL rejects MCP traffic, so point each client at `DEVBRAIN_MCP_URL` and sign in again. Remove the old redirect URI once nothing uses it.

The WAF policy uses custom rules only, so it works on the Standard tier:

| Rule | Behavior |
Expand Down
16 changes: 13 additions & 3 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ param frontDoorProfileName string = ''
@description('Resource group of the Front Door profile. Defaults to this deployment\'s resource group.')
param frontDoorResourceGroupName string = ''

@description('Subscription ID of the Front Door profile. Defaults to this deployment\'s subscription.')
param frontDoorSubscriptionId string = ''

@description('SKU of the existing Front Door profile; the DevBrain WAF policy is created with the matching SKU.')
@allowed([
'Standard_AzureFrontDoor'
Expand All @@ -71,6 +74,9 @@ param dnsZoneName string = ''
@description('Resource group of the Azure DNS zone. Defaults to the Front Door resource group.')
param dnsZoneResourceGroupName string = ''

@description('Subscription ID of the Azure DNS zone. Defaults to the Front Door subscription.')
param dnsZoneSubscriptionId string = ''

@description('Optional comma-separated two-letter country codes the Front Door WAF allows, for example "US,CA". Empty allows every country.')
param frontDoorAllowedCountryCodes string = ''

Expand Down Expand Up @@ -333,30 +339,34 @@ var containerAppBaseUrl = 'https://${containerAppHostName}'
var publicBaseUrl = empty(customDomainName) ? containerAppBaseUrl : 'https://${customDomainName}'
var allowedHosts = empty(customDomainName) ? containerAppHostName : '${customDomainName};${containerAppHostName}'

// The Front Door profile and DNS zone may live in other subscriptions of the same tenant, for example a
// shared edge subscription and a shared DNS subscription.
var useFrontDoor = !empty(frontDoorProfileName) && !empty(customDomainName)
var frontDoorSubscription = empty(frontDoorSubscriptionId) ? subscription().subscriptionId : frontDoorSubscriptionId
var frontDoorResourceGroup = empty(frontDoorResourceGroupName) ? resourceGroup().name : frontDoorResourceGroupName
var useFrontDoorDns = useFrontDoor && !empty(dnsZoneName)
var dnsZoneSubscription = empty(dnsZoneSubscriptionId) ? frontDoorSubscription : dnsZoneSubscriptionId
var dnsZoneResourceGroup = empty(dnsZoneResourceGroupName) ? frontDoorResourceGroup : dnsZoneResourceGroupName
var normalizedDnsZoneName = toLower(dnsZoneName)
var normalizedCustomDomainName = toLower(customDomainName)

module frontDoor 'modules/front-door.bicep' = if (useFrontDoor) {
name: 'devbrain-front-door-${substring(resourceToken, 0, 6)}'
scope: resourceGroup(frontDoorResourceGroup)
scope: resourceGroup(frontDoorSubscription, frontDoorResourceGroup)
params: {
profileName: frontDoorProfileName
skuName: frontDoorSkuName
resourceToken: substring(resourceToken, 0, 6)
customDomainName: normalizedCustomDomainName
originHostName: containerAppHostName
dnsZoneId: useFrontDoorDns ? resourceId(subscription().subscriptionId, dnsZoneResourceGroup, 'Microsoft.Network/dnsZones', normalizedDnsZoneName) : ''
dnsZoneId: useFrontDoorDns ? resourceId(dnsZoneSubscription, dnsZoneResourceGroup, 'Microsoft.Network/dnsZones', normalizedDnsZoneName) : ''
allowedCountryCodes: empty(frontDoorAllowedCountryCodes) ? [] : map(split(frontDoorAllowedCountryCodes, ','), code => toUpper(trim(code)))
}
}

module frontDoorDns 'modules/front-door-dns.bicep' = if (useFrontDoorDns) {
name: 'devbrain-front-door-dns-${substring(resourceToken, 0, 6)}'
scope: resourceGroup(dnsZoneResourceGroup)
scope: resourceGroup(dnsZoneSubscription, dnsZoneResourceGroup)
params: {
dnsZoneName: normalizedDnsZoneName
// The custom domain must sit below the zone apex, for example devbrain.contoso.com in contoso.com.
Expand Down
6 changes: 6 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,18 @@
"frontDoorResourceGroupName": {
"value": "${FRONT_DOOR_RESOURCE_GROUP=}"
},
"frontDoorSubscriptionId": {
"value": "${FRONT_DOOR_SUBSCRIPTION_ID=}"
},
"dnsZoneName": {
"value": "${DNS_ZONE_NAME=}"
},
"dnsZoneResourceGroupName": {
"value": "${DNS_ZONE_RESOURCE_GROUP=}"
},
"dnsZoneSubscriptionId": {
"value": "${DNS_ZONE_SUBSCRIPTION_ID=}"
},
"frontDoorAllowedCountryCodes": {
"value": "${FRONT_DOOR_ALLOWED_COUNTRIES=}"
},
Expand Down
Loading