diff --git a/data_safe_haven/commands/deploy_shm.py b/data_safe_haven/commands/deploy_shm.py index fd089682c4..929ca5e9cb 100644 --- a/data_safe_haven/commands/deploy_shm.py +++ b/data_safe_haven/commands/deploy_shm.py @@ -69,7 +69,8 @@ def deploy_shm( # Add the SHM domain as a custom domain in AzureAD graph_api.verify_custom_domain( - config.shm.fqdn, stack.output("fqdn_nameservers") + config.shm.fqdn, + stack.output("networking")["fqdn_nameservers"], ) # Add Pulumi infrastructure information to the config file diff --git a/data_safe_haven/pulumi/common/enums.py b/data_safe_haven/pulumi/common/enums.py index ec53f22474..46769413c4 100644 --- a/data_safe_haven/pulumi/common/enums.py +++ b/data_safe_haven/pulumi/common/enums.py @@ -15,11 +15,16 @@ class NetworkingPriorities(int, Enum): INTERNAL_SHM_LDAP_UDP = 1250 INTERNAL_SHM_MONITORING_TOOLS = 1300 INTERNAL_SHM_UPDATE_SERVERS = 1400 - INTERNAL_SRE_PRIVATE_DATA = 1500 - INTERNAL_SRE_REMOTE_DESKTOP = 1600 - INTERNAL_SRE_USER_SERVICES_CONTAINERS = 1700 - INTERNAL_SRE_USER_SERVICES_DATABASES = 1800 - INTERNAL_DSH_VIRTUAL_NETWORK = 1999 + INTERNAL_SRE_APPLICATION_GATEWAY = 1500 + INTERNAL_SRE_DATA_CONFIGURATION = 1600 + INTERNAL_SRE_DATA_PRIVATE = 1650 + INTERNAL_SRE_GUACAMOLE_CONTAINERS = 1700 + INTERNAL_SRE_GUACAMOLE_CONTAINERS_SUPPORT = 1750 + INTERNAL_SRE_USER_SERVICES_CONTAINERS = 1800 + INTERNAL_SRE_USER_SERVICES_CONTAINERS_SUPPORT = 1825 + INTERNAL_SRE_USER_SERVICES_DATABASES = 1850 + INTERNAL_SRE_USER_SERVICES_SOFTWARE_REPOSITORIES = 1875 + INTERNAL_SRE_WORKSPACES = 1900 # Authorised external IPs: 2000-2999 AUTHORISED_EXTERNAL_ADMIN_IPS = 2000 AUTHORISED_EXTERNAL_USER_IPS = 2100 diff --git a/data_safe_haven/pulumi/components/shm_networking.py b/data_safe_haven/pulumi/components/shm_networking.py index 837588444c..4bf88375d5 100644 --- a/data_safe_haven/pulumi/components/shm_networking.py +++ b/data_safe_haven/pulumi/components/shm_networking.py @@ -21,14 +21,14 @@ def __init__( ) -> None: # Virtual network and subnet IP ranges self.vnet_iprange = AzureIPv4Range("10.0.0.0", "10.0.255.255") - # Firewall subnet must be at least /26 in size (64 addresses) - self.subnet_firewall_iprange = self.vnet_iprange.next_subnet(64) # Bastion subnet must be at least /26 in size (64 addresses) 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) - self.subnet_identity_servers_iprange = self.vnet_iprange.next_subnet(8) # Other variables self.admin_ip_addresses = admin_ip_addresses self.fqdn = fqdn @@ -58,13 +58,6 @@ def __init__( ) # Define NSGs - nsg_monitoring = network.NetworkSecurityGroup( - f"{self._name}_nsg_monitoring", - network_security_group_name=f"{stack_name}-nsg-monitoring", - resource_group_name=resource_group.name, - security_rules=[], - opts=child_opts, - ) nsg_bastion = network.NetworkSecurityGroup( f"{self._name}_nsg_bastion", network_security_group_name=f"{stack_name}-nsg-bastion", @@ -195,6 +188,64 @@ def __init__( ], opts=child_opts, ) + 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, + ) + nsg_monitoring = network.NetworkSecurityGroup( + f"{self._name}_nsg_monitoring", + network_security_group_name=f"{stack_name}-nsg-monitoring", + resource_group_name=resource_group.name, + security_rules=[], + opts=child_opts, + ) nsg_update_servers = network.NetworkSecurityGroup( f"{self._name}_nsg_update_servers", network_security_group_name=f"{stack_name}-nsg-update-servers", @@ -208,7 +259,7 @@ def __init__( destination_port_range="*", direction=network.SecurityRuleDirection.INBOUND, name="AllowVirtualNetworkInbound", - priority=NetworkingPriorities.INTERNAL_DSH_VIRTUAL_NETWORK, + priority=NetworkingPriorities.INTERNAL_SELF, protocol=network.SecurityRuleProtocol.ASTERISK, source_address_prefix="VirtualNetwork", source_port_range="*", @@ -265,57 +316,6 @@ def __init__( ], opts=child_opts, ) - 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="*", - 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="*", - 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, - ) # Define route table route_table = network.RouteTable( @@ -335,9 +335,9 @@ 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" - subnet_identity_servers_name = "IdentityServersSubnet" virtual_network = network.VirtualNetwork( f"{self._name}_virtual_network", address_space=network.AddressSpaceArgs( @@ -345,6 +345,15 @@ def __init__( ), resource_group_name=resource_group.name, subnets=[ # Note that we define subnets inline to avoid creation order issues + # Bastion subnet + network.SubnetArgs( + address_prefix=str(props.subnet_bastion_iprange), + name=subnet_bastion_name, + network_security_group=network.NetworkSecurityGroupArgs( + id=nsg_bastion.id + ), + route_table=None, # the bastion subnet must NOT be attached to the route table + ), # AzureFirewall subnet network.SubnetArgs( address_prefix=str(props.subnet_firewall_iprange), @@ -352,14 +361,14 @@ 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 ), - # Bastion subnet + # Identity servers subnet network.SubnetArgs( - address_prefix=str(props.subnet_bastion_iprange), - name=subnet_bastion_name, + address_prefix=str(props.subnet_identity_servers_iprange), + name=subnet_identity_servers_name, network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_bastion.id + id=nsg_identity_servers.id ), - route_table=None, # the bastion subnet must NOT be attached to the route table + route_table=network.RouteTableArgs(id=route_table.id), ), # Monitoring subnet network.SubnetArgs( @@ -379,15 +388,6 @@ def __init__( ), route_table=network.RouteTableArgs(id=route_table.id), ), - # 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), - ), ], virtual_network_name=f"{stack_name}-vnet", virtual_network_peerings=[], @@ -502,6 +502,7 @@ def __init__( # Register exports self.exports = { + "fqdn_nameservers": self.dns_zone.name_servers, "private_dns_zone_base_id": self.private_dns_zone_base_id, "resource_group_name": resource_group.name, "subnet_bastion_prefix": self.subnet_bastion.apply( diff --git a/data_safe_haven/pulumi/components/sre_backup.py b/data_safe_haven/pulumi/components/sre_backup.py index 58681ebdcc..888a1d2126 100644 --- a/data_safe_haven/pulumi/components/sre_backup.py +++ b/data_safe_haven/pulumi/components/sre_backup.py @@ -1,4 +1,4 @@ -"""Pulumi component for SRE state""" +"""Pulumi component for SRE backup""" from pulumi import ComponentResource, Input, ResourceOptions from pulumi_azure_native import dataprotection, resources @@ -9,12 +9,16 @@ class SREBackupProps: def __init__( self, location: Input[str], - storage_account_securedata_id: Input[str], - storage_account_securedata_name: Input[str], + storage_account_data_private_sensitive_id: Input[str], + storage_account_data_private_sensitive_name: Input[str], ) -> None: self.location = location - self.storage_account_securedata_id = storage_account_securedata_id - self.storage_account_securedata_name = storage_account_securedata_name + self.storage_account_data_private_sensitive_id = ( + storage_account_data_private_sensitive_id + ) + self.storage_account_data_private_sensitive_name = ( + storage_account_data_private_sensitive_name + ) class SREBackupComponent(ComponentResource): @@ -164,19 +168,19 @@ def __init__( backup_instance_name="backup-instance-blobs", properties=dataprotection.BackupInstanceArgs( data_source_info=dataprotection.DatasourceArgs( - resource_id=props.storage_account_securedata_id, + resource_id=props.storage_account_data_private_sensitive_id, datasource_type="Microsoft.Storage/storageAccounts/blobServices", object_type="Datasource", resource_location=props.location, - resource_name=props.storage_account_securedata_name, + resource_name=props.storage_account_data_private_sensitive_name, resource_type="Microsoft.Storage/storageAccounts", - resource_uri=props.storage_account_securedata_id, + resource_uri=props.storage_account_data_private_sensitive_id, ), object_type="BackupInstance", policy_info=dataprotection.PolicyInfoArgs( policy_id=backup_policy_blobs.id, ), - friendly_name="BlobBackupSecureData", + friendly_name="BlobBackupSensitiveData", ), resource_group_name=resource_group.name, vault_name=backup_vault.name, diff --git a/data_safe_haven/pulumi/components/sre_data.py b/data_safe_haven/pulumi/components/sre_data.py index 7b236171e1..900f2d3a10 100644 --- a/data_safe_haven/pulumi/components/sre_data.py +++ b/data_safe_haven/pulumi/components/sre_data.py @@ -1,4 +1,4 @@ -"""Pulumi component for SRE state""" +"""Pulumi component for SRE data""" from collections.abc import Sequence from pulumi import ComponentResource, Config, Input, Output, ResourceOptions @@ -47,14 +47,16 @@ def __init__( networking_resource_group: Input[resources.ResourceGroup], pulumi_opts: Config, sre_fqdn: Input[str], - subnet_private_data: Input[network.GetSubnetResult], + subnet_data_configuration: Input[network.GetSubnetResult], + subnet_data_private: Input[network.GetSubnetResult], subscription_id: Input[str], subscription_name: Input[str], tenant_id: Input[str], ) -> None: self.admin_email_address = admin_email_address self.admin_group_id = admin_group_id - self.approved_ip_addresses = Output.all( + self.data_configuration_ip_addresses = admin_ip_addresses + self.data_private_sensitive_ip_addresses = Output.all( admin_ip_addresses, data_provider_ip_addresses ).apply( lambda address_lists: { @@ -86,7 +88,10 @@ def __init__( pulumi_opts, "shm-networking-private_dns_zone_base_id" ) self.sre_fqdn = sre_fqdn - self.subnet_private_data_id = Output.from_input(subnet_private_data).apply( + self.subnet_data_configuration_id = Output.from_input( + subnet_data_configuration + ).apply(get_id_from_subnet) + self.subnet_data_private_id = Output.from_input(subnet_data_private).apply( get_id_from_subnet ) self.subscription_id = subscription_id @@ -98,7 +103,7 @@ def get_secret(self, pulumi_opts: Config, secret_name: str) -> Output[str]: class SREDataComponent(ComponentResource): - """Deploy SRE state with Pulumi""" + """Deploy SRE data with Pulumi""" def __init__( self, @@ -296,21 +301,41 @@ def __init__( opts=ResourceOptions(parent=key_vault), ) - # Deploy state storage account - storage_account_state = storage.StorageAccount( - f"{self._name}_storage_account_state", + # Deploy configuration data storage account + storage_account_data_configuration = storage.StorageAccount( + f"{self._name}_storage_account_data_configuration", # Note that account names have a maximum of 24 characters account_name=alphanumeric( - f"{''.join(truncate_tokens(stack_name.split('-'), 19))}state" + f"{''.join(truncate_tokens(stack_name.split('-'), 14))}configdata" )[:24], kind=storage.Kind.STORAGE_V2, + location=props.location, + network_rule_set=storage.NetworkRuleSetArgs( + bypass=storage.Bypass.AZURE_SERVICES, + default_action=storage.DefaultAction.DENY, + ip_rules=Output.from_input(props.data_configuration_ip_addresses).apply( + lambda ip_ranges: [ + storage.IPRuleArgs( + action=storage.Action.ALLOW, + i_p_address_or_range=str(ip_address), + ) + for ip_range in sorted(ip_ranges) + for ip_address in AzureIPv4Range.from_cidr(ip_range).all_ips() + ] + ), + virtual_network_rules=[ + storage.VirtualNetworkRuleArgs( + virtual_network_resource_id=props.subnet_data_configuration_id, + ) + ], + ), resource_group_name=resource_group.name, sku=storage.SkuArgs(name=storage.SkuName.STANDARD_GRS), opts=child_opts, ) - # Retrieve storage account keys - storage_account_state_keys = Output.all( - account_name=storage_account_state.name, + # Retrieve configuration data storage account keys + storage_account_data_configuration_keys = Output.all( + account_name=storage_account_data_configuration.name, resource_group_name=resource_group.name, ).apply( lambda kwargs: storage.list_storage_account_keys( @@ -318,15 +343,55 @@ def __init__( resource_group_name=kwargs["resource_group_name"], ) ) + # Set up a private endpoint for the configuration data storage account + storage_account_data_configuration_private_endpoint = network.PrivateEndpoint( + f"{self._name}_storage_account_data_configuration_private_endpoint", + location=props.location, + private_endpoint_name=f"{stack_name}-pep-storage-account-data-configuration", + private_link_service_connections=[ + network.PrivateLinkServiceConnectionArgs( + group_ids=["file"], + name=f"{stack_name}-cnxn-pep-storage-account-data-configuration", + private_link_service_id=storage_account_data_configuration.id, + ) + ], + resource_group_name=resource_group.name, + subnet=network.SubnetArgs(id=props.subnet_data_configuration_id), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_data_configuration) + ), + ) + # Add a private DNS record for each configuration data endpoint custom DNS config + network.PrivateDnsZoneGroup( + f"{self._name}_storage_account_data_configuration_private_dns_zone_group", + private_dns_zone_configs=[ + network.PrivateDnsZoneConfigArgs( + name=replace_separators( + f"{stack_name}-storage-account-data-configuration-to-{dns_zone_name}", + "-", + ), + private_dns_zone_id=Output.concat( + props.private_dns_zone_base_id, dns_zone_name + ), + ) + for dns_zone_name in ordered_private_dns_zones("Storage account") + ], + private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-data-configuration", + private_endpoint_name=storage_account_data_configuration_private_endpoint.name, + resource_group_name=resource_group.name, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_data_configuration) + ), + ) - # Deploy secure data blob storage account + # Deploy sensitive data blob storage account # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer # - Store the /data and /output folders here - storage_account_securedata = storage.StorageAccount( - f"{self._name}_storage_account_securedata", + storage_account_data_private_sensitive = storage.StorageAccount( + f"{self._name}_storage_account_data_private_sensitive", # Storage account names have a maximum of 24 characters account_name=alphanumeric( - f"{''.join(truncate_tokens(stack_name.split('-'), 14))}securedata{sha256hash(self._name)}" + f"{''.join(truncate_tokens(stack_name.split('-'), 11))}sensitivedata{sha256hash(self._name)}" )[:24], enable_https_traffic_only=True, enable_nfs_v3=True, @@ -347,7 +412,9 @@ def __init__( network_rule_set=storage.NetworkRuleSetArgs( bypass=storage.Bypass.AZURE_SERVICES, default_action=storage.DefaultAction.DENY, - ip_rules=Output.from_input(props.approved_ip_addresses).apply( + ip_rules=Output.from_input( + props.data_private_sensitive_ip_addresses + ).apply( lambda ip_ranges: [ storage.IPRuleArgs( action=storage.Action.ALLOW, @@ -359,7 +426,7 @@ def __init__( ), virtual_network_rules=[ storage.VirtualNetworkRuleArgs( - virtual_network_resource_id=props.subnet_private_data_id, + virtual_network_resource_id=props.subnet_data_private_id, ) ], ), @@ -369,7 +436,7 @@ def __init__( ) # Give the "Storage Blob Data Owner" role to the Azure admin group authorization.RoleAssignment( - f"{self._name}_storage_account_securedata_data_owner_role_assignment", + f"{self._name}_storage_account_data_private_sensitive_data_owner_role_assignment", principal_id=props.admin_group_id, principal_type=authorization.PrincipalType.GROUP, role_assignment_name="b7e6dc6d-f1e8-4753-8033-0f276bb0955b", # Storage Blob Data Owner @@ -378,34 +445,37 @@ def __init__( props.subscription_id, "/providers/Microsoft.Authorization/roleDefinitions/b7e6dc6d-f1e8-4753-8033-0f276bb0955b", ), - scope=storage_account_securedata.id, + scope=storage_account_data_private_sensitive.id, opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_securedata) + child_opts, + ResourceOptions(parent=storage_account_data_private_sensitive), ), ) # Deploy storage containers storage_container_egress = storage.BlobContainer( f"{self._name}_storage_container_egress", - account_name=storage_account_securedata.name, + account_name=storage_account_data_private_sensitive.name, container_name="egress", default_encryption_scope="$account-encryption-key", deny_encryption_scope_override=False, public_access=storage.PublicAccess.NONE, resource_group_name=resource_group.name, opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_securedata) + child_opts, + ResourceOptions(parent=storage_account_data_private_sensitive), ), ) storage_container_ingress = storage.BlobContainer( f"{self._name}_storage_container_ingress", - account_name=storage_account_securedata.name, + account_name=storage_account_data_private_sensitive.name, container_name="ingress", default_encryption_scope="$account-encryption-key", deny_encryption_scope_override=False, public_access=storage.PublicAccess.NONE, resource_group_name=resource_group.name, opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_securedata) + child_opts, + ResourceOptions(parent=storage_account_data_private_sensitive), ), ) # Set storage container ACLs @@ -421,7 +491,7 @@ def __init__( apply_default_permissions=False, container_name=storage_container_egress.name, resource_group_name=resource_group.name, - storage_account_name=storage_account_securedata.name, + storage_account_name=storage_account_data_private_sensitive.name, subscription_name=props.subscription_name, ), opts=ResourceOptions.merge( @@ -439,38 +509,39 @@ def __init__( apply_default_permissions=True, container_name=storage_container_ingress.name, resource_group_name=resource_group.name, - storage_account_name=storage_account_securedata.name, + storage_account_name=storage_account_data_private_sensitive.name, subscription_name=props.subscription_name, ), opts=ResourceOptions.merge( child_opts, ResourceOptions(parent=storage_container_ingress) ), ) - # Set up a private endpoint for the securedata data account - storage_account_securedata_endpoint = network.PrivateEndpoint( - f"{self._name}_storage_account_securedata_private_endpoint", + # Set up a private endpoint for the sensitive data storage account + storage_account_data_private_sensitive_endpoint = network.PrivateEndpoint( + f"{self._name}_storage_account_data_private_sensitive_private_endpoint", location=props.location, - private_endpoint_name=f"{stack_name}-pep-storage-account-securedata", + private_endpoint_name=f"{stack_name}-pep-storage-account-data-private-sensitive", private_link_service_connections=[ network.PrivateLinkServiceConnectionArgs( group_ids=["blob"], - name=f"{stack_name}-cnxn-pep-storage-account-securedata", - private_link_service_id=storage_account_securedata.id, + name=f"{stack_name}-cnxn-pep-storage-account-data-private-sensitive", + private_link_service_id=storage_account_data_private_sensitive.id, ) ], resource_group_name=resource_group.name, - subnet=network.SubnetArgs(id=props.subnet_private_data_id), + subnet=network.SubnetArgs(id=props.subnet_data_private_id), opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_securedata) + child_opts, + ResourceOptions(parent=storage_account_data_private_sensitive), ), ) - # Add a private DNS record for each securedata data custom DNS config + # Add a private DNS record for each sensitive data endpoint custom DNS config network.PrivateDnsZoneGroup( - f"{self._name}_storage_account_securedata_private_dns_zone_group", + f"{self._name}_storage_account_data_private_sensitive_private_dns_zone_group", private_dns_zone_configs=[ network.PrivateDnsZoneConfigArgs( name=replace_separators( - f"{stack_name}-storage-account-securedata-to-{dns_zone_name}", + f"{stack_name}-storage-account-data-private-sensitive-to-{dns_zone_name}", "-", ), private_dns_zone_id=Output.concat( @@ -479,20 +550,21 @@ def __init__( ) for dns_zone_name in ordered_private_dns_zones("Storage account") ], - private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-securedata", - private_endpoint_name=storage_account_securedata_endpoint.name, + private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-data-private-sensitive", + private_endpoint_name=storage_account_data_private_sensitive_endpoint.name, resource_group_name=resource_group.name, opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_securedata) + child_opts, + ResourceOptions(parent=storage_account_data_private_sensitive), ), ) - # Deploy userdata files storage account + # Deploy data_private_user files storage account # - Azure Files has better NFS support and cannot be accessed with Azure Storage Explorer # - Allows root-squashing to be configured # - Store the /home and /shared folders here - storage_account_userdata = storage.StorageAccount( - f"{self._name}_storage_account_userdata", + storage_account_data_private_user = storage.StorageAccount( + f"{self._name}_storage_account_data_private_user", access_tier=storage.AccessTier.COOL, # Storage account names have a maximum of 24 characters account_name=alphanumeric( @@ -514,7 +586,7 @@ def __init__( default_action=storage.DefaultAction.DENY, virtual_network_rules=[ storage.VirtualNetworkRuleArgs( - virtual_network_resource_id=props.subnet_private_data_id, + virtual_network_resource_id=props.subnet_data_private_id, ) ], ), @@ -525,7 +597,7 @@ def __init__( storage.FileShare( f"{self._name}_storage_container_home", access_tier=storage.ShareAccessTier.PREMIUM, - account_name=storage_account_userdata.name, + account_name=storage_account_data_private_user.name, enabled_protocols=storage.EnabledProtocols.NFS, resource_group_name=resource_group.name, # Squashing prevents root from creating user home directories @@ -533,47 +605,48 @@ def __init__( share_name="home", share_quota=1024, opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_userdata) + child_opts, ResourceOptions(parent=storage_account_data_private_user) ), ) storage.FileShare( f"{self._name}_storage_container_shared", access_tier=storage.ShareAccessTier.PREMIUM, - account_name=storage_account_userdata.name, + account_name=storage_account_data_private_user.name, enabled_protocols=storage.EnabledProtocols.NFS, resource_group_name=resource_group.name, root_squash=storage.RootSquashType.ROOT_SQUASH, share_name="shared", share_quota=1024, opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_userdata) + child_opts, ResourceOptions(parent=storage_account_data_private_user) ), ) - # Set up a private endpoint for the userdata storage account - storage_account_userdata_endpoint = network.PrivateEndpoint( - f"{self._name}_storage_account_userdata_private_endpoint", + # Set up a private endpoint for the user data storage account + storage_account_data_private_user_endpoint = network.PrivateEndpoint( + f"{self._name}_storage_account_data_private_user_private_endpoint", location=props.location, - private_endpoint_name=f"{stack_name}-pep-storage-account-userdata", + private_endpoint_name=f"{stack_name}-pep-storage-account-data-private-user", private_link_service_connections=[ network.PrivateLinkServiceConnectionArgs( group_ids=["file"], - name=f"{stack_name}-cnxn-pep-storage-account-userdata", - private_link_service_id=storage_account_userdata.id, + name=f"{stack_name}-cnxn-pep-storage-account-data-private-user", + private_link_service_id=storage_account_data_private_user.id, ) ], resource_group_name=resource_group.name, - subnet=network.SubnetArgs(id=props.subnet_private_data_id), + subnet=network.SubnetArgs(id=props.subnet_data_private_id), opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_userdata) + child_opts, ResourceOptions(parent=storage_account_data_private_user) ), ) - # Add a private DNS record for each userdata custom DNS config + # Add a private DNS record for each user data endpoint custom DNS config network.PrivateDnsZoneGroup( - f"{self._name}_storage_account_userdata_private_dns_zone_group", + f"{self._name}_storage_account_data_private_user_private_dns_zone_group", private_dns_zone_configs=[ network.PrivateDnsZoneConfigArgs( name=replace_separators( - f"{stack_name}-storage-account-userdata-to-{dns_zone_name}", "-" + f"{stack_name}-storage-account-data-private-user-to-{dns_zone_name}", + "-", ), private_dns_zone_id=Output.concat( props.private_dns_zone_base_id, dns_zone_name @@ -581,23 +654,33 @@ def __init__( ) for dns_zone_name in ordered_private_dns_zones("Storage account") ], - private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-userdata", - private_endpoint_name=storage_account_userdata_endpoint.name, + private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-data-private-user", + private_endpoint_name=storage_account_data_private_user_endpoint.name, resource_group_name=resource_group.name, opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_account_userdata) + child_opts, ResourceOptions(parent=storage_account_data_private_user) ), ) # Register outputs self.sre_fqdn_certificate_secret_id = sre_fqdn_certificate.secret_id - self.storage_account_userdata_name = storage_account_userdata.name - self.storage_account_securedata_id = storage_account_securedata.id - self.storage_account_securedata_name = storage_account_securedata.name - self.storage_account_state_key = Output.secret( - storage_account_state_keys.keys[0].value + self.storage_account_data_private_user_name = ( + storage_account_data_private_user.name + ) + self.storage_account_data_private_sensitive_id = ( + storage_account_data_private_sensitive.id + ) + self.storage_account_data_private_sensitive_name = ( + storage_account_data_private_sensitive.name + ) + self.storage_account_data_configuration_key = Output.secret( + storage_account_data_configuration_keys.apply( + lambda keys: keys.keys[0].value + ) + ) + self.storage_account_data_configuration_name = ( + storage_account_data_configuration.name ) - self.storage_account_state_name = storage_account_state.name self.managed_identity = identity_key_vault_reader self.password_nexus_admin = Output.secret(props.password_nexus_admin) self.password_database_service_admin = Output.secret( diff --git a/data_safe_haven/pulumi/components/sre_hedgedoc_server.py b/data_safe_haven/pulumi/components/sre_hedgedoc_server.py index 59bd01529c..0c2efec319 100644 --- a/data_safe_haven/pulumi/components/sre_hedgedoc_server.py +++ b/data_safe_haven/pulumi/components/sre_hedgedoc_server.py @@ -282,12 +282,13 @@ def __init__( ), containerinstance.EnvironmentVariableArgs( name="CMD_LDAP_SEARCHFILTER", - value=( - "(&" - "(objectClass=user)" - f"(memberOf=CN={props.ldap_user_security_group_cn})" - f"(sAMAccountName={{{{username}}}})" - ")" + value=Output.concat( + "(&", + "(objectClass=user)", + "(memberOf=CN=", + props.ldap_user_security_group_cn, + ")", + "(sAMAccountName={{username}}))", ), ), containerinstance.EnvironmentVariableArgs( diff --git a/data_safe_haven/pulumi/components/sre_networking.py b/data_safe_haven/pulumi/components/sre_networking.py index ef8c7115e1..0409a268ba 100644 --- a/data_safe_haven/pulumi/components/sre_networking.py +++ b/data_safe_haven/pulumi/components/sre_networking.py @@ -30,15 +30,18 @@ def __init__( self.subnet_application_gateway_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(256) ) + self.subnet_data_configuration_iprange = self.vnet_iprange.apply( + lambda r: r.next_subnet(8) + ) + self.subnet_data_private_iprange = self.vnet_iprange.apply( + lambda r: r.next_subnet(8) + ) self.subnet_guacamole_containers_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(8) ) self.subnet_guacamole_containers_support_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(8) ) - self.subnet_private_data_iprange = self.vnet_iprange.apply( - lambda r: r.next_subnet(16) - ) self.subnet_user_services_containers_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(8) ) @@ -92,15 +95,18 @@ def __init__( subnet_application_gateway_prefix = ( props.subnet_application_gateway_iprange.apply(lambda r: str(r)) ) + subnet_data_configuration_prefix = ( + props.subnet_data_configuration_iprange.apply(lambda r: str(r)) + ) + subnet_data_private_prefix = props.subnet_data_private_iprange.apply( + lambda r: str(r) + ) subnet_guacamole_containers_prefix = ( props.subnet_guacamole_containers_iprange.apply(lambda r: str(r)) ) subnet_guacamole_containers_support_prefix = ( props.subnet_guacamole_containers_support_iprange.apply(lambda r: str(r)) ) - subnet_private_data_prefix = props.subnet_private_data_iprange.apply( - lambda r: str(r) - ) subnet_user_services_containers_prefix = ( props.subnet_user_services_containers_iprange.apply(lambda r: str(r)) ) @@ -127,85 +133,607 @@ def __init__( network_security_group_name=f"{stack_name}-nsg-application-gateway", resource_group_name=resource_group.name, security_rules=[ + # Inbound network.SecurityRuleArgs( - access="Allow", + access=network.SecurityRuleAccess.ALLOW, description="Allow inbound gateway management service traffic.", destination_address_prefix="*", - destination_port_range="*", - direction="Inbound", + destination_port_range="65200-65535", + direction=network.SecurityRuleDirection.INBOUND, name="AllowGatewayManagerServiceInbound", priority=NetworkingPriorities.AZURE_GATEWAY_MANAGER, - protocol="*", + protocol=network.SecurityRuleProtocol.TCP, source_address_prefix="GatewayManager", source_port_range="*", ), network.SecurityRuleArgs( - access="Allow", - description="Allow inbound gateway management traffic over the internet.", - destination_address_prefix=subnet_application_gateway_prefix, - destination_port_range="65200-65535", - direction="Inbound", - name="AllowGatewayManagerInternetInbound", - priority=NetworkingPriorities.EXTERNAL_INTERNET, - protocol="*", - source_address_prefix="Internet", + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound Azure load balancer traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="AllowAzureLoadBalancerServiceInbound", + priority=NetworkingPriorities.AZURE_LOAD_BALANCER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="AzureLoadBalancer", + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from users over the internet.", + destination_address_prefix=subnet_application_gateway_prefix, + destination_port_ranges=["80", "443"], + direction=network.SecurityRuleDirection.INBOUND, + name="AllowUsersInternetInbound", + priority=NetworkingPriorities.AUTHORISED_EXTERNAL_USER_IPS, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=props.public_ip_range_users, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to the Guacamole remote desktop gateway.", + destination_address_prefix=subnet_guacamole_containers_prefix, + destination_port_ranges=["80"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowGuacamoleContainersOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_GUACAMOLE_CONTAINERS, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_application_gateway_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound gateway management traffic over the internet.", + destination_address_prefix="Internet", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowGatewayManagerInternetOutbound", + priority=NetworkingPriorities.EXTERNAL_INTERNET, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Attempting to add our standard DenyAllOtherOutbound rule will cause + # this NSG to fail validation. See: https://learn.microsoft.com/en-us/azure/application-gateway/configuration-infrastructure#network-security-groups + ], + opts=child_opts, + ) + nsg_data_configuration = network.NetworkSecurityGroup( + f"{self._name}_nsg_data_configuration", + network_security_group_name=f"{stack_name}-nsg-data-configuration", + resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from Guacamole remote desktop gateway.", + destination_address_prefix=subnet_data_configuration_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="AllowGuacamoleContainersInbound", + priority=NetworkingPriorities.INTERNAL_SRE_GUACAMOLE_CONTAINERS, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_guacamole_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from user services containers.", + destination_address_prefix=subnet_data_configuration_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="AllowUserServicesContainersInbound", + priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_CONTAINERS, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_user_services_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from user services software repositories.", + destination_address_prefix=subnet_data_configuration_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="AllowUserServicesSoftwareRepositoriesInbound", + priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_user_services_software_repositories_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + ], + opts=child_opts, + ) + nsg_data_private = network.NetworkSecurityGroup( + f"{self._name}_nsg_data_private", + network_security_group_name=f"{stack_name}-nsg-data-private", + resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from SRE workspaces.", + destination_address_prefix=subnet_data_private_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="AllowWorkspacesInbound", + priority=NetworkingPriorities.INTERNAL_SRE_WORKSPACES, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_workspaces_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + ], + opts=child_opts, + ) + nsg_guacamole_containers = network.NetworkSecurityGroup( + f"{self._name}_nsg_guacamole_containers", + network_security_group_name=f"{stack_name}-nsg-guacamole-containers", + resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from the Application Gateway.", + destination_address_prefix=subnet_guacamole_containers_prefix, + destination_port_ranges=["80"], + direction=network.SecurityRuleDirection.INBOUND, + name="AllowApplicationGatewayInbound", + priority=NetworkingPriorities.INTERNAL_SRE_APPLICATION_GATEWAY, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_application_gateway_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow LDAP client requests over TCP.", + destination_address_prefix=props.shm_subnet_identity_servers_prefix, + destination_port_ranges=["389", "636"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowLDAPClientTCPOutbound", + priority=NetworkingPriorities.INTERNAL_SHM_LDAP_TCP, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_guacamole_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to configuration data endpoints.", + destination_address_prefix=subnet_data_configuration_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowDataConfigurationEndpointsOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_DATA_CONFIGURATION, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_guacamole_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to Guacamole support services.", + destination_address_prefix=subnet_guacamole_containers_support_prefix, + destination_port_ranges=["5432"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowGuacamoleContainersSupportOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_GUACAMOLE_CONTAINERS_SUPPORT, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_guacamole_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to SRE workspaces.", + destination_address_prefix=subnet_workspaces_prefix, + destination_port_ranges=["22", "3389"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowWorkspacesOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_WORKSPACES, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_guacamole_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound OAuth connections over the internet.", + destination_address_prefix="Internet", + destination_port_ranges=["80", "443"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowOAuthInternetOutbound", + priority=NetworkingPriorities.EXTERNAL_INTERNET, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_guacamole_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + ], + opts=child_opts, + ) + nsg_guacamole_containers_support = network.NetworkSecurityGroup( + f"{self._name}_nsg_guacamole_containers_support", + network_security_group_name=f"{stack_name}-nsg-guacamole-containers-support", + resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from Guacamole remote desktop gateway.", + destination_address_prefix=subnet_guacamole_containers_support_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="AllowGuacamoleContainersInbound", + priority=NetworkingPriorities.INTERNAL_SRE_GUACAMOLE_CONTAINERS, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_guacamole_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + ], + opts=child_opts, + ) + nsg_user_services_containers = network.NetworkSecurityGroup( + f"{self._name}_nsg_user_services_containers", + network_security_group_name=f"{stack_name}-nsg-user-services-containers", + resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from SRE workspaces.", + destination_address_prefix=subnet_user_services_containers_prefix, + destination_port_ranges=["22", "80", "443"], + direction=network.SecurityRuleDirection.INBOUND, + name="AllowWorkspacesInbound", + priority=NetworkingPriorities.INTERNAL_SRE_WORKSPACES, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_workspaces_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow LDAP client requests over TCP.", + destination_address_prefix=props.shm_subnet_identity_servers_prefix, + destination_port_ranges=["389", "636"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowLDAPClientTCPOutbound", + priority=NetworkingPriorities.INTERNAL_SHM_LDAP_TCP, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_user_services_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to configuration data endpoints.", + destination_address_prefix=subnet_data_configuration_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowDataConfigurationEndpointsOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_DATA_CONFIGURATION, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_user_services_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to container support services.", + destination_address_prefix=subnet_user_services_containers_support_prefix, + destination_port_ranges=["5432"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowUserServicesContainersSupportOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_CONTAINERS_SUPPORT, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_user_services_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + ], + opts=child_opts, + ) + nsg_user_services_containers_support = network.NetworkSecurityGroup( + f"{self._name}_nsg_user_services_containers_support", + network_security_group_name=f"{stack_name}-nsg-user-services-containers-support", + resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from user services containers.", + destination_address_prefix=subnet_user_services_containers_support_prefix, + destination_port_ranges=["5432"], + direction=network.SecurityRuleDirection.INBOUND, + name="AllowUserServicesContainersInbound", + priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_CONTAINERS, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_user_services_containers_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", source_port_range="*", ), + # Outbound network.SecurityRuleArgs( - access="Allow", - description="Allow inbound internet to Application Gateway.", - destination_address_prefix=subnet_application_gateway_prefix, - destination_port_ranges=["80", "443"], - direction="Inbound", - name="AllowInternetInbound", - priority=NetworkingPriorities.AUTHORISED_EXTERNAL_USER_IPS, - protocol="TCP", - source_address_prefix=props.public_ip_range_users, + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", source_port_range="*", ), ], opts=child_opts, ) - nsg_guacamole_containers = network.NetworkSecurityGroup( - f"{self._name}_nsg_guacamole_containers", - network_security_group_name=f"{stack_name}-nsg-guacamole-containers", - resource_group_name=resource_group.name, - opts=child_opts, - ) - nsg_guacamole_containers_support = network.NetworkSecurityGroup( - f"{self._name}_nsg_guacamole_containers_support", - network_security_group_name=f"{stack_name}-nsg-guacamole-containers-support", - resource_group_name=resource_group.name, - opts=child_opts, - ) - nsg_private_data = network.NetworkSecurityGroup( - f"{self._name}_nsg_private_data", - network_security_group_name=f"{stack_name}-nsg-private-data", - resource_group_name=resource_group.name, - opts=child_opts, - ) - nsg_user_services_containers = network.NetworkSecurityGroup( - f"{self._name}_nsg_user_services_containers", - network_security_group_name=f"{stack_name}-nsg-user-services-containers", - resource_group_name=resource_group.name, - opts=child_opts, - ) - nsg_user_services_containers_support = network.NetworkSecurityGroup( - f"{self._name}_nsg_user_services_containers_support", - network_security_group_name=f"{stack_name}-nsg-user-services-containers-support", - resource_group_name=resource_group.name, - opts=child_opts, - ) nsg_user_services_databases = network.NetworkSecurityGroup( f"{self._name}_nsg_user_services_databases", network_security_group_name=f"{stack_name}-nsg-user-services-databases", resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from SRE workspaces.", + destination_address_prefix=subnet_user_services_databases_prefix, + destination_port_ranges=["1433", "5432"], + direction=network.SecurityRuleDirection.INBOUND, + name="AllowWorkspacesInbound", + priority=NetworkingPriorities.INTERNAL_SRE_WORKSPACES, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_workspaces_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to configuration data endpoints.", + destination_address_prefix=subnet_data_configuration_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowDataConfigurationEndpointsOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_DATA_CONFIGURATION, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_user_services_databases_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + ], opts=child_opts, ) nsg_user_services_software_repositories = network.NetworkSecurityGroup( f"{self._name}_nsg_user_services_software_repositories", network_security_group_name=f"{stack_name}-nsg-user-services-software-repositories", resource_group_name=resource_group.name, + security_rules=[ + # Inbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow inbound connections from SRE workspaces.", + destination_address_prefix=subnet_user_services_software_repositories_prefix, + destination_port_ranges=["80", "443", "3128"], + direction=network.SecurityRuleDirection.INBOUND, + name="AllowWorkspacesInbound", + priority=NetworkingPriorities.INTERNAL_SRE_WORKSPACES, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_workspaces_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other inbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.INBOUND, + name="DenyAllOtherInbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + # Outbound + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to configuration data endpoints.", + destination_address_prefix=subnet_data_configuration_prefix, + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowDataConfigurationEndpointsOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_DATA_CONFIGURATION, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix=subnet_user_services_software_repositories_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to external repositories over the internet.", + destination_address_prefix="Internet", + destination_port_ranges=["80", "443"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowPackagesInternetOutbound", + priority=NetworkingPriorities.EXTERNAL_INTERNET, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_user_services_software_repositories_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), + ], opts=child_opts, ) nsg_workspaces = network.NetworkSecurityGroup( @@ -216,12 +744,12 @@ def __init__( # Inbound network.SecurityRuleArgs( access=network.SecurityRuleAccess.ALLOW, - description="Allow connections to SRDs from remote desktop gateway.", + description="Allow inbound connections from Guacamole remote desktop gateway.", destination_address_prefix=subnet_workspaces_prefix, destination_port_ranges=["22", "3389"], direction=network.SecurityRuleDirection.INBOUND, - name="AllowRemoteDesktopGatewayInbound", - priority=NetworkingPriorities.INTERNAL_SRE_REMOTE_DESKTOP, + name="AllowGuacamoleContainersInbound", + priority=NetworkingPriorities.INTERNAL_SRE_GUACAMOLE_CONTAINERS, protocol=network.SecurityRuleProtocol.ASTERISK, source_address_prefix=subnet_guacamole_containers_prefix, source_port_range="*", @@ -241,64 +769,64 @@ def __init__( # Outbound network.SecurityRuleArgs( access=network.SecurityRuleAccess.ALLOW, - description="Allow outbound connections to local monitoring tools.", - destination_address_prefix=str(props.shm_subnet_monitoring_prefix), - destination_port_ranges=["443"], + description=( + "Allow LDAP client requests over TCP. " + "See https://devopstales.github.io/linux/pfsense-ad-join/ for details." + ), + destination_address_prefix=props.shm_subnet_identity_servers_prefix, + destination_port_ranges=["389", "636"], direction=network.SecurityRuleDirection.OUTBOUND, - name="AllowMonitoringToolsOutbound", - priority=NetworkingPriorities.INTERNAL_SHM_MONITORING_TOOLS, + name="AllowLDAPClientTCPOutbound", + priority=NetworkingPriorities.INTERNAL_SHM_LDAP_TCP, protocol=network.SecurityRuleProtocol.TCP, source_address_prefix=subnet_workspaces_prefix, source_port_range="*", ), network.SecurityRuleArgs( access=network.SecurityRuleAccess.ALLOW, - description="Allow outbound connections to private data endpoints.", - destination_address_prefix=subnet_private_data_prefix, - destination_port_range="*", + description="Allow LDAP client requests over UDP.", + destination_address_prefix=props.shm_subnet_identity_servers_prefix, + destination_port_ranges=["389", "636"], direction=network.SecurityRuleDirection.OUTBOUND, - name="AllowPrivateDataEndpointsOutbound", - priority=NetworkingPriorities.INTERNAL_SRE_PRIVATE_DATA, - protocol=network.SecurityRuleProtocol.ASTERISK, + name="AllowLDAPClientUDPOutbound", + priority=NetworkingPriorities.INTERNAL_SHM_LDAP_UDP, + protocol=network.SecurityRuleProtocol.UDP, source_address_prefix=subnet_workspaces_prefix, source_port_range="*", ), network.SecurityRuleArgs( access=network.SecurityRuleAccess.ALLOW, - description="Allow outbound connections to Linux update servers.", - destination_address_prefix=props.shm_subnet_update_servers_prefix, - destination_port_ranges=["8000"], + description="Allow outbound connections to SHM monitoring tools.", + destination_address_prefix=str(props.shm_subnet_monitoring_prefix), + destination_port_ranges=["443"], direction=network.SecurityRuleDirection.OUTBOUND, - name="AllowLinuxUpdatesOutbound", - priority=NetworkingPriorities.INTERNAL_SHM_UPDATE_SERVERS, + name="AllowMonitoringToolsOutbound", + priority=NetworkingPriorities.INTERNAL_SHM_MONITORING_TOOLS, protocol=network.SecurityRuleProtocol.TCP, source_address_prefix=subnet_workspaces_prefix, source_port_range="*", ), network.SecurityRuleArgs( access=network.SecurityRuleAccess.ALLOW, - description="Allow LDAP client requests over UDP.", - destination_address_prefix=props.shm_subnet_identity_servers_prefix, - destination_port_ranges=["389", "636"], + description="Allow outbound connections to Linux update servers.", + destination_address_prefix=props.shm_subnet_update_servers_prefix, + destination_port_ranges=["8000"], direction=network.SecurityRuleDirection.OUTBOUND, - name="AllowLDAPClientUDPOutbound", - priority=NetworkingPriorities.INTERNAL_SHM_LDAP_UDP, - protocol=network.SecurityRuleProtocol.UDP, + name="AllowLinuxUpdatesOutbound", + priority=NetworkingPriorities.INTERNAL_SHM_UPDATE_SERVERS, + protocol=network.SecurityRuleProtocol.TCP, source_address_prefix=subnet_workspaces_prefix, source_port_range="*", ), network.SecurityRuleArgs( access=network.SecurityRuleAccess.ALLOW, - description=( - "Allow LDAP client requests over TCP. " - "See https://devopstales.github.io/linux/pfsense-ad-join/ for details." - ), - destination_address_prefix=props.shm_subnet_identity_servers_prefix, - destination_port_ranges=["389", "636"], + description="Allow outbound connections to private data endpoints.", + destination_address_prefix=subnet_data_private_prefix, + destination_port_range="*", direction=network.SecurityRuleDirection.OUTBOUND, - name="AllowLDAPClientTCPOutbound", - priority=NetworkingPriorities.INTERNAL_SHM_LDAP_TCP, - protocol=network.SecurityRuleProtocol.TCP, + name="AllowDataPrivateEndpointsOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_DATA_PRIVATE, + protocol=network.SecurityRuleProtocol.ASTERISK, source_address_prefix=subnet_workspaces_prefix, source_port_range="*", ), @@ -318,7 +846,7 @@ def __init__( access=network.SecurityRuleAccess.ALLOW, description="Allow outbound connections to user services databases.", destination_address_prefix=subnet_user_services_databases_prefix, - destination_port_ranges=["5432"], + destination_port_ranges=["1433", "5432"], direction=network.SecurityRuleDirection.OUTBOUND, name="AllowUserServicesDatabasesOutbound", priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_DATABASES, @@ -326,15 +854,52 @@ def __init__( source_address_prefix=subnet_workspaces_prefix, source_port_range="*", ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound connections to user services software repositories.", + destination_address_prefix=subnet_user_services_software_repositories_prefix, + destination_port_ranges=["80", "443", "3128"], + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowUserServicesSoftwareRepositoriesOutbound", + priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_workspaces_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.ALLOW, + description="Allow outbound configuration traffic over the internet.", + destination_address_prefix="Internet", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="AllowConfigurationInternetOutbound", + priority=NetworkingPriorities.EXTERNAL_INTERNET, + protocol=network.SecurityRuleProtocol.TCP, + source_address_prefix=subnet_workspaces_prefix, + source_port_range="*", + ), + network.SecurityRuleArgs( + access=network.SecurityRuleAccess.DENY, + description="Deny all other outbound traffic.", + destination_address_prefix="*", + destination_port_range="*", + direction=network.SecurityRuleDirection.OUTBOUND, + name="DenyAllOtherOutbound", + priority=NetworkingPriorities.ALL_OTHER, + protocol=network.SecurityRuleProtocol.ASTERISK, + source_address_prefix="*", + source_port_range="*", + ), ], opts=child_opts, ) # Define the virtual network and its subnets subnet_application_gateway_name = "ApplicationGatewaySubnet" + subnet_data_configuration_name = "DataConfigurationSubnet" + subnet_data_private_name = "DataPrivateSubnet" subnet_guacamole_containers_name = "GuacamoleContainersSubnet" subnet_guacamole_containers_support_name = "GuacamoleContainersSupportSubnet" - subnet_private_data_name = "PrivateDataSubnet" subnet_user_services_containers_name = "UserServicesContainersSubnet" subnet_user_services_containers_support_name = ( "UserServicesContainersSupportSubnet" @@ -359,6 +924,34 @@ def __init__( id=nsg_application_gateway.id ), ), + # Configuration data subnet + network.SubnetArgs( + address_prefix=subnet_data_configuration_prefix, + name=subnet_data_configuration_name, + network_security_group=network.NetworkSecurityGroupArgs( + id=nsg_data_configuration.id + ), + service_endpoints=[ + network.ServiceEndpointPropertiesFormatArgs( + locations=[props.location], + service="Microsoft.Storage", + ) + ], + ), + # Private data + network.SubnetArgs( + address_prefix=subnet_data_private_prefix, + name=subnet_data_private_name, + network_security_group=network.NetworkSecurityGroupArgs( + id=nsg_data_private.id + ), + service_endpoints=[ + network.ServiceEndpointPropertiesFormatArgs( + locations=[props.location], + service="Microsoft.Storage", + ) + ], + ), # Guacamole containers network.SubnetArgs( address_prefix=subnet_guacamole_containers_prefix, @@ -383,20 +976,6 @@ def __init__( ), private_endpoint_network_policies="Disabled", ), - # Private data - network.SubnetArgs( - address_prefix=subnet_private_data_prefix, - name=subnet_private_data_name, - network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_private_data.id - ), - service_endpoints=[ - network.ServiceEndpointPropertiesFormatArgs( - locations=[props.location], - service="Microsoft.Storage", - ) - ], - ), # User services containers network.SubnetArgs( address_prefix=subnet_user_services_containers_prefix, @@ -606,6 +1185,11 @@ def __init__( resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) + self.subnet_data_configuration = network.get_subnet_output( + subnet_name=subnet_data_configuration_name, + resource_group_name=resource_group.name, + virtual_network_name=sre_virtual_network.name, + ) self.subnet_guacamole_containers = network.get_subnet_output( subnet_name=subnet_guacamole_containers_name, resource_group_name=resource_group.name, @@ -616,8 +1200,8 @@ def __init__( resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) - self.subnet_private_data = network.get_subnet_output( - subnet_name=subnet_private_data_name, + self.subnet_data_private = network.get_subnet_output( + subnet_name=subnet_data_private_name, resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) diff --git a/data_safe_haven/pulumi/components/sre_workspace.py b/data_safe_haven/pulumi/components/sre_workspace.py index 4b321bf768..b309f96bc3 100644 --- a/data_safe_haven/pulumi/components/sre_workspace.py +++ b/data_safe_haven/pulumi/components/sre_workspace.py @@ -37,8 +37,8 @@ def __init__( log_analytics_workspace_key: Input[str], sre_fqdn: Input[str], sre_name: Input[str], - storage_account_userdata_name: Input[str], - storage_account_securedata_name: Input[str], + storage_account_data_private_user_name: Input[str], + storage_account_data_private_sensitive_name: Input[str], subnet_workspaces: Input[network.GetSubnetResult], virtual_network_resource_group: Input[resources.ResourceGroup], virtual_network: Input[network.VirtualNetwork], @@ -60,8 +60,12 @@ def __init__( self.log_analytics_workspace_key = log_analytics_workspace_key self.sre_fqdn = sre_fqdn self.sre_name = sre_name - self.storage_account_userdata_name = storage_account_userdata_name - self.storage_account_securedata_name = storage_account_securedata_name + self.storage_account_data_private_user_name = ( + storage_account_data_private_user_name + ) + self.storage_account_data_private_sensitive_name = ( + storage_account_data_private_sensitive_name + ) self.virtual_network_name = Output.from_input(virtual_network).apply( get_name_from_vnet ) @@ -119,8 +123,8 @@ def __init__( ldap_user_search_base=props.ldap_user_search_base, linux_update_server_ip=props.linux_update_server_ip, sre_fqdn=props.sre_fqdn, - storage_account_userdata_name=props.storage_account_userdata_name, - storage_account_securedata_name=props.storage_account_securedata_name, + storage_account_data_private_user_name=props.storage_account_data_private_user_name, + storage_account_data_private_sensitive_name=props.storage_account_data_private_sensitive_name, ).apply(lambda kwargs: self.read_cloudinit(**kwargs)) # Deploy a variable number of VMs depending on the input parameters @@ -179,8 +183,8 @@ def read_cloudinit( ldap_user_search_base: str, linux_update_server_ip: str, sre_fqdn: str, - storage_account_userdata_name: str, - storage_account_securedata_name: str, + storage_account_data_private_user_name: str, + storage_account_data_private_sensitive_name: str, ) -> str: resources_path = ( pathlib.Path(__file__).parent.parent.parent / "resources" / "workspace" @@ -199,8 +203,8 @@ def read_cloudinit( "ldap_user_search_base": ldap_user_search_base, "linux_update_server_ip": linux_update_server_ip, "sre_fqdn": sre_fqdn, - "storage_account_userdata_name": storage_account_userdata_name, - "storage_account_securedata_name": storage_account_securedata_name, + "storage_account_data_private_user_name": storage_account_data_private_user_name, + "storage_account_data_private_sensitive_name": storage_account_data_private_sensitive_name, } cloudinit = chevron.render(f_cloudinit, mustache_values) return b64encode(cloudinit) diff --git a/data_safe_haven/pulumi/declarative_shm.py b/data_safe_haven/pulumi/declarative_shm.py index 0aec8fb27a..a513ef086e 100644 --- a/data_safe_haven/pulumi/declarative_shm.py +++ b/data_safe_haven/pulumi/declarative_shm.py @@ -143,7 +143,6 @@ def run(self) -> None: # Export values for later use pulumi.export("domain_controllers", domain_controllers.exports) - pulumi.export("fqdn_nameservers", networking.dns_zone.name_servers) pulumi.export("monitoring", monitoring.exports) pulumi.export("networking", networking.exports) pulumi.export("update_servers", update_servers.exports) diff --git a/data_safe_haven/pulumi/declarative_sre.py b/data_safe_haven/pulumi/declarative_sre.py index 7d2a867bb9..7ded84a99b 100644 --- a/data_safe_haven/pulumi/declarative_sre.py +++ b/data_safe_haven/pulumi/declarative_sre.py @@ -119,7 +119,8 @@ def run(self) -> None: networking_resource_group=networking.resource_group, pulumi_opts=self.pulumi_opts, sre_fqdn=networking.sre_fqdn, - subnet_private_data=networking.subnet_private_data, + subnet_data_configuration=networking.subnet_data_configuration, + subnet_data_private=networking.subnet_data_private, subscription_id=self.cfg.azure.subscription_id, subscription_name=self.cfg.subscription_name, tenant_id=self.cfg.azure.tenant_id, @@ -161,8 +162,8 @@ def run(self) -> None: location=self.cfg.azure.location, subnet_guacamole_containers=networking.subnet_guacamole_containers, subnet_guacamole_containers_support=networking.subnet_guacamole_containers_support, - storage_account_key=data.storage_account_state_key, - storage_account_name=data.storage_account_state_name, + storage_account_key=data.storage_account_data_configuration_key, + storage_account_name=data.storage_account_data_configuration_name, storage_account_resource_group_name=data.resource_group_name, virtual_network_resource_group_name=networking.resource_group.name, virtual_network=networking.virtual_network, @@ -197,8 +198,8 @@ def run(self) -> None: ), sre_fqdn=networking.sre_fqdn, sre_name=self.sre_name, - storage_account_userdata_name=data.storage_account_userdata_name, - storage_account_securedata_name=data.storage_account_securedata_name, + storage_account_data_private_user_name=data.storage_account_data_private_user_name, + storage_account_data_private_sensitive_name=data.storage_account_data_private_sensitive_name, subnet_workspaces=networking.subnet_workspaces, virtual_network_resource_group=networking.resource_group, virtual_network=networking.virtual_network, @@ -230,8 +231,8 @@ def run(self) -> None: software_packages=self.cfg.sres[self.sre_name].software_packages, sre_fqdn=networking.sre_fqdn, sre_private_dns_zone_id=networking.sre_private_dns_zone_id, - storage_account_key=data.storage_account_state_key, - storage_account_name=data.storage_account_state_name, + storage_account_key=data.storage_account_data_configuration_key, + storage_account_name=data.storage_account_data_configuration_name, storage_account_resource_group_name=data.resource_group_name, subnet_containers=networking.subnet_user_services_containers, subnet_containers_support=networking.subnet_user_services_containers_support, @@ -248,8 +249,8 @@ def run(self) -> None: self.stack_name, SREBackupProps( location=self.cfg.azure.location, - storage_account_securedata_id=data.storage_account_securedata_id, - storage_account_securedata_name=data.storage_account_securedata_name, + storage_account_data_private_sensitive_id=data.storage_account_data_private_sensitive_id, + storage_account_data_private_sensitive_name=data.storage_account_data_private_sensitive_name, ), ) diff --git a/data_safe_haven/pulumi/dynamic/file_share_file.py b/data_safe_haven/pulumi/dynamic/file_share_file.py index e4005da3b1..cd1a0b7a01 100644 --- a/data_safe_haven/pulumi/dynamic/file_share_file.py +++ b/data_safe_haven/pulumi/dynamic/file_share_file.py @@ -113,6 +113,7 @@ def delete(self, id_: str, props: dict[str, Any]) -> None: props["destination_path"], ) if self.file_exists(file_client): + file_client.close_all_handles() file_client.delete_file() except Exception as exc: file_name = file_client.file_name if file_client else "" diff --git a/data_safe_haven/resources/software_repositories/allowlists/pypi.allowlist b/data_safe_haven/resources/software_repositories/allowlists/pypi.allowlist index d3f79b6448..59cd132ab7 100644 --- a/data_safe_haven/resources/software_repositories/allowlists/pypi.allowlist +++ b/data_safe_haven/resources/software_repositories/allowlists/pypi.allowlist @@ -7,7 +7,7 @@ numpy packaging pandas pillow -pscopyg2-binary +pscopg2-binary pyodbc pyparsing python-dateutil diff --git a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml index c1b93e2cda..c6a48dbbaa 100644 --- a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml +++ b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml @@ -57,11 +57,11 @@ write_files: mounts: # Secure data is in a blob container mounted as NFSv3 - - ["{{storage_account_securedata_name}}.blob.core.windows.net:/{{storage_account_securedata_name}}/ingress", /data, nfs, "ro,_netdev,sec=sys,vers=3,nolock,proto=tcp"] - - ["{{storage_account_securedata_name}}.blob.core.windows.net:/{{storage_account_securedata_name}}/egress", /output, nfs, "rw,_netdev,sec=sys,vers=3,nolock,proto=tcp"] + - ["{{storage_account_data_private_sensitive_name}}.blob.core.windows.net:/{{storage_account_data_private_sensitive_name}}/ingress", /data, nfs, "ro,_netdev,sec=sys,vers=3,nolock,proto=tcp"] + - ["{{storage_account_data_private_sensitive_name}}.blob.core.windows.net:/{{storage_account_data_private_sensitive_name}}/egress", /output, nfs, "rw,_netdev,sec=sys,vers=3,nolock,proto=tcp"] # User data is in a file share mounted as NFSv4 - - ["{{storage_account_userdata_name}}.file.core.windows.net:/{{storage_account_userdata_name}}/shared", /shared, nfs, "_netdev,sec=sys,nconnect=4"] - - ["{{storage_account_userdata_name}}.file.core.windows.net:/{{storage_account_userdata_name}}/home", /home, nfs, "_netdev,sec=sys,nconnect=4"] + - ["{{storage_account_data_private_user_name}}.file.core.windows.net:/{{storage_account_data_private_user_name}}/shared", /shared, nfs, "_netdev,sec=sys,nconnect=4"] + - ["{{storage_account_data_private_user_name}}.file.core.windows.net:/{{storage_account_data_private_user_name}}/home", /home, nfs, "_netdev,sec=sys,nconnect=4"] # Add additional apt repositories apt: