Skip to content

Commit 8976982

Browse files
Merge pull request #306 from microsoft/psl-sw/resolve-incident
fix: BOLA/IDOR: enforce process ownership on process and file APIs
2 parents 94adfec + 5dd26be commit 8976982

14 files changed

Lines changed: 723 additions & 15 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# Each line is a file pattern followed by one or more owners.
33

44
# These owners will be the default owners for everything in the repo.
5-
* @Avijit-Microsoft @Roopan-Microsoft @Prajwal-Microsoft @Vinay-Microsoft @aniaroramsft @Dongbumlee @sethsteenken @toherman-msft @nchandhi @dgp10801
5+
* @Avijit-Microsoft @Roopan-Microsoft @Prajwal1-Microsoft @VinaySh-Microsoft @aniaroramsft @Dongbumlee @sethsteenken @toherman-msft @nchandhi @dgp10801

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,10 @@ flowchart TB
205205
%% LLM usage
206206
PROC -->|LLM call| AOAI
207207
208-
%% Image pulls
209-
ACR -->|Pull image| FE
210-
ACR -->|Pull image| API
211-
ACR -->|Pull image| PROC
208+
%% Image pulls (identity-based, AcrPull via managed identity - no anonymous pull)
209+
ACR -->|Pull image · AcrPull| FE
210+
ACR -->|Pull image · AcrPull| API
211+
ACR -->|Pull image · AcrPull| PROC
212212
213213
%% Identity usage
214214
ID -.-> FE

azure.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ hooks:
1212
posix:
1313
shell: sh
1414
run: |
15+
echo "==> Building and pushing container images to the dedicated ACR (remote build)"
16+
bash ./scripts/deploy_container_images.sh
1517
echo "-----"
1618
echo "🧭 Web App Details:"
1719
echo "✅ Name: $CONTAINER_WEB_APP_NAME"
@@ -27,6 +29,8 @@ hooks:
2729
windows:
2830
shell: pwsh
2931
run: |
32+
Write-Host "==> Building and pushing container images to the dedicated ACR (remote build)"
33+
./scripts/deploy_container_images.ps1
3034
Write-Host "-----"
3135
Write-Host "🧭 Web App Details:"
3236
Write-Host "✅ Name: $env:CONTAINER_WEB_APP_NAME"

infra/main.bicep

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@ param azureAiServiceLocation string
4141

4242

4343

44-
@description('Optional. The endpoint (excluding https://) of an existing container registry. This is the `loginServer` when using Azure Container Registry.')
45-
param containerRegistryEndpoint string = 'containermigrationacr.azurecr.io'
44+
@description('Optional. [Deprecated] The endpoint (excluding https://) of an existing container registry. Retained only for backward compatibility with existing parameter files/pipelines; each deployment now provisions its own dedicated Azure Container Registry and no longer depends on a shared/public registry.')
45+
#disable-next-line no-unused-params
46+
param containerRegistryEndpoint string = ''
4647

4748
@description('Optional. The image tag to use for container images. Defaults to "latest_v2".')
4849
param imageTag string = 'latest_v2'
4950

51+
@description('''Optional. Placeholder container image used to initially provision the container apps.
52+
The dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps quickstart image.''')
53+
param placeholderContainerImage string = 'mcr.microsoft.com/k8se/quickstart:latest'
54+
5055
@minLength(1)
5156
@allowed(['Standard', 'GlobalStandard'])
5257
@description('Optional. Model deployment type. Defaults to GlobalStandard.')
@@ -220,6 +225,36 @@ module appIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.
220225
}
221226
}
222227

