Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Modules] Microsoft.Storage/StorageAccounts and Microsoft.KeyVault/vaults policy exemption #2997

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ param virtualNetworkName string
@description('Required. The name of the Managed Identity to create.')
param managedIdentityName string

@description('Optional. The name of the policy assignment.')
param policyAssignmentName string = 'DisableKeyVaultPublicAccess'

@description('Optional. The policy definition Id to be assigned.')
param policyDefinitionId string = '/providers/Microsoft.Authorization/policyDefinitions/405c5871-3e91-4644-8a63-58e19d68ff5b'

var addressPrefix = '10.0.0.0/16'

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-01-01' = {
Expand Down Expand Up @@ -39,6 +45,22 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-
location: location
}

resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
name: policyAssignmentName
scope: resourceGroup()
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
properties: {
policyDefinitionId: policyDefinitionId
enforcementMode: 'Default'
}
}

resource privateDNSZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: 'privatelink.vaultcore.azure.net'
location: 'global'
Expand All @@ -63,3 +85,6 @@ output managedIdentityPrincipalId string = managedIdentity.properties.principalI

@description('The resource ID of the created Private DNS Zone.')
output privateDNSResourceId string = privateDNSZone.id

@description('The resource ID of the Policy Assignment.')
output policyAssignmentId string = policyAssignment.id
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ module testDeployment '../../deploy.bicep' = {
}
]
}
policyExemptions: [
{
name: '<<namePrefix>>${serviceShort}001-PolicyExemption001'
displayName: '<<namePrefix>>${serviceShort}001 Policy Exception'
description: 'Test Policy Exemption 1'
assignmentScopeValidation: 'DoNotValidate'
policyAssignmentId: nestedDependencies.outputs.policyAssignmentId
exemptionCategory: 'Waiver'
}
]
privateEndpoints: [
{
privateDnsZoneGroup: {
Expand Down
25 changes: 25 additions & 0 deletions modules/Microsoft.KeyVault/vaults/deploy.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ param diagnosticEventHubName string = ''
@description('Optional. Specify the type of lock.')
param lock string = ''

@description('''Optional. Array of policy exemption objects that contain the \'name\' and \'policyAssignmentId\' to policy exemptions on this resource.

Exemptions have extra security measures because of the impact of granting an exemption.

Beyond requiring the Microsoft.Authorization/policyExemptions/write operation on the resource hierarchy or individual resource,
the creator of an exemption must have the exempt/Action verb on the target assignment which could be at the Management Group, Subscription, or Resource Group levels.

The built-in roles Resource Policy Contributor and Security Admin both have the read and write permissions.
''')
param policyExemptions array = []

@description('Optional. Array of role assignment objects that contain the \'roleDefinitionIdOrName\' and \'principalId\' to define RBAC role assignments on this resource. In the roleDefinitionIdOrName attribute, you can provide either the display name of the role definition, or its fully qualified ID in the following format: \'/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11\'.')
param roleAssignments array = []

Expand Down Expand Up @@ -295,6 +306,20 @@ module keyVault_privateEndpoints '../../Microsoft.Network/privateEndpoints/deplo
}
}]

