Skip to content
Open
1 change: 1 addition & 0 deletions data_safe_haven/infrastructure/common/ip_ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class SREIpRanges:
# Virtual networks for Container Apps need a CIDR of length /27 or larger
dns_sidecar = vnet.next_subnet(32)
user_services_gitea_mirror = vnet.next_subnet(8)
user_services_software_repositories_support = vnet.next_subnet(8)


@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ def __init__(
database_username: Input[str],
disable_secure_transport: bool,
location: Input[str],
azure_extensions: Input[str] | None = None,
) -> None:
self.azure_extensions = azure_extensions
self.database_names = Output.from_input(database_names)
self.database_password = Output.secret(database_password)
self.database_resource_group_name = database_resource_group_name
Expand Down Expand Up @@ -96,6 +98,25 @@ def __init__(
ResourceOptions(parent=db_server, retain_on_delete=True),
),
)

# Specify allowed extensions
if props.azure_extensions:
dbforpostgresql.Configuration(
f"{self._name}_azure_extensions",
configuration_name="azure.extensions",
resource_group_name=props.database_resource_group_name,
server_name=db_server.name,
source="user-override",
value=props.azure_extensions,
opts=ResourceOptions.merge(
child_opts,
# Pulumi workaround for being unable to delete Configuration
# resource
# https://github.com/pulumi/pulumi-azure-native/issues/3072
ResourceOptions(parent=db_server, retain_on_delete=True),
),
)

# Add any databases that are requested
props.database_names.apply(
lambda db_names: [
Expand Down
11 changes: 8 additions & 3 deletions data_safe_haven/infrastructure/programs/declarative_sre.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,6 @@ def __call__(self) -> None:
"sre_user_services",
self.stack_name,
SREUserServicesProps(
allow_workspace_internet=self.config.sre.allow_workspace_internet,
database_service_admin_password=data.password_database_service_admin,
databases=self.config.sre.databases,
dns_server_ip=dns.ip_address,
Expand All @@ -394,6 +393,7 @@ def __call__(self) -> None:
resource_group_name=resource_group.name,
repository_data=self.config.user_services.gitea_mirror,
software_packages=self.config.sre.software_packages,
software_repositories_database_password=data.password_nexus_database_admin,
sre_fqdn=networking.sre_fqdn,
nexus_persistent_quota_gb=self.config.user_services.nexus.persistent_quota_gb,
storage_account_key=data.storage_account_data_configuration_key,
Expand All @@ -403,6 +403,7 @@ def __call__(self) -> None:
subnet_gitea_mirrors=networking.subnet_user_services_gitea_mirror,
subnet_databases=networking.subnet_user_services_databases,
subnet_software_repositories=networking.subnet_user_services_software_repositories,
subnet_software_repositories_support=networking.subnet_user_services_software_repositories_support,
),
tags=self.tags,
)
Expand Down Expand Up @@ -430,7 +431,7 @@ def __call__(self) -> None:
resource_group=resource_group,
software_repository_hostname=(
user_services.software_repositories.hostname
if not self.config.sre.allow_workspace_internet
if hasattr(user_services, "software_repositories")
else ""
),
subnet_desired_state=networking.subnet_desired_state,
Expand Down Expand Up @@ -500,7 +501,7 @@ def __call__(self) -> None:
)

# Export values for later use
if not self.config.sre.allow_workspace_internet:
if hasattr(user_services, "software_repositories"):
pulumi.export(
"allowlist_share_name",
user_services.software_repositories.allowlist_file_share_name,
Expand All @@ -509,6 +510,10 @@ def __call__(self) -> None:
"allowlist_share_filenames",
user_services.software_repositories.allowlist_file_names,
)
pulumi.export(
"software_repositories", user_services.software_repositories.exports
)

pulumi.export("data", data.exports)
pulumi.export("ldap", ldap_group_names)
pulumi.export("remote_desktop", remote_desktop.exports)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections.abc import Mapping

from pulumi import ComponentResource, Input, Output, ResourceOptions
from pulumi_azure_native import containerinstance, storage
from pulumi_azure_native import containerinstance, network, storage

from data_safe_haven.infrastructure.common import (
get_id_from_subnet,
Expand All @@ -22,7 +22,7 @@ class SREAptProxyServerProps:

def __init__(
self,
containers_subnet: Input[str],
containers_subnet: Input[network.GetSubnetResult],
dns_server_ip: Input[str],
location: Input[str],
log_analytics_workspace: Input[WrappedLogAnalyticsWorkspace],
Expand Down
26 changes: 26 additions & 0 deletions data_safe_haven/infrastructure/programs/sre/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,28 @@ def __init__(
tags=child_tags,
)

# Secret: Nexus database admin password
password_nexus_database_admin = pulumi_random.RandomPassword(
f"{self._name}_password_nexus_database_admin",
length=20,
special=True,
opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)),
)

