diff --git a/data_safe_haven/commands/deploy_shm.py b/data_safe_haven/commands/deploy_shm.py index 808a95ffb1..f23d52afae 100644 --- a/data_safe_haven/commands/deploy_shm.py +++ b/data_safe_haven/commands/deploy_shm.py @@ -50,17 +50,7 @@ def deploy_shm( ) stack.add_option("azure-native:tenantId", config.azure.tenant_id, replace=False) # Add necessary secrets - stack.add_secret("password-domain-admin", password(20), replace=False) - stack.add_secret( - "password-domain-azure-ad-connect", password(20), replace=False - ) - stack.add_secret( - "password-domain-computer-manager", password(20), replace=False - ) stack.add_secret("password-domain-ldap-searcher", password(20), replace=False) - stack.add_secret( - "password-update-server-linux-admin", password(20), replace=False - ) stack.add_secret( "verification-azuread-custom-domain", verification_record, replace=False ) diff --git a/data_safe_haven/commands/deploy_sre.py b/data_safe_haven/commands/deploy_sre.py index c88f2df404..7f95227e17 100644 --- a/data_safe_haven/commands/deploy_sre.py +++ b/data_safe_haven/commands/deploy_sre.py @@ -4,7 +4,7 @@ DataSafeHavenError, ) from data_safe_haven.external import GraphApi -from data_safe_haven.functions import alphanumeric, bcrypt_salt, password +from data_safe_haven.functions import alphanumeric, bcrypt_salt from data_safe_haven.infrastructure import SHMStackManager, SREStackManager from data_safe_haven.provisioning import SREProvisioningManager from data_safe_haven.utility import DatabaseSystem, SoftwarePackageCategory @@ -49,7 +49,7 @@ def deploy_sre( # Initialise Pulumi stack shm_stack = SHMStackManager(config) - stack = SREStackManager(config, sre_name) + stack = SREStackManager(config, sre_name, graph_api_token=graph_api.token) # Set Azure options stack.add_option("azure-native:location", config.azure.location, replace=False) stack.add_option( @@ -141,17 +141,7 @@ def deploy_sre( ) # Add necessary secrets stack.copy_secret("password-domain-ldap-searcher", shm_stack) - stack.add_secret("password-database-service-admin", password(20), replace=False) - stack.add_secret("password-dns-server-admin", password(20), replace=False) - stack.add_secret("password-gitea-database-admin", password(20), replace=False) - stack.add_secret( - "password-hedgedoc-database-admin", password(20), replace=False - ) - stack.add_secret("password-nexus-admin", password(20), replace=False) - stack.add_secret("password-user-database-admin", password(20), replace=False) - stack.add_secret("password-workspace-admin", password(20), replace=False) stack.add_secret("salt-dns-server-admin", bcrypt_salt(), replace=False) - stack.add_secret("token-azuread-graphapi", graph_api.token, replace=True) # Deploy Azure infrastructure with Pulumi if force is None: diff --git a/data_safe_haven/commands/teardown_sre.py b/data_safe_haven/commands/teardown_sre.py index 32c97d6cb2..6c51ca856a 100644 --- a/data_safe_haven/commands/teardown_sre.py +++ b/data_safe_haven/commands/teardown_sre.py @@ -4,6 +4,7 @@ DataSafeHavenError, DataSafeHavenInputError, ) +from data_safe_haven.external import GraphApi from data_safe_haven.functions import alphanumeric from data_safe_haven.infrastructure import SREStackManager @@ -18,9 +19,16 @@ def teardown_sre(name: str) -> None: # Load config file config = Config() + # Load GraphAPI as this may require user-interaction that is not possible as + # part of a Pulumi declarative command + graph_api = GraphApi( + tenant_id=config.shm.aad_tenant_id, + default_scopes=["Application.ReadWrite.All", "Group.ReadWrite.All"], + ) + # Remove infrastructure deployed with Pulumi try: - stack = SREStackManager(config, sre_name) + stack = SREStackManager(config, sre_name, graph_api_token=graph_api.token) if stack.work_dir.exists(): stack.teardown() else: diff --git a/data_safe_haven/infrastructure/stack_manager.py b/data_safe_haven/infrastructure/stack_manager.py index 0acc55b7e6..d86b994b4f 100644 --- a/data_safe_haven/infrastructure/stack_manager.py +++ b/data_safe_haven/infrastructure/stack_manager.py @@ -253,6 +253,9 @@ def install_plugins(self) -> None: self.stack.workspace.install_plugin( "azure-native", metadata.version("pulumi-azure-native") ) + self.stack.workspace.install_plugin( + "random", metadata.version("pulumi-random") + ) except Exception as exc: msg = f"Installing Pulumi plugins failed.\n{exc}." raise DataSafeHavenPulumiError(msg) from exc @@ -398,6 +401,11 @@ def __init__( self, config: Config, sre_name: str, + *, + graph_api_token: str | None = None, ) -> None: """Constructor""" - super().__init__(config, DeclarativeSRE(config, config.shm.name, sre_name)) + token = graph_api_token if graph_api_token else "" + super().__init__( + config, DeclarativeSRE(config, config.shm.name, sre_name, token) + ) diff --git a/data_safe_haven/infrastructure/stacks/declarative_shm.py b/data_safe_haven/infrastructure/stacks/declarative_shm.py index af274fa951..777f042b27 100644 --- a/data_safe_haven/infrastructure/stacks/declarative_shm.py +++ b/data_safe_haven/infrastructure/stacks/declarative_shm.py @@ -130,7 +130,6 @@ def run(self) -> None: log_analytics_workspace=monitoring.log_analytics_workspace, password_domain_admin=data.password_domain_admin, password_domain_azuread_connect=data.password_domain_azure_ad_connect, - password_domain_computer_manager=data.password_domain_computer_manager, password_domain_searcher=data.password_domain_searcher, private_ip_address=networking.domain_controller_private_ip, subnet_identity_servers=networking.subnet_identity_servers, diff --git a/data_safe_haven/infrastructure/stacks/declarative_sre.py b/data_safe_haven/infrastructure/stacks/declarative_sre.py index 907ebfe667..245339bf29 100644 --- a/data_safe_haven/infrastructure/stacks/declarative_sre.py +++ b/data_safe_haven/infrastructure/stacks/declarative_sre.py @@ -45,8 +45,11 @@ class DeclarativeSRE: """Deploy with Pulumi""" - def __init__(self, config: Config, shm_name: str, sre_name: str) -> None: + def __init__( + self, config: Config, shm_name: str, sre_name: str, graph_api_token: str + ) -> None: self.cfg = config + self.graph_api_token = graph_api_token self.shm_name = shm_name self.sre_name = sre_name self.short_name = f"sre-{sre_name}" @@ -80,7 +83,6 @@ def run(self) -> None: "sre_dns_server", self.stack_name, SREDnsServerProps( - admin_password=self.pulumi_opts.require("password-dns-server-admin"), admin_password_salt=self.pulumi_opts.require("salt-dns-server-admin"), location=self.cfg.azure.location, shm_fqdn=self.cfg.shm.fqdn, @@ -163,6 +165,7 @@ def run(self) -> None: self.sre_name ].data_provider_ip_addresses, dns_record=networking.shm_ns_record, + dns_server_admin_password=dns.password_admin, location=self.cfg.azure.location, networking_resource_group=networking.resource_group, pulumi_opts=self.pulumi_opts, @@ -198,7 +201,7 @@ def run(self) -> None: SRERemoteDesktopProps( aad_application_name=f"sre-{self.sre_name}-azuread-guacamole", aad_application_fqdn=networking.sre_fqdn, - aad_auth_token=self.pulumi_opts.require("token-azuread-graphapi"), + aad_auth_token=self.graph_api_token, aad_tenant_id=self.cfg.shm.aad_tenant_id, allow_copy=self.cfg.sres[self.sre_name].remote_desktop.allow_copy, allow_paste=self.cfg.sres[self.sre_name].remote_desktop.allow_paste, @@ -313,6 +316,7 @@ def run(self) -> None: ) # Export values for later use + pulumi.export("data", data.exports) pulumi.export( "ldap", { diff --git a/data_safe_haven/infrastructure/stacks/shm/data.py b/data_safe_haven/infrastructure/stacks/shm/data.py index 36a70e5d16..2ccc002744 100644 --- a/data_safe_haven/infrastructure/stacks/shm/data.py +++ b/data_safe_haven/infrastructure/stacks/shm/data.py @@ -1,6 +1,7 @@ """Pulumi component for SHM state""" from collections.abc import Mapping, Sequence +import pulumi_random from pulumi import ComponentResource, Config, Input, Output, ResourceOptions from pulumi_azure_native import keyvault, resources, storage @@ -22,21 +23,9 @@ def __init__( self.admin_group_id = admin_group_id self.admin_ip_addresses = admin_ip_addresses self.location = location - self.password_domain_admin = self.get_secret( - pulumi_opts, "password-domain-admin" - ) - self.password_domain_azure_ad_connect = self.get_secret( - pulumi_opts, "password-domain-azure-ad-connect" - ) - self.password_domain_computer_manager = self.get_secret( - pulumi_opts, "password-domain-computer-manager" - ) self.password_domain_searcher = self.get_secret( pulumi_opts, "password-domain-ldap-searcher" ) - self.password_update_server_linux_admin = self.get_secret( - pulumi_opts, "password-update-server-linux-admin" - ) self.tenant_id = tenant_id def get_secret(self, pulumi_opts: Config, secret_name: str) -> Output[str]: @@ -138,38 +127,70 @@ def __init__( tags=child_tags, ) - # Deploy key vault secrets + # Secret: Domain admin password + password_domain_admin = pulumi_random.RandomPassword( + f"{self._name}_password_domain_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( f"{self._name}_kvs_password_domain_admin", - properties=keyvault.SecretPropertiesArgs(value=props.password_domain_admin), + properties=keyvault.SecretPropertiesArgs( + value=password_domain_admin.result + ), resource_group_name=resource_group.name, secret_name="password-domain-admin", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_domain_admin) + ), tags=child_tags, ) + + # Secret: Azure ADConnect password + password_domain_azure_ad_connect = pulumi_random.RandomPassword( + f"{self._name}_password_domain_azure_ad_connect", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( f"{self._name}_kvs_password_domain_azure_ad_connect", properties=keyvault.SecretPropertiesArgs( - value=props.password_domain_azure_ad_connect + value=password_domain_azure_ad_connect.result ), resource_group_name=resource_group.name, secret_name="password-domain-azure-ad-connect", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_domain_azure_ad_connect) + ), tags=child_tags, ) + + # Secret: Linux update server admin password + password_update_server_linux_admin = pulumi_random.RandomPassword( + f"{self._name}_password_update_server_linux_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( - f"{self._name}_kvs_password_domain_computer_manager", + f"{self._name}_kvs_password_update_server_linux_admin", properties=keyvault.SecretPropertiesArgs( - value=props.password_domain_computer_manager + value=password_update_server_linux_admin.result ), resource_group_name=resource_group.name, - secret_name="password-domain-computer-manager", + secret_name="password-update-server-linux-admin", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_update_server_linux_admin) + ), tags=child_tags, ) + + # Add other Pulumi secrets to key vault keyvault.Secret( f"{self._name}_kvs_password_domain_searcher", properties=keyvault.SecretPropertiesArgs( @@ -181,17 +202,6 @@ def __init__( opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), tags=child_tags, ) - keyvault.Secret( - f"{self._name}_kvs_password_update_server_linux_admin", - properties=keyvault.SecretPropertiesArgs( - value=props.password_update_server_linux_admin - ), - resource_group_name=resource_group.name, - secret_name="password-update-server-linux-admin", - vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), - tags=child_tags, - ) # Deploy persistent data account storage_account_persistent_data = storage.StorageAccount( @@ -249,12 +259,13 @@ def __init__( ) # Register outputs - self.password_domain_admin = props.password_domain_admin - self.password_domain_azure_ad_connect = props.password_domain_azure_ad_connect - self.password_domain_computer_manager = props.password_domain_computer_manager - self.password_domain_searcher = props.password_domain_searcher - self.password_update_server_linux_admin = ( - props.password_update_server_linux_admin + self.password_domain_admin = Output.secret(password_domain_admin.result) + self.password_domain_azure_ad_connect = Output.secret( + password_domain_azure_ad_connect.result + ) + self.password_domain_searcher = Output.secret(props.password_domain_searcher) + self.password_update_server_linux_admin = Output.secret( + password_update_server_linux_admin.result ) self.resource_group_name = Output.from_input(resource_group.name) self.vault = key_vault diff --git a/data_safe_haven/infrastructure/stacks/shm/domain_controllers.py b/data_safe_haven/infrastructure/stacks/shm/domain_controllers.py index cbeec81bc0..b0f9a81ea3 100644 --- a/data_safe_haven/infrastructure/stacks/shm/domain_controllers.py +++ b/data_safe_haven/infrastructure/stacks/shm/domain_controllers.py @@ -33,7 +33,6 @@ def __init__( log_analytics_workspace: Input[WrappedLogAnalyticsWorkspace], password_domain_admin: Input[str], password_domain_azuread_connect: Input[str], - password_domain_computer_manager: Input[str], password_domain_searcher: Input[str], private_ip_address: Input[str], subnet_identity_servers: Input[network.GetSubnetResult], @@ -55,7 +54,6 @@ def __init__( self.log_analytics_workspace = log_analytics_workspace self.password_domain_admin = password_domain_admin self.password_domain_azuread_connect = password_domain_azuread_connect - self.password_domain_computer_manager = password_domain_computer_manager self.password_domain_searcher = password_domain_searcher self.private_ip_address = private_ip_address self.subnet_name = Output.from_input(subnet_identity_servers).apply( @@ -65,7 +63,6 @@ def __init__( # Note that usernames have a maximum of 20 characters self.username_domain_admin = "dshdomainadmin" self.username_domain_azuread_connect = "dshazureadsync" - self.username_domain_computer_manager = "dshcomputermanager" self.username_domain_searcher = "dshldapsearcher" self.virtual_network_name = virtual_network_name self.virtual_network_resource_group_name = virtual_network_resource_group_name @@ -136,8 +133,6 @@ def __init__( AzureADConnectUsername=props.username_domain_azuread_connect, DomainAdministratorPassword=props.password_domain_admin, DomainAdministratorUsername=props.username_domain_admin, - DomainComputerManagerPassword=props.password_domain_computer_manager, - DomainComputerManagerUsername=props.username_domain_computer_manager, DomainName=props.domain_fqdn, DomainNetBios=props.domain_netbios_name, DomainRootDn=props.domain_root_dn, diff --git a/data_safe_haven/infrastructure/stacks/sre/data.py b/data_safe_haven/infrastructure/stacks/sre/data.py index e66649aa2e..e1e5832462 100644 --- a/data_safe_haven/infrastructure/stacks/sre/data.py +++ b/data_safe_haven/infrastructure/stacks/sre/data.py @@ -2,6 +2,7 @@ from collections.abc import Mapping, Sequence from typing import ClassVar +import pulumi_random from pulumi import ComponentResource, Config, Input, Output, ResourceOptions from pulumi_azure_native import ( authorization, @@ -43,6 +44,7 @@ def __init__( admin_ip_addresses: Input[Sequence[str]], data_provider_ip_addresses: Input[Sequence[str]], dns_record: Input[network.RecordSet], + dns_server_admin_password: Input[pulumi_random.RandomPassword], location: Input[str], networking_resource_group: Input[resources.ResourceGroup], pulumi_opts: Config, @@ -64,29 +66,11 @@ def __init__( } ) self.dns_record = dns_record + self.password_dns_server_admin = dns_server_admin_password self.location = location self.networking_resource_group_name = Output.from_input( networking_resource_group ).apply(get_name_from_rg) - self.password_database_service_admin = self.get_secret( - pulumi_opts, "password-database-service-admin" - ) - self.password_dns_server_admin = self.get_secret( - pulumi_opts, "password-dns-server-admin" - ) - self.password_gitea_database_admin = self.get_secret( - pulumi_opts, "password-gitea-database-admin" - ) - self.password_hedgedoc_database_admin = self.get_secret( - pulumi_opts, "password-hedgedoc-database-admin" - ) - self.password_nexus_admin = self.get_secret(pulumi_opts, "password-nexus-admin") - self.password_user_database_admin = self.get_secret( - pulumi_opts, "password-user-database-admin" - ) - self.password_workspace_admin = self.get_secret( - pulumi_opts, "password-workspace-admin" - ) self.private_dns_zone_base_id = self.get_secret( pulumi_opts, "shm-networking-private_dns_zone_base_id" ) @@ -254,22 +238,32 @@ def __init__( ), ) - # Deploy key vault secrets + # Secret: database service admin password + password_database_service_admin = pulumi_random.RandomPassword( + f"{self._name}_password_database_service_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( f"{self._name}_kvs_password_database_service_admin", properties=keyvault.SecretPropertiesArgs( - value=props.password_database_service_admin + value=password_database_service_admin.result, ), resource_group_name=resource_group.name, secret_name="password-database-service-admin", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_database_service_admin) + ), tags=child_tags, ) + + # Secret: database service admin password keyvault.Secret( f"{self._name}_kvs_password_dns_server_admin", properties=keyvault.SecretPropertiesArgs( - value=props.password_dns_server_admin + value=props.password_dns_server_admin.result, ), resource_group_name=resource_group.name, secret_name="password-dns-server-admin", @@ -277,57 +271,105 @@ def __init__( opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), tags=child_tags, ) + + # Secret: Gitea database admin password + password_gitea_database_admin = pulumi_random.RandomPassword( + f"{self._name}_password_gitea_database_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( f"{self._name}_kvs_password_gitea_database_admin", properties=keyvault.SecretPropertiesArgs( - value=props.password_gitea_database_admin + value=password_gitea_database_admin.result ), resource_group_name=resource_group.name, secret_name="password-gitea-database-admin", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_gitea_database_admin) + ), tags=child_tags, ) + + # Secret: Hedgedoc database admin password + password_hedgedoc_database_admin = pulumi_random.RandomPassword( + f"{self._name}_password_hedgedoc_database_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( f"{self._name}_kvs_password_hedgedoc_database_admin", properties=keyvault.SecretPropertiesArgs( - value=props.password_hedgedoc_database_admin + value=password_hedgedoc_database_admin.result ), resource_group_name=resource_group.name, secret_name="password-hedgedoc-database-admin", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_hedgedoc_database_admin) + ), tags=child_tags, ) + + # Secret: Nexus admin password + password_nexus_admin = pulumi_random.RandomPassword( + f"{self._name}_password_nexus_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( f"{self._name}_kvs_password_nexus_admin", - properties=keyvault.SecretPropertiesArgs(value=props.password_nexus_admin), + properties=keyvault.SecretPropertiesArgs(value=password_nexus_admin.result), resource_group_name=resource_group.name, secret_name="password-nexus-admin", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_nexus_admin) + ), tags=child_tags, ) - keyvault.Secret( + + # Secret: Guacamole user database admin password + password_user_database_admin = pulumi_random.RandomPassword( + f"{self._name}_password_user_database_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) + kvs_password_user_database_admin = keyvault.Secret( f"{self._name}_kvs_password_user_database_admin", properties=keyvault.SecretPropertiesArgs( - value=props.password_user_database_admin + value=password_user_database_admin.result ), resource_group_name=resource_group.name, secret_name="password-user-database-admin", vault_name=key_vault.name, - opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=password_user_database_admin) + ), tags=child_tags, ) + + # Secret: Workspace admin password + password_workspace_admin = pulumi_random.RandomPassword( + f"{self._name}_password_workspace_admin", + length=20, + special=True, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), + ) keyvault.Secret( f"{self._name}_kvs_password_workspace_admin", properties=keyvault.SecretPropertiesArgs( - value=props.password_workspace_admin + value=password_workspace_admin.result ), resource_group_name=resource_group.name, secret_name="password-workspace-admin", vault_name=key_vault.name, - opts=ResourceOptions(parent=key_vault), + opts=ResourceOptions(parent=password_workspace_admin), tags=child_tags, ) @@ -727,19 +769,27 @@ def __init__( storage_account_data_configuration.name ) self.managed_identity = identity_key_vault_reader - self.password_nexus_admin = Output.secret(props.password_nexus_admin) + self.password_nexus_admin = Output.secret(password_nexus_admin.result) self.password_database_service_admin = Output.secret( - props.password_database_service_admin + password_database_service_admin.result + ) + self.password_dns_server_admin = Output.secret( + props.password_dns_server_admin.result ) - self.password_dns_server_admin = Output.secret(props.password_dns_server_admin) self.password_gitea_database_admin = Output.secret( - props.password_gitea_database_admin + password_gitea_database_admin.result ) self.password_hedgedoc_database_admin = Output.secret( - props.password_hedgedoc_database_admin + password_hedgedoc_database_admin.result ) self.password_user_database_admin = Output.secret( - props.password_user_database_admin + password_user_database_admin.result ) - self.password_workspace_admin = Output.secret(props.password_workspace_admin) + self.password_workspace_admin = Output.secret(password_workspace_admin.result) self.resource_group_name = resource_group.name + + # Register exports + self.exports = { + "key_vault_name": key_vault.name, + "password_user_database_admin_secret": kvs_password_user_database_admin.name, + } diff --git a/data_safe_haven/infrastructure/stacks/sre/dns_server.py b/data_safe_haven/infrastructure/stacks/sre/dns_server.py index ee5e08f7df..4d904be94d 100644 --- a/data_safe_haven/infrastructure/stacks/sre/dns_server.py +++ b/data_safe_haven/infrastructure/stacks/sre/dns_server.py @@ -1,6 +1,7 @@ """Pulumi component for SRE DNS server""" from collections.abc import Mapping +import pulumi_random from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import containerinstance, network, resources @@ -25,7 +26,6 @@ class SREDnsServerProps: def __init__( self, - admin_password: Input[str], admin_password_salt: Input[str], location: Input[str], shm_fqdn: Input[str], @@ -33,7 +33,6 @@ def __init__( sre_index: Input[int], ) -> None: subnet_ranges = Output.from_input(sre_index).apply(lambda idx: SREIpRanges(idx)) - self.admin_password = Output.secret(admin_password) self.admin_password_salt = Output.secret(admin_password_salt) self.admin_username = "dshadmin" self.ip_range_prefix = str(SREDnsIpRanges().vnet) @@ -67,6 +66,11 @@ def __init__( tags=child_tags, ) + # Generate admin password + password_admin = pulumi_random.RandomPassword( + f"{self._name}_password_admin", length=20, special=True, opts=child_opts + ) + # Read AdGuardHome setup files adguard_entrypoint_sh_reader = FileReader( resources_path / "dns_server" / "entrypoint.sh" @@ -79,7 +83,7 @@ def __init__( adguard_adguardhome_yaml_contents = Output.all( admin_username=props.admin_username, admin_password_encrypted=Output.all( - password=props.admin_password, salt=props.admin_password_salt + password=password_admin.result, salt=props.admin_password_salt ).apply(lambda kwargs: bcrypt_encode(kwargs["password"], kwargs["salt"])), # Use Azure virtual DNS server as upstream # https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16 @@ -327,5 +331,6 @@ def __init__( # Register outputs self.ip_address = get_ip_address_from_container_group(container_group) + self.password_admin = password_admin self.resource_group = resource_group self.virtual_network = virtual_network diff --git a/data_safe_haven/provisioning/sre_provisioning_manager.py b/data_safe_haven/provisioning/sre_provisioning_manager.py index 16e99ad71d..e79dcae35a 100644 --- a/data_safe_haven/provisioning/sre_provisioning_manager.py +++ b/data_safe_haven/provisioning/sre_provisioning_manager.py @@ -29,11 +29,19 @@ def __init__( self.sre_name = sre_name self.subscription_name = subscription_name + # Read secrets from key vault + keyvault_name = sre_stack.output("data")["key_vault_name"] + secret_name = sre_stack.output("data")["password_user_database_admin_secret"] + azure_api = AzureApi(self.subscription_name) + connection_db_server_password = azure_api.get_keyvault_secret( + keyvault_name, secret_name + ) + # Construct remote desktop parameters self.remote_desktop_params = sre_stack.output("remote_desktop") - self.remote_desktop_params["connection_db_server_password"] = sre_stack.secret( - "password-user-database-admin" - ) + self.remote_desktop_params[ + "connection_db_server_password" + ] = connection_db_server_password self.remote_desktop_params["timezone"] = timezone # Construct security group parameters diff --git a/data_safe_haven/resources/desired_state_configuration/PrimaryDomainController.ps1 b/data_safe_haven/resources/desired_state_configuration/PrimaryDomainController.ps1 index 2690968137..d1d856af16 100644 --- a/data_safe_haven/resources/desired_state_configuration/PrimaryDomainController.ps1 +++ b/data_safe_haven/resources/desired_state_configuration/PrimaryDomainController.ps1 @@ -138,14 +138,6 @@ Configuration ConfigureActiveDirectory { [ValidateNotNullOrEmpty()] [String]$CADDomainAdministratorUsername, - [Parameter(Mandatory = $true, HelpMessage = "Domain computer manager password")] - [ValidateNotNullOrEmpty()] - [String]$CADDomainComputerManagerPassword, - - [Parameter(Mandatory = $true, HelpMessage = "Domain computer manager username")] - [ValidateNotNullOrEmpty()] - [String]$CADDomainComputerManagerUsername, - [Parameter(Mandatory = $true, HelpMessage = "FQDN for the SHM domain")] [ValidateNotNullOrEmpty()] [String]$CADDomainName, @@ -193,11 +185,6 @@ Configuration ConfigureActiveDirectory { Password = $CADAzureADConnectPassword Username = $CADAzureADConnectUsername } - ComputerManager = @{ - Description = "DSH domain computers manager" - Password = $CADDomainComputerManagerPassword - Username = $CADDomainComputerManagerUsername - } LDAPSearcher = @{ Description = "DSH LDAP searcher" Password = $CADLDAPSearcherPassword @@ -344,50 +331,6 @@ Configuration ConfigureActiveDirectory { TestScript = { $false } DependsOn = "[ADUser]AzureADSynchroniser" } - - # Allow the computer manager to register computers in the domain - Script SetComputerManagerPermissions { - SetScript = { - try { - $success = $true - $DomainComputerManagerUsername = $using:DataSafeHavenServiceAccounts.ComputerManager.Username - # $OuDescription = $using:DataSafeHavenUnits.DomainComputers.Description - # $OrganisationalUnit = Get-ADObject -Filter "Name -eq '$OuDescription'" - $OrganisationalUnit = Get-ADObject -Filter "Name -eq '$($using:DataSafeHavenUnits.DomainComputers.Description)'" - $DomainComputerManagerSID = (Get-ADUser -Identity $DomainComputerManagerUsername).SID - # Add permission to create child computer objects - $null = dsacls $OrganisationalUnit /I:T /G "${userPrincipalName}:CC;computer" - $success = $success -and $? - # Give 'write property' permissions over several attributes of child computer objects - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;DNS Host Name Attributes;computer" - $success = $success -and $? - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;msDS-SupportedEncryptionTypes;computer" - $success = $success -and $? - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;operatingSystem;computer" - $success = $success -and $? - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;operatingSystemVersion;computer" - $success = $success -and $? - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;operatingSystemServicePack;computer" - $success = $success -and $? - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;sAMAccountName;computer" - $success = $success -and $? - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;servicePrincipalName;computer" - $success = $success -and $? - $null = dsacls $OrganisationalUnit /I:S /G "${DomainComputerManagerSID}:WP;userPrincipalName;computer" - $success = $success -and $? - if ($success) { - Write-Verbose -Message "Successfully delegated Active Directory permissions on '$OrganisationalUnit' to '$DomainComputerManagerUsername'" - } else { - throw "Failed to delegate Active Directory permissions on '$OrganisationalUnit' to '$DomainComputerManagerUsername'!" - } - } catch { - Write-Error "SetComputerManagerPermissions: $($_.Exception)" - } - } - GetScript = { @{} } - TestScript = { $false } - DependsOn = "[ADUser]ComputerManager" - } } } @@ -444,14 +387,6 @@ Configuration PrimaryDomainController { [ValidateNotNullOrEmpty()] [String]$DomainAdministratorUsername, - [Parameter(Mandatory = $true, HelpMessage = "Domain computer manager password")] - [ValidateNotNullOrEmpty()] - [String]$DomainComputerManagerPassword, - - [Parameter(Mandatory = $true, HelpMessage = "Domain computer manager username")] - [ValidateNotNullOrEmpty()] - [String]$DomainComputerManagerUsername, - [Parameter(Mandatory = $true, HelpMessage = "FQDN for the SHM domain")] [ValidateNotNullOrEmpty()] [String]$DomainName, @@ -510,8 +445,6 @@ Configuration PrimaryDomainController { CADAzureADConnectUsername = $AzureADConnectUsername CADDomainAdministratorPassword = $DomainAdministratorPassword CADDomainAdministratorUsername = $DomainAdministratorUsername - CADDomainComputerManagerPassword = $DomainComputerManagerPassword - CADDomainComputerManagerUsername = $DomainComputerManagerUsername CADDomainName = $DomainName CADDomainRootDn = $DomainRootDn CADLDAPSearcherPassword = $LDAPSearcherPassword diff --git a/pyproject.toml b/pyproject.toml index 45d64e4052..f8538fb66d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "psycopg~=3.1.10", "pulumi~=3.80.0", "pulumi-azure-native~=1.104.0", + "pulumi-random~=4.14.0", "pytz~=2022.7.0", "PyYAML~=6.0", "rich~=13.4.2", @@ -157,6 +158,7 @@ module = [ "psycopg.*", "pulumi.*", "pulumi_azure_native.*", + "pulumi_random.*", "pymssql.*", "rich.*", "simple_acme_dns.*",