resource keyVault_policyExemptions 'Microsoft.Authorization/policyExemptions@2022-07-01-preview' = [for policyExemption in policyExemptions: if (!empty(policyExemptions)) {
name: policyExemption.name
properties: {
assignmentScopeValidation: contains(policyExemption, 'assignmentScopeValidation') ? policyExemption.assignmentScopeValidation : ''
displayName: contains(policyExemption, 'displayName') ? policyExemption.description : ''
description: contains(policyExemption, 'description') ? policyExemption.description : ''
metadata: contains(policyExemption, 'metadata') ? policyExemption.metadata : {}
policyAssignmentId: policyExemption.policyAssignmentId
policyDefinitionReferenceIds: contains(policyExemption, 'policyDefinitionReferenceIds') ? policyExemption.policyDefinitionReferenceIds : null
exemptionCategory: contains(policyExemption, 'exemptionCategory') ? policyExemption.exemptionCategory : null
expiresOn: contains(policyExemption, 'expiresOn') ? policyExemption.expiresOn : ''
}
scope: keyVault
}]
module keyVault_roleAssignments '.bicep/nested_roleAssignments.bicep' = [for (roleAssignment, index) in roleAssignments: {
name: '${uniqueString(deployment().name, location)}-KeyVault-Rbac-${index}'
params: {
Expand Down
24 changes: 24 additions & 0 deletions modules/Microsoft.KeyVault/vaults/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This module deploys a key vault and its child resources.
| Resource Type | API Version |
| :-- | :-- |
| `Microsoft.Authorization/locks` | [2020-05-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-05-01/locks) |
| `Microsoft.Authorization/policyExemptions` | [2022-07-01-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-07-01-preview/policyExemptions) |
| `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) |
| `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) |
| `Microsoft.KeyVault/vaults` | [2022-07-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.KeyVault/2022-07-01/vaults) |
Expand Down Expand Up @@ -57,6 +58,7 @@ This module deploys a key vault and its child resources.
| `location` | string | `[resourceGroup().location]` | | Location for all resources. |
| `lock` | string | `''` | `['', CanNotDelete, ReadOnly]` | Specify the type of lock. |
| `networkAcls` | object | `{object}` | | Service endpoint object information. For security reasons, it is recommended to set the DefaultAction Deny. |
| `policyExemptions` | array | `[]` | | Array of policy exemption objects that contain the \'name\' and \'policyAssignmentId\' to policy exemptions on this resource.<p><p>Exemptions have extra security measures because of the impact of granting an exemption.<p><p>Beyond requiring the Microsoft.Authorization/policyExemptions/write operation on the resource hierarchy or individual resource,<p>the creator of an exemption must have the exempt/Action verb on the target assignment which could be at the Management Group, Subscription, or Resource Group levels.<p><p>The built-in roles Resource Policy Contributor and Security Admin both have the read and write permissions.<p> |
| `privateEndpoints` | array | `[]` | | Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible. |
| `publicNetworkAccess` | string | `''` | `['', Disabled, Enabled]` | Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set. |
| `roleAssignments` | array | `[]` | | Array of role assignment objects that contain the 'roleDefinitionIdOrName' and 'principalId' to define RBAC role assignments on this resource. In the roleDefinitionIdOrName attribute, you can provide either the display name of the role definition, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'. |
Expand Down Expand Up @@ -506,6 +508,16 @@ module vaults './Microsoft.KeyVault/vaults/deploy.bicep' = {
}
]
}
policyExemptions: [
{
assignmentScopeValidation: 'DoNotValidate'
description: 'Test Policy Exemption 1'
displayName: '<<namePrefix>>kvvcom001 Policy Exception'
exemptionCategory: 'Waiver'
name: '<<namePrefix>>kvvcom001-PolicyExemption001'
policyAssignmentId: '<policyAssignmentId>'
}
]
privateEndpoints: [
{
privateDnsZoneGroup: {
Expand Down Expand Up @@ -690,6 +702,18 @@ module vaults './Microsoft.KeyVault/vaults/deploy.bicep' = {
]
}
},
"policyExemptions": {
"value": [
{
"assignmentScopeValidation": "DoNotValidate",
"description": "Test Policy Exemption 1",
"displayName": "<<namePrefix>>kvvcom001 Policy Exception",
"exemptionCategory": "Waiver",
"name": "<<namePrefix>>kvvcom001-PolicyExemption001",
"policyAssignmentId": "<policyAssignmentId>"
}
]
},
"privateEndpoints": {
"value": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@sys.description('Required. The resource ID of the resource to apply the policy exemption to.')
AlexanderSehr marked this conversation as resolved.
Show resolved Hide resolved
param resourceId string

@sys.description('Required. Specifies the name of the policy exemption. Maximum length is 64 characters.')
@maxLength(64)
param name string

@sys.description('Optional. The display name of the policy exemption. Maximum length is 128 characters.')
@maxLength(128)
param displayName string = ''

@sys.description('Optional. The description of the policy exemption.')
param description string = ''

@sys.description('Optional. The policy exemption metadata. Metadata is an open ended object and is typically a collection of key-value pairs.')
param metadata object = {}

@sys.description('Optional. The policy exemption category. Possible values are Waiver and Mitigated. Default is Mitigated.')
@allowed([
'Mitigated'
'Waiver'
])
param exemptionCategory string = 'Mitigated'

@sys.description('Required. The resource ID of the policy assignment that is being exempted.')
param policyAssignmentId string

@sys.description('Optional. The policy definition reference ID list when the associated policy assignment is an assignment of a policy set definition.')
param policyDefinitionReferenceIds array = []

@sys.description('Optional. The expiration date and time (in UTC ISO 8601 format yyyy-MM-ddTHH:mm:ssZ) of the policy exemption. e.g. 2021-10-02T03:57:00.000Z.')
param expiresOn string = ''

@sys.description('Optional. The option whether validate the exemption is at or under the assignment scope.')
@allowed([
''
'Default'
'DoNotValidate'
])
param assignmentScopeValidation string = ''

@sys.description('Optional. The resource selector list to filter policies by resource properties.')
param resourceSelectors array = []

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' existing = {
name: last(split(resourceId, '/'))!
}

resource policyExemption 'Microsoft.Authorization/policyExemptions@2022-07-01-preview' = {
name: name
properties: {
assignmentScopeValidation: !empty(assignmentScopeValidation) ? assignmentScopeValidation : null
displayName: !empty(displayName) ? displayName : null
description: !empty(description) ? description : null
exemptionCategory: exemptionCategory
expiresOn: !empty(expiresOn) ? expiresOn : null
metadata: !empty(metadata) ? metadata : null
policyAssignmentId: policyAssignmentId
policyDefinitionReferenceIds: !empty(policyDefinitionReferenceIds) ? policyDefinitionReferenceIds : null
resourceSelectors: resourceSelectors
}
scope: storageAccount
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ param virtualNetworkName string
@description('Required. The name of the Managed Identity to create.')
param managedIdentityName string

@description('Optional. The name of the policy assignment.')
param policyAssignmentName string = 'DisableStoragePublicNetworkAccess'

@description('Optional. The policy definition Id to be assigned.')
param policyDefinitionId string = '/providers/Microsoft.Authorization/policyDefinitions/a06d0189-92e8-4dba-b0c4-08d7669fce7d'

var addressPrefix = '10.0.0.0/16'

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-01-01' = {
Expand Down Expand Up @@ -55,6 +61,22 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-
location: location
}

resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
name: policyAssignmentName
scope: resourceGroup()
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
properties: {
policyDefinitionId: policyDefinitionId
enforcementMode: 'Default'
}
}