kvs_password_nexus_database_admin = keyvault.Secret(
f"{self._name}_kvs_password_nexus_database_admin",
properties=keyvault.SecretPropertiesArgs(
value=password_nexus_database_admin.result
),
resource_group_name=props.resource_group_name,
secret_name="password-nexus-database-admin",
vault_name=key_vault.name,
opts=ResourceOptions.merge(
child_opts, ResourceOptions(parent=password_nexus_database_admin)
),
tags=child_tags,
)

# Secret: Guacamole user database admin password
password_user_database_admin = pulumi_random.RandomPassword(
f"{self._name}_password_user_database_admin",
Expand Down Expand Up @@ -833,6 +855,9 @@ def __init__(
self.password_hedgedoc_database_admin = Output.secret(
password_hedgedoc_database_admin.result
)
self.password_nexus_database_admin = Output.secret(
password_nexus_database_admin.result
)
self.password_user_database_admin = Output.secret(
password_user_database_admin.result
)
Expand All @@ -841,6 +866,7 @@ def __init__(
# Register exports
self.exports = {
"key_vault_name": key_vault.name,
"password_nexus_database_admin_secret": kvs_password_nexus_database_admin.name,
"password_user_database_admin_secret": kvs_password_user_database_admin.name,
"storage_account_data_configuration_name": storage_account_data_configuration.name,
}
125 changes: 122 additions & 3 deletions data_safe_haven/infrastructure/programs/sre/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,10 @@ def __init__(
network.NetworkSecurityGroup | None
) = None

self.nsg_user_services_software_repositories_support: (
network.NetworkSecurityGroup | None
) = None

if props.use_software_repositories:
self.nsg_user_services_software_repositories = (
self.get_nsg_user_services_software_repositories(
Expand All @@ -1327,6 +1331,15 @@ def __init__(
)
)

self.nsg_user_services_software_repositories_support = (
self.get_nsg_user_services_software_repositories_support(
stack_name,
props,
child_opts,
child_tags,
)
)

self.nsg_user_services_gitea_mirror: network.NetworkSecurityGroup | None = None
if props.use_gitea_mirror:
self.nsg_user_services_gitea_mirror = (
Expand Down Expand Up @@ -1658,6 +1671,9 @@ def __init__(
self.subnet_user_services_software_repositories_name = (
"UserServicesSoftwareRepositoriesSubnet"
)
self.subnet_user_services_software_repositories_support_name = (
"UserServicesSoftwareRepositoriesSupportSubnet"
)
self.subnet_workspaces_name = "WorkspacesSubnet"
self.subnet_dns_sidecar_name = "DnsSidecarSubnet"
sre_virtual_network = network.VirtualNetwork(
Expand Down Expand Up @@ -1793,7 +1809,7 @@ def __init__(

# Link SRE private DNS zone to DNS virtual network
privatedns.VirtualNetworkLink(
f"{self._name}_private_zone_internal_vnet_link",
resource_name=f"{self._name}_private_zone_internal_vnet_link",
location="Global",
private_zone_name=sre_private_dns_zone.name,
registration_enabled=False,
Expand All @@ -1814,7 +1830,7 @@ def __init__(
# need to be able to resolve the "Storage Account" private DNS zones.
for dns_zone_name, private_dns_zone in props.dns_private_zones.items():
privatedns.VirtualNetworkLink(
replace_separators(
resource_name=replace_separators(
f"{self._name}_private_zone_{dns_zone_name}_vnet_link", "_"
),
location="Global",
Expand Down Expand Up @@ -1924,6 +1940,9 @@ def __init__(
self.subnet_user_services_software_repositories: (
Output[network.GetSubnetResult] | None
) = None
self.subnet_user_services_software_repositories_support: (
Output[network.GetSubnetResult] | None
) = None

self.subnet_user_services_gitea_mirror: (
Output[network.GetSubnetResult] | None
Expand All @@ -1936,6 +1955,12 @@ def __init__(
virtual_network_name=sre_virtual_network.name,
)

self.subnet_user_services_software_repositories_support = network.get_subnet_output(
subnet_name=self.subnet_user_services_software_repositories_support_name,
resource_group_name=props.resource_group_name,
virtual_network_name=sre_virtual_network.name,
)

if props.use_gitea_mirror:
self.subnet_user_services_gitea_mirror = network.get_subnet_output(
subnet_name=self.subnet_user_services_gitea_mirror_name,
Expand Down Expand Up @@ -2153,6 +2178,18 @@ def get_nsg_user_services_software_repositories(
source_address_prefix=SREIpRanges.user_services_software_repositories.prefix,
source_port_range="*",
),
network.SecurityRuleArgs(
access=network.SecurityRuleAccess.ALLOW,
description="Allow outbound connections to Software Repositores support services.",
destination_address_prefix=SREIpRanges.user_services_software_repositories_support.prefix,
destination_port_ranges=[Ports.POSTGRESQL],
direction=network.SecurityRuleDirection.OUTBOUND,
name="AllowSoftwareRepositoriesSupportOutbound",
priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_SOFTWARE_REPOSITORIES_SUPPORT,
protocol=network.SecurityRuleProtocol.TCP,
source_address_prefix=SREIpRanges.user_services_software_repositories.prefix,
source_port_range="*",
),
network.SecurityRuleArgs(
access=network.SecurityRuleAccess.ALLOW,
description="Allow outbound connections to external repositories over the internet.",
Expand Down Expand Up @@ -2182,6 +2219,74 @@ def get_nsg_user_services_software_repositories(
tags=child_tags,
)

def get_nsg_user_services_software_repositories_support(
self,
stack_name: str,
props: SRENetworkingProps,
child_opts: ResourceOptions | None,
child_tags: Input[Mapping[str, Input[str]]] | None,
) -> network.NetworkSecurityGroup:
return network.NetworkSecurityGroup(
f"{self._name}_nsg_user_services_software_repositories_support",
location=props.location,
network_security_group_name=f"{stack_name}-nsg-user-services-software-repositories-support",
resource_group_name=props.resource_group_name,
security_rules=[
# Inbound
network.SecurityRuleArgs(
access=network.SecurityRuleAccess.ALLOW,
description="Allow inbound connections from Software repositories containers.",
destination_address_prefix=SREIpRanges.user_services_software_repositories_support.prefix,
destination_port_range="*",
direction=network.SecurityRuleDirection.INBOUND,
name="AllowSoftwareRepositoriesContainersInbound",
priority=NetworkingPriorities.INTERNAL_SRE_USER_SERVICES_SOFTWARE_REPOSITORIES,
protocol=network.SecurityRuleProtocol.ASTERISK,
source_address_prefix=SREIpRanges.user_services_software_repositories.prefix,
source_port_range="*",
),
network.SecurityRuleArgs(
access=network.SecurityRuleAccess.DENY,
description="Deny all other inbound traffic.",
destination_address_prefix="*",
destination_port_range="*",
direction=network.SecurityRuleDirection.INBOUND,
name="DenyAllOtherInbound",
priority=NetworkingPriorities.ALL_OTHER,
protocol=network.SecurityRuleProtocol.ASTERISK,
source_address_prefix="*",
source_port_range="*",
),
# Outbound
network.SecurityRuleArgs(
access=network.SecurityRuleAccess.DENY,
description="Deny outbound connections to Azure Platform DNS endpoints (including 168.63.129.16), which are not included in the 'Internet' service tag.",
destination_address_prefix="AzurePlatformDNS",
destination_port_range="*",
direction=network.SecurityRuleDirection.OUTBOUND,
name="DenyAzurePlatformDnsOutbound",
priority=NetworkingPriorities.AZURE_PLATFORM_DNS,
protocol=network.SecurityRuleProtocol.ASTERISK,
source_address_prefix="*",
source_port_range="*",
),
network.SecurityRuleArgs(
access=network.SecurityRuleAccess.DENY,
description="Deny all other outbound traffic.",
destination_address_prefix="*",
destination_port_range="*",
direction=network.SecurityRuleDirection.OUTBOUND,
name="DenyAllOtherOutbound",
priority=NetworkingPriorities.ALL_OTHER,
protocol=network.SecurityRuleProtocol.ASTERISK,
source_address_prefix="*",
source_port_range="*",
),
],
opts=child_opts,
tags=child_tags,
)

def get_virtual_network_subnet_args(
self,
props: SRENetworkingProps,
Expand Down Expand Up @@ -2374,7 +2479,8 @@ def get_virtual_network_subnet_args(
# User services software repositories
if (
props.use_software_repositories
and self.nsg_user_services_software_repositories is not None
and self.nsg_user_services_software_repositories
and self.nsg_user_services_software_repositories_support
):
subnets.append(
network.SubnetArgs(
Expand All @@ -2394,6 +2500,19 @@ def get_virtual_network_subnet_args(
)
)

# Software repositories support
subnets.append(
network.SubnetArgs(
address_prefix=SREIpRanges.user_services_software_repositories_support.prefix,
name=self.subnet_user_services_software_repositories_support_name,
network_security_group=network.NetworkSecurityGroupArgs(
id=self.nsg_user_services_software_repositories_support.id
),
private_endpoint_network_policies=network.VirtualNetworkPrivateEndpointNetworkPolicies.ENABLED,
route_table=network.RouteTableArgs(id=self.route_table.id),
)
)

# User services Gitea mirrors
if props.use_gitea_mirror and self.nsg_user_services_gitea_mirror is not None:
subnets.append(
Expand Down
Loading
Loading