From 41c7b7a3fb7f3e92333f53e1aaa5657d42aa82c4 Mon Sep 17 00:00:00 2001 From: Bin Xia Date: Wed, 7 Oct 2026 02:58:49 +0000 Subject: [PATCH 1/3] [K2P] az aks create/update: Add --kms-infrastructure-encryption for KMS PMK Port KMS platform-managed keys (PMK) support from the aks-preview extension into the GA azure-cli ACS module ahead of the KMS PMK GA release. Adds a new `--kms-infrastructure-encryption` parameter to `az aks create` and `az aks update`, which sets `securityProfile.kubernetesResourceObjectEncryptionProfile.infrastructureEncryption` to enable encryption at rest of Kubernetes resource objects using service-managed keys. The stable API only defines `Enabled` for the `InfrastructureEncryption` enum, so the parameter accepts `Enabled` only and defaults to unset. When unset, the encryption profile is left untouched, so PMK is never implicitly disabled. Other changes: - PMK requires a versionless key ID, so the Key Vault key ID validator no longer rejects IDs without a version segment. - The Key Vault resource ID validator now also verifies the provider is Microsoft.KeyVault and the type is vaults or managedHsms. - PMK requires a Key Vault resource ID even when the Key Vault network access is Public, so that combination is now allowed and the resource ID is set in both the create and update paths. - Add unit tests for the new context getter, the create and update decorator paths and the updated validators, plus live scenario tests covering PMK alone, PMK with CMK, and enabling PMK on an existing cluster. - Add linter exclusions for the new option name and a HISTORY.rst entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671828b6-e9d0-4bbe-95e3-576ec728c597 --- .../azure/cli/command_modules/acs/_consts.py | 3 + .../azure/cli/command_modules/acs/_help.py | 8 + .../azure/cli/command_modules/acs/_params.py | 4 + .../cli/command_modules/acs/_validators.py | 28 +- .../azure/cli/command_modules/acs/custom.py | 2 + .../command_modules/acs/linter_exclusions.yml | 6 + .../acs/managed_cluster_decorator.py | 76 ++++- .../acs/tests/latest/test_aks_commands.py | 230 ++++++++++++++ .../latest/test_managed_cluster_decorator.py | 295 ++++++++++++++++++ .../acs/tests/latest/test_validators.py | 36 ++- 10 files changed, 679 insertions(+), 9 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_consts.py b/src/azure-cli/azure/cli/command_modules/acs/_consts.py index 71cefedd269..9d5af0098f0 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_consts.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_consts.py @@ -222,6 +222,9 @@ CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC = "Public" CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE = "Private" +# kms infrastructure encryption (platform-managed keys) +CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED = "Enabled" + # app routing nginx config options CONST_WEB_APPLICATION_ROUTING_KEY_NAME = "ingress/webApplicationRouting" CONST_APP_ROUTING_ANNOTATION_CONTROLLED_NGINX = "AnnotationControlled" diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index a1020fb25a4..a4b536cd1fd 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -530,6 +530,10 @@ - name: --azure-keyvault-kms-key-vault-resource-id type: string short-summary: Resource ID of Azure Key Vault. + - name: --kms-infrastructure-encryption + type: string + short-summary: Enable encryption at rest of Kubernetes resource objects using service-managed keys. + long-summary: Enable infrastructure encryption for Kubernetes resource objects. This feature provides encryption at rest for cluster secrets and configuration using service-managed keys. The only accepted value is "Enabled"; once enabled it cannot be disabled. For more information see https://aka.ms/aks/kubernetesResourceObjectEncryption. - name: --enable-image-cleaner type: bool short-summary: Enable ImageCleaner Service. @@ -1080,6 +1084,10 @@ - name: --azure-keyvault-kms-key-vault-resource-id type: string short-summary: Resource ID of Azure Key Vault. + - name: --kms-infrastructure-encryption + type: string + short-summary: Enable encryption at rest of Kubernetes resource objects using service-managed keys. + long-summary: Enable infrastructure encryption for Kubernetes resource objects. This feature provides encryption at rest for cluster secrets and configuration using service-managed keys. The only accepted value is "Enabled"; once enabled it cannot be disabled. For more information see https://aka.ms/aks/kubernetesResourceObjectEncryption. - name: --enable-image-cleaner type: bool short-summary: Enable ImageCleaner Service. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index dea20bf437c..603b318eeb3 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -12,6 +12,7 @@ from azure.cli.command_modules.acs._consts import ( CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE, CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC, + CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED, CONST_GPU_INSTANCE_PROFILE_MIG1_G, CONST_GPU_INSTANCE_PROFILE_MIG2_G, CONST_GPU_INSTANCE_PROFILE_MIG3_G, CONST_GPU_INSTANCE_PROFILE_MIG4_G, CONST_GPU_INSTANCE_PROFILE_MIG7_G, CONST_LOAD_BALANCER_SKU_BASIC, @@ -262,6 +263,7 @@ dev_space_endpoint_types = ['Public', 'Private', 'None'] keyvault_network_access_types = [CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC, CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE] +kms_infrastructure_encryption_types = [CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED] gpu_instance_profiles = [ CONST_GPU_INSTANCE_PROFILE_MIG1_G, @@ -514,6 +516,7 @@ def load_arguments(self, _): c.argument('azure_keyvault_kms_key_id', validator=validate_azure_keyvault_kms_key_id) c.argument('azure_keyvault_kms_key_vault_network_access', arg_type=get_enum_type(keyvault_network_access_types)) c.argument('azure_keyvault_kms_key_vault_resource_id', validator=validate_azure_keyvault_kms_key_vault_resource_id) + c.argument('kms_infrastructure_encryption', arg_type=get_enum_type(kms_infrastructure_encryption_types)) c.argument('enable_image_cleaner', action='store_true') c.argument('image_cleaner_interval_hours', type=int) c.argument('http_proxy_config') @@ -855,6 +858,7 @@ def load_arguments(self, _): c.argument('azure_keyvault_kms_key_id', validator=validate_azure_keyvault_kms_key_id) c.argument('azure_keyvault_kms_key_vault_network_access', arg_type=get_enum_type(keyvault_network_access_types)) c.argument('azure_keyvault_kms_key_vault_resource_id', validator=validate_azure_keyvault_kms_key_vault_resource_id) + c.argument('kms_infrastructure_encryption', arg_type=get_enum_type(kms_infrastructure_encryption_types)) c.argument('enable_image_cleaner', action='store_true') c.argument('disable_image_cleaner', action='store_true', validator=validate_image_cleaner_enable_disable_mutually_exclusive) c.argument('image_cleaner_interval_hours', type=int) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index bab4888fc5f..a92c47d8925 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -870,8 +870,11 @@ def validate_azure_keyvault_kms_key_id(namespace): if not key_id.startswith(https_prefix): raise InvalidArgumentValueError(err_msg) + # A versioned key ID has 4 segments ({vault-url}/keys/{key-name}/{key-version}), while a versionless + # key ID has 3 segments ({vault-url}/keys/{key-name}). Versionless key IDs are required when KMS + # infrastructure encryption (platform-managed keys) is enabled. segments = key_id[len(https_prefix):].split("/") - if len(segments) != 4 or segments[1] != "keys": + if len(segments) < 3 or segments[1] != "keys": raise InvalidArgumentValueError(err_msg) @@ -879,10 +882,31 @@ def validate_azure_keyvault_kms_key_vault_resource_id(namespace): key_vault_resource_id = namespace.azure_keyvault_kms_key_vault_resource_id if key_vault_resource_id is None or key_vault_resource_id == '': return - from azure.mgmt.core.tools import is_valid_resource_id + from azure.mgmt.core.tools import is_valid_resource_id, parse_resource_id if not is_valid_resource_id(key_vault_resource_id): raise InvalidArgumentValueError("--azure-keyvault-kms-key-vault-resource-id is not a valid Azure resource ID.") + try: + parsed = parse_resource_id(key_vault_resource_id) + provider = parsed.get('namespace', '').lower() + if provider != 'microsoft.keyvault': + raise InvalidArgumentValueError( + "--azure-keyvault-kms-key-vault-resource-id must reference a Microsoft.KeyVault resource." + ) + resource_type = parsed.get('type', '').lower() + if resource_type not in ['vaults', 'managedhsms']: + raise InvalidArgumentValueError( + "--azure-keyvault-kms-key-vault-resource-id must reference a Key Vault " + "(vaults) or Managed HSM (managedHSMs)." + ) + except InvalidArgumentValueError: + # Re-raise our validation errors + raise + except Exception as ex: + raise InvalidArgumentValueError( + f"--azure-keyvault-kms-key-vault-resource-id parsing failed: {str(ex)}" + ) + def validate_image_cleaner_enable_disable_mutually_exclusive(namespace): enable_image_cleaner = namespace.enable_image_cleaner diff --git a/src/azure-cli/azure/cli/command_modules/acs/custom.py b/src/azure-cli/azure/cli/command_modules/acs/custom.py index 70bf1c416a8..99b10394c52 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -938,6 +938,7 @@ def aks_create( azure_keyvault_kms_key_id=None, azure_keyvault_kms_key_vault_network_access=None, azure_keyvault_kms_key_vault_resource_id=None, + kms_infrastructure_encryption=None, enable_image_cleaner=False, image_cleaner_interval_hours=None, enable_keda=False, @@ -1191,6 +1192,7 @@ def aks_update( azure_keyvault_kms_key_id=None, azure_keyvault_kms_key_vault_network_access=None, azure_keyvault_kms_key_vault_resource_id=None, + kms_infrastructure_encryption=None, enable_image_cleaner=False, disable_image_cleaner=False, image_cleaner_interval_hours=None, diff --git a/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml b/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml index 4530e07e60f..949010910cf 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml +++ b/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml @@ -127,6 +127,9 @@ aks create: enable_upstream_kubescheduler_user_configuration: rule_exclusions: - option_length_too_long + kms_infrastructure_encryption: + rule_exclusions: + - option_length_too_long aks enable-addons: parameters: appgw_watch_namespace: @@ -287,6 +290,9 @@ aks update: disable_upstream_kubescheduler_user_configuration: rule_exclusions: - option_length_too_long + kms_infrastructure_encryption: + rule_exclusions: + - option_length_too_long aks nodepool add: parameters: disable_windows_outbound_nat: diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index 9233be1f48b..1d090724b82 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -34,6 +34,7 @@ CONST_PRIVATE_DNS_ZONE_SYSTEM, CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE, CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC, + CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED, AgentPoolDecoratorMode, DecoratorEarlyExitException, DecoratorMode, @@ -5845,6 +5846,28 @@ def get_workload_identity_profile(self) -> Optional[ManagedClusterSecurityProfil return profile + def get_kms_infrastructure_encryption(self) -> Union[str, None]: + """Obtain the value of kms_infrastructure_encryption. + + :return: string or None + """ + # read the original value passed by the command + kms_infrastructure_encryption = self.raw_param.get("kms_infrastructure_encryption") + + # try to read the property value corresponding to the parameter from the `mc` object + if kms_infrastructure_encryption is None and self.mc: + security_profile = getattr(self.mc, "security_profile", None) + if security_profile: + encryption_profile = getattr( + security_profile, "kubernetes_resource_object_encryption_profile", None + ) + if encryption_profile: + infrastructure_encryption = getattr(encryption_profile, "infrastructure_encryption", None) + if infrastructure_encryption is not None: + kms_infrastructure_encryption = infrastructure_encryption + + return kms_infrastructure_encryption + def _get_enable_azure_keyvault_kms(self, enable_validation: bool = False) -> bool: """Internal function to obtain the value of enable_azure_keyvault_kms. @@ -6047,6 +6070,9 @@ def _get_azure_keyvault_kms_key_vault_resource_id(self, enable_validation: bool # validation if enable_validation: + is_pmk_enabled = ( + self.get_kms_infrastructure_encryption() == CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED + ) enable_azure_keyvault_kms = self._get_enable_azure_keyvault_kms( enable_validation=False) if ( @@ -6060,6 +6086,12 @@ def _get_azure_keyvault_kms_key_vault_resource_id(self, enable_validation: bool '"--azure-keyvault-kms-key-vault-resource-id" requires "--enable-azure-keyvault-kms".' ) + if is_pmk_enabled and not azure_keyvault_kms_key_vault_resource_id: + raise RequiredArgumentMissingError( + '"--azure-keyvault-kms-key-vault-resource-id" is required when ' + '"--kms-infrastructure-encryption" is set to "Enabled".' + ) + key_vault_network_access = self._get_azure_keyvault_kms_key_vault_network_access( enable_validation=False) if ( @@ -6074,6 +6106,7 @@ def _get_azure_keyvault_kms_key_vault_resource_id(self, enable_validation: bool '"--azure-keyvault-kms-key-vault-network-access" is "Private".' ) if ( + not is_pmk_enabled and key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC and ( azure_keyvault_kms_key_vault_resource_id is not None and @@ -7238,12 +7271,25 @@ def set_up_defender(self, mc: ManagedCluster) -> ManagedCluster: return mc def set_up_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: - """Set up security profile azureKeyVaultKms for the ManagedCluster object. + """Set up security profile kubernetesResourceObjectEncryptionProfile and azureKeyVaultKms for + the ManagedCluster object. :return: the ManagedCluster object """ self._ensure_mc(mc) + kms_infrastructure_encryption = self.context.get_kms_infrastructure_encryption() + if kms_infrastructure_encryption: + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + if mc.security_profile.kubernetes_resource_object_encryption_profile is None: + mc.security_profile.kubernetes_resource_object_encryption_profile = ( + self.models.KubernetesResourceObjectEncryptionProfile() + ) + mc.security_profile.kubernetes_resource_object_encryption_profile.infrastructure_encryption = ( + kms_infrastructure_encryption + ) + if self.context.get_enable_azure_keyvault_kms(): key_id = self.context.get_azure_keyvault_kms_key_id() if key_id: @@ -7255,7 +7301,10 @@ def set_up_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: ) key_vault_network_access = self.context.get_azure_keyvault_kms_key_vault_network_access() mc.security_profile.azure_key_vault_kms.key_vault_network_access = key_vault_network_access - if key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE: + if ( + key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE or + kms_infrastructure_encryption == CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED + ): mc.security_profile.azure_key_vault_kms.key_vault_resource_id = ( self.context.get_azure_keyvault_kms_key_vault_resource_id() ) @@ -10410,12 +10459,25 @@ def update_k8s_support_plan(self, mc: ManagedCluster) -> ManagedCluster: return mc def update_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: - """Update security profile azureKeyvaultKms for the ManagedCluster object. + """Update security profile kubernetesResourceObjectEncryptionProfile and azureKeyvaultKms for + the ManagedCluster object. :return: the ManagedCluster object """ self._ensure_mc(mc) + kms_infrastructure_encryption = self.context.get_kms_infrastructure_encryption() + if kms_infrastructure_encryption: + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + if mc.security_profile.kubernetes_resource_object_encryption_profile is None: + mc.security_profile.kubernetes_resource_object_encryption_profile = ( + self.models.KubernetesResourceObjectEncryptionProfile() + ) + mc.security_profile.kubernetes_resource_object_encryption_profile.infrastructure_encryption = ( + kms_infrastructure_encryption + ) + if self.context.get_enable_azure_keyvault_kms(): # get kms profile if mc.security_profile is None: @@ -10434,8 +10496,12 @@ def update_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: azure_key_vault_kms_profile.key_vault_network_access = ( self.context.get_azure_keyvault_kms_key_vault_network_access() ) - # set key vault resource id - if azure_key_vault_kms_profile.key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE: + # set key vault resource id, it is also required when infrastructure encryption (PMK) is enabled + if ( + azure_key_vault_kms_profile.key_vault_network_access == + CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE or + kms_infrastructure_encryption == CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED + ): azure_key_vault_kms_profile.key_vault_resource_id = ( self.context.get_azure_keyvault_kms_key_vault_resource_id() ) diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py index d2acece19d3..ddacbcab2b4 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py @@ -11477,6 +11477,236 @@ def test_aks_disable_azurekeyvaultkms(self, resource_group, resource_group_locat self.is_empty(), ]) + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westcentralus') + def test_aks_create_with_kms_pmk_and_update_cmk(self, resource_group, resource_group_location): + """Create a PMK-enabled cluster, then enable and disable CMK on it.""" + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + 'kv_name': kv_name, + 'identity_name': identity_name, + 'ssh_key_value': self.generate_ssh_keys(), + }) + + # create user-assigned identity + identity_id = self._get_user_assigned_identity(resource_group) + identity_object_id = self._get_principal_id_of_user_assigned_identity(identity_id) + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create cluster with KMS infrastructure encryption (PMK) enabled + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--kms-infrastructure-encryption Enabled ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.kubernetesResourceObjectEncryptionProfile.infrastructureEncryption', + 'Enabled'), + self.not_exists('securityProfile.azureKeyVaultKms'), + ]) + + # create key vault and key for CMK + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} ' \ + '--enable-rbac-authorization=false --no-self-perms -o json' + self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]) + + # set access policy for test identity + test_identity_object_id = self._get_test_identity_object_id() + test_identity_access_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--key-permissions all --object-id ' + test_identity_object_id + self.cmd(test_identity_access_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]) + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + # PMK requires a versionless key ID, drop the trailing version segment + key_id_versionless = key['key']['kid'].rsplit('/', 1)[0] + assert key_id_versionless is not None + + kv_resource_id = self.cmd( + 'keyvault show --resource-group={resource_group} --name={kv_name} --query id -o tsv' + ).output.strip() + self.kwargs.update({ + 'key_id': key_id_versionless, + 'kv_resource_id': kv_resource_id, + }) + + # assign access policy for the cluster identity + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]) + + # enable CMK on the existing PMK cluster + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} ' \ + '--azure-keyvault-kms-key-vault-network-access=Public ' \ + '--azure-keyvault-kms-key-vault-resource-id={kv_resource_id} -o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_versionless), + self.check('securityProfile.kubernetesResourceObjectEncryptionProfile.infrastructureEncryption', + 'Enabled'), + ]) + + # enabling CMK triggers an async re-encryption of the cluster's Kubernetes secrets, + # wait for the cluster to settle before disabling CMK + self.cmd('aks wait --resource-group={resource_group} --name={name} --updated ' + '--interval 30 --timeout 1800', checks=[self.is_empty()]) + + # disable CMK, PMK is preserved + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--disable-azure-keyvault-kms -o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', False), + self.check('securityProfile.kubernetesResourceObjectEncryptionProfile.infrastructureEncryption', + 'Enabled'), + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westcentralus') + def test_aks_create_with_kms_pmk_and_cmk(self, resource_group, resource_group_location): + """Create a cluster with both PMK and CMK enabled at the same time.""" + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + 'kv_name': kv_name, + 'identity_name': identity_name, + 'ssh_key_value': self.generate_ssh_keys(), + }) + + # create user-assigned identity + identity_id = self._get_user_assigned_identity(resource_group) + identity_object_id = self._get_principal_id_of_user_assigned_identity(identity_id) + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} ' \ + '--enable-rbac-authorization=false --no-self-perms -o json' + self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]) + + # set access policy for test identity + test_identity_object_id = self._get_test_identity_object_id() + test_identity_access_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--key-permissions all --object-id ' + test_identity_object_id + self.cmd(test_identity_access_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]) + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + # PMK requires a versionless key ID, drop the trailing version segment + key_id_versionless = key['key']['kid'].rsplit('/', 1)[0] + assert key_id_versionless is not None + + kv_resource_id = self.cmd( + 'keyvault show --resource-group={resource_group} --name={kv_name} --query id -o tsv' + ).output.strip() + self.kwargs.update({ + 'key_id': key_id_versionless, + 'kv_resource_id': kv_resource_id, + }) + + # assign access policy for the cluster identity + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]) + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} ' \ + '--azure-keyvault-kms-key-vault-network-access=Public ' \ + '--azure-keyvault-kms-key-vault-resource-id={kv_resource_id} ' \ + '--kms-infrastructure-encryption=Enabled ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_versionless), + self.check('securityProfile.kubernetesResourceObjectEncryptionProfile.infrastructureEncryption', + 'Enabled'), + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westcentralus') + def test_aks_update_with_kms_pmk(self, resource_group, resource_group_location): + """Enable KMS infrastructure encryption (PMK) on an existing cluster.""" + aks_name = self.create_random_name('cliakstest', 16) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + 'ssh_key_value': self.generate_ssh_keys(), + }) + + # create cluster without infrastructure encryption + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.not_exists('securityProfile.kubernetesResourceObjectEncryptionProfile'), + ]) + + # update cluster to enable infrastructure encryption + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--kms-infrastructure-encryption Enabled -o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.kubernetesResourceObjectEncryptionProfile.infrastructureEncryption', + 'Enabled'), + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + @AllowLargeResponse() @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westus2') def test_aks_create_with_network_plugin_none(self, resource_group, resource_group_location): diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index c06b203f6cb..a7935a8aeb9 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -6024,6 +6024,76 @@ def test_get_azure_keyvault_kms_key_vault_resource_id(self): with self.assertRaises(ArgumentUsageError): ctx_9.get_azure_keyvault_kms_key_vault_resource_id() + # PMK enabled: key vault resource id is required + ctx_10 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Public", + "kms_infrastructure_encryption": "Enabled", + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_10.get_azure_keyvault_kms_key_vault_resource_id() + + # PMK enabled: key vault resource id is allowed with "Public" network access + ctx_11 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Public", + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id_1, + "kms_infrastructure_encryption": "Enabled", + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_11.get_azure_keyvault_kms_key_vault_resource_id(), key_vault_resource_id_1) + + def test_get_kms_infrastructure_encryption(self): + ctx_0 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({}), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertIsNone(ctx_0.get_kms_infrastructure_encryption()) + + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"kms_infrastructure_encryption": "Enabled"}), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_kms_infrastructure_encryption(), "Enabled") + + # backfill from the existing mc + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"kms_infrastructure_encryption": None}), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.kubernetes_resource_object_encryption_profile = ( + self.models.KubernetesResourceObjectEncryptionProfile(infrastructure_encryption="Enabled") + ) + mc = self.models.ManagedCluster(location="test_location", security_profile=security_profile) + ctx_2.attach_mc(mc) + self.assertEqual(ctx_2.get_kms_infrastructure_encryption(), "Enabled") + + # the value passed by the command takes precedence + ctx_3 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"kms_infrastructure_encryption": "Enabled"}), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + ctx_3.attach_mc(self.models.ManagedCluster(location="test_location")) + self.assertEqual(ctx_3.get_kms_infrastructure_encryption(), "Enabled") + def test_get_enable_image_cleaner(self): ctx_0 = AKSManagedClusterContext( self.cmd, @@ -9875,6 +9945,119 @@ def test_set_up_http_proxy_config(self): ) self.assertEqual(dec_mc_1, ground_truth_mc_1) + def test_set_up_azure_keyvault_kms(self): + key_id = "https://fakekeyvault.vault.azure.net/keys/fakekeyname" + key_vault_resource_id = ( + "/subscriptions/8ecadfc9-d1a3-4ea4-b844-0d9f87e4d7c8/resourceGroups/foo/" + "providers/Microsoft.KeyVault/vaults/foo" + ) + + # no kms related changes + dec_0 = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + {}, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_0 = self.models.ManagedCluster(location="test_location") + dec_0.context.attach_mc(mc_0) + dec_mc_0 = dec_0.set_up_azure_keyvault_kms(mc_0) + self.assertEqual(dec_mc_0, self.models.ManagedCluster(location="test_location")) + + # pmk only + dec_1 = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "kms_infrastructure_encryption": "Enabled", + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_1 = self.models.ManagedCluster(location="test_location") + dec_1.context.attach_mc(mc_1) + dec_mc_1 = dec_1.set_up_azure_keyvault_kms(mc_1) + ground_truth_mc_1 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + kubernetes_resource_object_encryption_profile=( + self.models.KubernetesResourceObjectEncryptionProfile(infrastructure_encryption="Enabled") + ), + ), + ) + self.assertEqual(dec_mc_1, ground_truth_mc_1) + + # unset pmk should not set the encryption profile + dec_2 = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "kms_infrastructure_encryption": None, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_2 = self.models.ManagedCluster(location="test_location") + dec_2.context.attach_mc(mc_2) + dec_mc_2 = dec_2.set_up_azure_keyvault_kms(mc_2) + self.assertEqual(dec_mc_2, self.models.ManagedCluster(location="test_location")) + + # pmk and cmk, the key vault resource id is set even with "Public" network access + dec_3 = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "kms_infrastructure_encryption": "Enabled", + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_id": key_id, + "azure_keyvault_kms_key_vault_network_access": "Public", + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_3 = self.models.ManagedCluster(location="test_location") + dec_3.context.attach_mc(mc_3) + dec_mc_3 = dec_3.set_up_azure_keyvault_kms(mc_3) + ground_truth_mc_3 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + kubernetes_resource_object_encryption_profile=( + self.models.KubernetesResourceObjectEncryptionProfile(infrastructure_encryption="Enabled") + ), + azure_key_vault_kms=self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id, + key_vault_network_access="Public", + key_vault_resource_id=key_vault_resource_id, + ), + ), + ) + self.assertEqual(dec_mc_3, ground_truth_mc_3) + + # cmk only with "Public" network access, the key vault resource id is not set + dec_4 = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_id": key_id, + "azure_keyvault_kms_key_vault_network_access": "Public", + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_4 = self.models.ManagedCluster(location="test_location") + dec_4.context.attach_mc(mc_4) + dec_mc_4 = dec_4.set_up_azure_keyvault_kms(mc_4) + ground_truth_mc_4 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + azure_key_vault_kms=self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id, + key_vault_network_access="Public", + ), + ), + ) + self.assertEqual(dec_mc_4, ground_truth_mc_4) + def test_set_up_image_cleaner(self): dec_0 = AKSManagedClusterCreateDecorator( self.cmd, @@ -12000,6 +12183,118 @@ def test_update_azure_keyvault_secrets_provider_addon_profile(self): ground_truth_azure_keyvault_secrets_provider_addon_profile_5, ) + def test_update_azure_keyvault_kms(self): + key_id = "https://fakekeyvault.vault.azure.net/keys/fakekeyname" + key_vault_resource_id = ( + "/subscriptions/8ecadfc9-d1a3-4ea4-b844-0d9f87e4d7c8/resourceGroups/foo/" + "providers/Microsoft.KeyVault/vaults/foo" + ) + + # no kms related changes + dec_0 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + {}, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_0 = self.models.ManagedCluster(location="test_location") + dec_0.context.attach_mc(mc_0) + dec_mc_0 = dec_0.update_azure_keyvault_kms(mc_0) + self.assertEqual(dec_mc_0, self.models.ManagedCluster(location="test_location")) + + # enable pmk only + dec_1 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "kms_infrastructure_encryption": "Enabled", + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_1 = self.models.ManagedCluster(location="test_location") + dec_1.context.attach_mc(mc_1) + dec_mc_1 = dec_1.update_azure_keyvault_kms(mc_1) + ground_truth_mc_1 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + kubernetes_resource_object_encryption_profile=( + self.models.KubernetesResourceObjectEncryptionProfile(infrastructure_encryption="Enabled") + ), + ), + ) + self.assertEqual(dec_mc_1, ground_truth_mc_1) + + # enable pmk and cmk, the key vault resource id is kept with "Public" network access + dec_2 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "kms_infrastructure_encryption": "Enabled", + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_id": key_id, + "azure_keyvault_kms_key_vault_network_access": "Public", + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_2 = self.models.ManagedCluster(location="test_location") + dec_2.context.attach_mc(mc_2) + dec_mc_2 = dec_2.update_azure_keyvault_kms(mc_2) + ground_truth_mc_2 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + kubernetes_resource_object_encryption_profile=( + self.models.KubernetesResourceObjectEncryptionProfile(infrastructure_encryption="Enabled") + ), + azure_key_vault_kms=self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id, + key_vault_network_access="Public", + key_vault_resource_id=key_vault_resource_id, + ), + ), + ) + self.assertEqual(dec_mc_2, ground_truth_mc_2) + + # disable cmk, the pmk profile is preserved + dec_3 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "disable_azure_keyvault_kms": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_3 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + kubernetes_resource_object_encryption_profile=( + self.models.KubernetesResourceObjectEncryptionProfile(infrastructure_encryption="Enabled") + ), + azure_key_vault_kms=self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id, + key_vault_network_access="Public", + ), + ), + ) + dec_3.context.attach_mc(mc_3) + dec_mc_3 = dec_3.update_azure_keyvault_kms(mc_3) + ground_truth_mc_3 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + kubernetes_resource_object_encryption_profile=( + self.models.KubernetesResourceObjectEncryptionProfile(infrastructure_encryption="Enabled") + ), + azure_key_vault_kms=self.models.AzureKeyVaultKms( + enabled=False, + key_id=key_id, + key_vault_network_access="Public", + ), + ), + ) + self.assertEqual(dec_mc_3, ground_truth_mc_3) + def test_update_image_cleaner(self): dec_0 = AKSManagedClusterUpdateDecorator( self.cmd, diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index f84f4e107ad..29faddf9999 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -634,8 +634,15 @@ def test_invalid_azure_keyvault_kms_key_id_without_https(self): validators.validate_azure_keyvault_kms_key_id(namespace) self.assertEqual(str(cm.exception), err) - def test_invalid_azure_keyvault_kms_key_id_without_key_version(self): - invalid_azure_keyvault_kms_key_id = "https://fakekeyvault.vault.azure.net/keys/fakekeyname" + def test_valid_azure_keyvault_kms_key_id_without_key_version(self): + # versionless key IDs are valid, they are required when KMS infrastructure encryption (PMK) is enabled + valid_azure_keyvault_kms_key_id = "https://fakekeyvault.vault.azure.net/keys/fakekeyname" + namespace = AzureKeyVaultKmsKeyIdNamespace(azure_keyvault_kms_key_id=valid_azure_keyvault_kms_key_id) + + validators.validate_azure_keyvault_kms_key_id(namespace) + + def test_invalid_azure_keyvault_kms_key_id_with_insufficient_segments(self): + invalid_azure_keyvault_kms_key_id = "https://fakekeyvault.vault.azure.net/keys" namespace = AzureKeyVaultKmsKeyIdNamespace(azure_keyvault_kms_key_id=invalid_azure_keyvault_kms_key_id) err = '--azure-keyvault-kms-key-id is not a valid Key Vault key ID. ' \ 'See https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name' @@ -720,6 +727,31 @@ def test_valid_azure_keyvault_kms_key_vault_resource_id(self): validators.validate_azure_keyvault_kms_key_vault_resource_id(namespace) + def test_valid_azure_keyvault_kms_managed_hsm_resource_id(self): + valid_azure_keyvault_kms_key_vault_resource_id = "/subscriptions/8ecadfc9-d1a3-4ea4-b844-0d9f87e4d7c8/resourceGroups/foo/providers/Microsoft.KeyVault/managedHSMs/foo" + namespace = AzureKeyVaultKmsKeyVaultResourceIdNamespace(azure_keyvault_kms_key_vault_resource_id=valid_azure_keyvault_kms_key_vault_resource_id) + + validators.validate_azure_keyvault_kms_key_vault_resource_id(namespace) + + def test_invalid_azure_keyvault_kms_key_vault_resource_id_wrong_provider(self): + invalid_azure_keyvault_kms_key_vault_resource_id = "/subscriptions/8ecadfc9-d1a3-4ea4-b844-0d9f87e4d7c8/resourceGroups/foo/providers/Microsoft.Storage/storageAccounts/foo" + namespace = AzureKeyVaultKmsKeyVaultResourceIdNamespace(azure_keyvault_kms_key_vault_resource_id=invalid_azure_keyvault_kms_key_vault_resource_id) + err = '--azure-keyvault-kms-key-vault-resource-id must reference a Microsoft.KeyVault resource.' + + with self.assertRaises(InvalidArgumentValueError) as cm: + validators.validate_azure_keyvault_kms_key_vault_resource_id(namespace) + self.assertEqual(str(cm.exception), err) + + def test_empty_azure_keyvault_kms_key_vault_resource_id(self): + namespace = AzureKeyVaultKmsKeyVaultResourceIdNamespace(azure_keyvault_kms_key_vault_resource_id="") + + validators.validate_azure_keyvault_kms_key_vault_resource_id(namespace) + + def test_none_azure_keyvault_kms_key_vault_resource_id(self): + namespace = AzureKeyVaultKmsKeyVaultResourceIdNamespace(azure_keyvault_kms_key_vault_resource_id=None) + + validators.validate_azure_keyvault_kms_key_vault_resource_id(namespace) + class TestValidateNodepoolName(unittest.TestCase): def test_invalid_nodepool_name_too_long(self): From 9203fd8e09f751934ae209862a91e4670337bff4 Mon Sep 17 00:00:00 2001 From: Bin Xia Date: Fri, 9 Oct 2026 08:38:08 +0000 Subject: [PATCH 2/3] [K2P] az aks create/update: Align KMS key vault resource id validation with aks-preview Remove the key vault network access constraints on --azure-keyvault-kms-key-vault-resource-id so that the behavior matches the aks-preview extension: * Drop the "resource id can not be empty if network access is Private" and "resource id must be empty if network access is Public" checks from _get_azure_keyvault_kms_key_vault_resource_id. KMS PMK requires a key vault resource id regardless of the network access, so these rules no longer hold. The "Private requires a resource id" guarantee is still enforced by _get_azure_keyvault_kms_key_vault_network_access. * Always assign key_vault_resource_id when CMK is enabled, instead of only when network access is Private or PMK is enabled, so that a resource id supplied with Public network access is no longer silently dropped. The update path keeps falling back to "" so that the field is still cleared when no resource id is given. Update the affected unit tests accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671828b6-e9d0-4bbe-95e3-576ec728c597 --- .../acs/managed_cluster_decorator.py | 53 +++---------------- .../latest/test_managed_cluster_decorator.py | 11 ++-- 2 files changed, 15 insertions(+), 49 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index 1d090724b82..0fa7a008657 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -33,7 +33,6 @@ CONST_PRIVATE_DNS_ZONE_NONE, CONST_PRIVATE_DNS_ZONE_SYSTEM, CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE, - CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC, CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED, AgentPoolDecoratorMode, DecoratorEarlyExitException, @@ -6092,32 +6091,6 @@ def _get_azure_keyvault_kms_key_vault_resource_id(self, enable_validation: bool '"--kms-infrastructure-encryption" is set to "Enabled".' ) - key_vault_network_access = self._get_azure_keyvault_kms_key_vault_network_access( - enable_validation=False) - if ( - key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE and - ( - azure_keyvault_kms_key_vault_resource_id is None or - azure_keyvault_kms_key_vault_resource_id == "" - ) - ): - raise ArgumentUsageError( - '"--azure-keyvault-kms-key-vault-resource-id" can not be empty if ' - '"--azure-keyvault-kms-key-vault-network-access" is "Private".' - ) - if ( - not is_pmk_enabled and - key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC and - ( - azure_keyvault_kms_key_vault_resource_id is not None and - azure_keyvault_kms_key_vault_resource_id != "" - ) - ): - raise ArgumentUsageError( - '"--azure-keyvault-kms-key-vault-resource-id" must be empty if ' - '"--azure-keyvault-kms-key-vault-network-access" is "Public".' - ) - return azure_keyvault_kms_key_vault_resource_id def get_azure_keyvault_kms_key_vault_resource_id(self) -> Union[str, None]: @@ -7301,13 +7274,9 @@ def set_up_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: ) key_vault_network_access = self.context.get_azure_keyvault_kms_key_vault_network_access() mc.security_profile.azure_key_vault_kms.key_vault_network_access = key_vault_network_access - if ( - key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE or - kms_infrastructure_encryption == CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED - ): - mc.security_profile.azure_key_vault_kms.key_vault_resource_id = ( - self.context.get_azure_keyvault_kms_key_vault_resource_id() - ) + mc.security_profile.azure_key_vault_kms.key_vault_resource_id = ( + self.context.get_azure_keyvault_kms_key_vault_resource_id() + ) return mc @@ -10496,17 +10465,11 @@ def update_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: azure_key_vault_kms_profile.key_vault_network_access = ( self.context.get_azure_keyvault_kms_key_vault_network_access() ) - # set key vault resource id, it is also required when infrastructure encryption (PMK) is enabled - if ( - azure_key_vault_kms_profile.key_vault_network_access == - CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE or - kms_infrastructure_encryption == CONST_KMS_INFRASTRUCTURE_ENCRYPTION_ENABLED - ): - azure_key_vault_kms_profile.key_vault_resource_id = ( - self.context.get_azure_keyvault_kms_key_vault_resource_id() - ) - else: - azure_key_vault_kms_profile.key_vault_resource_id = "" + # set key vault resource id, it is required when network access is "Private" and when + # infrastructure encryption (PMK) is enabled; fall back to "" so that it is cleared otherwise + azure_key_vault_kms_profile.key_vault_resource_id = ( + self.context.get_azure_keyvault_kms_key_vault_resource_id() or "" + ) if self.context.get_disable_azure_keyvault_kms(): # get kms profile diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index a7935a8aeb9..02a3af58251 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -5998,6 +5998,7 @@ def test_get_azure_keyvault_kms_key_vault_resource_id(self): with self.assertRaises(RequiredArgumentMissingError): ctx_7.get_azure_keyvault_kms_key_vault_resource_id() + # key vault resource id is allowed with "Public" network access ctx_8 = AKSManagedClusterContext( self.cmd, AKSManagedClusterParamDict({ @@ -6008,9 +6009,10 @@ def test_get_azure_keyvault_kms_key_vault_resource_id(self): self.models, decorator_mode=DecoratorMode.CREATE, ) - with self.assertRaises(ArgumentUsageError): - ctx_8.get_azure_keyvault_kms_key_vault_resource_id() + self.assertEqual(ctx_8.get_azure_keyvault_kms_key_vault_resource_id(), key_vault_resource_id_1) + # "Private" network access without a key vault resource id is rejected by + # get_azure_keyvault_kms_key_vault_network_access ctx_9 = AKSManagedClusterContext( self.cmd, AKSManagedClusterParamDict({ @@ -6021,8 +6023,9 @@ def test_get_azure_keyvault_kms_key_vault_resource_id(self): self.models, decorator_mode=DecoratorMode.CREATE, ) - with self.assertRaises(ArgumentUsageError): - ctx_9.get_azure_keyvault_kms_key_vault_resource_id() + self.assertEqual(ctx_9.get_azure_keyvault_kms_key_vault_resource_id(), "") + with self.assertRaises(RequiredArgumentMissingError): + ctx_9.get_azure_keyvault_kms_key_vault_network_access() # PMK enabled: key vault resource id is required ctx_10 = AKSManagedClusterContext( From 9745a9aed4b13e2d0c50dc5b1a23cfc91a991f5a Mon Sep 17 00:00:00 2001 From: Bin Xia Date: Tue, 13 Oct 2026 02:16:17 +0000 Subject: [PATCH 3/3] [K2P] az aks create/update: Reject KMS key IDs with extra path segments Address PR review feedback on --azure-keyvault-kms-key-id validation. Supporting versionless key IDs for KMS infrastructure encryption (PMK) relaxed the segment check from "len(segments) != 4" to "len(segments) < 3", which also accepted malformed key IDs with extra path components such as https://fakekeyvault.vault.azure.net/keys/name/version/extra. Only the versionless ({vault-url}/keys/{key-name}) and versioned ({vault-url}/keys/{key-name}/{key-version}) forms are valid, so accept exactly 3 or 4 segments and reject anything else in the CLI instead of forwarding it to the service. Add a regression test covering the extra path segment case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671828b6-e9d0-4bbe-95e3-576ec728c597 --- .../azure/cli/command_modules/acs/_validators.py | 4 ++-- .../acs/tests/latest/test_validators.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index a92c47d8925..67e4e9234b4 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -872,9 +872,9 @@ def validate_azure_keyvault_kms_key_id(namespace): # A versioned key ID has 4 segments ({vault-url}/keys/{key-name}/{key-version}), while a versionless # key ID has 3 segments ({vault-url}/keys/{key-name}). Versionless key IDs are required when KMS - # infrastructure encryption (platform-managed keys) is enabled. + # infrastructure encryption (platform-managed keys) is enabled. Any other number of segments is invalid. segments = key_id[len(https_prefix):].split("/") - if len(segments) < 3 or segments[1] != "keys": + if len(segments) not in (3, 4) or segments[1] != "keys": raise InvalidArgumentValueError(err_msg) diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 29faddf9999..6a71d976116 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -651,6 +651,17 @@ def test_invalid_azure_keyvault_kms_key_id_with_insufficient_segments(self): validators.validate_azure_keyvault_kms_key_id(namespace) self.assertEqual(str(cm.exception), err) + def test_invalid_azure_keyvault_kms_key_id_with_extra_segments(self): + invalid_azure_keyvault_kms_key_id = \ + "https://fakekeyvault.vault.azure.net/keys/fakekeyname/fakekeyversion/extra" + namespace = AzureKeyVaultKmsKeyIdNamespace(azure_keyvault_kms_key_id=invalid_azure_keyvault_kms_key_id) + err = '--azure-keyvault-kms-key-id is not a valid Key Vault key ID. ' \ + 'See https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name' + + with self.assertRaises(CLIError) as cm: + validators.validate_azure_keyvault_kms_key_id(namespace) + self.assertEqual(str(cm.exception), err) + def test_invalid_azure_keyvault_kms_key_id_with_wrong_object_type(self): invalid_azure_keyvault_kms_key_id = "https://fakekeyvault.vault.azure.net/secrets/fakesecretname/fakesecretversion" namespace = AzureKeyVaultKmsKeyIdNamespace(azure_keyvault_kms_key_id=invalid_azure_keyvault_kms_key_id)