Skip to content

Commit 60f85ee

Browse files
Merge pull request #97 from NHSDigital/DTOSS-12687-windows-admin-center-arc-policy
DTOSS-12687: Deploy Windows Admin Center to Arc-enabled servers
2 parents 83386f9 + beda2a9 commit 60f85ee

5 files changed

Lines changed: 214 additions & 11 deletions

File tree

.gitleaksignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# Gitleaks allowlist for false positives in git history
1+
# Gitleaks allowlist for false positives in git history and working tree
2+
#
3+
# ARM template contentVersion in WAC policy JSON (matches IPv4 rule)
4+
infrastructure/modules/arc-infra/policies/deploy-wac-extension-arc-windows.json:ipv4:56
25
#
36
# These are DICOM UIDs that match the IPv4 pattern in old commits.
47
# Current files use inline `# gitleaks:allow` comments instead.

infrastructure/modules/arc-infra/arc.tf

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ data "azuread_service_principal" "arc_onboarding" {
1212
display_name = "spn-azure-arc-onboarding-screening-${var.env_config}"
1313
}
1414

15+
# Look up the Entra ID group that manages this environment
16+
data "azuread_group" "screening" {
17+
count = var.enable_arc_servers ? 1 : 0
18+
display_name = "screening_${var.app_short_name}_${var.env_config}"
19+
}
20+
1521
# Assign "Azure Connected Machine Onboarding" role to allow Arc server enrollment
1622
module "arc_onboarding_role" {
1723
count = var.enable_arc_servers ? 1 : 0
@@ -22,3 +28,14 @@ module "arc_onboarding_role" {
2228
role_definition_name = "Azure Connected Machine Onboarding"
2329
principal_id = data.azuread_service_principal.arc_onboarding[0].object_id
2430
}
31+
32+
# Assign "Windows Admin Center Administrator Login" to allow the group to connect via WAC
33+
module "arc_wac_admin_login_role" {
34+
count = var.enable_arc_servers ? 1 : 0
35+
36+
source = "../dtos-devops-templates/infrastructure/modules/rbac-assignment"
37+
38+
scope = data.azurerm_resource_group.arc_enabled_servers[0].id
39+
role_definition_name = "Windows Admin Center Administrator Login"
40+
principal_id = data.azuread_group.screening[0].object_id
41+
}

infrastructure/modules/arc-infra/azure_monitor.tf

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,23 +137,83 @@ resource "azurerm_resource_group_policy_assignment" "dcr_association" {
137137
})
138138
}
139139

140+
# ---------------------------------------------------------------------------
141+
# Custom Policy Definition + Assignment for Windows Admin Center
142+
# Replicates the built-in "Configure Windows Arc-enabled machines to enable
143+
# Windows Admin Center Extension" (0e4b8929), which is not available in this tenant.
144+
# Policy rule is defined in policies/deploy-wac-extension-arc-windows.json.
145+
# ---------------------------------------------------------------------------
146+
resource "azurerm_policy_definition" "wac" {
147+
count = var.enable_arc_servers ? 1 : 0
148+
149+
name = "deploy-wac-extension-arc-windows-${var.env_config}"
150+
policy_type = "Custom"
151+
mode = "Indexed"
152+
display_name = "Configure Windows Arc-enabled machines to enable Windows Admin Center Extension"
153+
description = "Deploys the AdminCenter extension to Windows Arc-enabled machines to enable browser-based management via the Azure portal."
154+
155+
parameters = jsonencode({
156+
effect = {
157+
type = "String"
158+
metadata = { displayName = "Effect", description = "Enable or disable the execution of the policy" }
159+
allowedValues = ["DeployIfNotExists", "Disabled"]
160+
defaultValue = "DeployIfNotExists"
161+
}
162+
port = {
163+
type = "Integer"
164+
metadata = { displayName = "Port", description = "The port number to use for Windows Admin Center" }
165+
defaultValue = 6516
166+
}
167+
proxyURL = {
168+
type = "String"
169+
metadata = { displayName = "Proxy URL", description = "Optional proxy URL for Windows Admin Center" }
170+
defaultValue = ""
171+
}
172+
})
173+
174+
policy_rule = file("${path.module}/policies/deploy-wac-extension-arc-windows.json")
175+
}
176+
177+
resource "azurerm_resource_group_policy_assignment" "wac" {
178+
count = var.enable_arc_servers ? 1 : 0
179+
180+
name = "wac-arc-${var.env_config}"
181+
resource_group_id = data.azurerm_resource_group.arc_enabled_servers[0].id
182+
policy_definition_id = azurerm_policy_definition.wac[0].id
183+
location = var.region
184+
185+
identity {
186+
type = "UserAssigned"
187+
identity_ids = [module.arc_monitor_policy_identity[0].id]
188+
}
189+
}
190+
140191
# Remediation tasks — re-evaluate compliance and deploy AMA/DCR association on
141192
# any non-compliant Arc machines each time Terraform applies. Combined with the
142193
# DeployIfNotExists effect, this makes the monitoring setup self-healing.
143194
resource "azurerm_resource_group_policy_remediation" "ama_install" {
144195
count = var.enable_arc_servers ? 1 : 0
145196

146-
name = "remediation-ama-install-${var.env_config}"
147-
resource_group_id = data.azurerm_resource_group.arc_enabled_servers[0].id
148-
policy_assignment_id = azurerm_resource_group_policy_assignment.ama_install[0].id
197+
name = "remediation-ama-install-${var.env_config}"
198+
resource_group_id = data.azurerm_resource_group.arc_enabled_servers[0].id
199+
policy_assignment_id = azurerm_resource_group_policy_assignment.ama_install[0].id
149200
resource_discovery_mode = "ReEvaluateCompliance"
150201
}
151202

152203
resource "azurerm_resource_group_policy_remediation" "dcr_association" {
153204
count = var.enable_arc_servers ? 1 : 0
154205

155-
name = "remediation-dcr-assoc-${var.env_config}"
156-
resource_group_id = data.azurerm_resource_group.arc_enabled_servers[0].id
157-
policy_assignment_id = azurerm_resource_group_policy_assignment.dcr_association[0].id
206+
name = "remediation-dcr-assoc-${var.env_config}"
207+
resource_group_id = data.azurerm_resource_group.arc_enabled_servers[0].id
208+
policy_assignment_id = azurerm_resource_group_policy_assignment.dcr_association[0].id
209+
resource_discovery_mode = "ReEvaluateCompliance"
210+
}
211+
212+
resource "azurerm_resource_group_policy_remediation" "wac" {
213+
count = var.enable_arc_servers ? 1 : 0
214+
215+
name = "remediation-wac-${var.env_config}"
216+
resource_group_id = data.azurerm_resource_group.arc_enabled_servers[0].id
217+
policy_assignment_id = azurerm_resource_group_policy_assignment.wac[0].id
158218
resource_discovery_mode = "ReEvaluateCompliance"
159219
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
{
2+
"if": {
3+
"allOf": [
4+
{
5+
"field": "type",
6+
"equals": "Microsoft.HybridCompute/machines"
7+
},
8+
{
9+
"field": "Microsoft.HybridCompute/machines/osName",
10+
"equals": "Windows"
11+
}
12+
]
13+
},
14+
"then": {
15+
"effect": "[parameters('effect')]",
16+
"details": {
17+
"type": "Microsoft.HybridCompute/machines/extensions",
18+
"roleDefinitionIds": [
19+
"/providers/Microsoft.Authorization/roleDefinitions/cd570a14-e51a-42ad-bac8-bafd67325302"
20+
],
21+
"existenceCondition": {
22+
"allOf": [
23+
{
24+
"field": "Microsoft.HybridCompute/machines/extensions/type",
25+
"equals": "AdminCenter"
26+
},
27+
{
28+
"field": "Microsoft.HybridCompute/machines/extensions/publisher",
29+
"equals": "Microsoft.AdminCenter"
30+
},
31+
{
32+
"field": "Microsoft.HybridCompute/machines/extensions/provisioningState",
33+
"equals": "Succeeded"
34+
}
35+
]
36+
},
37+
"deployment": {
38+
"properties": {
39+
"mode": "incremental",
40+
"parameters": {
41+
"vmName": {
42+
"value": "[field('name')]"
43+
},
44+
"location": {
45+
"value": "[field('location')]"
46+
},
47+
"proxyURL": {
48+
"value": "[parameters('proxyURL')]"
49+
},
50+
"port": {
51+
"value": "[parameters('port')]"
52+
}
53+
},
54+
"template": {
55+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
56+
"contentVersion": "1.0.0.0",
57+
"parameters": {
58+
"vmName": {
59+
"type": "string"
60+
},
61+
"location": {
62+
"type": "string"
63+
},
64+
"proxyURL": {
65+
"type": "string"
66+
},
67+
"port": {
68+
"type": "int"
69+
}
70+
},
71+
"variables": {
72+
"extensionName": "AdminCenter",
73+
"extensionPublisher": "Microsoft.AdminCenter",
74+
"extensionType": "AdminCenter"
75+
},
76+
"resources": [
77+
{
78+
"condition": "[empty(parameters('proxyURL'))]",
79+
"name": "[concat(parameters('vmName'), '/', variables('extensionName'))]",
80+
"type": "Microsoft.HybridCompute/machines/extensions",
81+
"location": "[parameters('location')]",
82+
"apiVersion": "2019-12-12",
83+
"properties": {
84+
"publisher": "[variables('extensionPublisher')]",
85+
"type": "[variables('extensionType')]",
86+
"autoUpgradeMinorVersion": true,
87+
"enableAutomaticUpgrade": true,
88+
"settings": {
89+
"port": "[parameters('port')]",
90+
"proxy": {
91+
"mode": "none"
92+
}
93+
}
94+
}
95+
},
96+
{
97+
"condition": "[not(empty(parameters('proxyURL')))]",
98+
"name": "[concat(parameters('vmName'), '/', variables('extensionName'))]",
99+
"type": "Microsoft.HybridCompute/machines/extensions",
100+
"location": "[parameters('location')]",
101+
"apiVersion": "2019-12-12",
102+
"properties": {
103+
"publisher": "[variables('extensionPublisher')]",
104+
"type": "[variables('extensionType')]",
105+
"autoUpgradeMinorVersion": true,
106+
"enableAutomaticUpgrade": true,
107+
"settings": {
108+
"port": "[parameters('port')]",
109+
"proxy": {
110+
"address": "[toLower(parameters('proxyURL'))]",
111+
"mode": "application"
112+
}
113+
}
114+
}
115+
}
116+
]
117+
}
118+
}
119+
}
120+
}
121+
}
122+
}

infrastructure/terraform/resource_group_init/core.bicep

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var roleID = {
3535
logAnalyticsContributor: '92aaf0da-9dab-42b6-94a3-d43ce8d16293'
3636
resourcePolicyContributor: '36243c78-bf99-498c-9df9-86d9f8d28608'
3737
virtualMachineAdministratorLogin: '1c0163c0-47e6-4577-8991-ea5c82e286e4'
38+
windowsAdminCenterAdministratorLogin: 'a6333a3e-0164-44c3-b281-7a577aff287f'
3839
}
3940

4041
// Define role assignments for managed identity
@@ -57,10 +58,10 @@ var miRoleAssignments = [
5758
{
5859
roleName: 'rbacAdmin'
5960
roleId: roleID.rbacAdmin
60-
description: 'RBAC Administrator. Restricted to only assign/remove: Storage Blob Data Contributor, Storage Queue Data Contributor, Azure Connected Machine Onboarding, Azure Connected Machine Resource Administrator, Log Analytics Contributor, and Virtual Machine Administrator Login.'
61+
description: 'RBAC Administrator. Restricted to only assign/remove: Storage Blob Data Contributor, Storage Queue Data Contributor, Azure Connected Machine Onboarding, Azure Connected Machine Resource Administrator, Log Analytics Contributor, Virtual Machine Administrator Login, and Windows Admin Center Administrator Login.'
6162
// Delegated RBAC: This condition restricts the RBAC Administrator to only manage specific roles.
6263
// This is a security best practice that prevents the identity from granting itself or others sensitive roles like 'Owner' or 'User Access Administrator'.
63-
condition: '((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/write\'})) OR (@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}})) AND ((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/delete\'})) OR (@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}}))'
64+
condition: '((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/write\'})) OR (@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}, ${roleID.windowsAdminCenterAdministratorLogin}})) AND ((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/delete\'})) OR (@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}, ${roleID.windowsAdminCenterAdministratorLogin}}))'
6465
conditionVersion: '2.0'
6566
}
6667
]
@@ -80,10 +81,10 @@ var groupRoleAssignments = [
8081
{
8182
roleName: 'rbacAdmin'
8283
roleId: roleID.rbacAdmin
83-
description: 'RBAC Administrator. Restricted to only assign/remove: Storage Blob Data Contributor, Storage Queue Data Contributor, Azure Connected Machine Onboarding, Azure Connected Machine Resource Administrator, Log Analytics Contributor, and Virtual Machine Administrator Login.'
84+
description: 'RBAC Administrator. Restricted to only assign/remove: Storage Blob Data Contributor, Storage Queue Data Contributor, Azure Connected Machine Onboarding, Azure Connected Machine Resource Administrator, Log Analytics Contributor, Virtual Machine Administrator Login, and Windows Admin Center Administrator Login.'
8485
// Delegated RBAC: This condition restricts the RBAC Administrator to only manage specific roles.
8586
// This is a security best practice that prevents the identity from granting itself or others sensitive roles like 'Owner' or 'User Access Administrator'.
86-
condition: '((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/write\'})) OR (@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}})) AND ((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/delete\'})) OR (@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}}))'
87+
condition: '((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/write\'})) OR (@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}, ${roleID.windowsAdminCenterAdministratorLogin}})) AND ((!(ActionMatches{\'Microsoft.Authorization/roleAssignments/delete\'})) OR (@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${roleID.storageBlobDataContributor}, ${roleID.storageQueueDataContributor}, ${roleID.AzureConnectedMachineOnboarding}, ${roleID.AzureConnectedMachineResourceAdministrator}, ${roleID.logAnalyticsContributor}, ${roleID.virtualMachineAdministratorLogin}, ${roleID.windowsAdminCenterAdministratorLogin}}))'
8788
conditionVersion: '2.0'
8889
}
8990
]

0 commit comments

Comments
 (0)