From b421eae005397d24c613650e16a544806929ab05 Mon Sep 17 00:00:00 2001 From: Andrew Au <3410332+cshung@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:27:32 +0000 Subject: [PATCH] [ACR] `az acr create`, `az acr encryption rotate-key`: Add Managed HSM key guidance Clarify that customer-managed encryption accepts Azure Key Vault and Managed HSM key URIs, add creation and rotation examples, and protect URI pass-through behavior with focused tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/cli/command_modules/acr/_help.py | 16 +++- .../azure/cli/command_modules/acr/_params.py | 4 +- .../acr/tests/latest/test_acr_encryption.py | 86 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_encryption.py diff --git a/src/azure-cli/azure/cli/command_modules/acr/_help.py b/src/azure-cli/azure/cli/command_modules/acr/_help.py index 6a0eb168315..898081bf5e5 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acr/_help.py @@ -191,6 +191,9 @@ - name: Create a managed container registry with writable cache repositories enabled. text: > az acr create -n myregistry -g MyResourceGroup --sku Premium --writable-cache-repos enabled + - name: Create a Premium registry encrypted with a customer-managed key in Managed HSM. + text: > + az acr create --name myregistry --resource-group MyResourceGroup --sku Premium --identity myidentity --key-encryption-key https://myhsm.managedhsm.azure.net/keys/mykey """ helps['acr credential'] = """ @@ -1856,14 +1859,21 @@ # region encryption helps['acr encryption'] = """ type: group -short-summary: Manage container registry encryption +short-summary: Manage customer-managed encryption for a container registry. long-summary: For more information, see http://aka.ms/acr/cmk """ helps['acr encryption rotate-key'] = """ type: command -short-summary: Rotate (update) the container registry's encryption key -long-summary: For more information, see http://aka.ms/acr/cmk +short-summary: Rotate the customer-managed encryption key for a container registry. +long-summary: The registry must already have customer-managed key encryption enabled. The managed identity must have permission to use the new Azure Key Vault or Managed HSM key. For more information, see http://aka.ms/acr/cmk +examples: + - name: Rotate to a versionless Managed HSM key to enable automatic key rotation. + text: > + az acr encryption rotate-key --name myregistry --resource-group MyResourceGroup --identity myidentity --key-encryption-key https://myhsm.managedhsm.azure.net/keys/mykey + - name: Rotate manually to a specific Azure Key Vault key version. + text: > + az acr encryption rotate-key --name myregistry --resource-group MyResourceGroup --identity myidentity --key-encryption-key https://myvault.vault.azure.net/keys/mykey/00000000000000000000000000000000 """ helps['acr encryption show'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/acr/_params.py b/src/azure-cli/azure/cli/command_modules/acr/_params.py index b21117b5ee5..277c94d52e5 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acr/_params.py @@ -126,7 +126,7 @@ def load_arguments(self, _): # pylint: disable=too-many-statements with self.argument_context('acr create', arg_group="Customer managed key") as c: c.argument('identity', help="Use assigned managed identity resource id or name if in the same resource group") - c.argument('key_encryption_key', help="Key vault key uri. To enable automated rotation, provide a version-less key uri. For manual rotation, provide a versioned key uri.") + c.argument('key_encryption_key', help="Azure Key Vault or Managed HSM key URI. To enable automated rotation, provide a versionless key URI. For manual rotation, provide a versioned key URI.") with self.argument_context('acr create') as c: c.argument('allow_metadata_search', arg_type=get_three_state_flag(), is_preview=True, help="Enable or disable the metadata-search feature for the registry. If not specified, this is set to disabled by default.") @@ -555,7 +555,7 @@ def load_arguments(self, _): # pylint: disable=too-many-statements c.argument('identities', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity") with self.argument_context('acr encryption') as c: - c.argument('key_encryption_key', help="Key vault key uri. To enable automated rotation, provide a version-less key uri. For manual rotation, provide a versioned key uri.") + c.argument('key_encryption_key', help="Azure Key Vault or Managed HSM key URI. To enable automated rotation, provide a versionless key URI. For manual rotation, provide a versioned key URI.") c.argument('identity', help="client id of managed identity, resource name or id of user assigned identity. Use '[system]' to refer to the system assigned identity") with self.argument_context('acr connected-registry') as c: diff --git a/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_encryption.py b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_encryption.py new file mode 100644 index 00000000000..72ba6b9cb2b --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_encryption.py @@ -0,0 +1,86 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest +from unittest import mock + +from azure.cli.command_modules.acr import custom + + +class AcrEncryptionTests(unittest.TestCase): + + @mock.patch('azure.cli.core.commands.client_factory.get_subscription_id', return_value='subscription-id') + @mock.patch.object(custom, '_ensure_identity_resource_id', return_value='identity-resource-id') + @mock.patch.object(custom, 'resolve_identity_client_id', return_value='identity-client-id') + def test_configure_cmk_preserves_managed_hsm_key_uri( + self, resolve_identity_client_id, ensure_identity_resource_id, get_subscription_id): + key_uri = 'https://myhsm.managedhsm.azure.net/keys/mykey' + key_vault_properties = mock.Mock() + encryption_property = mock.Mock() + resource_identity_type = mock.Mock(user_assigned='UserAssigned') + identity_properties = mock.Mock() + cmd = mock.Mock() + cmd.get_models.side_effect = [ + (key_vault_properties, encryption_property), + (resource_identity_type, identity_properties), + ] + registry = mock.Mock() + + custom._configure_cmk( + cmd, + registry, + 'resource-group', + 'myidentity', + key_uri) + + get_subscription_id.assert_called_once_with(cmd.cli_ctx) + ensure_identity_resource_id.assert_called_once_with( + subscription_id='subscription-id', + resource_group='resource-group', + resource='myidentity') + resolve_identity_client_id.assert_called_once_with( + cmd.cli_ctx, + 'identity-resource-id') + key_vault_properties.assert_called_once_with( + key_identifier=key_uri, + identity='identity-client-id') + encryption_property.assert_called_once_with( + status='enabled', + key_vault_properties=key_vault_properties.return_value) + identity_properties.assert_called_once_with( + type='UserAssigned', + user_assigned_identities={'identity-resource-id': {}}) + self.assertIs(registry.encryption, encryption_property.return_value) + self.assertIs(registry.identity, identity_properties.return_value) + + @mock.patch.object(custom, 'get_registry_by_name') + def test_rotate_key_preserves_managed_hsm_key_uri(self, get_registry_by_name): + registry = mock.Mock() + registry.encryption = mock.Mock() + registry.encryption.key_vault_properties = mock.Mock() + get_registry_by_name.return_value = registry, 'resource-group' + cmd = mock.Mock() + client = mock.Mock() + + for key_uri in [ + 'https://myhsm.managedhsm.azure.net/keys/mykey', + 'https://myhsm.managedhsm.azure.net/keys/mykey/00000000000000000000000000000000']: + with self.subTest(key_uri=key_uri): + custom.rotate_key( + cmd, + client, + 'myregistry', + key_encryption_key=key_uri, + resource_group_name='resource-group') + + self.assertEqual( + registry.encryption.key_vault_properties.key_identifier, + key_uri) + + self.assertEqual(client.begin_update.call_count, 2) + client.begin_update.assert_called_with( + 'resource-group', + 'myregistry', + registry)