@description('The resource ID of the created Virtual Network Subnet.')
output subnetResourceId string = virtualNetwork.properties.subnets[0].id

Expand All @@ -66,3 +88,6 @@ output managedIdentityResourceId string = managedIdentity.id

@description('The resource ID of the created Private DNS Zone.')
output privateDNSZoneResourceId string = privateDNSZone.id

@description('The resource ID of the Policy Assignment.')
output policyAssignmentId string = policyAssignment.id
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ module testDeployment '../../deploy.bicep' = {
enableHierarchicalNamespace: true
enableSftp: true
enableNfsV3: true
policyExemptions: [
{
name: '<<namePrefix>>${serviceShort}001-PolicyExemption001'
displayName: '<<namePrefix>>${serviceShort}001 Policy Exception'
description: 'Test Policy Exemption 1'
assignmentScopeValidation: 'DoNotValidate'
policyAssignmentId: nestedDependencies.outputs.policyAssignmentId
exemptionCategory: 'Waiver'
}
]
privateEndpoints: [
{
service: 'blob'
Expand Down
26 changes: 26 additions & 0 deletions modules/Microsoft.Storage/storageAccounts/deploy.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ param name string
@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('''Optional. Array of policy exemption objects that contain the \'name\' and \'policyAssignmentId\' to policy exemptions on this resource.

Exemptions have extra security measures because of the impact of granting an exemption.

Beyond requiring the Microsoft.Authorization/policyExemptions/write operation on the resource hierarchy or individual resource,
the creator of an exemption must have the exempt/Action verb on the target assignment which could be at the Management Group, Subscription, or Resource Group levels.

The built-in roles Resource Policy Contributor and Security Admin both have the read and write permissions.
''')
param policyExemptions array = []

@description('Optional. Array of role assignment objects that contain the \'roleDefinitionIdOrName\' and \'principalId\' to define RBAC role assignments on this resource. In the roleDefinitionIdOrName attribute, you can provide either the display name of the role definition, or its fully qualified ID in the following format: \'/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11\'.')
param roleAssignments array = []

Expand Down Expand Up @@ -338,6 +349,21 @@ module storageAccount_roleAssignments '.bicep/nested_roleAssignments.bicep' = [f
}
}]

resource storageAccount_policyExemptions 'Microsoft.Authorization/policyExemptions@2022-07-01-preview' = [for policyExemption in policyExemptions: if (!empty(policyExemptions)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the storage account deployment be already blocked before we even make it to the excemption?

Copy link
Contributor Author

@shawntmeyer shawntmeyer Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not, the storage account is deployed. The common tests for both storage accounts and key vaults deploy a policy assignment that denies the creation of a resource that doesn't meet the policy. I verified that the test is a true test in that the resource doesn't meet the policy, but the exception works and the deployment succeeds.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the policy a deny or an audit policy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I'm inclined to see this as an unexpected behaviour of the policy. Probably due to timing? I'm very confused as I'd expect the deny policy to deny the creation of an uncompliant resource, by definition 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly why I needed the PR. We can not create Resource Level exemptions without the resource ID and we can't create the resource with a deny policy in place without the exemption. The only way to do this is through ARM as a single deployment. ARM sorts it out and allows the resource to be created due to the exemption. Without this we would have to temporarily place an exemption at the RG level or disable the policy.

name: policyExemption.name
properties: {
assignmentScopeValidation: contains(policyExemption, 'assignmentScopeValidation') ? policyExemption.assignmentScopeValidation : ''
displayName: contains(policyExemption, 'displayName') ? policyExemption.description : ''
description: contains(policyExemption, 'description') ? policyExemption.description : ''
metadata: contains(policyExemption, 'metadata') ? policyExemption.metadata : {}
policyAssignmentId: policyExemption.policyAssignmentId
policyDefinitionReferenceIds: contains(policyExemption, 'policyDefinitionReferenceIds') ? policyExemption.policyDefinitionReferenceIds : null
exemptionCategory: contains(policyExemption, 'exemptionCategory') ? policyExemption.exemptionCategory : null
expiresOn: contains(policyExemption, 'expiresOn') ? policyExemption.expiresOn : ''
}
scope: storageAccount
}]

module storageAccount_privateEndpoints '../../Microsoft.Network/privateEndpoints/deploy.bicep' = [for (privateEndpoint, index) in privateEndpoints: {
name: '${uniqueString(deployment().name, location)}-StorageAccount-PrivateEndpoint-${index}'
params: {
Expand Down
Loading