228+
// ========== Dedicated Azure Container Registry ========== //
229+
// Each deployment provisions its own ACR instead of relying on a shared/public
230+
// registry with anonymous pull. Images are pulled using identity-based
231+
// authentication (AcrPull role granted to the application managed identity).
232+
var containerRegistryName = take('cr${solutionSuffix}', 50)
233+
module containerRegistry './modules/containerRegistry.bicep' = {
234+
name: take('module.container-registry.${solutionSuffix}', 64)
235+
params: {
236+
name: containerRegistryName
237+
location: solutionLocation
238+
tags: allTags
239+
// Premium SKU in WAF/private-networking mode (supports higher throughput and
240+
// future private endpoints). Public network access is kept Enabled in both
241+
// modes so remote `az acr build` (ACR Tasks) and managed-identity pulls work;
242+
// AzureServices bypass lets trusted ACR Tasks reach the registry.
243+
sku: enablePrivateNetworking ? 'Premium' : 'Standard'
244+
publicNetworkAccess: 'Enabled'
245+
networkRuleBypassOptions: 'AzureServices'
246+
// Application managed identity gets AcrPull for identity-based image pulls.
247+
acrPullPrincipalIds: [
248+
appIdentity.outputs.principalId
249+
]
250+
// Deployer gets a registry-scoped AcrPush role so it can push/pull images
251+
// from the post-deployment script. Remote build (az acr build) additionally
252+
// needs scheduleRun/action, which the deployer holds via its higher-scope role.
253+
buildPrincipalId: deployingUserPrincipalId
254+
buildPrincipalType: deployingUserType
255+
}
256+
}
257+
223258
// ========== Log Analytics Workspace ========== //
224259
// WAF best practices for Log Analytics: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-log-analytics
225260
// WAF PSRules for Log Analytics: https://azure.github.io/PSRule.Rules.Azure/en/rules/resource/#azure-monitor-logs
@@ -1335,10 +1370,16 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = {
13351370
appIdentity.outputs.resourceId
13361371
]
13371372
}
1373+
registries: [
1374+
{
1375+
server: containerRegistry.outputs.loginServer
1376+
identity: appIdentity.outputs.resourceId
1377+
}
1378+
]
13381379
containers: [
13391380
{
13401381
name: 'backend-api'
1341-
image: '${containerRegistryEndpoint}/backend-api:${imageTag}'
1382+
image: placeholderContainerImage
13421383
env: concat(
13431384
[
13441385
{
@@ -1422,10 +1463,16 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = {
14221463
appIdentity.outputs.resourceId
14231464
]
14241465
}
1466+
registries: [
1467+
{
1468+
server: containerRegistry.outputs.loginServer
1469+
identity: appIdentity.outputs.resourceId
1470+
}
1471+
]
14251472
containers: [
14261473
{
14271474
name: 'frontend'
1428-
image: '${containerRegistryEndpoint}/frontend:${imageTag}'
1475+
image: placeholderContainerImage
14291476
env: [
14301477
{
14311478
name: 'API_URL'
@@ -1490,10 +1537,16 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = {
14901537
appIdentity.outputs.resourceId
14911538
]
14921539
}
1540+
registries: [
1541+
{
1542+
server: containerRegistry.outputs.loginServer
1543+
identity: appIdentity.outputs.resourceId
1544+
}
1545+
]
14931546
containers: [
14941547
{
14951548
name: 'processor'
1496-
image: '${containerRegistryEndpoint}/processor:${imageTag}'
1549+
image: placeholderContainerImage
14971550
env: concat(
14981551
[
14991552
{
@@ -1572,6 +1625,18 @@ output AZURE_SUBSCRIPTION_ID string = subscription().subscriptionId
15721625
@description('The Azure resource group name.')
15731626
output AZURE_RESOURCE_GROUP string = resourceGroup().name
15741627

1628+
@description('The name of the dedicated Azure Container Registry.')
1629+
output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name
1630+
1631+
@description('The login server (endpoint) of the dedicated Azure Container Registry.')
1632+
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
1633+
1634+
@description('The name of the processor container app.')
1635+
output CONTAINER_PROCESSOR_APP_NAME string = containerAppProcessor.outputs.name
1636+
1637+
@description('The image tag used for deployment-specific container images.')
1638+
output AZURE_ENV_IMAGE_TAG string = imageTag
1639+
15751640
// Log deployer information for debugging
15761641
output deployerObjectId string = deployingUserPrincipalId
15771642
output deployerType string = deployingUserType
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
metadata name = 'Dedicated Azure Container Registry'
2+
metadata description = '''Provisions a dedicated Azure Container Registry (ACR) for a single deployment and configures identity-based authentication.
3+
Admin user and anonymous pull are disabled. The provided application managed identity principals are granted the AcrPull role, and the deployer is granted a registry-scoped AcrPush role so it can push/pull images (remote builds via `az acr build` additionally rely on the deployer's higher-scope role for scheduleRun/action).'''
4+
5+
@description('Required. Name of the Azure Container Registry. Must be globally unique and 5-50 alphanumeric characters.')
6+
@maxLength(50)
7+
param name string
8+
9+
@description('Optional. Azure region for the registry. Defaults to the resource group location.')
10+
param location string = resourceGroup().location
11+
12+
@description('Optional. Tags to apply to the registry.')
13+
param tags object = {}
14+
15+
@description('Optional. SKU for the registry. Premium is required for private networking. Defaults to Standard.')
16+
@allowed([
17+
'Basic'
18+
'Standard'
19+
'Premium'
20+
])
21+
param sku string = 'Standard'
22+
23+
@description('Optional. Public network access for the registry. Defaults to Enabled. Note: `az acr build` (ACR Tasks quick builds) and Container Apps image pulls require reachability; disabling public access requires VNet agent pools and private endpoints, so it is left Enabled by default for both WAF and non-WAF deployments.')
24+
@allowed([
25+
'Enabled'
26+
'Disabled'
27+
])
28+
param publicNetworkAccess string = 'Enabled'
29+
30+
@description('Optional. Whether to allow trusted Azure services (e.g. ACR Tasks used by `az acr build`) to bypass network rules. Defaults to AzureServices.')
31+
@allowed([
32+
'AzureServices'
33+
'None'
34+
])
35+
param networkRuleBypassOptions string = 'AzureServices'
36+
37+
@description('Optional. Principal IDs (managed identities) to grant the AcrPull role so they can pull images using identity-based authentication.')
38+
param acrPullPrincipalIds array = []
39+
40+
@description('Optional. Principal ID (e.g. the deployer) to grant a registry-scoped AcrPush role so it can push/pull images. Leave empty to skip.')
41+
param buildPrincipalId string = ''
42+
43+
@description('Optional. Principal type for the build principal.')
44+
@allowed([
45+
'Device'
46+
'ForeignGroup'
47+
'Group'
48+
'ServicePrincipal'
49+
'User'
50+
])
51+
param buildPrincipalType string = 'User'
52+
53+
// AcrPull role definition ID (allows pulling images).
54+
var acrPullRoleDefinitionId = '7f951dda-4ed3-4680-a7ca-43fe172d538d'
55+
// AcrPush role definition ID (least-privilege push/pull for the deployer).
56+
// Note: az acr build (ACR Tasks remote build) additionally needs
57+
// scheduleRun/action, which the deployer holds via its higher-scope role
58+
// (e.g. Owner/Contributor on the subscription or resource group used by azd).
59+
var acrPushRoleDefinitionId = '8311e382-0749-4cb8-b61a-304f252e45ec'
60+
61+
resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
62+
name: name
63+
location: location
64+
tags: tags
65+
sku: {
66+
name: sku
67+
}
68+
properties: {
69+
// Identity-based authentication only - no admin credentials.
70+
adminUserEnabled: false
71+
// Explicitly disable anonymous pull; access requires an authenticated identity.
72+
anonymousPullEnabled: false
73+
publicNetworkAccess: publicNetworkAccess
74+
networkRuleBypassOptions: networkRuleBypassOptions
75+
}
76+
}
77+
78+
resource acrPullRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
79+
for principalId in acrPullPrincipalIds: {
80+
name: guid(registry.id, principalId, acrPullRoleDefinitionId)
81+
scope: registry
82+
properties: {
83+
roleDefinitionId: subscriptionResourceId(
84+
'Microsoft.Authorization/roleDefinitions',
85+
acrPullRoleDefinitionId
86+
)
87+
principalId: principalId
88+
principalType: 'ServicePrincipal'
89+
}
90+
}
91+
]
92+
93+
resource acrBuildRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(buildPrincipalId)) {
94+
name: guid(registry.id, buildPrincipalId, acrPushRoleDefinitionId)
95+
scope: registry
96+
properties: {
97+
roleDefinitionId: subscriptionResourceId(
98+
'Microsoft.Authorization/roleDefinitions',
99+
acrPushRoleDefinitionId
100+
)
101+
principalId: buildPrincipalId
102+
principalType: buildPrincipalType
103+
}
104+
}
105+
106+
@description('The resource ID of the container registry.')
107+
output resourceId string = registry.id
108+
109+
@description('The name of the container registry.')
110+
output name string = registry.name
111+
112+
@description('The login server (endpoint) of the container registry, e.g. myregistry.azurecr.io.')
113+
output loginServer string = registry.properties.loginServer

0 commit comments

Comments
 (0)