From 2daf01943252e9c55013c03d5f57c7b132516f4c Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Fri, 29 Nov 2024 15:54:18 +0000 Subject: [PATCH 01/42] [WIP] Adding an extra flag to config-SRE --- data_safe_haven/config/config_sections.py | 1 + .../infrastructure/programs/sre/firewall.py | 58 +++++++++++-------- tests/conftest.py | 1 + 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/data_safe_haven/config/config_sections.py b/data_safe_haven/config/config_sections.py index 62bfec0833..e1a41d2c6c 100644 --- a/data_safe_haven/config/config_sections.py +++ b/data_safe_haven/config/config_sections.py @@ -56,6 +56,7 @@ class ConfigSectionSRE(BaseModel, validate_assignment=True): # https://docs.pydantic.dev/latest/concepts/models/#fields-with-non-hashable-default-values admin_email_address: EmailAddress admin_ip_addresses: list[IpAddress] = [] + allow_workspace_internet: bool = False databases: UniqueList[DatabaseSystem] = [] data_provider_ip_addresses: list[IpAddress] | AzureServiceTag = [] remote_desktop: ConfigSubsectionRemoteDesktopOpts diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index 97f7a885b7..e6dac740a9 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -108,6 +108,38 @@ def __init__( tags=child_tags, ) + # TODO: Check how to better implement this. + # Add allow_workspace_internet boolean config. + if props.allow_workspace_internet: + workspace_deny_firewall_collection = network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.DENY + ), + name="workspaces-deny", + priority=FirewallPriorities.SRE_WORKSPACES_DENY, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Deny external Ubuntu Snap Store upload and login access", + name="DenyUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, + ), + ], + ) + else: + workspace_deny_firewall_collection = None + + # Deploy firewall firewall = network.AzureFirewall( f"{self._name}_firewall", @@ -282,31 +314,7 @@ def __init__( ), ], ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.DENY - ), - name="workspaces-deny", - priority=FirewallPriorities.SRE_WORKSPACES_DENY, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Deny external Ubuntu Snap Store upload and login access", - name="DenyUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, - ), - ], - ), + workspace_deny_firewall_collection, ], azure_firewall_name=f"{stack_name}-firewall", ip_configurations=[ diff --git a/tests/conftest.py b/tests/conftest.py index 8734d39ba1..494660d74c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -530,6 +530,7 @@ def sre_config_yaml(request): admin_email_address: admin@example.com admin_ip_addresses: - 1.2.3.4/32 + allow_workspace_internet: false data_provider_ip_addresses: [] databases: [] remote_desktop: From d0bd20cfc142d603c4af4f998fd4d58465e40f28 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Fri, 29 Nov 2024 18:25:35 +0000 Subject: [PATCH 02/42] [WIP] Setting up firewall tests --- .../programs/declarative_sre.py | 1 + .../infrastructure/programs/sre/firewall.py | 415 +++++++++--------- tests/config/test_config_sections.py | 1 + tests/infrastructure/programs/sre/conftest.py | 56 +++ .../programs/sre/test_firewall.py | 31 ++ 5 files changed, 301 insertions(+), 203 deletions(-) create mode 100644 tests/infrastructure/programs/sre/test_firewall.py diff --git a/data_safe_haven/infrastructure/programs/declarative_sre.py b/data_safe_haven/infrastructure/programs/declarative_sre.py index 78467f201b..913f7c6bd7 100644 --- a/data_safe_haven/infrastructure/programs/declarative_sre.py +++ b/data_safe_haven/infrastructure/programs/declarative_sre.py @@ -168,6 +168,7 @@ def __call__(self) -> None: "sre_firewall", self.stack_name, SREFirewallProps( + allow_workspace_internet=self.config.sre.allow_workspace_internet, location=self.config.azure.location, resource_group_name=resource_group.name, route_table_name=networking.route_table_name, diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index e6dac740a9..29194f574e 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -22,6 +22,7 @@ class SREFirewallProps: def __init__( self, + allow_workspace_internet: Input[bool], location: Input[str], resource_group_name: Input[str], route_table_name: Input[str], @@ -34,6 +35,7 @@ def __init__( subnet_user_services_software_repositories: Input[network.GetSubnetResult], subnet_workspaces: Input[network.GetSubnetResult], ) -> None: + self.allow_workspace_internet = allow_workspace_internet self.location = location self.resource_group_name = resource_group_name self.route_table_name = route_table_name @@ -108,214 +110,13 @@ def __init__( tags=child_tags, ) - # TODO: Check how to better implement this. + # TODO: Check how to better implement this. # Add allow_workspace_internet boolean config. - if props.allow_workspace_internet: - workspace_deny_firewall_collection = network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.DENY - ), - name="workspaces-deny", - priority=FirewallPriorities.SRE_WORKSPACES_DENY, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Deny external Ubuntu Snap Store upload and login access", - name="DenyUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, - ), - ], - ) - else: - workspace_deny_firewall_collection = None - # Deploy firewall firewall = network.AzureFirewall( f"{self._name}_firewall", - application_rule_collections=[ - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="apt-proxy-server", - priority=FirewallPriorities.SRE_APT_PROXY_SERVER, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external apt repository requests", - name="AllowAptRepositories", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_apt_proxy_server_prefixes, - target_fqdns=PermittedDomains.APT_REPOSITORIES, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="clamav-mirror", - priority=FirewallPriorities.SRE_CLAMAV_MIRROR, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external ClamAV definition update requests", - name="AllowClamAVDefinitionUpdates", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_clamav_mirror_prefixes, - target_fqdns=PermittedDomains.CLAMAV_UPDATES, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="identity-server", - priority=FirewallPriorities.SRE_IDENTITY_CONTAINERS, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow Microsoft OAuth login requests", - name="AllowMicrosoftOAuthLogin", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_identity_containers_prefixes, - target_fqdns=PermittedDomains.MICROSOFT_IDENTITY, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="remote-desktop-gateway", - priority=FirewallPriorities.SRE_GUACAMOLE_CONTAINERS, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow Microsoft OAuth login requests", - name="AllowMicrosoftOAuthLogin", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_guacamole_containers_prefixes, - target_fqdns=PermittedDomains.MICROSOFT_LOGIN, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="software-repositories", - priority=FirewallPriorities.SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external CRAN package requests", - name="AllowCRANPackageDownload", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_user_services_software_repositories_prefixes, - target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_R, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external PyPI package requests", - name="AllowPyPIPackageDownload", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_user_services_software_repositories_prefixes, - target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_PYTHON, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="workspaces", - priority=FirewallPriorities.SRE_WORKSPACES, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu keyserver requests", - name="AllowUbuntuKeyserver", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HKP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu Snap Store access", - name="AllowUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external RStudio deb downloads", - name="AllowRStudioDeb", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.RSTUDIO_DEB, - ), - ], - ), - workspace_deny_firewall_collection, - ], + application_rule_collections=self._get_application_rule_collections(props), azure_firewall_name=f"{stack_name}-firewall", ip_configurations=[ network.AzureFirewallIPConfigurationArgs( @@ -361,3 +162,211 @@ def __init__( route_table_name=props.route_table_name, opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=firewall)), ) + + def _get_application_rule_collections( + self, props: SREFirewallProps + ) -> list[network.AzureFirewallApplicationRuleCollectionArgs]: + application_rule_collections: list[ + network.AzureFirewallApplicationRuleCollectionArgs + ] = [ + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="apt-proxy-server", + priority=FirewallPriorities.SRE_APT_PROXY_SERVER, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external apt repository requests", + name="AllowAptRepositories", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_apt_proxy_server_prefixes, + target_fqdns=PermittedDomains.APT_REPOSITORIES, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="clamav-mirror", + priority=FirewallPriorities.SRE_CLAMAV_MIRROR, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external ClamAV definition update requests", + name="AllowClamAVDefinitionUpdates", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_clamav_mirror_prefixes, + target_fqdns=PermittedDomains.CLAMAV_UPDATES, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="identity-server", + priority=FirewallPriorities.SRE_IDENTITY_CONTAINERS, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow Microsoft OAuth login requests", + name="AllowMicrosoftOAuthLogin", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_identity_containers_prefixes, + target_fqdns=PermittedDomains.MICROSOFT_IDENTITY, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="remote-desktop-gateway", + priority=FirewallPriorities.SRE_GUACAMOLE_CONTAINERS, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow Microsoft OAuth login requests", + name="AllowMicrosoftOAuthLogin", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_guacamole_containers_prefixes, + target_fqdns=PermittedDomains.MICROSOFT_LOGIN, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="software-repositories", + priority=FirewallPriorities.SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external CRAN package requests", + name="AllowCRANPackageDownload", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_user_services_software_repositories_prefixes, + target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_R, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external PyPI package requests", + name="AllowPyPIPackageDownload", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_user_services_software_repositories_prefixes, + target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_PYTHON, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="workspaces", + priority=FirewallPriorities.SRE_WORKSPACES, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu keyserver requests", + name="AllowUbuntuKeyserver", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HKP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu Snap Store access", + name="AllowUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external RStudio deb downloads", + name="AllowRStudioDeb", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.RSTUDIO_DEB, + ), + ], + ), + ] + + if not props.allow_workspace_internet: + application_rule_collections.append( + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.DENY + ), + name="workspaces-deny", + priority=FirewallPriorities.SRE_WORKSPACES_DENY, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Deny external Ubuntu Snap Store upload and login access", + name="DenyUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, + ), + ], + ) + ) + return application_rule_collections diff --git a/tests/config/test_config_sections.py b/tests/config/test_config_sections.py index 7d9a0ba873..178d833487 100644 --- a/tests/config/test_config_sections.py +++ b/tests/config/test_config_sections.py @@ -155,6 +155,7 @@ def test_constructor_defaults( ) assert sre_config.admin_email_address == "admin@example.com" assert sre_config.admin_ip_addresses == [] + assert not sre_config.allow_workspace_internet assert sre_config.databases == [] assert sre_config.data_provider_ip_addresses == [] assert sre_config.remote_desktop == config_subsection_remote_desktop diff --git a/tests/infrastructure/programs/sre/conftest.py b/tests/infrastructure/programs/sre/conftest.py index efcbe0c921..c1c5276304 100644 --- a/tests/infrastructure/programs/sre/conftest.py +++ b/tests/infrastructure/programs/sre/conftest.py @@ -75,3 +75,59 @@ def subnet_guacamole_containers() -> network.GetSubnetResult: address_prefix=SREIpRanges.guacamole_containers.prefix, id="subnet_guacamole_containers_id", ) + + +@fixture +def subnet_apt_proxy_server() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.apt_proxy_server.prefix, + id="subnet_apt_proxy_server_id", + ) + + +@fixture +def subnet_clamav_mirror() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.clamav_mirror.prefix, + id="subnet_clamav_mirror_id", + ) + + +@fixture +def subnet_firewall() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.firewall.prefix, + id="subnet_firewall_id", + ) + + +@fixture +def subnet_firewall_management() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.firewall_management.prefix, + id="subnet_firewall_management_id", + ) + + +@fixture +def subnet_identity_containers() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.identity_containers.prefix, + id="subnet_identity_containers_id", + ) + + +@fixture +def subnet_user_services_software_repositories() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.user_services_software_repositories.prefix, + id="subnet_user_services_software_repositories_id", + ) + + +@fixture +def subnet_workspaces() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.subnet_workspaces.prefix, + id="subnet_workspaces_id", + ) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py new file mode 100644 index 0000000000..af9d2cfbcd --- /dev/null +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -0,0 +1,31 @@ +import pytest +from data_safe_haven.infrastructure.programs.sre.firewall import SREFirewallProps + + +@pytest.fixture +def firewall_props_internet_enabled( + location, + resource_group, + subnet_apt_proxy_server, + subnet_clamav_mirror, + subnet_firewall, + subnet_firewall_management, + subnet_guacamole_containers, + subnet_identity_containers, + subnet_user_services_software_repositories, + subnet_workspaces, +) -> SREFirewallProps: + return SREFirewallProps( + allow_workspace_internet=True, + location=location, + resource_group_name=resource_group.name, + route_table_name="test-route-table", + subnet_apt_proxy_server=subnet_apt_proxy_server, + subnet_clamav_mirror=subnet_clamav_mirror, + subnet_firewall=subnet_firewall, + subnet_firewall_management=subnet_firewall_management, + subnet_guacamole_containers=subnet_guacamole_containers, + subnet_identity_containers=subnet_identity_containers, + subnet_user_services_software_repositories=subnet_user_services_software_repositories, + subnet_workspaces=subnet_workspaces, + ) From 732567207f56fa42800c0aff8e2df479781566f9 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Mon, 2 Dec 2024 17:14:17 +0000 Subject: [PATCH 03/42] [WIP] The component test does not work --- .../infrastructure/programs/sre/firewall.py | 9 +- tests/infrastructure/programs/sre/conftest.py | 2 +- .../programs/sre/test_firewall.py | 136 ++++++++++++++---- 3 files changed, 113 insertions(+), 34 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index 29194f574e..a2646eb242 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -110,11 +110,8 @@ def __init__( tags=child_tags, ) - # TODO: Check how to better implement this. - # Add allow_workspace_internet boolean config. - # Deploy firewall - firewall = network.AzureFirewall( + self.firewall = network.AzureFirewall( f"{self._name}_firewall", application_rule_collections=self._get_application_rule_collections(props), azure_firewall_name=f"{stack_name}-firewall", @@ -141,7 +138,7 @@ def __init__( ) # Retrieve the private IP address for the firewall - private_ip_address = firewall.ip_configurations.apply( + private_ip_address = self.firewall.ip_configurations.apply( lambda cfgs: "" if not cfgs else cfgs[0].private_ip_address ) @@ -160,7 +157,7 @@ def __init__( resource_group_name=props.resource_group_name, route_name="ViaFirewall", route_table_name=props.route_table_name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=firewall)), + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=self.firewall)), ) def _get_application_rule_collections( diff --git a/tests/infrastructure/programs/sre/conftest.py b/tests/infrastructure/programs/sre/conftest.py index c1c5276304..dcd998f99c 100644 --- a/tests/infrastructure/programs/sre/conftest.py +++ b/tests/infrastructure/programs/sre/conftest.py @@ -128,6 +128,6 @@ def subnet_user_services_software_repositories() -> network.GetSubnetResult: @fixture def subnet_workspaces() -> network.GetSubnetResult: return network.GetSubnetResult( - address_prefix=SREIpRanges.subnet_workspaces.prefix, + address_prefix=SREIpRanges.workspaces.prefix, id="subnet_workspaces_id", ) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index af9d2cfbcd..fdda776aeb 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -1,31 +1,113 @@ +from functools import partial +from typing import Callable + +import pulumi +import pulumi.runtime import pytest -from data_safe_haven.infrastructure.programs.sre.firewall import SREFirewallProps +from pulumi_azure_native import network + +from data_safe_haven.infrastructure.programs.sre.firewall import ( + SREFirewallComponent, + SREFirewallProps, +) + +from ..resource_assertions import assert_equal, assert_equal_json + + +@pytest.fixture +def allow_internet_props_setter( + location: str, + resource_group_name: str, + subnet_apt_proxy_server: network.GetSubnetResult, + subnet_clamav_mirror: network.GetSubnetResult, + subnet_firewall: network.GetSubnetResult, + subnet_firewall_management: network.GetSubnetResult, + subnet_guacamole_containers: network.GetSubnetResult, + subnet_identity_containers: network.GetSubnetResult, + subnet_user_services_software_repositories: network.GetSubnetResult, + subnet_workspaces: network.GetSubnetResult, +) -> Callable[[bool], SREFirewallProps]: + + def set_allow_workspace_internet( + allow_workspace_internet: bool, # noqa: FBT001 + ) -> SREFirewallProps: + return SREFirewallProps( + allow_workspace_internet=allow_workspace_internet, + location=location, + resource_group_name=resource_group_name, + route_table_name="test-route-table", # TODO: Move to fixture if works. + subnet_apt_proxy_server=subnet_apt_proxy_server, + subnet_clamav_mirror=subnet_clamav_mirror, + subnet_firewall=subnet_firewall, + subnet_firewall_management=subnet_firewall_management, + subnet_guacamole_containers=subnet_guacamole_containers, + subnet_identity_containers=subnet_identity_containers, + subnet_user_services_software_repositories=subnet_user_services_software_repositories, + subnet_workspaces=subnet_workspaces, + ) + + return set_allow_workspace_internet @pytest.fixture -def firewall_props_internet_enabled( - location, - resource_group, - subnet_apt_proxy_server, - subnet_clamav_mirror, - subnet_firewall, - subnet_firewall_management, - subnet_guacamole_containers, - subnet_identity_containers, - subnet_user_services_software_repositories, - subnet_workspaces, -) -> SREFirewallProps: - return SREFirewallProps( - allow_workspace_internet=True, - location=location, - resource_group_name=resource_group.name, - route_table_name="test-route-table", - subnet_apt_proxy_server=subnet_apt_proxy_server, - subnet_clamav_mirror=subnet_clamav_mirror, - subnet_firewall=subnet_firewall, - subnet_firewall_management=subnet_firewall_management, - subnet_guacamole_containers=subnet_guacamole_containers, - subnet_identity_containers=subnet_identity_containers, - subnet_user_services_software_repositories=subnet_user_services_software_repositories, - subnet_workspaces=subnet_workspaces, - ) +def allow_internet_component_setter( + stack_name: str, + allow_internet_props_setter: Callable[[bool], SREFirewallProps], + tags: dict[str, str], +) -> Callable[[bool], SREFirewallComponent]: + + def set_allow_workspace_internet(allow_workspace_internet) -> SREFirewallComponent: + firewall_props: SREFirewallProps = allow_internet_props_setter( + allow_workspace_internet + ) + + return SREFirewallComponent( + name="firewall-name", + stack_name=stack_name, + props=firewall_props, + tags=tags, + ) + + return set_allow_workspace_internet + + +class TestSREFirewallProps: + + @pulumi.runtime.test + def test_props_allow_workspace_internet_enabled( + self, allow_internet_props_setter: Callable[[bool], SREFirewallProps] + ): + firewall_props: SREFirewallProps = allow_internet_props_setter( + allow_workspace_internet=True + ) + pulumi.Output.from_input(firewall_props.allow_workspace_internet).apply( + partial(assert_equal, True), run_with_unknowns=True # noqa: FBT003 + ) + + @pulumi.runtime.test + def test_props_allow_workspace_internet_disabled( + self, allow_internet_props_setter: Callable[[bool], SREFirewallProps] + ): + firewall_props: SREFirewallProps = allow_internet_props_setter( + allow_workspace_internet=False + ) + pulumi.Output.from_input(firewall_props.allow_workspace_internet).apply( + partial(assert_equal, False), run_with_unknowns=True # noqa: FBT003 + ) + + +class TestSREFirewallComponent: + + @pulumi.runtime.test + def test_component_allow_workspace_internet_enabled( + self, + allow_internet_component_setter: Callable[[bool], SREFirewallComponent], + ): + firewall_component: SREFirewallComponent = allow_internet_component_setter( + allow_workspace_internet=True + ) + + firewall_component.firewall.application_rule_collections.apply( + partial(assert_equal_json, []), + run_with_unknowns=True, + ) From a5a13012b0d71975b330da95315647b0754b81a3 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Tue, 3 Dec 2024 08:19:01 +0000 Subject: [PATCH 04/42] Tests seem to be working --- .../programs/sre/test_firewall.py | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index fdda776aeb..2abc7a86c9 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -1,18 +1,33 @@ +from collections.abc import Callable +from enum import Enum from functools import partial -from typing import Callable import pulumi import pulumi.runtime import pytest from pulumi_azure_native import network +from ..resource_assertions import assert_equal + + +class MyMocks(pulumi.runtime.Mocks): + def new_resource(self, args: pulumi.runtime.MockResourceArgs): + return [args.name + "_id", args.inputs] + + def call(self, _: pulumi.runtime.MockCallArgs): + return {} + + +pulumi.runtime.set_mocks( + MyMocks(), + preview=False, # Sets the flag `dry_run`, which is true at runtime during a preview. +) + from data_safe_haven.infrastructure.programs.sre.firewall import ( SREFirewallComponent, SREFirewallProps, ) -from ..resource_assertions import assert_equal, assert_equal_json - @pytest.fixture def allow_internet_props_setter( @@ -71,6 +86,11 @@ def set_allow_workspace_internet(allow_workspace_internet) -> SREFirewallCompone return set_allow_workspace_internet +class InternetAccess(Enum): + ENABLED = True + DISABLED = False + + class TestSREFirewallProps: @pulumi.runtime.test @@ -108,6 +128,45 @@ def test_component_allow_workspace_internet_enabled( ) firewall_component.firewall.application_rule_collections.apply( - partial(assert_equal_json, []), + partial(TestSREFirewallComponent.assert_allow_internet_access, InternetAccess.ENABLED), # type: ignore run_with_unknowns=True, ) + + @pulumi.runtime.test + def test_component_allow_workspace_internet_disabled( + self, + allow_internet_component_setter: Callable[[bool], SREFirewallComponent], + ): + firewall_component: SREFirewallComponent = allow_internet_component_setter( + allow_workspace_internet=False + ) + + firewall_component.firewall.application_rule_collections.apply( + partial(TestSREFirewallComponent.assert_allow_internet_access, InternetAccess.DISABLED), # type: ignore + run_with_unknowns=True, + ) + + @staticmethod + def assert_allow_internet_access( + internet_access: InternetAccess, + application_rule_collections: ( + list[network.outputs.AzureFirewallApplicationRuleCollectionResponse] | None + ), + ): + + if application_rule_collections is not None: + + workspace_deny_collection: list[ + network.outputs.AzureFirewallApplicationRuleCollectionResponse + ] = [ + rule_collection + for rule_collection in application_rule_collections + if rule_collection.name == "workspaces-deny" + ] + + if internet_access == InternetAccess.ENABLED: + assert not workspace_deny_collection + else: + assert len(workspace_deny_collection) == 1 + else: + raise AssertionError() From 4ba2d7e301e0f330a749b7a61ae3e9d3ba8da79e Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Tue, 3 Dec 2024 09:06:24 +0000 Subject: [PATCH 05/42] [WIP] My tests break other tests --- .../programs/sre/test_firewall.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index 2abc7a86c9..2d442d1836 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -7,25 +7,27 @@ import pytest from pulumi_azure_native import network +from data_safe_haven.infrastructure.programs.sre.firewall import ( + SREFirewallComponent, + SREFirewallProps, +) + from ..resource_assertions import assert_equal class MyMocks(pulumi.runtime.Mocks): def new_resource(self, args: pulumi.runtime.MockResourceArgs): - return [args.name + "_id", args.inputs] + resources = [args.name + "_id", args.inputs] + return resources def call(self, _: pulumi.runtime.MockCallArgs): return {} +# TODO: These breaks many other tests! pulumi.runtime.set_mocks( MyMocks(), - preview=False, # Sets the flag `dry_run`, which is true at runtime during a preview. -) - -from data_safe_haven.infrastructure.programs.sre.firewall import ( - SREFirewallComponent, - SREFirewallProps, + preview=False, ) @@ -168,5 +170,6 @@ def assert_allow_internet_access( assert not workspace_deny_collection else: assert len(workspace_deny_collection) == 1 + else: raise AssertionError() From 57e5fdcd2ecf3283886fba847a485710a635d033 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Wed, 4 Dec 2024 16:21:23 +0000 Subject: [PATCH 06/42] [WIP] Using network rules for allowing internet access --- .../infrastructure/programs/sre/firewall.py | 174 ++++++++++-------- .../programs/sre/test_firewall.py | 51 +++-- 2 files changed, 129 insertions(+), 96 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index a2646eb242..7f7fb40b64 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -22,7 +22,8 @@ class SREFirewallProps: def __init__( self, - allow_workspace_internet: Input[bool], + *, + allow_workspace_internet: bool, location: Input[str], resource_group_name: Input[str], route_table_name: Input[str], @@ -110,59 +111,6 @@ def __init__( tags=child_tags, ) - # Deploy firewall - self.firewall = network.AzureFirewall( - f"{self._name}_firewall", - application_rule_collections=self._get_application_rule_collections(props), - azure_firewall_name=f"{stack_name}-firewall", - ip_configurations=[ - network.AzureFirewallIPConfigurationArgs( - name="FirewallIpConfiguration", - public_ip_address=network.SubResourceArgs(id=public_ip.id), - subnet=network.SubResourceArgs(id=props.subnet_firewall_id), - ) - ], - location=props.location, - management_ip_configuration=network.AzureFirewallIPConfigurationArgs( - name="FirewallManagementIpConfiguration", - public_ip_address=network.SubResourceArgs(id=public_ip_management.id), - subnet=network.SubResourceArgs(id=props.subnet_firewall_management_id), - ), - resource_group_name=props.resource_group_name, - sku=network.AzureFirewallSkuArgs( - name=network.AzureFirewallSkuName.AZF_W_V_NET, - tier=network.AzureFirewallSkuTier.BASIC, - ), - opts=child_opts, - tags=child_tags, - ) - - # Retrieve the private IP address for the firewall - private_ip_address = self.firewall.ip_configurations.apply( - lambda cfgs: "" if not cfgs else cfgs[0].private_ip_address - ) - - # Route all external traffic through the firewall. - # - # We use the system default route "0.0.0.0/0" as this will be overruled by - # anything more specific, such as VNet <-> VNet traffic which we do not want to - # send via the firewall. - # - # See https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-udr-overview - network.Route( - f"{self._name}_route_via_firewall", - address_prefix="0.0.0.0/0", - next_hop_ip_address=private_ip_address, - next_hop_type=network.RouteNextHopType.VIRTUAL_APPLIANCE, - resource_group_name=props.resource_group_name, - route_name="ViaFirewall", - route_table_name=props.route_table_name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=self.firewall)), - ) - - def _get_application_rule_collections( - self, props: SREFirewallProps - ) -> list[network.AzureFirewallApplicationRuleCollectionArgs]: application_rule_collections: list[ network.AzureFirewallApplicationRuleCollectionArgs ] = [ @@ -336,34 +284,108 @@ def _get_application_rule_collections( ), ], ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.DENY + ), + name="workspaces-deny", + priority=FirewallPriorities.SRE_WORKSPACES_DENY, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Deny external Ubuntu Snap Store upload and login access", + name="DenyUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, + ), + ], + ), ] - if not props.allow_workspace_internet: - application_rule_collections.append( - network.AzureFirewallApplicationRuleCollectionArgs( + network_rule_collections: list[ + network.AzureFirewallNetworkRuleCollectionArgs + ] = [] + + if props.allow_workspace_internet: + application_rule_collections = [] + network_rule_collections.append( + network.AzureFirewallNetworkRuleCollectionArgs( action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.DENY + type=network.AzureFirewallRCActionType.ALLOW ), - name="workspaces-deny", - priority=FirewallPriorities.SRE_WORKSPACES_DENY, + name="workspaces-all-allow", # TODO: Fix other names. + priority=FirewallPriorities.SRE_WORKSPACES, rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Deny external Ubuntu Snap Store upload and login access", - name="DenyUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], + network.AzureFirewallNetworkRuleArgs( + description="Enables internet access to workspaces.", + destination_addresses=["*"], + destination_ports=["*"], + name="allow-internet-access", + protocols=[network.AzureFirewallNetworkRuleProtocol.ANY], source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, - ), + ) ], ) ) - return application_rule_collections + + # Deploy firewall + self.firewall = network.AzureFirewall( + f"{self._name}_firewall", + application_rule_collections=application_rule_collections, + azure_firewall_name=f"{stack_name}-firewall", + ip_configurations=[ + network.AzureFirewallIPConfigurationArgs( + name="FirewallIpConfiguration", + public_ip_address=network.SubResourceArgs(id=public_ip.id), + subnet=network.SubResourceArgs(id=props.subnet_firewall_id), + ) + ], + location=props.location, + management_ip_configuration=network.AzureFirewallIPConfigurationArgs( + name="FirewallManagementIpConfiguration", + public_ip_address=network.SubResourceArgs(id=public_ip_management.id), + subnet=network.SubResourceArgs(id=props.subnet_firewall_management_id), + ), + network_rule_collections=network_rule_collections, + resource_group_name=props.resource_group_name, + sku=network.AzureFirewallSkuArgs( + name=network.AzureFirewallSkuName.AZF_W_V_NET, + tier=network.AzureFirewallSkuTier.BASIC, + ), + opts=child_opts, + tags=child_tags, + ) + + # Retrieve the private IP address for the firewall + private_ip_address = self.firewall.ip_configurations.apply( + lambda cfgs: "" if not cfgs else cfgs[0].private_ip_address + ) + + # Route all external traffic through the firewall. + # + # We use the system default route "0.0.0.0/0" as this will be overruled by + # anything more specific, such as VNet <-> VNet traffic which we do not want to + # send via the firewall. + # + # See https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-udr-overview + network.Route( + f"{self._name}_route_via_firewall", + address_prefix="0.0.0.0/0", + next_hop_ip_address=private_ip_address, + next_hop_type=network.RouteNextHopType.VIRTUAL_APPLIANCE, + resource_group_name=props.resource_group_name, + route_name="ViaFirewall", + route_table_name=props.route_table_name, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=self.firewall) + ), + ) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index 2d442d1836..29c30b601a 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -88,6 +88,7 @@ def set_allow_workspace_internet(allow_workspace_internet) -> SREFirewallCompone return set_allow_workspace_internet +# TODO: Move to production code. class InternetAccess(Enum): ENABLED = True DISABLED = False @@ -102,9 +103,8 @@ def test_props_allow_workspace_internet_enabled( firewall_props: SREFirewallProps = allow_internet_props_setter( allow_workspace_internet=True ) - pulumi.Output.from_input(firewall_props.allow_workspace_internet).apply( - partial(assert_equal, True), run_with_unknowns=True # noqa: FBT003 - ) + + assert firewall_props.allow_workspace_internet @pulumi.runtime.test def test_props_allow_workspace_internet_disabled( @@ -113,9 +113,8 @@ def test_props_allow_workspace_internet_disabled( firewall_props: SREFirewallProps = allow_internet_props_setter( allow_workspace_internet=False ) - pulumi.Output.from_input(firewall_props.allow_workspace_internet).apply( - partial(assert_equal, False), run_with_unknowns=True # noqa: FBT003 - ) + + assert not firewall_props.allow_workspace_internet class TestSREFirewallComponent: @@ -129,8 +128,11 @@ def test_component_allow_workspace_internet_enabled( allow_workspace_internet=True ) - firewall_component.firewall.application_rule_collections.apply( - partial(TestSREFirewallComponent.assert_allow_internet_access, InternetAccess.ENABLED), # type: ignore + pulumi.Output.all( + firewall_component.firewall.application_rule_collections, + firewall_component.firewall.network_rule_collections, + ).apply( + partial(TestSREFirewallComponent.assert_allow_internet_access, internet_access=InternetAccess.ENABLED), # type: ignore run_with_unknowns=True, ) @@ -143,33 +145,42 @@ def test_component_allow_workspace_internet_disabled( allow_workspace_internet=False ) - firewall_component.firewall.application_rule_collections.apply( - partial(TestSREFirewallComponent.assert_allow_internet_access, InternetAccess.DISABLED), # type: ignore + pulumi.Output.all( + firewall_component.firewall.application_rule_collections, + firewall_component.firewall.network_rule_collections, + ).apply( + partial(TestSREFirewallComponent.assert_allow_internet_access, internet_access=InternetAccess.DISABLED), # type: ignore run_with_unknowns=True, ) @staticmethod def assert_allow_internet_access( + args: list, internet_access: InternetAccess, - application_rule_collections: ( - list[network.outputs.AzureFirewallApplicationRuleCollectionResponse] | None - ), ): - if application_rule_collections is not None: + application_rule_collections: list[dict] | None = args[0] + network_rule_collections: list[dict] | None = args[1] + + if ( + application_rule_collections is not None + and network_rule_collections is not None + ): - workspace_deny_collection: list[ - network.outputs.AzureFirewallApplicationRuleCollectionResponse + allow_internet_collection: list[ + network.outputs.AzureFirewallNetworkRuleCollectionResponse ] = [ rule_collection - for rule_collection in application_rule_collections - if rule_collection.name == "workspaces-deny" + for rule_collection in network_rule_collections + if rule_collection["name"] == "workspaces-all-allow" ] if internet_access == InternetAccess.ENABLED: - assert not workspace_deny_collection + assert len(application_rule_collections) == 0 + assert len(allow_internet_collection) == 1 else: - assert len(workspace_deny_collection) == 1 + assert len(application_rule_collections) > 0 + assert len(allow_internet_collection) == 0 else: raise AssertionError() From 3f469a8c55c6335731c2052d64ea35975f8239ea Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Thu, 5 Dec 2024 14:30:31 +0000 Subject: [PATCH 07/42] [WIP] Enable Mocks globally, and fixing tests broken by this --- .../infrastructure/programs/sre/firewall.py | 2 +- tests/infrastructure/programs/sre/conftest.py | 19 ++ .../programs/sre/test_application_gateway.py | 76 ++++-- .../programs/sre/test_firewall.py | 221 +++++++----------- 4 files changed, 164 insertions(+), 154 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index 7f7fb40b64..ab873491e6 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -377,7 +377,7 @@ def __init__( # send via the firewall. # # See https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-udr-overview - network.Route( + self.route = network.Route( f"{self._name}_route_via_firewall", address_prefix="0.0.0.0/0", next_hop_ip_address=private_ip_address, diff --git a/tests/infrastructure/programs/sre/conftest.py b/tests/infrastructure/programs/sre/conftest.py index dcd998f99c..142dc07947 100644 --- a/tests/infrastructure/programs/sre/conftest.py +++ b/tests/infrastructure/programs/sre/conftest.py @@ -1,8 +1,27 @@ +import pulumi +import pulumi.runtime from pulumi_azure_native import managedidentity, network, resources from pytest import fixture from data_safe_haven.infrastructure.common import SREIpRanges +# Mock configuration. + + +class DataSafeHavenMocks(pulumi.runtime.Mocks): + def new_resource(self, args: pulumi.runtime.MockResourceArgs): + resources = [args.name + "_id", args.inputs] + return resources + + def call(self, _: pulumi.runtime.MockCallArgs): + return {} + + +pulumi.runtime.set_mocks( + DataSafeHavenMocks(), + preview=False, +) + # # Constants diff --git a/tests/infrastructure/programs/sre/test_application_gateway.py b/tests/infrastructure/programs/sre/test_application_gateway.py index 7e919a29cf..3583fa6cbc 100644 --- a/tests/infrastructure/programs/sre/test_application_gateway.py +++ b/tests/infrastructure/programs/sre/test_application_gateway.py @@ -67,7 +67,7 @@ def test_props_resource_group_id( self, application_gateway_props: SREApplicationGatewayProps ): application_gateway_props.resource_group_id.apply( - partial(assert_equal, pulumi.UNKNOWN), + partial(assert_equal, "resource_group_id"), run_with_unknowns=True, ) @@ -112,7 +112,7 @@ def test_props_user_assigned_identities( self, application_gateway_props: SREApplicationGatewayProps ): application_gateway_props.user_assigned_identities.apply( - partial(assert_equal, pulumi.UNKNOWN), + partial(assert_equal, {"identity_key_vault_reader_id": {}}), run_with_unknowns=True, ) @@ -282,7 +282,7 @@ def test_application_gateway_frontend_ip_configurations( "name": "appGatewayFrontendIP", "private_ip_allocation_method": "Dynamic", "provisioning_state": None, - "public_ip_address": {"id": None}, + "public_ip_address": {"id": "ag-name_public_ip_id"}, "type": None, } ], @@ -356,8 +356,12 @@ def test_application_gateway_http_listeners( [ { "etag": None, - "frontend_ip_configuration": {"id": None}, - "frontend_port": {"id": None}, + "frontend_ip_configuration": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/frontendIPConfigurations/appGatewayFrontendIP" + }, + "frontend_port": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/frontendPorts/appGatewayFrontendHttp" + }, "host_name": "sre.example.com", "name": "GuacamoleHttpListener", "protocol": "Http", @@ -366,13 +370,19 @@ def test_application_gateway_http_listeners( }, { "etag": None, - "frontend_ip_configuration": {"id": None}, - "frontend_port": {"id": None}, + "frontend_ip_configuration": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/frontendIPConfigurations/appGatewayFrontendIP" + }, + "frontend_port": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/frontendPorts/appGatewayFrontendHttps" + }, "host_name": "sre.example.com", "name": "GuacamoleHttpsListener", "protocol": "Https", "provisioning_state": None, - "ssl_certificate": {"id": None}, + "ssl_certificate": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/sslCertificates/letsencryptcertificate" + }, "type": None, }, ], @@ -387,7 +397,17 @@ def test_application_gateway_identity( application_gateway_component.application_gateway.identity.apply( partial( assert_equal_json, - {"principal_id": None, "tenant_id": None, "type": "UserAssigned"}, + { + "principal_id": None, + "tenant_id": None, + "type": "UserAssigned", + "user_assigned_identities": { + "identity_key_vault_reader_id": { + "client_id": None, + "principal_id": None, + } + }, + }, ), run_with_unknowns=True, ) @@ -489,8 +509,14 @@ def test_application_gateway_redirect_configurations( "include_query_string": True, "name": "GuacamoleHttpToHttpsRedirection", "redirect_type": "Permanent", - "request_routing_rules": [{"id": None}], - "target_listener": {"id": None}, + "request_routing_rules": [ + { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/requestRoutingRules/HttpToHttpsRedirection" + } + ], + "target_listener": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/httpListeners/GuacamoleHttpsListener" + }, "type": None, } ], @@ -508,24 +534,38 @@ def test_application_gateway_request_routing_rules( [ { "etag": None, - "http_listener": {"id": None}, + "http_listener": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/httpListeners/GuacamoleHttpListener" + }, "name": "GuacamoleHttpRouting", "priority": 200, "provisioning_state": None, - "redirect_configuration": {"id": None}, - "rewrite_rule_set": {"id": None}, + "redirect_configuration": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/redirectConfigurations/GuacamoleHttpToHttpsRedirection" + }, + "rewrite_rule_set": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/rewriteRuleSets/ResponseHeaders" + }, "rule_type": "Basic", "type": None, }, { - "backend_address_pool": {"id": None}, - "backend_http_settings": {"id": None}, + "backend_address_pool": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/backendAddressPools/appGatewayBackendGuacamole" + }, + "backend_http_settings": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/backendHttpSettingsCollection/appGatewayBackendHttpSettings" + }, "etag": None, - "http_listener": {"id": None}, + "http_listener": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/httpListeners/GuacamoleHttpsListener" + }, "name": "GuacamoleHttpsRouting", "priority": 100, "provisioning_state": None, - "rewrite_rule_set": {"id": None}, + "rewrite_rule_set": { + "id": "resource_group_id/providers/Microsoft.Network/applicationGateways/stack-example-ag-entrypoint/rewriteRuleSets/ResponseHeaders" + }, "rule_type": "Basic", "type": None, }, diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index 29c30b601a..d73d21def7 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -1,6 +1,3 @@ -from collections.abc import Callable -from enum import Enum -from functools import partial import pulumi import pulumi.runtime @@ -12,29 +9,12 @@ SREFirewallProps, ) -from ..resource_assertions import assert_equal - - -class MyMocks(pulumi.runtime.Mocks): - def new_resource(self, args: pulumi.runtime.MockResourceArgs): - resources = [args.name + "_id", args.inputs] - return resources - - def call(self, _: pulumi.runtime.MockCallArgs): - return {} - - -# TODO: These breaks many other tests! -pulumi.runtime.set_mocks( - MyMocks(), - preview=False, -) - @pytest.fixture -def allow_internet_props_setter( +def firewall_props_internet_enabled( location: str, resource_group_name: str, + stack_name: str, subnet_apt_proxy_server: network.GetSubnetResult, subnet_clamav_mirror: network.GetSubnetResult, subnet_firewall: network.GetSubnetResult, @@ -43,144 +23,115 @@ def allow_internet_props_setter( subnet_identity_containers: network.GetSubnetResult, subnet_user_services_software_repositories: network.GetSubnetResult, subnet_workspaces: network.GetSubnetResult, -) -> Callable[[bool], SREFirewallProps]: - - def set_allow_workspace_internet( - allow_workspace_internet: bool, # noqa: FBT001 - ) -> SREFirewallProps: - return SREFirewallProps( - allow_workspace_internet=allow_workspace_internet, - location=location, - resource_group_name=resource_group_name, - route_table_name="test-route-table", # TODO: Move to fixture if works. - subnet_apt_proxy_server=subnet_apt_proxy_server, - subnet_clamav_mirror=subnet_clamav_mirror, - subnet_firewall=subnet_firewall, - subnet_firewall_management=subnet_firewall_management, - subnet_guacamole_containers=subnet_guacamole_containers, - subnet_identity_containers=subnet_identity_containers, - subnet_user_services_software_repositories=subnet_user_services_software_repositories, - subnet_workspaces=subnet_workspaces, - ) - - return set_allow_workspace_internet +) -> SREFirewallProps: + return SREFirewallProps( + allow_workspace_internet=True, + location=location, + resource_group_name=resource_group_name, + route_table_name=f"{stack_name}-route-table", + subnet_apt_proxy_server=subnet_apt_proxy_server, + subnet_clamav_mirror=subnet_clamav_mirror, + subnet_firewall=subnet_firewall, + subnet_firewall_management=subnet_firewall_management, + subnet_guacamole_containers=subnet_guacamole_containers, + subnet_identity_containers=subnet_identity_containers, + subnet_user_services_software_repositories=subnet_user_services_software_repositories, + subnet_workspaces=subnet_workspaces, + ) @pytest.fixture -def allow_internet_component_setter( +def firewall_props_internet_disabled( + location: str, + resource_group_name: str, stack_name: str, - allow_internet_props_setter: Callable[[bool], SREFirewallProps], - tags: dict[str, str], -) -> Callable[[bool], SREFirewallComponent]: - - def set_allow_workspace_internet(allow_workspace_internet) -> SREFirewallComponent: - firewall_props: SREFirewallProps = allow_internet_props_setter( - allow_workspace_internet - ) - - return SREFirewallComponent( - name="firewall-name", - stack_name=stack_name, - props=firewall_props, - tags=tags, - ) - - return set_allow_workspace_internet - - -# TODO: Move to production code. -class InternetAccess(Enum): - ENABLED = True - DISABLED = False + subnet_apt_proxy_server: network.GetSubnetResult, + subnet_clamav_mirror: network.GetSubnetResult, + subnet_firewall: network.GetSubnetResult, + subnet_firewall_management: network.GetSubnetResult, + subnet_guacamole_containers: network.GetSubnetResult, + subnet_identity_containers: network.GetSubnetResult, + subnet_user_services_software_repositories: network.GetSubnetResult, + subnet_workspaces: network.GetSubnetResult, +) -> SREFirewallProps: + return SREFirewallProps( + allow_workspace_internet=False, + location=location, + resource_group_name=resource_group_name, + route_table_name=f"{stack_name}-route-table", + subnet_apt_proxy_server=subnet_apt_proxy_server, + subnet_clamav_mirror=subnet_clamav_mirror, + subnet_firewall=subnet_firewall, + subnet_firewall_management=subnet_firewall_management, + subnet_guacamole_containers=subnet_guacamole_containers, + subnet_identity_containers=subnet_identity_containers, + subnet_user_services_software_repositories=subnet_user_services_software_repositories, + subnet_workspaces=subnet_workspaces, + ) -class TestSREFirewallProps: +class TestSREFirewallComponent: @pulumi.runtime.test - def test_props_allow_workspace_internet_enabled( - self, allow_internet_props_setter: Callable[[bool], SREFirewallProps] + def test_component_allow_workspace_internet_enabled( + self, + firewall_props_internet_enabled: SREFirewallProps, + stack_name: str, + tags: dict[str, str], ): - firewall_props: SREFirewallProps = allow_internet_props_setter( - allow_workspace_internet=True - ) - assert firewall_props.allow_workspace_internet - - @pulumi.runtime.test - def test_props_allow_workspace_internet_disabled( - self, allow_internet_props_setter: Callable[[bool], SREFirewallProps] - ): - firewall_props: SREFirewallProps = allow_internet_props_setter( - allow_workspace_internet=False + firewall_component: SREFirewallComponent = SREFirewallComponent( + name="sre_firewall_with_internet", + stack_name=stack_name, + props=firewall_props_internet_enabled, + tags=tags, ) - assert not firewall_props.allow_workspace_internet - + def assert_on_firewall_rules( + args: list, + ): + application_rule_collections: list[dict] = args[0] + network_rule_collections: list[dict] = args[1] -class TestSREFirewallComponent: + # TODO: Be more precise in rule filtering. + allow_internet_collection: list[dict] = [ + rule_collection + for rule_collection in network_rule_collections + if rule_collection["name"] == "workspaces-all-allow" + ] - @pulumi.runtime.test - def test_component_allow_workspace_internet_enabled( - self, - allow_internet_component_setter: Callable[[bool], SREFirewallComponent], - ): - firewall_component: SREFirewallComponent = allow_internet_component_setter( - allow_workspace_internet=True - ) + assert len(application_rule_collections) == 0 + assert len(allow_internet_collection) == 1 pulumi.Output.all( firewall_component.firewall.application_rule_collections, firewall_component.firewall.network_rule_collections, - ).apply( - partial(TestSREFirewallComponent.assert_allow_internet_access, internet_access=InternetAccess.ENABLED), # type: ignore - run_with_unknowns=True, - ) + ).apply(assert_on_firewall_rules) @pulumi.runtime.test def test_component_allow_workspace_internet_disabled( self, - allow_internet_component_setter: Callable[[bool], SREFirewallComponent], + firewall_props_internet_disabled: SREFirewallProps, + stack_name: str, + tags: dict[str, str], ): - firewall_component: SREFirewallComponent = allow_internet_component_setter( - allow_workspace_internet=False - ) - - pulumi.Output.all( - firewall_component.firewall.application_rule_collections, - firewall_component.firewall.network_rule_collections, - ).apply( - partial(TestSREFirewallComponent.assert_allow_internet_access, internet_access=InternetAccess.DISABLED), # type: ignore - run_with_unknowns=True, + firewall_component: SREFirewallComponent = SREFirewallComponent( + name="sre_firewall_with_internet", + stack_name=stack_name, + props=firewall_props_internet_disabled, + tags=tags, ) - @staticmethod - def assert_allow_internet_access( - args: list, - internet_access: InternetAccess, - ): - - application_rule_collections: list[dict] | None = args[0] - network_rule_collections: list[dict] | None = args[1] - - if ( - application_rule_collections is not None - and network_rule_collections is not None + def assert_on_firewall_rules( + args: list, ): + application_rule_collections: list[dict] = args[0] + network_rule_collections: list[dict] = args[1] - allow_internet_collection: list[ - network.outputs.AzureFirewallNetworkRuleCollectionResponse - ] = [ - rule_collection - for rule_collection in network_rule_collections - if rule_collection["name"] == "workspaces-all-allow" - ] + assert len(application_rule_collections) > 0 + assert len(network_rule_collections) == 0 - if internet_access == InternetAccess.ENABLED: - assert len(application_rule_collections) == 0 - assert len(allow_internet_collection) == 1 - else: - assert len(application_rule_collections) > 0 - assert len(allow_internet_collection) == 0 - - else: - raise AssertionError() + pulumi.Output.all( + firewall_component.firewall.application_rule_collections, + firewall_component.firewall.network_rule_collections, + ).apply(assert_on_firewall_rules) From c49a2278989e0963b26bd142049a7f9eee15ef18 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Thu, 5 Dec 2024 14:46:08 +0000 Subject: [PATCH 08/42] Remove unnecessary white space --- tests/infrastructure/programs/sre/test_firewall.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index d73d21def7..600cb132c1 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -1,4 +1,3 @@ - import pulumi import pulumi.runtime import pytest From a57a8bbc093de80c34ef2ddd333312812d2c952e Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Mon, 9 Dec 2024 11:21:01 +0000 Subject: [PATCH 09/42] Fixing bug --- data_safe_haven/infrastructure/programs/sre/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index a256ed749f..a7506d87e3 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -394,7 +394,7 @@ def __init__( }, } ], - resource_uri=firewall.id, + resource_uri=self.firewall.id, workspace_id=props.log_analytics_workspace.id, ) From d1e4f230eb4cb7b3804f14bb4df51a00814c83c7 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Mon, 9 Dec 2024 15:09:28 +0000 Subject: [PATCH 10/42] [WIP] Enabling al sources on the network rule --- data_safe_haven/infrastructure/programs/sre/firewall.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index a7506d87e3..8408ff89b5 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -334,7 +334,9 @@ def __init__( destination_ports=["*"], name="allow-internet-access", protocols=[network.AzureFirewallNetworkRuleProtocol.ANY], - source_addresses=props.subnet_workspaces_prefixes, + source_addresses=[ + "*" + ], # TODO: Check if we can make this more restrictive. ) ], ) From 6a0c4d7426b73bd41ce7a0c5267db0f96e7c8819 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Tue, 10 Dec 2024 15:54:36 +0000 Subject: [PATCH 11/42] Fixing tests, and relaxing DNS rules when internet is allowed --- .../infrastructure/programs/declarative_sre.py | 1 + .../infrastructure/programs/sre/dns_server.py | 10 ++++++++-- .../resources/dns_server/AdGuardHome.mustache.yaml | 5 +++++ tests/infrastructure/programs/sre/conftest.py | 8 ++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/declarative_sre.py b/data_safe_haven/infrastructure/programs/declarative_sre.py index dc3ab43ea1..02cc59b71c 100644 --- a/data_safe_haven/infrastructure/programs/declarative_sre.py +++ b/data_safe_haven/infrastructure/programs/declarative_sre.py @@ -123,6 +123,7 @@ def __call__(self) -> None: "sre_dns_server", self.stack_name, SREDnsServerProps( + allow_workspace_internet=self.config.sre.allow_workspace_internet, dockerhub_credentials=dockerhub_credentials, location=self.config.azure.location, resource_group_name=resource_group.name, diff --git a/data_safe_haven/infrastructure/programs/sre/dns_server.py b/data_safe_haven/infrastructure/programs/sre/dns_server.py index df85e09d83..b50c908f86 100644 --- a/data_safe_haven/infrastructure/programs/sre/dns_server.py +++ b/data_safe_haven/infrastructure/programs/sre/dns_server.py @@ -28,12 +28,15 @@ class SREDnsServerProps: def __init__( self, + *, + allow_workspace_internet: bool, dockerhub_credentials: DockerHubCredentials, location: Input[str], resource_group_name: Input[str], shm_fqdn: Input[str], ) -> None: self.admin_username = "dshadmin" + self.allow_workspace_internet: bool = allow_workspace_internet self.dockerhub_credentials = dockerhub_credentials self.location = location self.resource_group_name = resource_group_name @@ -69,6 +72,9 @@ def __init__( ) # Expand AdGuardHome YAML configuration + mustache_values: dict[str, object] = { + "allow_workspace_internet": props.allow_workspace_internet + } adguard_adguardhome_yaml_contents = Output.all( admin_username=props.admin_username, # Only the first 72 bytes of the generated random string will be used but a @@ -85,8 +91,8 @@ def __init__( ] ), ).apply( - lambda mustache_values: adguard_adguardhome_yaml_reader.file_contents( - mustache_values + lambda mustache_config: adguard_adguardhome_yaml_reader.file_contents( + mustache_config | mustache_values ) ) diff --git a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml index eef58d5a0a..9c75067956 100644 --- a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml +++ b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml @@ -11,12 +11,17 @@ dns: querylog: enabled: true filters: +{{#allow_workspace_internet}} +user_rules: [] +{{/allow_workspace_internet}} +{{^allow_workspace_internet}} user_rules: # https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#adblock-style-syntax - "*.*" {{#filter_allow}} - "@@||{{.}}" {{/filter_allow}} +{{/allow_workspace_internet}} log: verbose: true # Note that because we are only providing a partial config file we need the diff --git a/tests/infrastructure/programs/sre/conftest.py b/tests/infrastructure/programs/sre/conftest.py index 142dc07947..11e9997c11 100644 --- a/tests/infrastructure/programs/sre/conftest.py +++ b/tests/infrastructure/programs/sre/conftest.py @@ -150,3 +150,11 @@ def subnet_workspaces() -> network.GetSubnetResult: address_prefix=SREIpRanges.workspaces.prefix, id="subnet_workspaces_id", ) + + +@fixture +def subnet_monitoring() -> network.GetSubnetResult: + return network.GetSubnetResult( + address_prefix=SREIpRanges.monitoring.prefix, + id="subnet_monitoring_id", + ) From 5e46b73c1ee885ee8db7e1b1f0bc2788b7f29bb8 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Wed, 11 Dec 2024 16:51:57 +0000 Subject: [PATCH 12/42] Fixing firewall tests --- .../programs/sre/test_firewall.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index 600cb132c1..083016ce1b 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -3,10 +3,49 @@ import pytest from pulumi_azure_native import network +from data_safe_haven.functions import replace_separators from data_safe_haven.infrastructure.programs.sre.firewall import ( SREFirewallComponent, SREFirewallProps, ) +from data_safe_haven.infrastructure.programs.sre.monitoring import ( + SREMonitoringComponent, + SREMonitoringProps, +) +from data_safe_haven.types import AzureDnsZoneNames + + +@pytest.fixture +def sre_monitoring_component( + location: str, + resource_group_name: str, + stack_name: str, + subnet_monitoring: network.GetSubnetResult, + tags: dict[str, str], +) -> SREMonitoringComponent: + return SREMonitoringComponent( + "test_sre_monitoring", + stack_name, + SREMonitoringProps( + dns_private_zones={ + dns_zone_name: network.PrivateZone( + replace_separators( + f"test_sre_dns_server_private_zone_{dns_zone_name}", "_" + ), + location="Global", + private_zone_name=f"privatelink.{dns_zone_name}", + resource_group_name=resource_group_name, + tags=tags, + ) + for dns_zone_name in AzureDnsZoneNames.ALL + }, # TODO: Check if this works + location=location, + resource_group_name=resource_group_name, + subnet=subnet_monitoring, + timezone="Europe/London", + ), + tags=tags, + ) @pytest.fixture @@ -14,6 +53,7 @@ def firewall_props_internet_enabled( location: str, resource_group_name: str, stack_name: str, + sre_monitoring_component: SREMonitoringComponent, subnet_apt_proxy_server: network.GetSubnetResult, subnet_clamav_mirror: network.GetSubnetResult, subnet_firewall: network.GetSubnetResult, @@ -26,6 +66,7 @@ def firewall_props_internet_enabled( return SREFirewallProps( allow_workspace_internet=True, location=location, + log_analytics_workspace=sre_monitoring_component, resource_group_name=resource_group_name, route_table_name=f"{stack_name}-route-table", subnet_apt_proxy_server=subnet_apt_proxy_server, @@ -44,6 +85,7 @@ def firewall_props_internet_disabled( location: str, resource_group_name: str, stack_name: str, + sre_monitoring_component: SREMonitoringComponent, subnet_apt_proxy_server: network.GetSubnetResult, subnet_clamav_mirror: network.GetSubnetResult, subnet_firewall: network.GetSubnetResult, @@ -56,6 +98,7 @@ def firewall_props_internet_disabled( return SREFirewallProps( allow_workspace_internet=False, location=location, + log_analytics_workspace=sre_monitoring_component, resource_group_name=resource_group_name, route_table_name=f"{stack_name}-route-table", subnet_apt_proxy_server=subnet_apt_proxy_server, From 3394d9201c02a8d11e195dcedb47f18205beb3a4 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Thu, 19 Dec 2024 12:18:40 +0000 Subject: [PATCH 13/42] Supporting custom tests for Mustache templates --- .github/workflows/lint_code.yaml | 17 ++++++++++++++--- tests/mustache/AdGuardHome.mustache.config.json | 7 +++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/mustache/AdGuardHome.mustache.config.json diff --git a/.github/workflows/lint_code.yaml b/.github/workflows/lint_code.yaml index 4d0caed16c..658fcd468d 100644 --- a/.github/workflows/lint_code.yaml +++ b/.github/workflows/lint_code.yaml @@ -102,9 +102,20 @@ jobs: run: | echo '{"array": ["dummy"], "variable": "dummy"}' > .mustache_config.json for yamlfile in $(find . -name "*.yml" -o -name "*.yaml"); do - sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" $yamlfile > expanded.tmp # replace mustache arrays - sed -i "s|{{[^#/].\{1,\}}}|{{variable}}|g" expanded.tmp # replace mustache variables - mustache .mustache_config.json expanded.tmp > $yamlfile # perform mustache expansion overwriting original file + + filename=$(basename -- "$yamlfile") + extension="${filename##*.}" + filename="${filename%.*}" + test_config="tests/mustache/$filename.config.json" + + if [ -e "$test_config" ]; then + mustache $test_config $yamlfile > $yamlfile + else + sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" $yamlfile > expanded.tmp # replace mustache arrays + sed -i "s|{{[^#/].\{1,\}}}|{{variable}}|g" expanded.tmp # replace mustache variables + mustache .mustache_config.json expanded.tmp > $yamlfile # perform mustache expansion overwriting original file + fi + done rm expanded.tmp - name: Lint YAML diff --git a/tests/mustache/AdGuardHome.mustache.config.json b/tests/mustache/AdGuardHome.mustache.config.json new file mode 100644 index 0000000000..fef6575236 --- /dev/null +++ b/tests/mustache/AdGuardHome.mustache.config.json @@ -0,0 +1,7 @@ +{ + "filter_allow": [ + "clamav.net", + "current.cvd.clamav.net" + ], + "allow_workspace_internet": true +} \ No newline at end of file From 08e2006f26ef260e0a1b8a69504db7d9851f78b1 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Thu, 19 Dec 2024 12:58:41 +0000 Subject: [PATCH 14/42] Removing trailing space --- data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml index 9c75067956..5f2b2a8fab 100644 --- a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml +++ b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml @@ -26,4 +26,4 @@ log: verbose: true # Note that because we are only providing a partial config file we need the # `schema_version` key or the full set of YAML migrations will get run. -schema_version: 24 +schema_version: 24 \ No newline at end of file From 21d8a5526f4943332c321462b301be5cee3a0252 Mon Sep 17 00:00:00 2001 From: Carlos Gavidia-Calderon Date: Thu, 19 Dec 2024 13:12:25 +0000 Subject: [PATCH 15/42] Removing trailing spaces from GitHub Action YAML file --- .github/workflows/lint_code.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint_code.yaml b/.github/workflows/lint_code.yaml index 658fcd468d..fd56acb37f 100644 --- a/.github/workflows/lint_code.yaml +++ b/.github/workflows/lint_code.yaml @@ -110,11 +110,11 @@ jobs: if [ -e "$test_config" ]; then mustache $test_config $yamlfile > $yamlfile - else + else sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" $yamlfile > expanded.tmp # replace mustache arrays sed -i "s|{{[^#/].\{1,\}}}|{{variable}}|g" expanded.tmp # replace mustache variables mustache .mustache_config.json expanded.tmp > $yamlfile # perform mustache expansion overwriting original file - fi + fi done rm expanded.tmp From 6d21bd7a74b6e37e934c690f5c392c5016c33b49 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 10:12:28 +0000 Subject: [PATCH 16/42] Move mustache template expansion file --- .../resources}/AdGuardHome.mustache.config.json | 0 .github/workflows/lint_code.yaml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {tests/mustache => .github/resources}/AdGuardHome.mustache.config.json (100%) diff --git a/tests/mustache/AdGuardHome.mustache.config.json b/.github/resources/AdGuardHome.mustache.config.json similarity index 100% rename from tests/mustache/AdGuardHome.mustache.config.json rename to .github/resources/AdGuardHome.mustache.config.json diff --git a/.github/workflows/lint_code.yaml b/.github/workflows/lint_code.yaml index fd56acb37f..ef0f9b05dc 100644 --- a/.github/workflows/lint_code.yaml +++ b/.github/workflows/lint_code.yaml @@ -106,7 +106,7 @@ jobs: filename=$(basename -- "$yamlfile") extension="${filename##*.}" filename="${filename%.*}" - test_config="tests/mustache/$filename.config.json" + test_config=".github/resources/$filename.config.json" if [ -e "$test_config" ]; then mustache $test_config $yamlfile > $yamlfile From 8cfc0b6a0177cd8a3336a694b44d52d7a991124a Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 10:16:28 +0000 Subject: [PATCH 17/42] Move mustache expansion to a script --- .github/scripts/expand_mustache.sh | 18 ++++++++++++++++++ .github/workflows/lint_code.yaml | 21 +-------------------- 2 files changed, 19 insertions(+), 20 deletions(-) create mode 100755 .github/scripts/expand_mustache.sh diff --git a/.github/scripts/expand_mustache.sh b/.github/scripts/expand_mustache.sh new file mode 100755 index 0000000000..159d851c2b --- /dev/null +++ b/.github/scripts/expand_mustache.sh @@ -0,0 +1,18 @@ +echo '{"array": ["dummy"], "variable": "dummy"}' > .mustache_config.json +for yamlfile in $(find . -name "*.yml" -o -name "*.yaml"); do + + filename=$(basename -- "$yamlfile") + extension="${filename##*.}" + filename="${filename%.*}" + test_config=".github/resources/$filename.config.json" + + if [ -e "$test_config" ]; then + mustache $test_config $yamlfile > $yamlfile + else + sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" $yamlfile > expanded.tmp # replace mustache arrays + sed -i "s|{{[^#/].\{1,\}}}|{{variable}}|g" expanded.tmp # replace mustache variables + mustache .mustache_config.json expanded.tmp > $yamlfile # perform mustache expansion overwriting original file + fi + +done +rm expanded.tmp diff --git a/.github/workflows/lint_code.yaml b/.github/workflows/lint_code.yaml index ef0f9b05dc..7dc3e0b6c5 100644 --- a/.github/workflows/lint_code.yaml +++ b/.github/workflows/lint_code.yaml @@ -98,26 +98,7 @@ jobs: run: | npm install -g mustache - name: Expand mustache templates - shell: bash - run: | - echo '{"array": ["dummy"], "variable": "dummy"}' > .mustache_config.json - for yamlfile in $(find . -name "*.yml" -o -name "*.yaml"); do - - filename=$(basename -- "$yamlfile") - extension="${filename##*.}" - filename="${filename%.*}" - test_config=".github/resources/$filename.config.json" - - if [ -e "$test_config" ]; then - mustache $test_config $yamlfile > $yamlfile - else - sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" $yamlfile > expanded.tmp # replace mustache arrays - sed -i "s|{{[^#/].\{1,\}}}|{{variable}}|g" expanded.tmp # replace mustache variables - mustache .mustache_config.json expanded.tmp > $yamlfile # perform mustache expansion overwriting original file - fi - - done - rm expanded.tmp + run: .github/scripts/expand_mustache.sh - name: Lint YAML uses: karancode/yamllint-github-action@v3.0.0 with: From 92c0caddcd9ecf61f764519ccfd7bd41598bd748 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 10:31:17 +0000 Subject: [PATCH 18/42] Fix issues in mustache script --- .github/scripts/expand_mustache.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/scripts/expand_mustache.sh b/.github/scripts/expand_mustache.sh index 159d851c2b..dd1db56a6a 100755 --- a/.github/scripts/expand_mustache.sh +++ b/.github/scripts/expand_mustache.sh @@ -1,18 +1,23 @@ +#!/usr/bin/env bash echo '{"array": ["dummy"], "variable": "dummy"}' > .mustache_config.json -for yamlfile in $(find . -name "*.yml" -o -name "*.yaml"); do + +while read -r -d '' yamlfile; do filename=$(basename -- "$yamlfile") - extension="${filename##*.}" filename="${filename%.*}" test_config=".github/resources/$filename.config.json" if [ -e "$test_config" ]; then - mustache $test_config $yamlfile > $yamlfile + mustache "$test_config" "$yamlfile" | sponge "$yamlfile" else - sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" $yamlfile > expanded.tmp # replace mustache arrays - sed -i "s|{{[^#/].\{1,\}}}|{{variable}}|g" expanded.tmp # replace mustache variables - mustache .mustache_config.json expanded.tmp > $yamlfile # perform mustache expansion overwriting original file + # replace mustache arrays + sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" "$yamlfile" > expanded.tmp + # replace mustache variables + sed -i "s|{{[^#/].\{1,\}}}|{{variable}}|g" expanded.tmp + # perform mustache expansion overwriting original file + mustache .mustache_config.json expanded.tmp > "$yamlfile" fi -done +done < <(find . -name "*.yml" -o -name "*.yaml") + rm expanded.tmp From 5e68a5ffbaa2d000cf36f3f3c2e658b77e5ec252 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 10:34:58 +0000 Subject: [PATCH 19/42] Tidy mustache template completion --- data_safe_haven/infrastructure/programs/sre/dns_server.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/dns_server.py b/data_safe_haven/infrastructure/programs/sre/dns_server.py index b50c908f86..531fa40a2d 100644 --- a/data_safe_haven/infrastructure/programs/sre/dns_server.py +++ b/data_safe_haven/infrastructure/programs/sre/dns_server.py @@ -72,14 +72,12 @@ def __init__( ) # Expand AdGuardHome YAML configuration - mustache_values: dict[str, object] = { - "allow_workspace_internet": props.allow_workspace_internet - } adguard_adguardhome_yaml_contents = Output.all( admin_username=props.admin_username, # Only the first 72 bytes of the generated random string will be used but a # 20 character UTF-8 string (alphanumeric + special) will not exceed that. admin_password_encrypted=password_admin.bcrypt_hash, + allow_workspace_internet=props.allow_workspace_internet, # Use Azure virtual DNS server as upstream # https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16 # This server is aware of private DNS zones @@ -92,7 +90,7 @@ def __init__( ), ).apply( lambda mustache_config: adguard_adguardhome_yaml_reader.file_contents( - mustache_config | mustache_values + mustache_config ) ) From de3912c0c1137a3d8d66ca820ecaccddf2a85ff1 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 10:38:07 +0000 Subject: [PATCH 20/42] Remove TODO --- data_safe_haven/infrastructure/programs/sre/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index 8408ff89b5..d39e3ab09c 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -325,7 +325,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="workspaces-all-allow", # TODO: Fix other names. + name="workspaces-all-allow", priority=FirewallPriorities.SRE_WORKSPACES, rules=[ network.AzureFirewallNetworkRuleArgs( From 39964dd1f33950f9a9ea4a80864aae41d53cee97 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:02:50 +0000 Subject: [PATCH 21/42] Remove unnecessary type hint --- data_safe_haven/infrastructure/programs/sre/dns_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/infrastructure/programs/sre/dns_server.py b/data_safe_haven/infrastructure/programs/sre/dns_server.py index 531fa40a2d..f7d3cc5899 100644 --- a/data_safe_haven/infrastructure/programs/sre/dns_server.py +++ b/data_safe_haven/infrastructure/programs/sre/dns_server.py @@ -36,7 +36,7 @@ def __init__( shm_fqdn: Input[str], ) -> None: self.admin_username = "dshadmin" - self.allow_workspace_internet: bool = allow_workspace_internet + self.allow_workspace_internet = allow_workspace_internet self.dockerhub_credentials = dockerhub_credentials self.location = location self.resource_group_name = resource_group_name From 04535804d1991366c1a4bf701b489bdd385e6da2 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:07:20 +0000 Subject: [PATCH 22/42] Add newlines at end of files --- .github/resources/AdGuardHome.mustache.config.json | 2 +- data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/resources/AdGuardHome.mustache.config.json b/.github/resources/AdGuardHome.mustache.config.json index fef6575236..13822b9123 100644 --- a/.github/resources/AdGuardHome.mustache.config.json +++ b/.github/resources/AdGuardHome.mustache.config.json @@ -4,4 +4,4 @@ "current.cvd.clamav.net" ], "allow_workspace_internet": true -} \ No newline at end of file +} diff --git a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml index 5f2b2a8fab..9c75067956 100644 --- a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml +++ b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml @@ -26,4 +26,4 @@ log: verbose: true # Note that because we are only providing a partial config file we need the # `schema_version` key or the full set of YAML migrations will get run. -schema_version: 24 \ No newline at end of file +schema_version: 24 From 83ce8611ce70e8ca0b0ea22bd4123dca88f96b6f Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:13:25 +0000 Subject: [PATCH 23/42] Remove delimiter option --- .github/scripts/expand_mustache.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/expand_mustache.sh b/.github/scripts/expand_mustache.sh index dd1db56a6a..6ae96cb69f 100755 --- a/.github/scripts/expand_mustache.sh +++ b/.github/scripts/expand_mustache.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash echo '{"array": ["dummy"], "variable": "dummy"}' > .mustache_config.json -while read -r -d '' yamlfile; do +while read -r yamlfile; do filename=$(basename -- "$yamlfile") filename="${filename%.*}" From 074873351fed04b3d22c3e1a01ae77f38ee157e8 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:15:26 +0000 Subject: [PATCH 24/42] Tidy import in firewall tests --- tests/infrastructure/programs/sre/test_firewall.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index 083016ce1b..8dcfba534c 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -1,7 +1,7 @@ import pulumi import pulumi.runtime -import pytest from pulumi_azure_native import network +from pytest import fixture from data_safe_haven.functions import replace_separators from data_safe_haven.infrastructure.programs.sre.firewall import ( @@ -15,7 +15,7 @@ from data_safe_haven.types import AzureDnsZoneNames -@pytest.fixture +@fixture def sre_monitoring_component( location: str, resource_group_name: str, @@ -48,7 +48,7 @@ def sre_monitoring_component( ) -@pytest.fixture +@fixture def firewall_props_internet_enabled( location: str, resource_group_name: str, @@ -80,7 +80,7 @@ def firewall_props_internet_enabled( ) -@pytest.fixture +@fixture def firewall_props_internet_disabled( location: str, resource_group_name: str, From da89565eaf4ded771e4bc449be44bdf1ceed00a8 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:20:59 +0000 Subject: [PATCH 25/42] Tidy allow internet logic --- .../infrastructure/programs/sre/firewall.py | 415 +++++++++--------- 1 file changed, 208 insertions(+), 207 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index d39e3ab09c..9c4f922c31 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -114,213 +114,10 @@ def __init__( tags=child_tags, ) - application_rule_collections: list[ - network.AzureFirewallApplicationRuleCollectionArgs - ] = [ - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="apt-proxy-server", - priority=FirewallPriorities.SRE_APT_PROXY_SERVER, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external apt repository requests", - name="AllowAptRepositories", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_apt_proxy_server_prefixes, - target_fqdns=PermittedDomains.APT_REPOSITORIES, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="clamav-mirror", - priority=FirewallPriorities.SRE_CLAMAV_MIRROR, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external ClamAV definition update requests", - name="AllowClamAVDefinitionUpdates", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_clamav_mirror_prefixes, - target_fqdns=PermittedDomains.CLAMAV_UPDATES, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="identity-server", - priority=FirewallPriorities.SRE_IDENTITY_CONTAINERS, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow Microsoft OAuth login requests", - name="AllowMicrosoftOAuthLogin", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_identity_containers_prefixes, - target_fqdns=PermittedDomains.MICROSOFT_IDENTITY, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="remote-desktop-gateway", - priority=FirewallPriorities.SRE_GUACAMOLE_CONTAINERS, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow Microsoft OAuth login requests", - name="AllowMicrosoftOAuthLogin", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_guacamole_containers_prefixes, - target_fqdns=PermittedDomains.MICROSOFT_LOGIN, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="software-repositories", - priority=FirewallPriorities.SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external CRAN package requests", - name="AllowCRANPackageDownload", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_user_services_software_repositories_prefixes, - target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_R, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external PyPI package requests", - name="AllowPyPIPackageDownload", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_user_services_software_repositories_prefixes, - target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_PYTHON, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="workspaces", - priority=FirewallPriorities.SRE_WORKSPACES, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu keyserver requests", - name="AllowUbuntuKeyserver", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HKP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu Snap Store access", - name="AllowUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external RStudio deb downloads", - name="AllowRStudioDeb", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.RSTUDIO_DEB, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.DENY - ), - name="workspaces-deny", - priority=FirewallPriorities.SRE_WORKSPACES_DENY, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Deny external Ubuntu Snap Store upload and login access", - name="DenyUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, - ), - ], - ), - ] - - network_rule_collections: list[ - network.AzureFirewallNetworkRuleCollectionArgs - ] = [] - if props.allow_workspace_internet: application_rule_collections = [] - network_rule_collections.append( + + network_rule_collections = [ network.AzureFirewallNetworkRuleCollectionArgs( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW @@ -339,8 +136,212 @@ def __init__( ], # TODO: Check if we can make this more restrictive. ) ], - ) - ) + ), + ] + else: + application_rule_collections: list[ + network.AzureFirewallApplicationRuleCollectionArgs + ] = [ + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="apt-proxy-server", + priority=FirewallPriorities.SRE_APT_PROXY_SERVER, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external apt repository requests", + name="AllowAptRepositories", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_apt_proxy_server_prefixes, + target_fqdns=PermittedDomains.APT_REPOSITORIES, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="clamav-mirror", + priority=FirewallPriorities.SRE_CLAMAV_MIRROR, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external ClamAV definition update requests", + name="AllowClamAVDefinitionUpdates", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_clamav_mirror_prefixes, + target_fqdns=PermittedDomains.CLAMAV_UPDATES, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="identity-server", + priority=FirewallPriorities.SRE_IDENTITY_CONTAINERS, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow Microsoft OAuth login requests", + name="AllowMicrosoftOAuthLogin", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_identity_containers_prefixes, + target_fqdns=PermittedDomains.MICROSOFT_IDENTITY, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="remote-desktop-gateway", + priority=FirewallPriorities.SRE_GUACAMOLE_CONTAINERS, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow Microsoft OAuth login requests", + name="AllowMicrosoftOAuthLogin", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_guacamole_containers_prefixes, + target_fqdns=PermittedDomains.MICROSOFT_LOGIN, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="software-repositories", + priority=FirewallPriorities.SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external CRAN package requests", + name="AllowCRANPackageDownload", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_user_services_software_repositories_prefixes, + target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_R, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external PyPI package requests", + name="AllowPyPIPackageDownload", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_user_services_software_repositories_prefixes, + target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_PYTHON, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="workspaces", + priority=FirewallPriorities.SRE_WORKSPACES, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu keyserver requests", + name="AllowUbuntuKeyserver", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HKP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu Snap Store access", + name="AllowUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external RStudio deb downloads", + name="AllowRStudioDeb", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.RSTUDIO_DEB, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.DENY + ), + name="workspaces-deny", + priority=FirewallPriorities.SRE_WORKSPACES_DENY, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Deny external Ubuntu Snap Store upload and login access", + name="DenyUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, + ), + ], + ), + ] + + network_rule_collections: list[ + network.AzureFirewallNetworkRuleCollectionArgs + ] = [] # Deploy firewall self.firewall = network.AzureFirewall( From 265c7e0becd6979a2459ee3982112a816626aa38 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:25:45 +0000 Subject: [PATCH 26/42] Restore application collection rules --- .../infrastructure/programs/sre/firewall.py | 404 +++++++++--------- 1 file changed, 199 insertions(+), 205 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index 9c4f922c31..fdd9a93092 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -114,9 +114,205 @@ def __init__( tags=child_tags, ) - if props.allow_workspace_internet: - application_rule_collections = [] + application_rule_collections = [ + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="apt-proxy-server", + priority=FirewallPriorities.SRE_APT_PROXY_SERVER, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external apt repository requests", + name="AllowAptRepositories", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_apt_proxy_server_prefixes, + target_fqdns=PermittedDomains.APT_REPOSITORIES, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="clamav-mirror", + priority=FirewallPriorities.SRE_CLAMAV_MIRROR, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external ClamAV definition update requests", + name="AllowClamAVDefinitionUpdates", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_clamav_mirror_prefixes, + target_fqdns=PermittedDomains.CLAMAV_UPDATES, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="identity-server", + priority=FirewallPriorities.SRE_IDENTITY_CONTAINERS, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow Microsoft OAuth login requests", + name="AllowMicrosoftOAuthLogin", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_identity_containers_prefixes, + target_fqdns=PermittedDomains.MICROSOFT_IDENTITY, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="remote-desktop-gateway", + priority=FirewallPriorities.SRE_GUACAMOLE_CONTAINERS, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow Microsoft OAuth login requests", + name="AllowMicrosoftOAuthLogin", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_guacamole_containers_prefixes, + target_fqdns=PermittedDomains.MICROSOFT_LOGIN, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="software-repositories", + priority=FirewallPriorities.SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external CRAN package requests", + name="AllowCRANPackageDownload", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_user_services_software_repositories_prefixes, + target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_R, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external PyPI package requests", + name="AllowPyPIPackageDownload", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ) + ], + source_addresses=props.subnet_user_services_software_repositories_prefixes, + target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_PYTHON, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="workspaces", + priority=FirewallPriorities.SRE_WORKSPACES, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu keyserver requests", + name="AllowUbuntuKeyserver", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HKP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu Snap Store access", + name="AllowUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external RStudio deb downloads", + name="AllowRStudioDeb", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.RSTUDIO_DEB, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.DENY + ), + name="workspaces-deny", + priority=FirewallPriorities.SRE_WORKSPACES_DENY, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Deny external Ubuntu Snap Store upload and login access", + name="DenyUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, + ), + ], + ), + ] + if props.allow_workspace_internet: network_rule_collections = [ network.AzureFirewallNetworkRuleCollectionArgs( action=network.AzureFirewallRCActionArgs( @@ -139,209 +335,7 @@ def __init__( ), ] else: - application_rule_collections: list[ - network.AzureFirewallApplicationRuleCollectionArgs - ] = [ - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="apt-proxy-server", - priority=FirewallPriorities.SRE_APT_PROXY_SERVER, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external apt repository requests", - name="AllowAptRepositories", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_apt_proxy_server_prefixes, - target_fqdns=PermittedDomains.APT_REPOSITORIES, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="clamav-mirror", - priority=FirewallPriorities.SRE_CLAMAV_MIRROR, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external ClamAV definition update requests", - name="AllowClamAVDefinitionUpdates", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_clamav_mirror_prefixes, - target_fqdns=PermittedDomains.CLAMAV_UPDATES, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="identity-server", - priority=FirewallPriorities.SRE_IDENTITY_CONTAINERS, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow Microsoft OAuth login requests", - name="AllowMicrosoftOAuthLogin", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_identity_containers_prefixes, - target_fqdns=PermittedDomains.MICROSOFT_IDENTITY, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="remote-desktop-gateway", - priority=FirewallPriorities.SRE_GUACAMOLE_CONTAINERS, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow Microsoft OAuth login requests", - name="AllowMicrosoftOAuthLogin", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_guacamole_containers_prefixes, - target_fqdns=PermittedDomains.MICROSOFT_LOGIN, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="software-repositories", - priority=FirewallPriorities.SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external CRAN package requests", - name="AllowCRANPackageDownload", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_user_services_software_repositories_prefixes, - target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_R, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external PyPI package requests", - name="AllowPyPIPackageDownload", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ) - ], - source_addresses=props.subnet_user_services_software_repositories_prefixes, - target_fqdns=PermittedDomains.SOFTWARE_REPOSITORIES_PYTHON, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="workspaces", - priority=FirewallPriorities.SRE_WORKSPACES, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu keyserver requests", - name="AllowUbuntuKeyserver", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HKP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu Snap Store access", - name="AllowUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external RStudio deb downloads", - name="AllowRStudioDeb", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.RSTUDIO_DEB, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.DENY - ), - name="workspaces-deny", - priority=FirewallPriorities.SRE_WORKSPACES_DENY, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Deny external Ubuntu Snap Store upload and login access", - name="DenyUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, - ), - ], - ), - ] - - network_rule_collections: list[ - network.AzureFirewallNetworkRuleCollectionArgs - ] = [] + network_rule_collections = [] # Deploy firewall self.firewall = network.AzureFirewall( From 31270861896306d776bbb7e1de6dc25e736590a0 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:27:31 +0000 Subject: [PATCH 27/42] Use workspaces subnet for allow internet rule --- data_safe_haven/infrastructure/programs/sre/firewall.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index fdd9a93092..39b6bdf11b 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -327,9 +327,7 @@ def __init__( destination_ports=["*"], name="allow-internet-access", protocols=[network.AzureFirewallNetworkRuleProtocol.ANY], - source_addresses=[ - "*" - ], # TODO: Check if we can make this more restrictive. + source_addresses=[props.subnet_workspaces_prefixes], ) ], ), From 357fc575f258f3a9c3290b8899285c7da166a089 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:30:21 +0000 Subject: [PATCH 28/42] Make rule collection names more consistent --- .../infrastructure/programs/sre/firewall.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index 39b6bdf11b..e01fd2e3d4 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -119,7 +119,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="apt-proxy-server", + name="apt-proxy-server-allow", priority=FirewallPriorities.SRE_APT_PROXY_SERVER, rules=[ network.AzureFirewallApplicationRuleArgs( @@ -144,7 +144,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="clamav-mirror", + name="clamav-mirror-allow", priority=FirewallPriorities.SRE_CLAMAV_MIRROR, rules=[ network.AzureFirewallApplicationRuleArgs( @@ -169,7 +169,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="identity-server", + name="identity-server-allow", priority=FirewallPriorities.SRE_IDENTITY_CONTAINERS, rules=[ network.AzureFirewallApplicationRuleArgs( @@ -190,7 +190,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="remote-desktop-gateway", + name="remote-desktop-gateway-allow", priority=FirewallPriorities.SRE_GUACAMOLE_CONTAINERS, rules=[ network.AzureFirewallApplicationRuleArgs( @@ -211,7 +211,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="software-repositories", + name="software-repositories-allow", priority=FirewallPriorities.SRE_USER_SERVICES_SOFTWARE_REPOSITORIES, rules=[ network.AzureFirewallApplicationRuleArgs( @@ -244,7 +244,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="workspaces", + name="workspaces-allow", priority=FirewallPriorities.SRE_WORKSPACES, rules=[ network.AzureFirewallApplicationRuleArgs( @@ -318,7 +318,7 @@ def __init__( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW ), - name="workspaces-all-allow", + name="workspaces-allow-all", priority=FirewallPriorities.SRE_WORKSPACES, rules=[ network.AzureFirewallNetworkRuleArgs( From b25ee64bc826ef13761b2c288d0cfe865fe484d5 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:37:22 +0000 Subject: [PATCH 29/42] Collect common application rules --- .../infrastructure/programs/sre/firewall.py | 146 +++++++++--------- 1 file changed, 75 insertions(+), 71 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index e01fd2e3d4..7ad9a11e2b 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -114,7 +114,7 @@ def __init__( tags=child_tags, ) - application_rule_collections = [ + application_rule_collections_common = [ network.AzureFirewallApplicationRuleCollectionArgs( action=network.AzureFirewallRCActionArgs( type=network.AzureFirewallRCActionType.ALLOW @@ -240,79 +240,10 @@ def __init__( ), ], ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.ALLOW - ), - name="workspaces-allow", - priority=FirewallPriorities.SRE_WORKSPACES, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu keyserver requests", - name="AllowUbuntuKeyserver", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HKP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external Ubuntu Snap Store access", - name="AllowUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, - ), - network.AzureFirewallApplicationRuleArgs( - description="Allow external RStudio deb downloads", - name="AllowRStudioDeb", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=PermittedDomains.RSTUDIO_DEB, - ), - ], - ), - network.AzureFirewallApplicationRuleCollectionArgs( - action=network.AzureFirewallRCActionArgs( - type=network.AzureFirewallRCActionType.DENY - ), - name="workspaces-deny", - priority=FirewallPriorities.SRE_WORKSPACES_DENY, - rules=[ - network.AzureFirewallApplicationRuleArgs( - description="Deny external Ubuntu Snap Store upload and login access", - name="DenyUbuntuSnapcraft", - protocols=[ - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTP), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, - ), - network.AzureFirewallApplicationRuleProtocolArgs( - port=int(Ports.HTTPS), - protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, - ), - ], - source_addresses=props.subnet_workspaces_prefixes, - target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, - ), - ], - ), ] if props.allow_workspace_internet: + application_rule_collections = application_rule_collections_common network_rule_collections = [ network.AzureFirewallNetworkRuleCollectionArgs( action=network.AzureFirewallRCActionArgs( @@ -333,6 +264,79 @@ def __init__( ), ] else: + application_rule_collections = [ + *application_rule_collections_common, + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.ALLOW + ), + name="workspaces-allow-restricted", + priority=FirewallPriorities.SRE_WORKSPACES, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu keyserver requests", + name="AllowUbuntuKeyserver", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HKP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_KEYSERVER, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external Ubuntu Snap Store access", + name="AllowUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.UBUNTU_SNAPCRAFT, + ), + network.AzureFirewallApplicationRuleArgs( + description="Allow external RStudio deb downloads", + name="AllowRStudioDeb", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=PermittedDomains.RSTUDIO_DEB, + ), + ], + ), + network.AzureFirewallApplicationRuleCollectionArgs( + action=network.AzureFirewallRCActionArgs( + type=network.AzureFirewallRCActionType.DENY + ), + name="workspaces-deny", + priority=FirewallPriorities.SRE_WORKSPACES_DENY, + rules=[ + network.AzureFirewallApplicationRuleArgs( + description="Deny external Ubuntu Snap Store upload and login access", + name="DenyUbuntuSnapcraft", + protocols=[ + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTP), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTP, + ), + network.AzureFirewallApplicationRuleProtocolArgs( + port=int(Ports.HTTPS), + protocol_type=network.AzureFirewallApplicationRuleProtocolType.HTTPS, + ), + ], + source_addresses=props.subnet_workspaces_prefixes, + target_fqdns=ForbiddenDomains.UBUNTU_SNAPCRAFT, + ), + ], + ), + ] network_rule_collections = [] # Deploy firewall From 5fa8e9c6212c999ffec1e2e6539fc69ac6f9a5ba Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 8 Jan 2025 14:45:33 +0000 Subject: [PATCH 30/42] Avoid sponge --- .github/scripts/expand_mustache.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/expand_mustache.sh b/.github/scripts/expand_mustache.sh index 6ae96cb69f..c197e5ff70 100755 --- a/.github/scripts/expand_mustache.sh +++ b/.github/scripts/expand_mustache.sh @@ -8,7 +8,8 @@ while read -r yamlfile; do test_config=".github/resources/$filename.config.json" if [ -e "$test_config" ]; then - mustache "$test_config" "$yamlfile" | sponge "$yamlfile" + cp "$yamlfile" expanded.tmp + mustache "$test_config" expanded.tmp > "$yamlfile" else # replace mustache arrays sed "s|{{\([/#]\)[^}]*}}|{{\1array}}|g" "$yamlfile" > expanded.tmp From f842c2ae9c7ee4443c6c3fbc093f313de7b5c1cb Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 9 Jan 2025 10:13:52 +0000 Subject: [PATCH 31/42] Add missing dummy template values --- .github/resources/AdGuardHome.mustache.config.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/resources/AdGuardHome.mustache.config.json b/.github/resources/AdGuardHome.mustache.config.json index 13822b9123..81b831389f 100644 --- a/.github/resources/AdGuardHome.mustache.config.json +++ b/.github/resources/AdGuardHome.mustache.config.json @@ -1,7 +1,9 @@ { + "admin_username": "admin", + "admin_password_encrypted": "password", + "allow_workspace_internet": true, "filter_allow": [ "clamav.net", "current.cvd.clamav.net" - ], - "allow_workspace_internet": true + ] } From b1367982e331588c1bbcfa72e02a8653783723ed Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 9 Jan 2025 14:27:16 +0000 Subject: [PATCH 32/42] Correct source addresses --- data_safe_haven/infrastructure/programs/sre/firewall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index 7ad9a11e2b..cd56cbf48c 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -258,7 +258,7 @@ def __init__( destination_ports=["*"], name="allow-internet-access", protocols=[network.AzureFirewallNetworkRuleProtocol.ANY], - source_addresses=[props.subnet_workspaces_prefixes], + source_addresses=props.subnet_workspaces_prefixes, ) ], ), @@ -337,7 +337,7 @@ def __init__( ], ), ] - network_rule_collections = [] + network_rule_collections = None # Deploy firewall self.firewall = network.AzureFirewall( From a7f927574cc70f34ea0b34500b4cdd3ab88a48ed Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 9 Jan 2025 14:36:17 +0000 Subject: [PATCH 33/42] Fix firewall tests --- tests/infrastructure/programs/sre/test_firewall.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/infrastructure/programs/sre/test_firewall.py b/tests/infrastructure/programs/sre/test_firewall.py index 8dcfba534c..ab93cc1bbe 100644 --- a/tests/infrastructure/programs/sre/test_firewall.py +++ b/tests/infrastructure/programs/sre/test_firewall.py @@ -132,17 +132,17 @@ def test_component_allow_workspace_internet_enabled( def assert_on_firewall_rules( args: list, ): - application_rule_collections: list[dict] = args[0] - network_rule_collections: list[dict] = args[1] + application_rule_collections = args[0] + network_rule_collections = args[1] # TODO: Be more precise in rule filtering. allow_internet_collection: list[dict] = [ rule_collection for rule_collection in network_rule_collections - if rule_collection["name"] == "workspaces-all-allow" + if rule_collection["name"] == "workspaces-allow-all" ] - assert len(application_rule_collections) == 0 + assert len(application_rule_collections) == 5 assert len(allow_internet_collection) == 1 pulumi.Output.all( @@ -167,11 +167,11 @@ def test_component_allow_workspace_internet_disabled( def assert_on_firewall_rules( args: list, ): - application_rule_collections: list[dict] = args[0] - network_rule_collections: list[dict] = args[1] + application_rule_collections = args[0] + network_rule_collections = args[1] assert len(application_rule_collections) > 0 - assert len(network_rule_collections) == 0 + assert network_rule_collections is None pulumi.Output.all( firewall_component.firewall.application_rule_collections, From ef05a6cb8efdabfe2a1586ee6120cc8a8d9cf1e8 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 9 Jan 2025 15:46:41 +0000 Subject: [PATCH 34/42] Add allow_workspace_internet parameter to template --- data_safe_haven/config/sre_config.py | 9 ++++++++- docs/source/deployment/deploy_sre.md | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/data_safe_haven/config/sre_config.py b/data_safe_haven/config/sre_config.py index 53adb673e0..df39458457 100644 --- a/data_safe_haven/config/sre_config.py +++ b/data_safe_haven/config/sre_config.py @@ -50,29 +50,35 @@ def template(cls: type[Self], tier: int | None = None) -> SREConfig: """Create SREConfig without validation to allow "replace me" prompts.""" # Set tier-dependent defaults if tier == 0: + allow_workspace_internet = True remote_desktop_allow_copy = True remote_desktop_allow_paste = True software_packages = SoftwarePackageCategory.ANY elif tier == 1: + allow_workspace_internet = True remote_desktop_allow_copy = True remote_desktop_allow_paste = True software_packages = SoftwarePackageCategory.ANY elif tier == 2: # noqa: PLR2004 + allow_workspace_internet = False remote_desktop_allow_copy = False remote_desktop_allow_paste = False software_packages = SoftwarePackageCategory.ANY elif tier == 3: # noqa: PLR2004 + allow_workspace_internet = False remote_desktop_allow_copy = False remote_desktop_allow_paste = False software_packages = SoftwarePackageCategory.PRE_APPROVED elif tier == 4: # noqa: PLR2004 + allow_workspace_internet = False remote_desktop_allow_copy = False remote_desktop_allow_paste = False software_packages = SoftwarePackageCategory.NONE else: + allow_workspace_internet = "True/False: whether to allow outbound internet access from workspaces." # type: ignore remote_desktop_allow_copy = "True/False: whether to allow copying text out of the environment." # type: ignore remote_desktop_allow_paste = "True/False: whether to allow pasting text into the environment." # type: ignore - software_packages = "Which Python/R packages to allow users to install: [any/pre-approved/none]" # type: ignore + software_packages = "[any/pre-approved/none]: which Python/R packages to allow users to install." # type: ignore return SREConfig.model_construct( azure=ConfigSectionAzure.model_construct( @@ -89,6 +95,7 @@ def template(cls: type[Self], tier: int | None = None) -> SREConfig: sre=ConfigSectionSRE.model_construct( admin_email_address="Email address shared by all administrators", admin_ip_addresses=["List of IP addresses belonging to administrators"], + allow_workspace_internet=allow_workspace_internet, databases=["List of database systems to deploy"], # type:ignore data_provider_ip_addresses=[ "List of IP addresses belonging to data providers" diff --git a/docs/source/deployment/deploy_sre.md b/docs/source/deployment/deploy_sre.md index ebf1aa425a..7df293023e 100644 --- a/docs/source/deployment/deploy_sre.md +++ b/docs/source/deployment/deploy_sre.md @@ -58,6 +58,7 @@ name: # A name for your SRE deployment containing only letters, numbers, hyphens sre: admin_email_address: # Email address shared by all administrators admin_ip_addresses: # List of IP addresses belonging to administrators + allow_workspace_internet: # True/False: whether to allow outbound internet access from workspaces. data_provider_ip_addresses: # List of IP addresses belonging to data providers databases: # List of database systems to deploy remote_desktop: From f995e01eed518a36938a93e5b53efe0d0a743a4a Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 9 Jan 2025 15:56:05 +0000 Subject: [PATCH 35/42] Add tests for template tiers --- tests/config/test_sre_config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/config/test_sre_config.py b/tests/config/test_sre_config.py index 7ac6d61981..a2b6785c0a 100644 --- a/tests/config/test_sre_config.py +++ b/tests/config/test_sre_config.py @@ -89,6 +89,23 @@ def test_template_validation(self) -> None: with pytest.raises(DataSafeHavenTypeError): SREConfig.from_yaml(config.to_yaml()) + @pytest.mark.parametrize( + "tier,allow_internet,copy,paste,packages", + [ + (0, True, True, True, SoftwarePackageCategory.ANY), + (1, True, True, True, SoftwarePackageCategory.ANY), + (2, False, False, False, SoftwarePackageCategory.ANY), + (3, False, False, False, SoftwarePackageCategory.PRE_APPROVED), + (4, False, False, False, SoftwarePackageCategory.NONE), + ] + ) + def test_template_tiers(self, tier, allow_internet, copy, paste, packages): + config = SREConfig.template(tier=tier) + assert config.sre.allow_workspace_internet == allow_internet + assert config.sre.remote_desktop.allow_copy == copy + assert config.sre.remote_desktop.allow_paste == paste + assert config.sre.software_packages == packages + def test_from_yaml(self, sre_config, sre_config_yaml) -> None: config = SREConfig.from_yaml(sre_config_yaml) assert config == sre_config From b6811c40bd55119655bcde168a3e79f8caf0d429 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 9 Jan 2025 16:04:58 +0000 Subject: [PATCH 36/42] Add warning to config template --- data_safe_haven/config/sre_config.py | 6 +++++- docs/source/deployment/deploy_sre.md | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/data_safe_haven/config/sre_config.py b/data_safe_haven/config/sre_config.py index df39458457..56d8b1cdbe 100644 --- a/data_safe_haven/config/sre_config.py +++ b/data_safe_haven/config/sre_config.py @@ -75,7 +75,11 @@ def template(cls: type[Self], tier: int | None = None) -> SREConfig: remote_desktop_allow_paste = False software_packages = SoftwarePackageCategory.NONE else: - allow_workspace_internet = "True/False: whether to allow outbound internet access from workspaces." # type: ignore + allow_workspace_internet = ( + "True/False: whether to allow outbound internet access from workspaces. " + "WARNING setting this to True will allow data to be moved out of the SRE " + "WITHOUT OVERSIGHT OR APPROVAL" + ) # type: ignore remote_desktop_allow_copy = "True/False: whether to allow copying text out of the environment." # type: ignore remote_desktop_allow_paste = "True/False: whether to allow pasting text into the environment." # type: ignore software_packages = "[any/pre-approved/none]: which Python/R packages to allow users to install." # type: ignore diff --git a/docs/source/deployment/deploy_sre.md b/docs/source/deployment/deploy_sre.md index 7df293023e..640bf97dcc 100644 --- a/docs/source/deployment/deploy_sre.md +++ b/docs/source/deployment/deploy_sre.md @@ -58,7 +58,7 @@ name: # A name for your SRE deployment containing only letters, numbers, hyphens sre: admin_email_address: # Email address shared by all administrators admin_ip_addresses: # List of IP addresses belonging to administrators - allow_workspace_internet: # True/False: whether to allow outbound internet access from workspaces. + allow_workspace_internet: # True/False: whether to allow outbound internet access from workspaces. WARNING setting this to True will allow data to be moved out of the SRE WITHOUT OVERSIGHT OR APPROVAL data_provider_ip_addresses: # List of IP addresses belonging to data providers databases: # List of database systems to deploy remote_desktop: From 39549f92cf7f8ff582fe10fbb2ab53bc8820feae Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 9 Jan 2025 16:21:15 +0000 Subject: [PATCH 37/42] Add CLI warning and confirmation --- data_safe_haven/commands/sre.py | 12 ++++++++++++ data_safe_haven/config/sre_config.py | 4 ++-- tests/config/test_sre_config.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/data_safe_haven/commands/sre.py b/data_safe_haven/commands/sre.py index 14330c2cff..c687984c71 100644 --- a/data_safe_haven/commands/sre.py +++ b/data_safe_haven/commands/sre.py @@ -52,6 +52,18 @@ def deploy( ) sre_config = SREConfig.from_remote_by_name(context, name) + # Confirm outbound internet access from workspaces + if sre_config.sre.allow_workspace_internet: + logger.warning( + "Workspaces will be allowed outbound access to the internet " + "data may be moved out of the SRE WITHOUT OVERSIGHT OR APPROVAL." + ) + if not console.confirm( + "Do you wish to continue deploying the SRE?", default_to_yes=False + ): + console.print("SRE deployment cancelled by user.") + raise typer.Exit(0) + # Check whether current IP address is authorised to take administrator actions if not ip_address_in_list(sre_config.sre.admin_ip_addresses): logger.warning( diff --git a/data_safe_haven/config/sre_config.py b/data_safe_haven/config/sre_config.py index 56d8b1cdbe..3d7119d285 100644 --- a/data_safe_haven/config/sre_config.py +++ b/data_safe_haven/config/sre_config.py @@ -76,10 +76,10 @@ def template(cls: type[Self], tier: int | None = None) -> SREConfig: software_packages = SoftwarePackageCategory.NONE else: allow_workspace_internet = ( - "True/False: whether to allow outbound internet access from workspaces. " + "True/False: whether to allow outbound internet access from workspaces. " # type: ignore "WARNING setting this to True will allow data to be moved out of the SRE " "WITHOUT OVERSIGHT OR APPROVAL" - ) # type: ignore + ) remote_desktop_allow_copy = "True/False: whether to allow copying text out of the environment." # type: ignore remote_desktop_allow_paste = "True/False: whether to allow pasting text into the environment." # type: ignore software_packages = "[any/pre-approved/none]: which Python/R packages to allow users to install." # type: ignore diff --git a/tests/config/test_sre_config.py b/tests/config/test_sre_config.py index a2b6785c0a..4d445c2ee9 100644 --- a/tests/config/test_sre_config.py +++ b/tests/config/test_sre_config.py @@ -97,7 +97,7 @@ def test_template_validation(self) -> None: (2, False, False, False, SoftwarePackageCategory.ANY), (3, False, False, False, SoftwarePackageCategory.PRE_APPROVED), (4, False, False, False, SoftwarePackageCategory.NONE), - ] + ], ) def test_template_tiers(self, tier, allow_internet, copy, paste, packages): config = SREConfig.template(tier=tier) From 75b8ae4d0197d9aebaa4f916c963dd379d6b3bd1 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Mon, 13 Jan 2025 11:27:52 +0000 Subject: [PATCH 38/42] Update tests/infrastructure/programs/sre/conftest.py Co-authored-by: James Robinson --- tests/infrastructure/programs/sre/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/infrastructure/programs/sre/conftest.py b/tests/infrastructure/programs/sre/conftest.py index 11e9997c11..78d0e96e3a 100644 --- a/tests/infrastructure/programs/sre/conftest.py +++ b/tests/infrastructure/programs/sre/conftest.py @@ -5,10 +5,8 @@ from data_safe_haven.infrastructure.common import SREIpRanges -# Mock configuration. - - class DataSafeHavenMocks(pulumi.runtime.Mocks): + """Configuration for Pulumi mocks""" def new_resource(self, args: pulumi.runtime.MockResourceArgs): resources = [args.name + "_id", args.inputs] return resources From 8b2eee02430e6114a625b46bd655558948cfbd75 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 14 Jan 2025 10:01:26 +0000 Subject: [PATCH 39/42] Add comment about use of network rule --- data_safe_haven/infrastructure/programs/sre/firewall.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data_safe_haven/infrastructure/programs/sre/firewall.py b/data_safe_haven/infrastructure/programs/sre/firewall.py index cd56cbf48c..b579c9014e 100644 --- a/data_safe_haven/infrastructure/programs/sre/firewall.py +++ b/data_safe_haven/infrastructure/programs/sre/firewall.py @@ -244,6 +244,8 @@ def __init__( if props.allow_workspace_internet: application_rule_collections = application_rule_collections_common + # A network rule is used as application rules are restricted to certain + # types of traffic, e.g. HTTP, HTTPS network_rule_collections = [ network.AzureFirewallNetworkRuleCollectionArgs( action=network.AzureFirewallRCActionArgs( From 2b2ab841832fd5096c18272c76e068acfcabdb53 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 14 Jan 2025 11:25:39 +0000 Subject: [PATCH 40/42] Change AdGuard template structure --- .../infrastructure/programs/sre/dns_server.py | 23 +++++++++++++------ .../dns_server/AdGuardHome.mustache.yaml | 11 ++++----- tests/infrastructure/programs/sre/conftest.py | 2 ++ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/dns_server.py b/data_safe_haven/infrastructure/programs/sre/dns_server.py index f7d3cc5899..5b4a2ec70c 100644 --- a/data_safe_haven/infrastructure/programs/sre/dns_server.py +++ b/data_safe_haven/infrastructure/programs/sre/dns_server.py @@ -71,23 +71,32 @@ def __init__( resources_path / "dns_server" / "AdGuardHome.mustache.yaml" ) + # Construct permitted and blocked domains + if not props.allow_workspace_internet: + filter_allow = Output.from_input(props.shm_fqdn).apply( + lambda fqdn: [ + f"*.{fqdn}", + *PermittedDomains.ALL, + ] + ) + filter_block = ["*.*"] + + else: + filter_allow = [] + filter_block = [] + # Expand AdGuardHome YAML configuration adguard_adguardhome_yaml_contents = Output.all( admin_username=props.admin_username, # Only the first 72 bytes of the generated random string will be used but a # 20 character UTF-8 string (alphanumeric + special) will not exceed that. admin_password_encrypted=password_admin.bcrypt_hash, - allow_workspace_internet=props.allow_workspace_internet, + filter_allow=filter_allow, + filter_block=filter_block, # Use Azure virtual DNS server as upstream # https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16 # This server is aware of private DNS zones upstream_dns="168.63.129.16", - filter_allow=Output.from_input(props.shm_fqdn).apply( - lambda fqdn: [ - f"*.{fqdn}", - *PermittedDomains.ALL, - ] - ), ).apply( lambda mustache_config: adguard_adguardhome_yaml_reader.file_contents( mustache_config diff --git a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml index 9c75067956..e4e7f9101b 100644 --- a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml +++ b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml @@ -11,17 +11,14 @@ dns: querylog: enabled: true filters: -{{#allow_workspace_internet}} -user_rules: [] -{{/allow_workspace_internet}} -{{^allow_workspace_internet}} +# https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#adblock-style-syntax user_rules: - # https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#adblock-style-syntax - - "*.*" + {{#filter_block}} + - "{{.}}" + {{/filter_block}} {{#filter_allow}} - "@@||{{.}}" {{/filter_allow}} -{{/allow_workspace_internet}} log: verbose: true # Note that because we are only providing a partial config file we need the diff --git a/tests/infrastructure/programs/sre/conftest.py b/tests/infrastructure/programs/sre/conftest.py index 78d0e96e3a..2815f94549 100644 --- a/tests/infrastructure/programs/sre/conftest.py +++ b/tests/infrastructure/programs/sre/conftest.py @@ -5,8 +5,10 @@ from data_safe_haven.infrastructure.common import SREIpRanges + class DataSafeHavenMocks(pulumi.runtime.Mocks): """Configuration for Pulumi mocks""" + def new_resource(self, args: pulumi.runtime.MockResourceArgs): resources = [args.name + "_id", args.inputs] return resources From 29bf408817e06a6a38eec82be15ac3fe47c04fb4 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 15 Jan 2025 11:07:53 +0000 Subject: [PATCH 41/42] Use explicit empty list --- data_safe_haven/infrastructure/programs/sre/dns_server.py | 7 ++++--- .../resources/dns_server/AdGuardHome.mustache.yaml | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/dns_server.py b/data_safe_haven/infrastructure/programs/sre/dns_server.py index 5b4a2ec70c..affcbd2cea 100644 --- a/data_safe_haven/infrastructure/programs/sre/dns_server.py +++ b/data_safe_haven/infrastructure/programs/sre/dns_server.py @@ -80,10 +80,10 @@ def __init__( ] ) filter_block = ["*.*"] - else: - filter_allow = [] - filter_block = [] + filter_allow = None + filter_block = None + user_rules = filter_allow or filter_block # Expand AdGuardHome YAML configuration adguard_adguardhome_yaml_contents = Output.all( @@ -97,6 +97,7 @@ def __init__( # https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16 # This server is aware of private DNS zones upstream_dns="168.63.129.16", + user_rules=user_rules, ).apply( lambda mustache_config: adguard_adguardhome_yaml_reader.file_contents( mustache_config diff --git a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml index e4e7f9101b..c6e9f8bd14 100644 --- a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml +++ b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml @@ -11,7 +11,8 @@ dns: querylog: enabled: true filters: -# https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#adblock-style-syntax +# https://adblockplus.org/filter-cheatsheet#blocking +{{#user_rules}} user_rules: {{#filter_block}} - "{{.}}" @@ -19,6 +20,10 @@ user_rules: {{#filter_allow}} - "@@||{{.}}" {{/filter_allow}} +{{/user_rules}} +{{^user_rules}} +user_rules: [] +{{/user_rules}} log: verbose: true # Note that because we are only providing a partial config file we need the From 81f37659e8065c5262298a517fbf299414683b59 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 15 Jan 2025 11:45:54 +0000 Subject: [PATCH 42/42] Adjust user rules construction Co-authored-by: Matt Craddock --- data_safe_haven/infrastructure/programs/sre/dns_server.py | 4 +--- .../resources/dns_server/AdGuardHome.mustache.yaml | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/dns_server.py b/data_safe_haven/infrastructure/programs/sre/dns_server.py index affcbd2cea..a8934825da 100644 --- a/data_safe_haven/infrastructure/programs/sre/dns_server.py +++ b/data_safe_haven/infrastructure/programs/sre/dns_server.py @@ -82,8 +82,7 @@ def __init__( filter_block = ["*.*"] else: filter_allow = None - filter_block = None - user_rules = filter_allow or filter_block + filter_block = ["example.local"] # Expand AdGuardHome YAML configuration adguard_adguardhome_yaml_contents = Output.all( @@ -97,7 +96,6 @@ def __init__( # https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16 # This server is aware of private DNS zones upstream_dns="168.63.129.16", - user_rules=user_rules, ).apply( lambda mustache_config: adguard_adguardhome_yaml_reader.file_contents( mustache_config diff --git a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml index c6e9f8bd14..5cb12df9bd 100644 --- a/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml +++ b/data_safe_haven/resources/dns_server/AdGuardHome.mustache.yaml @@ -12,7 +12,6 @@ querylog: enabled: true filters: # https://adblockplus.org/filter-cheatsheet#blocking -{{#user_rules}} user_rules: {{#filter_block}} - "{{.}}" @@ -20,10 +19,6 @@ user_rules: {{#filter_allow}} - "@@||{{.}}" {{/filter_allow}} -{{/user_rules}} -{{^user_rules}} -user_rules: [] -{{/user_rules}} log: verbose: true # Note that because we are only providing a partial config file we need the