Azure Key Vault로 CMEK 구성하기
Azure Key Vault로 CMEK 구성하기
이 가이드는 Azure Key Vault 키를 Anthropic 조직의 고객 관리 암호화 키(CMEK)로 구성하는 방법을 안내해 드릴게요.
CMEK 활성화는 영구적이에요. Key Vault 키가 삭제되거나 비활성화되면 Anthropic은 그 아래에서 암호화된 데이터를 복구할 수 없어요. 시작하기 전에 경고와 제한 사항을 검토하세요.
출처: 문서
본문
전제 조건
- RBAC 인증이 활성화된(
enableRbacAuthorization: true) Azure Key Vault와 공용 네트워크 접근 허용. Anthropic은 공용 데이터 플레인 엔드포인트로 볼트를 호출해요. 프라이빗 엔드포인트는 지원되지 않아요. - 볼트에 퍼지 보호 활성화(
enablePurgeProtection: true). 없으면 삭제된 키가 소프트 삭제 보존 기간 동안 영구히 퍼지되어 CMEK 보호 데이터가 되돌릴 수 없게 손실될 수 있어요. 퍼지 보호는 한 번 활성화하면 비활성화할 수 없어요. - 볼트에서 키 생성과 RBAC 역할 할당 권한.
- Entra 테넌트에서 서비스 principal 생성 권한(
Application Administrator,Cloud Application Administrator, 또는 동등한 커스텀 역할). - 조직의 Anthropic Admin API 키.
- 설치·인증된
azCLI. - 볼트에
AuditEvent로그 범주를 Log Analytics, 스토리지 계정, 또는 이벤트 허브로 라우팅하도록 Diagnostic Settings 구성. Azure Key Vault는 기본적으로 데이터 플레인 감사 로그(KeyWrap,KeyUnwrap,KeyGet같은 것)를 내보내지 않으므로, 이것이 없으면 Anthropic의 키 작업에 대한 감사 추적이 없어요.
Anthropic 앱 정보
Anthropic이 암호화 키를 사용하게 하려면 Anthropic 멀티테넌트 애플리케이션 ID와 표시 이름을 구성해야 해요. 그 값들은:
| 필드 | 값 |
|---|---|
| 멀티테넌트 앱 클라이언트 ID(US) | 8635ae1a-3e5d-44e8-a4ed-e0f614466f87 |
| 앱 표시 이름 | anthropic-cmek-client-us |
게시된 이 클라이언트 ID와 표시 이름만 사용하세요. 이메일, 채팅, 온보딩 채널로 제공된 식별자는 절대 신뢰하지 마세요.
암호화 키 설정
Anthropic 멀티테넌트 애플리케이션에 동의하기
이 단계는 테넌트에 Anthropic CMEK 클라이언트 애플리케이션용 서비스 principal을 만들어요. 애플리케이션은 Microsoft Graph 권한을 요청하지 않으며, Key Vault 데이터 플레인 접근을 위한 페더레이션 대상으로만 존재해요.
az ad sp create --id 8635ae1a-3e5d-44e8-a4ed-e0f614466f87
출력에서 id 필드를 기록하세요. 이것은 테넌트의 서비스 principal 객체 ID이며, RBAC 역할을 할당할 때 사용해요.
{
"appId": "8635ae1a-3e5d-44e8-a4ed-e0f614466f87",
"displayName": "anthropic-cmek-client-us",
"id": "<sp-object-id>"
}
서비스 principal이 테넌트에 이미 존재한다면(이전 시도나 다른 통합에서) az ad sp create는 "already exists" 오류로 종료돼요. 대신 객체 ID를 가져오세요:
az ad sp show --id 8635ae1a-3e5d-44e8-a4ed-e0f614466f87 --query id -o tsv
이 단계에는 Portal에 해당하는 것이 없어요. Azure CLI가 로컬에 없다면 Portal 상단 탐색 모음에서 Cloud Shell을 여세요. 명령이 성공한 후에는 Microsoft Entra ID > Enterprise applications에서 기본 애플리케이션 유형 필터를 지우고 anthropic-cmek-client-us를 검색해 서비스 principal의 객체 ID를 찾을 수 있어요.
볼트에 RSA 키 만들기
Azure Key Vault는 대칭 키 래핑을 지원하지 않으므로 키는 허용된 작업에 wrapKey와 unwrapKey가 있는 RSA(3072비트 이상)여야 해요.
--tags 옵션은 조직 태그 anthropic-org-<ORGANIZATION_UUID>(값 true)를 추가해요. 여기서 <ORGANIZATION_UUID>는 소문자 Anthropic 조직 ID예요. 태그는 Anthropic이 키를 검증하는 데 필요해요.
조직 ID 찾기: Claude Console의 Settings > Organization 아래 Organization ID 필드를 복사하거나, claude.ai의 Organization settings > Organization 아래에서 복사하거나, Organization Info 엔드포인트에서
id필드를 읽으세요.org_접두사가 없는 순수 UUID를 사용하세요.
az keyvault key create \
--vault-name <VAULT_NAME> \
--name <KEY_NAME> \
--kty RSA --size 3072 \
--ops wrapKey unwrapKey \
--tags anthropic-org-<ORGANIZATION_UUID>=true
HSM 지원 키에는 --kty RSA-HSM을 사용하세요(Premium-SKU 볼트 필요). 소프트웨어 보호 RSA 키도 이 통합에 허용돼요.
Portal에서 Key Vault를 열고 Keys를 선택한 뒤 Generate/Import를 선택하세요. 키 유형을 RSA로, 크기를 3072 이상으로 설정하세요. 키를 wrap·unwrap만으로 제한하려면 키 버전을 열고 Permitted operations까지 스크롤해 Wrap Key와 Unwrap Key를 제외한 모든 것을 해제하세요.
Create a key 페이지에서 Tags 아래 조직 태그도 추가하세요.
한 번에 여러 Anthropic 조직 간에 하나의 키를 공유하려면 조직마다 태그를 하나씩 추가하세요. 키 버전은 자신의 것을 포함해 최대 15개 태그를 담을 수 있어요.
이미 보유한 키에 태그를 추가하려면 Portal에서 키의 현재 버전을 열고 Tags 옆 링크를 선택해 태그를 추가한 뒤 Save를 클릭하세요. Azure CLI로는
az keyvault key set-attributes --vault-name <VAULT_NAME> --name <KEY_NAME> --tags anthropic-org-<ORGANIZATION_UUID>=true를 실행하세요. 그--tags옵션은 버전의 태그를 교체하므로 버전이 이미 가진 각 태그도name=value로--tags에 넣으세요. Managed HSM의 키에는--vault-name대신--hsm-name <HSM_NAME>을 사용하세요.
Anthropic 서비스 principal에 키 접근 권한 부여
첫 단계의 서비스 principal에 볼트 전체가 아니라 개별 키로 범위를 지정해 Key Vault Crypto User 역할을 할당하세요.
VAULT_ID=$(az keyvault show --name <your-vault-name> --query id -o tsv)
az role assignment create \
--role "Key Vault Crypto User" \
--assignee-object-id <sp-object-id> \
--assignee-principal-type ServicePrincipal \
--scope "${VAULT_ID}/keys/<your-key-name>"
기본 제공 Key Vault Crypto User 역할은 할당된 범위에서 키 암호화 작업(encrypt, decrypt, wrap, unwrap, sign, verify)과 키 읽기를 부여해요. 이전 단계에서 키에 설정한 --ops wrapKey unwrapKey 제한은 허용된 작업 중 어떤 것이 이 키에 대해 성공할 수 있는지를 더 좁혀서, 실질적으로 Anthropic은 wrap과 unwrap만 할 수 있어요.
Portal에서 (볼트가 아닌) 키를 열고 Access control (IAM) 탭을 선택한 뒤 Add > Add role assignment를 클릭하고 Key Vault Crypto User를 선택해 anthropic-cmek-client-us 서비스 principal에 할당하세요.
전용 볼트 대안: Microsoft는 앱당 전용 볼트에 역할을 볼트 범위로 할당하는 것을 권장해요. 이 Anthropic CMEK 키만 담는 볼트를 프로비저닝한다면 역할을 볼트 범위로 할당해도 되고 효과는 동일해요. 키가 공유 볼트에 있다면 개별 키로 범위를 지정하세요.
볼트 구성 확인
az keyvault show --name <your-vault-name> \
--query "{rbac:properties.enableRbacAuthorization, purge:properties.enablePurgeProtection, pub:properties.publicNetworkAccess, net:properties.networkAcls.defaultAction, ipRules:properties.networkAcls.ipRules, uri:properties.vaultUri, tenantId:properties.tenantId}"
다음을 확인하세요:
rbac가true.purge가true.false나null이면 진행 전에 볼트에 퍼지 보호를 활성화하세요. 없으면 소프트 삭제 키가 보존 기간 동안 영구히 퍼지되어 CMEK 보호 데이터를 복구할 수 없게 돼요.pub가"Enabled"."Disabled"면 Anthropic이 공용 데이터 플레인 엔드포인트로 볼트에 도달할 수 없고 검증이 실패해요.net가"Allow", 또는"Deny"라면ipRules에 Anthropic의 이그레스 범위가 포함(현재 목록은 Anthropic에 문의).uri가 키를 등록할 때 사용하는 볼트 URI.tenantId가 볼트를 관장하는 테넌트. 키를 등록할 때tenant_id로 이 값을 사용하고, 현재 활성 구독의 테넌트를 사용하지 마세요(크로스 테넌트 설정에서 둘은 다를 수 있어요).
Anthropic에 키 등록
키 등록 방법은 사용하는 제품에 따라 달라져요.
Claude Platform
키는 Claude Console이나 Admin API로 설정할 수 있으며 결과는 같아요.
Claude Console:
- Anthropic에 키 등록 — Claude Console에서 Settings > Encryption keys를 열고 Add key를 클릭하세요. 표시 이름을 입력하고 Azure Key Vault를 선택한 뒤 Continue를 클릭하세요. Vault URI, Key name, Tenant ID를 입력하고 Add를 클릭하세요. 키 세부 사항 단계에 조직 태그가 표시돼요. Add를 클릭하기 전에 만드는 단계에서 설명한 대로 키에 추가하세요.
- 키 검증 — Encryption keys 페이지에서 키 옆 Verify를 클릭하세요. 확인이 통과하면 Connected가 표시돼요. 실패하면 메시지가 이유를 알려줘요.
- 키를 워크스페이스에 연결 — Claude Console에서 Manage > Security로 이동해 사이드바 상단의 워크스페이스 선택기에서 워크스페이스를 선택하세요. Encryption key 아래에서 키를 선택하고 Save를 클릭한 뒤 확인하세요. 키 연결은 되돌릴 수 없어요. 이미 요청을 받는 워크스페이스의 경우 키가 적용되는 데 최대 하루 걸릴 수 있어요.
API:
-
Anthropic에 키 등록 — Admin API를 통해 외부 키 구성을 만드세요.
```bash cURL curl -sS "https://api.anthropic.com/v1/organizations/external_keys" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "display_name": " ", "geo": "us", "provider_config": { "type": "azure", "vault_uri": "https:// .vault.azure.net/", "key_name": " ", "tenant_id": " " } }' ``` ant beta:organization:external-keys create <<'YAML' display_name: "<friendly-name>" geo: us provider_config: type: azure vault_uri: "https://<your-vault-name>.vault.azure.net/" key_name: "<your-key-name>" tenant_id: "<your-tenant-id>" YAMLclient = anthropic.Anthropic() external_key = client.beta.organization.external_keys.create( display_name="<friendly-name>", geo="us", provider_config={ "type": "azure", "vault_uri": "https://<your-vault-name>.vault.azure.net/", "key_name": "<your-key-name>", "tenant_id": "<your-tenant-id>", }, ) print(f"id: {external_key.id}") print(f"display_name: {external_key.display_name}")const client = new Anthropic(); const externalKey = await client.beta.organization.externalKeys.create({ display_name: "<friendly-name>", geo: "us", provider_config: { type: "azure", vault_uri: "https://<your-vault-name>.vault.azure.net/", key_name: "<your-key-name>", tenant_id: "<your-tenant-id>" } }); console.log(`id: ${externalKey.id}`); console.log(`display_name: ${externalKey.display_name}`);using Anthropic.Models.Beta.Organization.ExternalKeys; AnthropicClient client = new(); var externalKey = await client.Beta.Organization.ExternalKeys.Create(new() { DisplayName = "<friendly-name>", Geo = Geo.Us, ProviderConfig = new BetaAzureExternalKeyConfigParam { VaultUri = "https://<your-vault-name>.vault.azure.net/", KeyName = "<your-key-name>", TenantID = "<your-tenant-id>" } }); Console.WriteLine($"id: {externalKey.ID}"); Console.WriteLine($"display_name: {externalKey.DisplayName}");client := anthropic.NewClient() externalKey, err := client.Beta.Organization.ExternalKeys.New(context.Background(), anthropic.BetaOrganizationExternalKeyNewParams{ DisplayName: anthropic.String("<friendly-name>"), Geo: anthropic.BetaOrganizationExternalKeyNewParamsGeoUs, ProviderConfig: anthropic.BetaOrganizationExternalKeyNewParamsProviderConfigUnion{ OfAzure: &anthropic.BetaAzureExternalKeyConfigParam{ VaultURI: "https://<your-vault-name>.vault.azure.net/", KeyName: "<your-key-name>", TenantID: "<your-tenant-id>", }, }, }) if err != nil { log.Fatal(err) } fmt.Printf("id: %s\n", externalKey.ID) fmt.Printf("display_name: %s\n", externalKey.DisplayName)import com.anthropic.models.beta.organization.externalkeys.BetaAzureExternalKeyConfigParam; import com.anthropic.models.beta.organization.externalkeys.ExternalKeyCreateParams; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); var params = ExternalKeyCreateParams.builder() .displayName("<friendly-name>") .geo(ExternalKeyCreateParams.Geo.US) .providerConfig(BetaAzureExternalKeyConfigParam.builder() .vaultUri("https://<your-vault-name>.vault.azure.net/") .keyName("<your-key-name>") .tenantId("<your-tenant-id>") .build()) .build(); var externalKey = client.beta().organization().externalKeys().create(params); IO.println("id: " + externalKey.id()); IO.println("display_name: " + externalKey.displayName().orElseThrow()); }use Anthropic\Beta\Organization\ExternalKeys\ExternalKeyCreateParams\Geo; // ... $client = new Client(); $externalKey = $client->beta->organization->externalKeys->create( displayName: '<friendly-name>', geo: Geo::US, providerConfig: [ 'type' => 'azure', 'vaultURI' => 'https://<your-vault-name>.vault.azure.net/', 'keyName' => '<your-key-name>', 'tenantID' => '<your-tenant-id>', ], ); echo "id: {$externalKey->id}\n"; echo "display_name: {$externalKey->displayName}\n";client = Anthropic::Client.new external_key = client.beta.organization.external_keys.create( display_name: "<friendly-name>", geo: :us, provider_config: { type: :azure, vault_uri: "https://<your-vault-name>.vault.azure.net/", key_name: "<your-key-name>", tenant_id: "<your-tenant-id>" } ) puts "id: #{external_key.id}" puts "display_name: #{external_key.display_name}"응답에 외부 키 ID가 포함돼요:
{ "type": "external_key", "id": "ekey_<id>", "display_name": "<friendly-name>" } -
키 검증 — 키에 대해 암호화·복호화 왕복을 트리거하세요. 이는 Anthropic이 테넌트에 인증하고 wrap·unwrap 작업을 수행할 수 있음을 확인해요.
```bash cURL curl -sS -X POST "https://api.anthropic.com/v1/organizations/external_keys/ekey_ /validate" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" ``` ant beta:organization:external-keys validate --external-key-id "ekey_<id>"client = anthropic.Anthropic() validation = client.beta.organization.external_keys.validate("ekey_<id>") print(f"status: {validation.status}") print(f"error: {validation.error}")const client = new Anthropic(); const validation = await client.beta.organization.externalKeys.validate("ekey_<id>"); console.log(`status: ${validation.status}`); console.log(`error: ${validation.error}`);AnthropicClient client = new(); var validation = await client.Beta.Organization.ExternalKeys.Validate("ekey_<id>"); Console.WriteLine($"status: {validation.Status.Raw()}"); Console.WriteLine($"error: {validation.Error}");client := anthropic.NewClient() validation, err := client.Beta.Organization.ExternalKeys.Validate(context.Background(), "ekey_<id>") if err != nil { log.Fatal(err) } fmt.Printf("status: %s\n", validation.Status) fmt.Printf("error: %s\n", validation.Error)AnthropicClient client = AnthropicOkHttpClient.fromEnv(); var validation = client.beta().organization().externalKeys().validate("ekey_<id>"); IO.println("status: " + validation.status().asString()); IO.println("error: " + validation.error().orElse(""));$client = new Client(); $validation = $client->beta->organization->externalKeys->validate( externalKeyID: 'ekey_<id>', ); echo "status: {$validation->status}\n"; echo "error: {$validation->error}\n";client = Anthropic::Client.new validation = client.beta.organization.external_keys.validate("ekey_<id>") puts "status: #{validation.status}" puts "error: #{validation.error}"성공 응답은 이렇게 생겼어요:
{ "type": "external_key_validation", "status": "success", "error": null }검증이 실패하면
error필드가 문제를 설명해요. 일반적인 원인은:- RBAC 전파 지연: 역할 할당은 적용되는 데 몇 분 걸릴 수 있어요. 잠시 기다렸다 다시 시도하세요.
- Anthropic을 차단하는 네트워크 ACL: 검증 단계에서 설명한 대로 공용 네트워크 접근과
ipRules를 확인하세요. - 워크로드 신원에 대한 조건부 액세스 정책: 테넌트에 서비스 principal을 대상으로 하는 조건부 액세스 정책이 있다면 Anthropic 서비스 principal을 제외하거나 정책의 명명된 위치에 Anthropic의 이그레스 범위를 추가하세요.
-
키를 워크스페이스에 연결 — 키가 검증되면 그 워크스페이스로 요청을 보내기 전에 새 워크스페이스에 연결하세요. 이미 요청을 받는 워크스페이스의 경우 키가 적용되는 데 최대 하루 걸릴 수 있어요.
```bash cURL curl -sS -X POST "https://api.anthropic.com/v1/organizations/workspaces/ " \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "external_key_id": "ekey_ " }' ``` ant beta:organization:workspaces update \ --workspace-id "<workspace-id>" \ --external-key-id "ekey_<id>"client = anthropic.Anthropic() workspace = client.beta.organization.workspaces.update( "<workspace-id>", external_key_id="ekey_<id>" ) print(f"id: {workspace.id}") print(f"external_key_id: {workspace.external_key_id}")const client = new Anthropic(); const workspace = await client.beta.organization.workspaces.update("<workspace-id>", { external_key_id: "ekey_<id>" }); console.log(`id: ${workspace.id}`); console.log(`external_key_id: ${workspace.external_key_id}`);AnthropicClient client = new(); var workspace = await client.Beta.Organization.Workspaces.Update("<workspace-id>", new() { ExternalKeyID = "ekey_<id>" }); Console.WriteLine($"id: {workspace.ID}"); Console.WriteLine($"external_key_id: {workspace.ExternalKeyID}");client := anthropic.NewClient() workspace, err := client.Beta.Organization.Workspaces.Update( context.Background(), "<workspace-id>", anthropic.BetaOrganizationWorkspaceUpdateParams{ ExternalKeyID: anthropic.String("ekey_<id>"), }, ) if err != nil { log.Fatal(err) } fmt.Printf("id: %s\n", workspace.ID) fmt.Printf("external_key_id: %s\n", workspace.ExternalKeyID)import com.anthropic.models.beta.organization.workspaces.WorkspaceUpdateParams; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); var params = WorkspaceUpdateParams.builder() .externalKeyId("ekey_<id>") .build(); var workspace = client.beta().organization().workspaces().update("<workspace-id>", params); IO.println("id: " + workspace.id()); IO.println("external_key_id: " + workspace.externalKeyId().orElseThrow()); }$client = new Client(); $workspace = $client->beta->organization->workspaces->update( workspaceID: '<workspace-id>', externalKeyID: 'ekey_<id>', ); echo "id: {$workspace->id}\n"; echo "external_key_id: {$workspace->externalKeyID}\n";client = Anthropic::Client.new workspace = client.beta.organization.workspaces.update( "<workspace-id>", external_key_id: "ekey_<id>" ) puts "id: #{workspace.id}" puts "external_key_id: #{workspace.external_key_id}"
Claude Enterprise
claude.ai > Organization settings > Data and privacy에서 Encryption keys를 연 뒤 Add key를 클릭하세요. Azure를 선택하고 검증 단계의 볼트 URI, 키 이름, 테넌트 ID를 입력한 뒤 Continue를 클릭하세요. Anthropic이 암호화·복호화 왕복으로 키를 검증해요. 검증된 것으로 표시되면 그 시점부터 조직이 CMEK로 보호돼요.
Claude Enterprise에서 CMEK는 전체 조직에 적용되므로 별도의 워크스페이스 연결 단계가 없고, 조직은 키를 하나만 가질 수 있어요.
Terraform
IaC(infrastructure-as-code) 배포의 경우 동일한 단계가 azurerm과 azuread 제공자로 매핑돼요.