diff --git a/data_safe_haven/commands/deploy.py b/data_safe_haven/commands/deploy.py index 5df156303f..e3f6341d0d 100644 --- a/data_safe_haven/commands/deploy.py +++ b/data_safe_haven/commands/deploy.py @@ -8,7 +8,7 @@ from data_safe_haven.exceptions import DataSafeHavenError from data_safe_haven.external import GraphApi from data_safe_haven.infrastructure import SHMStackManager, SREStackManager -from data_safe_haven.provisioning import SHMProvisioningManager, SREProvisioningManager +from data_safe_haven.provisioning import SREProvisioningManager from data_safe_haven.utility import LoggingSingleton deploy_command_group = typer.Typer() @@ -73,13 +73,6 @@ def shm( config.shm.fqdn, stack.output("networking")["fqdn_nameservers"], ) - - # Provision SHM with anything that could not be done in Pulumi - manager = SHMProvisioningManager( - subscription_name=config.context.subscription_name, - stack=stack, - ) - manager.run() except DataSafeHavenError as exc: msg = f"Could not deploy Data Safe Haven Management environment.\n{exc}" raise DataSafeHavenError(msg) from exc diff --git a/data_safe_haven/infrastructure/stacks/declarative_shm.py b/data_safe_haven/infrastructure/stacks/declarative_shm.py index d0c0ff5828..aefc7a1466 100644 --- a/data_safe_haven/infrastructure/stacks/declarative_shm.py +++ b/data_safe_haven/infrastructure/stacks/declarative_shm.py @@ -4,12 +4,7 @@ from data_safe_haven.config import Config -from .shm.bastion import SHMBastionComponent, SHMBastionProps from .shm.data import SHMDataComponent, SHMDataProps -from .shm.domain_controllers import ( - SHMDomainControllersComponent, - SHMDomainControllersProps, -) from .shm.firewall import SHMFirewallComponent, SHMFirewallProps from .shm.monitoring import SHMMonitoringComponent, SHMMonitoringProps from .shm.networking import SHMNetworkingComponent, SHMNetworkingProps @@ -49,30 +44,16 @@ def run(self) -> None: "shm_firewall", self.stack_name, SHMFirewallProps( - domain_controller_private_ip=networking.domain_controller_private_ip, dns_zone=networking.dns_zone, location=self.cfg.azure.location, resource_group_name=networking.resource_group_name, route_table_name=networking.route_table.name, subnet_firewall=networking.subnet_firewall, - subnet_identity_servers=networking.subnet_identity_servers, subnet_update_servers=networking.subnet_update_servers, ), tags=self.cfg.tags.model_dump(), ) - # Deploy firewall and routing - SHMBastionComponent( - "shm_bastion", - self.stack_name, - SHMBastionProps( - location=self.cfg.azure.location, - resource_group_name=networking.resource_group_name, - subnet=networking.subnet_bastion, - ), - tags=self.cfg.tags.model_dump(), - ) - # Deploy data storage data = SHMDataComponent( "shm_data", @@ -116,32 +97,7 @@ def run(self) -> None: tags=self.cfg.tags.model_dump(), ) - # Deploy domain controllers - domain_controllers = SHMDomainControllersComponent( - "shm_domain_controllers", - self.stack_name, - SHMDomainControllersProps( - automation_account=monitoring.automation_account, - automation_account_modules=monitoring.automation_account_modules, - automation_account_private_dns=monitoring.automation_account_private_dns, - domain_fqdn=networking.dns_zone.name, - domain_netbios_name=self.shm_name.upper(), - location=self.cfg.azure.location, - log_analytics_workspace=monitoring.log_analytics_workspace, - password_domain_admin=data.password_domain_admin, - password_domain_azuread_connect=data.password_domain_azure_ad_connect, - password_domain_searcher=data.password_domain_searcher, - private_ip_address=networking.domain_controller_private_ip, - subnet_identity_servers=networking.subnet_identity_servers, - subscription_name=self.cfg.context.subscription_name, - virtual_network_name=networking.virtual_network.name, - virtual_network_resource_group_name=networking.resource_group_name, - ), - tags=self.cfg.tags.model_dump(), - ) - # Export values for later use - pulumi.export("domain_controllers", domain_controllers.exports) pulumi.export("firewall", firewall.exports) pulumi.export("monitoring", monitoring.exports) pulumi.export("networking", networking.exports) diff --git a/data_safe_haven/infrastructure/stacks/shm/bastion.py b/data_safe_haven/infrastructure/stacks/shm/bastion.py deleted file mode 100644 index f3989f2c87..0000000000 --- a/data_safe_haven/infrastructure/stacks/shm/bastion.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Pulumi component for SHM monitoring""" - -from collections.abc import Mapping - -from pulumi import ComponentResource, Input, Output, ResourceOptions -from pulumi_azure_native import network - - -class SHMBastionProps: - """Properties for SHMBastionComponent""" - - def __init__( - self, - location: Input[str], - resource_group_name: Input[str], - subnet: Input[network.GetSubnetResult], - ) -> None: - # self.automation_account_name = automation_account_name - self.location = location - self.resource_group_name = resource_group_name - self.subnet_id = Output.from_input(subnet).apply(lambda s: s.id if s.id else "") - - -class SHMBastionComponent(ComponentResource): - """Deploy SHM bastion with Pulumi""" - - def __init__( - self, - name: str, - stack_name: str, - props: SHMBastionProps, - opts: ResourceOptions | None = None, - tags: Input[Mapping[str, Input[str]]] | None = None, - ) -> None: - super().__init__("dsh:shm:BastionComponent", name, {}, opts) - child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) - child_tags = tags if tags else {} - - # Deploy IP address - public_ip = network.PublicIPAddress( - f"{self._name}_pip_bastion", - public_ip_address_name=f"{stack_name}-pip-bastion", - public_ip_allocation_method=network.IPAllocationMethod.STATIC, - resource_group_name=props.resource_group_name, - sku=network.PublicIPAddressSkuArgs( - name=network.PublicIPAddressSkuName.STANDARD - ), - opts=child_opts, - tags=child_tags, - ) - - # Deploy bastion host - bastion_host = network.BastionHost( - f"{self._name}_bastion_host", - bastion_host_name=f"{stack_name}-bas", - ip_configurations=[ - network.BastionHostIPConfigurationArgs( - public_ip_address=network.SubResourceArgs(id=public_ip.id), - subnet=network.SubResourceArgs(id=props.subnet_id), - name=f"{stack_name}-bas-ipcfg", - private_ip_allocation_method=network.IPAllocationMethod.DYNAMIC, - ) - ], - resource_group_name=props.resource_group_name, - opts=child_opts, - tags=child_tags, - ) - - # Register outputs - self.bastion_host = bastion_host diff --git a/data_safe_haven/infrastructure/stacks/shm/data.py b/data_safe_haven/infrastructure/stacks/shm/data.py index e465af948e..db8ca800da 100644 --- a/data_safe_haven/infrastructure/stacks/shm/data.py +++ b/data_safe_haven/infrastructure/stacks/shm/data.py @@ -124,48 +124,6 @@ def __init__( tags=child_tags, ) - # Secret: Domain admin password - password_domain_admin = pulumi_random.RandomPassword( - f"{self._name}_password_domain_admin", - length=20, - special=True, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), - ) - keyvault.Secret( - f"{self._name}_kvs_password_domain_admin", - properties=keyvault.SecretPropertiesArgs( - value=password_domain_admin.result - ), - resource_group_name=resource_group.name, - secret_name="password-domain-admin", - vault_name=key_vault.name, - opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=password_domain_admin) - ), - tags=child_tags, - ) - - # Secret: Azure ADConnect password - password_domain_azure_ad_connect = pulumi_random.RandomPassword( - f"{self._name}_password_domain_azure_ad_connect", - length=20, - special=True, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), - ) - keyvault.Secret( - f"{self._name}_kvs_password_domain_azure_ad_connect", - properties=keyvault.SecretPropertiesArgs( - value=password_domain_azure_ad_connect.result - ), - resource_group_name=resource_group.name, - secret_name="password-domain-azure-ad-connect", - vault_name=key_vault.name, - opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=password_domain_azure_ad_connect) - ), - tags=child_tags, - ) - # Secret: Linux update server admin password password_update_server_linux_admin = pulumi_random.RandomPassword( f"{self._name}_password_update_server_linux_admin", @@ -243,10 +201,6 @@ def __init__( ) # Register outputs - self.password_domain_admin = Output.secret(password_domain_admin.result) - self.password_domain_azure_ad_connect = Output.secret( - password_domain_azure_ad_connect.result - ) self.password_update_server_linux_admin = Output.secret( password_update_server_linux_admin.result ) diff --git a/data_safe_haven/infrastructure/stacks/shm/domain_controllers.py b/data_safe_haven/infrastructure/stacks/shm/domain_controllers.py deleted file mode 100644 index ea43e06ab7..0000000000 --- a/data_safe_haven/infrastructure/stacks/shm/domain_controllers.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Pulumi component for SHM domain controllers""" - -from collections.abc import Mapping, Sequence - -from pulumi import ComponentResource, Input, Output, ResourceOptions -from pulumi_azure_native import network, resources - -from data_safe_haven.infrastructure.common import get_name_from_subnet -from data_safe_haven.infrastructure.components import ( - AutomationDscNode, - AutomationDscNodeProps, - VMComponent, - WindowsVMComponentProps, - WrappedAutomationAccount, - WrappedLogAnalyticsWorkspace, -) -from data_safe_haven.resources import resources_path -from data_safe_haven.utility import FileReader - - -class SHMDomainControllersProps: - """Properties for SHMDomainControllersComponent""" - - def __init__( - self, - automation_account: Input[WrappedAutomationAccount], - automation_account_modules: Input[Sequence[str]], - automation_account_private_dns: Input[network.PrivateDnsZoneGroup], - domain_fqdn: Input[str], - domain_netbios_name: Input[str], - location: Input[str], - log_analytics_workspace: Input[WrappedLogAnalyticsWorkspace], - password_domain_admin: Input[str], - password_domain_azuread_connect: Input[str], - password_domain_searcher: Input[str], - private_ip_address: Input[str], - subnet_identity_servers: Input[network.GetSubnetResult], - subscription_name: Input[str], - virtual_network_name: Input[str], - virtual_network_resource_group_name: Input[str], - ) -> None: - self.automation_account = automation_account - self.automation_account_modules = automation_account_modules - self.automation_account_private_dns = automation_account_private_dns - self.domain_fqdn = domain_fqdn - self.domain_root_dn = Output.from_input(domain_fqdn).apply( - lambda dn: f"DC={dn.replace('.', ',DC=')}" - ) - self.domain_netbios_name = Output.from_input(domain_netbios_name).apply( - lambda n: n[:15] - ) # maximum of 15 characters - self.location = location - self.log_analytics_workspace = log_analytics_workspace - self.password_domain_admin = password_domain_admin - self.password_domain_azuread_connect = password_domain_azuread_connect - self.password_domain_searcher = password_domain_searcher - self.private_ip_address = private_ip_address - self.subnet_name = Output.from_input(subnet_identity_servers).apply( - get_name_from_subnet - ) - self.subscription_name = subscription_name - # Note that usernames have a maximum of 20 characters - self.username_domain_admin = "dshdomainadmin" - self.username_domain_azuread_connect = "dshazureadsync" - self.username_domain_searcher = "dshldapsearcher" - self.virtual_network_name = virtual_network_name - self.virtual_network_resource_group_name = virtual_network_resource_group_name - - -class SHMDomainControllersComponent(ComponentResource): - """Deploy SHM secrets with Pulumi""" - - def __init__( - self, - name: str, - stack_name: str, - props: SHMDomainControllersProps, - opts: ResourceOptions | None = None, - tags: Input[Mapping[str, Input[str]]] | None = None, - ) -> None: - super().__init__("dsh:shm:DomainControllersComponent", name, {}, opts) - child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) - child_tags = tags if tags else {} - - # Deploy resource group - resource_group = resources.ResourceGroup( - f"{self._name}_resource_group", - location=props.location, - resource_group_name=f"{stack_name}-rg-identity", - opts=child_opts, - tags=child_tags, - ) - - # Create the Domain Controller - # We use the domain admin credentials here as the VM admin is promoted to a - # domain admin when setting up the domain - primary_domain_controller = VMComponent( - f"{self._name}_primary_domain_controller", - WindowsVMComponentProps( - admin_password=props.password_domain_admin, - admin_username=props.username_domain_admin, - ip_address_public=False, - ip_address_private=props.private_ip_address, - location=props.location, - log_analytics_workspace=props.log_analytics_workspace, - resource_group_name=resource_group.name, - subnet_name=props.subnet_name, - virtual_network_name=props.virtual_network_name, - virtual_network_resource_group_name=props.virtual_network_resource_group_name, - vm_name=f"{stack_name[:11]}-dc1", - vm_size="Standard_DS2_v2", - ), - opts=child_opts, - tags=child_tags, - ) - # Register the primary domain controller for automated DSC - dsc_configuration_name = "PrimaryDomainController" - dsc_reader = FileReader( - resources_path - / "desired_state_configuration" - / f"{dsc_configuration_name}.ps1" - ) - AutomationDscNode( - f"{self._name}_primary_domain_controller_dsc_node", - AutomationDscNodeProps( - automation_account=props.automation_account, - configuration_name=dsc_configuration_name, - dsc_description="DSC for Data Safe Haven primary domain controller", - dsc_file=dsc_reader, - dsc_parameters=Output.all( - AzureADConnectPassword=props.password_domain_azuread_connect, - AzureADConnectUsername=props.username_domain_azuread_connect, - DomainAdministratorPassword=props.password_domain_admin, - DomainAdministratorUsername=props.username_domain_admin, - DomainName=props.domain_fqdn, - DomainNetBios=props.domain_netbios_name, - DomainRootDn=props.domain_root_dn, - LDAPSearcherPassword=props.password_domain_searcher, - LDAPSearcherUsername=props.username_domain_searcher, - ), - dsc_required_modules=props.automation_account_modules, - location=props.location, - subscription_name=props.subscription_name, - vm=primary_domain_controller, - ), - opts=ResourceOptions.merge( - child_opts, - ResourceOptions( - depends_on=[props.automation_account_private_dns], - parent=primary_domain_controller, - ), - ), - tags=child_tags, - ) - - # Register outputs - self.resource_group_name = resource_group.name - - # Register exports - self.exports = { - "ldap_root_dn": props.domain_root_dn, - "ldap_search_username": props.username_domain_searcher, - "ldap_server_ip": primary_domain_controller.ip_address_private, - "netbios_name": props.domain_netbios_name, - "resource_group_name": resource_group.name, - "vm_name": primary_domain_controller.vm_name, - } diff --git a/data_safe_haven/infrastructure/stacks/shm/firewall.py b/data_safe_haven/infrastructure/stacks/shm/firewall.py index 2ba2476056..55e712b55a 100644 --- a/data_safe_haven/infrastructure/stacks/shm/firewall.py +++ b/data_safe_haven/infrastructure/stacks/shm/firewall.py @@ -17,16 +17,13 @@ class SHMFirewallProps: def __init__( self, - domain_controller_private_ip: Input[str], dns_zone: Input[network.Zone], location: Input[str], resource_group_name: Input[str], route_table_name: Input[str], subnet_firewall: Input[network.GetSubnetResult], - subnet_identity_servers: Input[network.GetSubnetResult], subnet_update_servers: Input[network.GetSubnetResult], ) -> None: - self.domain_controller_private_ip = domain_controller_private_ip self.dns_zone_name = Output.from_input(dns_zone).apply(lambda zone: zone.name) self.location = location self.resource_group_name = resource_group_name @@ -34,9 +31,6 @@ def __init__( self.subnet_firewall_id = Output.from_input(subnet_firewall).apply( get_id_from_subnet ) - self.subnet_identity_servers_iprange = Output.from_input( - subnet_identity_servers - ).apply(lambda s: str(s.address_prefix) if s.address_prefix else "") self.subnet_update_servers_iprange = Output.from_input( subnet_update_servers ).apply(lambda s: str(s.address_prefix) if s.address_prefix else "") @@ -107,211 +101,6 @@ def __init__( f"{self._name}_firewall", additional_properties={"Network.DNS.EnableProxy": "true"}, application_rule_collections=[ - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs(type="Allow"), - name=f"{stack_name}-identity-servers", - priority=FirewallPriorities.SHM_IDENTITY_SERVERS, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external operational requests from AzureAD Connect", - name="AllowExternalAzureADConnectOperations", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "*.blob.core.windows.net", - "*.servicebus.windows.net", - "aadconnecthealth.azure.com", - "adminwebservice.microsoftonline.com", - "s1.adhybridhealth.azure.com", - "umwatson.events.data.microsoft.com", - "v10.events.data.microsoft.com", - "v20.events.data.microsoft.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external password reset requests from AzureAD Connect", - name="AllowExternalAzureADConnectPasswordReset", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "*-sb.servicebus.windows.net", - "*.servicebus.windows.net", - "passwordreset.microsoftonline.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external setup requests from AzureAD Connect", - name="AllowExternalAzureADConnectSetup", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "s1.adhybridhealth.azure.com", - "management.azure.com", - "policykeyservice.dc.ad.msft.net", - "www.office.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external AzureAD login requests", - name="AllowExternalAzureADLogin", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "aadcdn.msftauth.net", - "login.live.com", - "login.microsoftonline.com", - "login.windows.net", - "secure.aadcdn.microsoftonline-p.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external AzureMFAConnect operational requests", - name="AllowExternalAzureMFAConnectOperations", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "css.phonefactor.net", - "pfd.phonefactor.net", - "pfd2.phonefactor.net", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external AzureMFAConnect setup requests", - name="AllowExternalAzureMFAConnectSetup", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "adnotifications.windowsazure.com", - "credentials.azure.com", - "strongauthenticationservice.auth.microsoft.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external certificate setup requests", - name="AllowExternalCertificateStatusCheck", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "crl.microsoft.com", - "crl3.digicert.com", - "crl4.digicert.com", - "ocsp.digicert.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external script downloads from GitHub", - name="AllowExternalGitHubScriptDownload", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "raw.githubusercontent.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external Powershell module installation requests", - name="AllowExternalPowershellModuleInstallation", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "psg-prod-eastus.azureedge.net", - "www.powershellgallery.com", - ], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external MSOnline connection requests", - name="AllowExternalPowershellModuleMSOnlineConnections", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ) - ], - source_addresses=[props.subnet_update_servers_iprange], - target_fqdns=["provisioningapi.microsoftonline.com"], - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external Windows update requests", - name="AllowExternalWindowsUpdate", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=80, - protocol_type="Http", - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=443, - protocol_type="Https", - ), - ], - source_addresses=[props.subnet_identity_servers_iprange], - target_fqdns=[ - "au.download.windowsupdate.com", - "ctldl.windowsupdate.com", - "download.microsoft.com", - "download.windowsupdate.com", - "fe2cr.update.microsoft.com", - "fe3cr.delivery.mp.microsoft.com", - "geo-prod.do.dsp.mp.microsoft.com", - "go.microsoft.com", - "ntservicepack.microsoft.com", - "onegetcdn.azureedge.net", - "settings-win.data.microsoft.com", - "slscr.update.microsoft.com", - "test.stats.update.microsoft.com", - "tlu.dl.delivery.mp.microsoft.com", - "umwatson.events.data.microsoft.com", - "v10.events.data.microsoft.com", - "v10.vortex-win.data.microsoft.com", - "v20.events.data.microsoft.com", - "windowsupdate.microsoft.com", - ], - ), - ], - ), network.AzureFirewallApplicationRuleCollectionArgs( action=network.AzureFirewallRCActionArgs(type="Allow"), name=f"{stack_name}-any", @@ -337,7 +126,7 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=123, - protocol_type="Http", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, ) ], source_addresses=["*"], @@ -356,11 +145,11 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=80, - protocol_type="Http", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, ), network.AzureFirewallApplicationRuleProtocolArgs( port=443, - protocol_type="Https", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, ), ], source_addresses=[props.subnet_update_servers_iprange], @@ -392,7 +181,7 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=443, - protocol_type="Https", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, ) ], source_addresses=sre_identity_server_subnets, @@ -414,7 +203,7 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=443, - protocol_type="Https", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, ) ], source_addresses=sre_package_repositories_subnets, @@ -426,7 +215,7 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=443, - protocol_type="Https", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, ) ], source_addresses=sre_package_repositories_subnets, @@ -445,7 +234,7 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=443, - protocol_type="Https", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, ) ], source_addresses=sre_remote_desktop_gateway_subnets, @@ -464,11 +253,11 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=80, - protocol_type="Http", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, ), network.AzureFirewallApplicationRuleProtocolArgs( port=443, - protocol_type="Https", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, ), ], source_addresses=sre_workspaces_subnets, @@ -484,7 +273,7 @@ def __init__( protocols=[ network.AzureFirewallApplicationRuleProtocolArgs( port=11371, - protocol_type="Http", + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, ), ], source_addresses=sre_workspaces_subnets, @@ -505,24 +294,6 @@ def __init__( ], location=props.location, network_rule_collections=[ - network.AzureFirewallNetworkRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs(type="Allow"), - name=f"{stack_name}-identity-servers", - priority=FirewallPriorities.SHM_IDENTITY_SERVERS, - rules=[ - network.AzureFirewallNetworkRuleArgs( - description="Allow external DNS resolver", - destination_addresses=[external_dns_resolver], - destination_ports=["53"], - name="AllowExternalDnsResolver", - protocols=[ - network.AzureFirewallNetworkRuleProtocol.TCP, - network.AzureFirewallNetworkRuleProtocol.UDP, - ], - source_addresses=[props.subnet_identity_servers_iprange], - ), - ], - ), network.AzureFirewallNetworkRuleCollectionArgs( action=network.AzureFirewallRCActionArgs(type="Allow"), name=f"{stack_name}-all", diff --git a/data_safe_haven/infrastructure/stacks/shm/networking.py b/data_safe_haven/infrastructure/stacks/shm/networking.py index f0271e50fc..b950b35b52 100644 --- a/data_safe_haven/infrastructure/stacks/shm/networking.py +++ b/data_safe_haven/infrastructure/stacks/shm/networking.py @@ -26,7 +26,6 @@ def __init__( self.subnet_bastion_iprange = self.vnet_iprange.next_subnet(64) # Firewall subnet must be at least /26 in size (64 addresses) self.subnet_firewall_iprange = self.vnet_iprange.next_subnet(64) - self.subnet_identity_servers_iprange = self.vnet_iprange.next_subnet(8) # Monitoring subnet needs 2 IP addresses for automation and 13 for log analytics self.subnet_monitoring_iprange = self.vnet_iprange.next_subnet(32) self.subnet_update_servers_iprange = self.vnet_iprange.next_subnet(8) @@ -193,58 +192,6 @@ def __init__( opts=child_opts, tags=child_tags, ) - nsg_identity_servers = network.NetworkSecurityGroup( - f"{self._name}_nsg_identity", - network_security_group_name=f"{stack_name}-nsg-identity", - resource_group_name=resource_group.name, - security_rules=[ - # Inbound - network.SecurityRuleArgs( - access=network.SecurityRuleAccess.ALLOW, - description="Allow inbound LDAP to domain controllers.", - destination_address_prefix=str( - props.subnet_identity_servers_iprange - ), - destination_port_ranges=["389", "636"], - direction=network.SecurityRuleDirection.INBOUND, - name="AllowLDAPClientUDPInbound", - priority=NetworkingPriorities.INTERNAL_SHM_LDAP_UDP, - protocol=network.SecurityRuleProtocol.UDP, - source_address_prefix="VirtualNetwork", - source_port_range="*", - ), - network.SecurityRuleArgs( - access=network.SecurityRuleAccess.ALLOW, - description="Allow inbound LDAP to domain controllers.", - destination_address_prefix=str( - props.subnet_identity_servers_iprange - ), - destination_port_ranges=["389", "636"], - direction=network.SecurityRuleDirection.INBOUND, - name="AllowLDAPClientTCPInbound", - priority=NetworkingPriorities.INTERNAL_SHM_LDAP_TCP, - protocol=network.SecurityRuleProtocol.TCP, - source_address_prefix="VirtualNetwork", - source_port_range="*", - ), - network.SecurityRuleArgs( - access=network.SecurityRuleAccess.ALLOW, - description="Allow inbound RDP connections from admins using AzureBastion.", - destination_address_prefix=str( - props.subnet_identity_servers_iprange - ), - destination_port_ranges=["3389"], - direction=network.SecurityRuleDirection.INBOUND, - name="AllowBastionAdminsInbound", - priority=NetworkingPriorities.INTERNAL_SHM_BASTION, - protocol=network.SecurityRuleProtocol.TCP, - source_address_prefix=str(props.subnet_bastion_iprange), - source_port_range="*", - ), - ], - opts=child_opts, - tags=child_tags, - ) nsg_monitoring = network.NetworkSecurityGroup( f"{self._name}_nsg_monitoring", network_security_group_name=f"{stack_name}-nsg-monitoring", @@ -344,7 +291,6 @@ def __init__( # Define the virtual network and its subnets subnet_firewall_name = "AzureFirewallSubnet" # this name is forced by https://docs.microsoft.com/en-us/azure/firewall/tutorial-firewall-deploy-portal subnet_bastion_name = "AzureBastionSubnet" # this name is forced by https://learn.microsoft.com/en-us/azure/bastion/configuration-settings#subnet - subnet_identity_servers_name = "IdentityServersSubnet" subnet_monitoring_name = "MonitoringSubnet" subnet_update_servers_name = "UpdateServersSubnet" virtual_network = network.VirtualNetwork( @@ -370,15 +316,6 @@ def __init__( network_security_group=None, # the firewall subnet must NOT have an NSG route_table=None, # the firewall subnet must NOT be attached to the route table ), - # Identity servers subnet - network.SubnetArgs( - address_prefix=str(props.subnet_identity_servers_iprange), - name=subnet_identity_servers_name, - network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_identity_servers.id - ), - route_table=network.RouteTableArgs(id=route_table.id), - ), # Monitoring subnet network.SubnetArgs( address_prefix=str(props.subnet_monitoring_iprange), @@ -487,9 +424,6 @@ def __init__( # Register outputs self.dns_zone = dns_zone - self.domain_controller_private_ip = str( - props.subnet_identity_servers_iprange.available()[0] - ) self.private_dns_zone_base_id = private_zone_ids[0] self.resource_group_name = Output.from_input(resource_group.name) self.route_table = route_table @@ -503,11 +437,6 @@ def __init__( resource_group_name=resource_group.name, virtual_network_name=virtual_network.name, ) - self.subnet_identity_servers = network.get_subnet_output( - subnet_name=subnet_identity_servers_name, - resource_group_name=resource_group.name, - virtual_network_name=virtual_network.name, - ) self.subnet_monitoring = network.get_subnet_output( subnet_name=subnet_monitoring_name, resource_group_name=resource_group.name, @@ -528,9 +457,6 @@ def __init__( "subnet_bastion_prefix": self.subnet_bastion.apply( lambda s: str(s.address_prefix) if s.address_prefix else "" ), - "subnet_identity_servers_prefix": self.subnet_identity_servers.apply( - lambda s: str(s.address_prefix) if s.address_prefix else "" - ), "subnet_monitoring_prefix": self.subnet_monitoring.apply( lambda s: str(s.address_prefix) if s.address_prefix else "" ), diff --git a/data_safe_haven/provisioning/__init__.py b/data_safe_haven/provisioning/__init__.py index 704c840043..53186736d6 100644 --- a/data_safe_haven/provisioning/__init__.py +++ b/data_safe_haven/provisioning/__init__.py @@ -1,9 +1,7 @@ """Provisioning for deployed Data Safe Haven infrastructure.""" -from .shm_provisioning_manager import SHMProvisioningManager from .sre_provisioning_manager import SREProvisioningManager __all__ = [ - "SHMProvisioningManager", "SREProvisioningManager", ] diff --git a/data_safe_haven/provisioning/shm_provisioning_manager.py b/data_safe_haven/provisioning/shm_provisioning_manager.py deleted file mode 100644 index 77429a88ea..0000000000 --- a/data_safe_haven/provisioning/shm_provisioning_manager.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Provisioning manager for a deployed SHM.""" - -from data_safe_haven.external import AzureApi -from data_safe_haven.infrastructure import SHMStackManager - - -class SHMProvisioningManager: - """Provisioning manager for a deployed SHM.""" - - def __init__( - self, - subscription_name: str, - stack: SHMStackManager, - ): - super().__init__() - self.subscription_name = subscription_name - domain_controllers_resource_group_name = stack.output("domain_controllers")[ - "resource_group_name" - ] - domain_controllers_vm_name = stack.output("domain_controllers")["vm_name"] - - # Construct DC restart parameters - self.dc_restart_params = { - "resource_group_name": domain_controllers_resource_group_name, - "vm_name": domain_controllers_vm_name, - } - - def restart_domain_controllers(self) -> None: - azure_api = AzureApi(self.subscription_name) - azure_api.restart_virtual_machine( - self.dc_restart_params["resource_group_name"], - self.dc_restart_params["vm_name"], - ) - - def run(self) -> None: - """Apply SHM configuration""" - self.restart_domain_controllers() diff --git a/data_safe_haven/resources/desired_state_configuration/DisconnectAD.ps1 b/data_safe_haven/resources/desired_state_configuration/DisconnectAD.ps1 deleted file mode 100644 index a237deac50..0000000000 --- a/data_safe_haven/resources/desired_state_configuration/DisconnectAD.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -# NB. This solves the issue of orphaned AAD users when the local AD is deleted -# https://support.microsoft.com/en-gb/help/2619062/you-can-t-manage-or-remove-objects-that-were-synchronized-through-the - -# Ensure that MSOnline is installed for current user -if (-not (Get-Module -ListAvailable -Name MSOnline)) { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Install-Module -Name MSOnline -Force -} - -if (Get-Module -ListAvailable -Name MSOnline) { - Write-Output "INFO: Please use the username and password for an Azure AD global admin." - Connect-MsolService - # Print the current synchronisation status - if ((Get-MSOLCompanyInformation).DirectorySynchronizationEnabled) { - Write-Output "INFO: Directory synchronisation is ENABLED" - Write-Output "INFO: Removing synchronised users..." - Get-MsolUser -Synchronized | Remove-MsolUser -Force - Write-Output "INFO: Disabling directory synchronisation..." - Set-MsolDirSyncEnabled -EnableDirSync $False -Force - # Print the current synchronisation status - if ((Get-MSOLCompanyInformation).DirectorySynchronizationEnabled) { - Write-Output "ERROR: Directory synchronisation is still ENABLED" - } else { - Write-Output "INFO: Directory synchronisation is now DISABLED" - } - } else { - Write-Output "WARNING: Directory synchronisation is already DISABLED" - } - # Remove user-added service principals except the MFA service principal - Write-Output "INFO: Removing any user-added service principals..." - $nServicePrincipalsBefore = (Get-MsolServicePrincipal | Measure-Object).Count - Get-MsolServicePrincipal | Where-Object { $_.AppPrincipalId -ne "981f26a1-7f43-403b-a875-f8b09b8cd720" } | Remove-MsolServicePrincipal 2>&1 | Out-Null - $nServicePrincipalsAfter = (Get-MsolServicePrincipal | Measure-Object).Count - Write-Output "INFO: Removed $($nServicePrincipalsBefore - $nServicePrincipalsAfter) service principal(s). There are $nServicePrincipalsAfter remaining" -} diff --git a/data_safe_haven/resources/desired_state_configuration/PrimaryDomainController.ps1 b/data_safe_haven/resources/desired_state_configuration/PrimaryDomainController.ps1 deleted file mode 100644 index 755603d8d5..0000000000 --- a/data_safe_haven/resources/desired_state_configuration/PrimaryDomainController.ps1 +++ /dev/null @@ -1,477 +0,0 @@ -# Note that we require the following DSC modules to be installed -# - ActiveDirectoryDsc -# - PSModulesDsc -# - xPendingReboot -# - xPSDesiredStateConfiguration -# Note that logs are in C:\Windows\System32\Configuration\ConfigurationStatus - -Configuration InstallPowershellModules { - Import-DscResource -ModuleName PSModulesDsc -ModuleVersion 1.0.13.0 - - Node localhost { - PowershellModule MSOnline { - Ensure = "Present" - Name = "MSOnline" - RequiredVersion = "1.1.183.66" - } - } -} - -Configuration InstallActiveDirectory { - param ( - [Parameter(HelpMessage = "Path to Active Directory log volume")] - [ValidateNotNullOrEmpty()] - [String]$IADActiveDirectoryLogPath, - - [Parameter(HelpMessage = "Path to Active Directory NTDS volume")] - [ValidateNotNullOrEmpty()] - [String]$IADActiveDirectoryNtdsPath, - - [Parameter(HelpMessage = "Path to Active Directory system volume")] - [ValidateNotNullOrEmpty()] - [String]$IADActiveDirectorySysvolPath, - - [Parameter(Mandatory = $true, HelpMessage = "Domain administrator password")] - [ValidateNotNullOrEmpty()] - [String]$IADDomainAdministratorPassword, - - [Parameter(Mandatory = $true, HelpMessage = "Domain administrator password")] - [ValidateNotNullOrEmpty()] - [String]$IADDomainAdministratorUsername, - - [Parameter(Mandatory = $true, HelpMessage = "FQDN for the SHM domain")] - [ValidateNotNullOrEmpty()] - [String]$IADDomainName, - - [Parameter(Mandatory = $true, HelpMessage = "NetBIOS name for the domain")] - [ValidateNotNullOrEmpty()] - [String]$IADDomainNetBiosName - ) - - Import-DscResource -ModuleName ActiveDirectoryDsc -ModuleVersion 6.2.0 - Import-DscResource -ModuleName xPendingReboot -ModuleVersion 0.4.0.0 # note that ComputerManagementDsc is too old to include PendingReboot - - # Construct variables for passing to DSC configurations - $DomainAdministratorPasswordSecure = ConvertTo-SecureString -String $IADDomainAdministratorPassword -AsPlainText -Force - $DomainAdministratorCredentials = New-Object System.Management.Automation.PSCredential ("${IADDomainName}\$($IADDomainAdministratorUsername)", $DomainAdministratorPasswordSecure) - $SafeModeAdministratorCredentials = New-Object System.Management.Automation.PSCredential ("safemode${IADDomainAdministratorUsername}".ToLower(), $DomainAdministratorPasswordSecure) - - Node localhost { - # Allow forced reboots - LocalConfigurationManager { - ActionAfterReboot = "ContinueConfiguration" - ConfigurationMode = "ApplyOnly" - RebootNodeIfNeeded = $true - } - - # Install Active Directory domain services - WindowsFeature ADDomainServices { # built-in - Ensure = "Present" - Name = "AD-Domain-Services" - } - - # Install Active Directory domain services tools - WindowsFeature ADDSTools { # built-in - Ensure = "Present" - Name = "RSAT-ADDS-Tools" - } - - # Install Active Directory admin centre - WindowsFeature ADAdminCenter { # built-in - Ensure = "Present" - Name = "RSAT-AD-AdminCenter" - } - - # Reboot - xPendingReboot RebootBeforePromotion { # from xPendingReboot - Name = "RebootBeforePromotion" - SkipCcmClientSDK = $true - DependsOn = @("[WindowsFeature]ADDomainServices", "[WindowsFeature]ADDSTools", "[WindowsFeature]ADAdminCenter") - } - - # Create the domain - ADDomain Domain { # from ActiveDirectoryDsc - Credential = $DomainAdministratorCredentials - DatabasePath = $IADActiveDirectoryNtdsPath - DomainName = $IADDomainName - DomainNetBiosName = $IADDomainNetBiosName - LogPath = $IADActiveDirectoryLogPath - SafeModeAdministratorPassword = $SafeModeAdministratorCredentials - SysvolPath = $IADActiveDirectorySysvolPath - DependsOn = @("[WindowsFeature]ADDomainServices", "[xPendingReboot]RebootBeforePromotion") - } - - # Disable network-level authentication - Registry DisableRDPNLA { # built-in - Key = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" - ValueName = "UserAuthentication" - ValueData = 0 - ValueType = "Dword" - Ensure = "Present" - DependsOn = "[ADDomain]Domain" - } - - # Reboot - xPendingReboot RebootAfterPromotion { # from xPendingReboot - Name = "RebootAfterPromotion" - SkipCcmClientSDK = $true - DependsOn = "[Registry]DisableRDPNLA" - } - } -} - -Configuration ConfigureActiveDirectory { - param ( - [Parameter(HelpMessage = "AzureAD connect password")] - [ValidateNotNullOrEmpty()] - [String]$CADAzureADConnectPassword, - - [Parameter(HelpMessage = "AzureAD connect user name")] - [ValidateNotNullOrEmpty()] - [String]$CADAzureADConnectUsername, - - [Parameter(Mandatory = $true, HelpMessage = "Domain administrator password")] - [ValidateNotNullOrEmpty()] - [String]$CADDomainAdministratorPassword, - - [Parameter(Mandatory = $true, HelpMessage = "Domain administrator password")] - [ValidateNotNullOrEmpty()] - [String]$CADDomainAdministratorUsername, - - [Parameter(Mandatory = $true, HelpMessage = "FQDN for the SHM domain")] - [ValidateNotNullOrEmpty()] - [String]$CADDomainName, - - [Parameter(Mandatory = $true, HelpMessage = "Root DN for the SHM domain")] - [ValidateNotNullOrEmpty()] - [String]$CADDomainRootDn, - - [Parameter(Mandatory = $true, HelpMessage = "LDAP searcher password")] - [ValidateNotNullOrEmpty()] - [String]$CADLDAPSearcherPassword, - - [Parameter(Mandatory = $true, HelpMessage = "LDAP searcher username")] - [ValidateNotNullOrEmpty()] - [String]$CADLDAPSearcherUsername - ) - - Import-DscResource -ModuleName ActiveDirectoryDsc -ModuleVersion 6.2.0 - - # Construct variables for passing to DSC configurations - $DomainAdministratorPasswordSecure = ConvertTo-SecureString -String $CADDomainAdministratorPassword -AsPlainText -Force - $DomainAdministratorCredentials = New-Object System.Management.Automation.PSCredential ("${CADDomainName}\$($CADDomainAdministratorUsername)", $DomainAdministratorPasswordSecure) - $CADDomainRootDn = "DC=$($CADDomainName.Replace('.',',DC='))" - $DataSafeHavenUnits = @{ - DomainComputers = @{ - Description = "Data Safe Haven Domain Computers" - Path = "OU=Data Safe Haven Domain Computers,${CADDomainRootDn}" - } - ResearchUsers = @{ - Description = "Data Safe Haven Research Users" - Path = "OU=Data Safe Haven Research Users,${CADDomainRootDn}" - } - SecurityGroups = @{ - Description = "Data Safe Haven Security Groups" - Path = "OU=Data Safe Haven Security Groups,${CADDomainRootDn}" - } - ServiceAccounts = @{ - Description = "Data Safe Haven Service Accounts" - Path = "OU=Data Safe Haven Service Accounts,${CADDomainRootDn}" - } - } - $DataSafeHavenServiceAccounts = @{ - AzureADSynchroniser = @{ - Description = "Azure Active Directory synchronisation manager" - Password = $CADAzureADConnectPassword - Username = $CADAzureADConnectUsername - } - LDAPSearcher = @{ - Description = "DSH LDAP searcher" - Password = $CADLDAPSearcherPassword - Username = $CADLDAPSearcherUsername - } - } - $DataSafeHavenGroups = @{ - ServerAdministrators = @{ - Description = "Data Safe Haven Server Administrators" - Members = @($CADDomainAdministratorUsername) - } - } - $ADGuid = @{ - "lockoutTime" = "28630ebf-41d5-11d1-a9c1-0000f80367c1"; - "mS-DS-ConsistencyGuid" = "23773dc2-b63a-11d2-90e1-00c04fd91ab1"; - "msDS-KeyCredentialLink" = "5b47d60f-6090-40b2-9f37-2a4de88f3063"; - "pwdLastSet" = "bf967a0a-0de6-11d0-a285-00aa003049e2"; - "user" = "bf967aba-0de6-11d0-a285-00aa003049e2"; - } - $ADExtendedRights = @{ - "Change Password" = "ab721a53-1e2f-11d0-9819-00aa0040529b"; - "Reset Password" = "00299570-246d-11d0-a768-00aa006e0529"; - } - - Node localhost { - # Enable the AD recycle bin - ADOptionalFeature RecycleBin { # from ActiveDirectoryDsc - EnterpriseAdministratorCredential = $DomainAdministratorCredentials - FeatureName = "Recycle Bin Feature" - ForestFQDN = $CADDomainName - } - - # Set domain admin password to never expire - ADUser SetAdminPasswordExpiry { # from ActiveDirectoryDsc - UserName = $CADDomainAdministratorUsername - DomainName = $CADDomainName - PasswordNeverExpires = $true - } - - # Disable domain admin minimum password age - ADDomainDefaultPasswordPolicy SetAdminPasswordMinimumAge { # from ActiveDirectoryDsc - Credential = $DomainAdministratorCredentials - DomainName = $CADDomainName - MinPasswordAge = 0 - } - - # Create organisational units - foreach ($Unit in $DataSafeHavenUnits.Keys) { - ADOrganizationalUnit "$Unit" { # from ActiveDirectoryDsc - Credential = $DomainAdministratorCredentials - Description = "$($DataSafeHavenUnits[$Unit].Description)" - Ensure = "Present" - Name = "$($DataSafeHavenUnits[$Unit].Description)" - Path = $CADDomainRootDn - ProtectedFromAccidentalDeletion = $true - } - } - - # Create service users - foreach ($User in $DataSafeHavenServiceAccounts.Keys) { - $UserCredentials = (New-Object System.Management.Automation.PSCredential ($DataSafeHavenServiceAccounts[$User].Username, (ConvertTo-SecureString -String $DataSafeHavenServiceAccounts[$User].Password -AsPlainText -Force))) - ADUser "$User" { - Description = "$($DataSafeHavenServiceAccounts[$User].Description)" - DisplayName = "$($DataSafeHavenServiceAccounts[$User].Description)" - DomainName = "$CADDomainName" - Ensure = "Present" - Password = $UserCredentials - PasswordNeverExpires = $true - Path = "$($DataSafeHavenUnits.ServiceAccounts.Path)" - UserName = "$($UserCredentials.UserName)" - } - } - - # Create security groups - foreach ($Group in $DataSafeHavenGroups.Keys) { - ADGroup $Group { # from ActiveDirectoryDsc - Category = "Security" - Description = "$($DataSafeHavenGroups[$Group].Description)" - Ensure = "Present" - GroupName = "$($DataSafeHavenGroups[$Group].Description)" - GroupScope = "Global" - Members = $DataSafeHavenGroups[$Group].Members - Path = $DataSafeHavenUnits.SecurityGroups.Path - } - } - - # Give write permissions to the local AD sync account - foreach ($Property in @("lockoutTime", "pwdLastSet", "mS-DS-ConsistencyGuid", "msDS-KeyCredentialLink")) { - ADObjectPermissionEntry "$Property" { - AccessControlType = "Allow" - ActiveDirectoryRights = "WriteProperty" - ActiveDirectorySecurityInheritance = "Descendents" - Ensure = "Present" - IdentityReference = $DataSafeHavenServiceAccounts.AzureADSynchroniser.UserName - InheritedObjectType = $ADGuid["user"] - ObjectType = $ADGuid[$Property] - Path = $CADDomainRootDn - DependsOn = "[ADUser]AzureADSynchroniser" - } - } - - # Give extended rights to the local AD sync account - foreach ($ExtendedRight in @("Change Password", "Reset Password")) { - ADObjectPermissionEntry "$ExtendedRight" { - AccessControlType = "Allow" - ActiveDirectoryRights = "ExtendedRight" - ActiveDirectorySecurityInheritance = "Descendents" - Ensure = "Present" - IdentityReference = $DataSafeHavenServiceAccounts.AzureADSynchroniser.UserName - InheritedObjectType = $ADGuid["user"] - ObjectType = $ADExtendedRights[$ExtendedRight] - Path = $CADDomainRootDn - DependsOn = "[ADUser]AzureADSynchroniser" - } - } - - # Allow the AzureAD synchroniser account to replicate directory changes - Script SetAzureADSynchroniserPermissions { - SetScript = { - try { - $success = $true - $AzureADSyncUsername = $using:DataSafeHavenServiceAccounts.AzureADSynchroniser.Username - $AzureADSyncSID = (Get-ADUser -Identity $AzureADSyncUsername).SID - $DefaultNamingContext = $(Get-ADRootDSE).DefaultNamingContext - $ConfigurationNamingContext = $(Get-ADRootDSE).ConfigurationNamingContext - $null = dsacls "$DefaultNamingContext" /G "${AzureADSyncSID}:CA;Replicating Directory Changes" - $success = $success -and $? - $null = dsacls "$ConfigurationNamingContext" /G "${AzureADSyncSID}:CA;Replicating Directory Changes" - $success = $success -and $? - $null = dsacls "$DefaultNamingContext" /G "${AzureADSyncSID}:CA;Replicating Directory Changes All" - $success = $success -and $? - $null = dsacls "$ConfigurationNamingContext" /G "${AzureADSyncSID}:CA;Replicating Directory Changes All" - $success = $success -and $? - if ($success) { - Write-Verbose -Message "Successfully updated ACL permissions for AD Sync Service account '$AzureADSyncUsername'" - } else { - throw "Failed to update ACL permissions for AD Sync Service account '$AzureADSyncUsername'!" - } - } catch { - Write-Error "SetAzureADSynchroniserPermissions::SetScript $($_.Exception)" - } - } - GetScript = { @{} } - TestScript = { - try { - $success = $true - $AzureADSyncUsername = $using:DataSafeHavenServiceAccounts.AzureADSynchroniser.Username - $DefaultNamingContext = $(Get-ADRootDSE).DefaultNamingContext - $ConfigurationNamingContext = $(Get-ADRootDSE).ConfigurationNamingContext - $success = $success -and $($null -ne $(dsacls "$DefaultNamingContext" | Select-String "$AzureADSyncUsername" | Select-String "Replicating Directory Changes$")) - $success = $success -and $($null -ne $(dsacls "$ConfigurationNamingContext" | Select-String "$AzureADSyncUsername" | Select-String "Replicating Directory Changes$")) - $success = $success -and $($null -ne $(dsacls "$DefaultNamingContext" | Select-String "$AzureADSyncUsername" | Select-String "Replicating Directory Changes All")) - $success = $success -and $($null -ne $(dsacls "$ConfigurationNamingContext" | Select-String "$AzureADSyncUsername" | Select-String "Replicating Directory Changes All")) - $success - } catch { - Write-Error "SetAzureADSynchroniserPermissions::TestScript $($_.Exception)" - $false - } - } - DependsOn = "[ADUser]AzureADSynchroniser" - } - } -} - -Configuration DownloadInstallers { - param ( - [Parameter(Mandatory = $true, HelpMessage = "Installer base path")] - [ValidateNotNullOrEmpty()] - [String]$DIInstallerBasePath - ) - - Import-DscResource -ModuleName xPSDesiredStateConfiguration -ModuleVersion 9.1.0 - - Node localhost { - Script AzureADConnect { - SetScript = { - try { - Invoke-RestMethod -Uri "https://download.microsoft.com/download/B/0/0/B00291D0-5A83-4DE7-86F5-980BC00DE05A/AzureADConnect.msi" -OutFile (Join-Path $using:DIInstallerBasePath "AzureADConnect.msi") -ErrorAction Stop - Write-Verbose -Message "Successfully downloaded AzureADConnect installer to '$using:DIInstallerBasePath'." - } catch { - Write-Error "AzureADConnect: $($_.Exception)" - } - } - GetScript = { @{} } - TestScript = { (Test-Path -Path (Join-Path $using:DIInstallerBasePath "AzureADConnect.msi")) } - } - - xRemoteFile DisconnectAD { # from xPSDesiredStateConfiguration - Uri = "https://raw.githubusercontent.com/alan-turing-institute/data-safe-haven/python-migration/data_safe_haven/resources/desired_state_configuration/DisconnectAD.ps1" - DestinationPath = Join-Path $DIInstallerBasePath "DisconnectAD.ps1" - } - - xRemoteFile UpdateAADSyncRule { # from xPSDesiredStateConfiguration - Uri = "https://raw.githubusercontent.com/alan-turing-institute/data-safe-haven/python-migration/data_safe_haven/resources/desired_state_configuration/UpdateAADSyncRule.ps1" - DestinationPath = Join-Path $DIInstallerBasePath "UpdateAADSyncRule.ps1" - } - } -} - -Configuration PrimaryDomainController { - param ( - [Parameter(Mandatory = $true, HelpMessage = "AzureAD connect password")] - [ValidateNotNullOrEmpty()] - [String]$AzureADConnectPassword, - - [Parameter(Mandatory = $true, HelpMessage = "AzureAD connect username")] - [ValidateNotNullOrEmpty()] - [String]$AzureADConnectUsername, - - [Parameter(Mandatory = $true, HelpMessage = "Domain administrator password")] - [ValidateNotNullOrEmpty()] - [String]$DomainAdministratorPassword, - - [Parameter(Mandatory = $true, HelpMessage = "Domain administrator username")] - [ValidateNotNullOrEmpty()] - [String]$DomainAdministratorUsername, - - [Parameter(Mandatory = $true, HelpMessage = "FQDN for the SHM domain")] - [ValidateNotNullOrEmpty()] - [String]$DomainName, - - [Parameter(Mandatory = $true, HelpMessage = "Root DN for the SHM domain")] - [ValidateNotNullOrEmpty()] - [String]$DomainRootDn, - - [Parameter(Mandatory = $true, HelpMessage = "NetBIOS name for the domain")] - [ValidateNotNullOrEmpty()] - [String]$DomainNetBios, - - [Parameter(Mandatory = $true, HelpMessage = "LDAP searcher password")] - [ValidateNotNullOrEmpty()] - [String]$LDAPSearcherPassword, - - [Parameter(Mandatory = $true, HelpMessage = "LDAP searcher username")] - [ValidateNotNullOrEmpty()] - [String]$LDAPSearcherUsername - ) - - Import-DscResource -ModuleName xPSDesiredStateConfiguration -ModuleVersion 9.1.0 - - # Common parameters - $DataSafeHavenBasePath = "C:\DataSafeHaven" - $ActiveDirectoryBasePath = Join-Path $DataSafeHavenBasePath "ActiveDirectory" - $InstallersBasePath = Join-Path $DataSafeHavenBasePath "Installers" - - Node localhost { - InstallPowershellModules InstallPowershellModules {} - - Script CreateBaseDirectories { - SetScript = { - New-Item -ItemType Directory -Force -Path $using:DataSafeHavenBasePath - Write-Verbose -Message "Ensured that $($using:DataSafeHavenBasePath) exists" - New-Item -ItemType Directory -Force -Path $using:ActiveDirectoryBasePath - Write-Verbose -Message "Ensured that $($using:ActiveDirectoryBasePath) exists" - New-Item -ItemType Directory -Force -Path $using:InstallersBasePath - Write-Verbose -Message "Ensured that $($using:InstallersBasePath) exists" - } - GetScript = { @{} } - TestScript = { ((Test-Path -Path $using:ActiveDirectoryBasePath) -and (Test-Path -Path $using:InstallersBasePath)) } - } - - InstallActiveDirectory InstallActiveDirectory { - IADActiveDirectoryLogPath = Join-Path $ActiveDirectoryBasePath "Logs" - IADActiveDirectoryNtdsPath = Join-Path $ActiveDirectoryBasePath "NTDS" - IADActiveDirectorySysvolPath = Join-Path $ActiveDirectoryBasePath "SYSVOL" - IADDomainAdministratorPassword = $DomainAdministratorPassword - IADDomainAdministratorUsername = $DomainAdministratorUsername - IADDomainName = $DomainName - IADDomainNetBiosName = $DomainNetBios - DependsOn = "[Script]CreateBaseDirectories" - } - - ConfigureActiveDirectory ConfigureActiveDirectory { - CADAzureADConnectPassword = $AzureADConnectPassword - CADAzureADConnectUsername = $AzureADConnectUsername - CADDomainAdministratorPassword = $DomainAdministratorPassword - CADDomainAdministratorUsername = $DomainAdministratorUsername - CADDomainName = $DomainName - CADDomainRootDn = $DomainRootDn - CADLDAPSearcherPassword = $LDAPSearcherPassword - CADLDAPSearcherUsername = $LDAPSearcherUsername - DependsOn = "[InstallActiveDirectory]InstallActiveDirectory" - } - - DownloadInstallers DownloadInstallers { - DIInstallerBasePath = $InstallersBasePath - DependsOn = @("[Script]CreateBaseDirectories") - } - } -} diff --git a/data_safe_haven/resources/desired_state_configuration/UpdateAADSyncRule.ps1 b/data_safe_haven/resources/desired_state_configuration/UpdateAADSyncRule.ps1 deleted file mode 100644 index 675981bba3..0000000000 --- a/data_safe_haven/resources/desired_state_configuration/UpdateAADSyncRule.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -Import-Module -Name "C:\Program Files\Microsoft Azure AD Sync\Bin\ADSync" -Force -ErrorAction Stop - -# Create a new rule that is a copy of the default rule -$defaultRule = Get-ADSyncRule | Where-Object { $_.Name -eq "Out to AAD - User Join" } -$newRule = New-ADSyncRule ` - -Name 'Out to AAD - User Join' ` - -Description $defaultRule.Description ` - -Direction 'Outbound' ` - -Precedence $defaultRule.Precedence ` - -PrecedenceAfter $defaultRule.PrecedenceAfter ` - -PrecedenceBefore $defaultRule.PrecedenceBefore ` - -SourceObjectType $defaultRule.SourceObjectType ` - -TargetObjectType $defaultRule.TargetObjectType ` - -Connector $defaultRule.Connector ` - -LinkType $defaultRule.LinkType ` - -SoftDeleteExpiryInterval $defaultRule.SoftDeleteExpiryInterval ` - -ImmutableTag '' ` - -EnablePasswordSync - -# Copy all flow mappings except the usage location one -foreach ($flow in ($defaultRule.AttributeFlowMappings | Where-Object { $_.Destination -ne "usageLocation" })) { - $params = @{ - Destination = $flow.Destination - FlowType = $flow.FlowType - ValueMergeType = $flow.ValueMergeType - } - if ($flow.Source) { $params["Source"] = $flow.Source } - if ($flow.Expression) { $params["Expression"] = $flow.Expression } - $null = Add-ADSyncAttributeFlowMapping -SynchronizationRule $newRule @params -} - -# Set the usage location flow mapping manually -$null = Add-ADSyncAttributeFlowMapping -SynchronizationRule $newRule -Source @('c') -Destination 'usageLocation' -FlowType 'Direct' -ValueMergeType 'Update' - -# Add appropriate scope and join conditions -$newRule.JoinFilter = $defaultRule.JoinFilter -$newRule.ScopeFilter = $defaultRule.ScopeFilter - -# Remove the old rule and add the new one -$null = Remove-ADSyncRule -SynchronizationRule $defaultRule -Add-ADSyncRule -SynchronizationRule $newRule