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 키.
  • 설치·인증된 az CLI.
  • 볼트에 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는 대칭 키 래핑을 지원하지 않으므로 키는 허용된 작업에 wrapKeyunwrapKey가 있는 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 KeyUnwrap 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}"

다음을 확인하세요:

  • rbactrue.
  • purgetrue. falsenull이면 진행 전에 볼트에 퍼지 보호를 활성화하세요. 없으면 소프트 삭제 키가 보존 기간 동안 영구히 퍼지되어 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:

  1. Anthropic에 키 등록 — Claude Console에서 Settings > Encryption keys를 열고 Add key를 클릭하세요. 표시 이름을 입력하고 Azure Key Vault를 선택한 뒤 Continue를 클릭하세요. Vault URI, Key name, Tenant ID를 입력하고 Add를 클릭하세요. 키 세부 사항 단계에 조직 태그가 표시돼요. Add를 클릭하기 전에 만드는 단계에서 설명한 대로 키에 추가하세요.
  2. 키 검증Encryption keys 페이지에서 키 옆 Verify를 클릭하세요. 확인이 통과하면 Connected가 표시돼요. 실패하면 메시지가 이유를 알려줘요.
  3. 키를 워크스페이스에 연결 — Claude Console에서 Manage > Security로 이동해 사이드바 상단의 워크스페이스 선택기에서 워크스페이스를 선택하세요. Encryption key 아래에서 키를 선택하고 Save를 클릭한 뒤 확인하세요. 키 연결은 되돌릴 수 없어요. 이미 요청을 받는 워크스페이스의 경우 키가 적용되는 데 최대 하루 걸릴 수 있어요.

API:

  1. 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>"
    YAML
    
    client = 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>"
    }
    
  2. 키 검증 — 키에 대해 암호화·복호화 왕복을 트리거하세요. 이는 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의 이그레스 범위를 추가하세요.
  3. 키를 워크스페이스에 연결 — 키가 검증되면 그 워크스페이스로 요청을 보내기 전에 새 워크스페이스에 연결하세요. 이미 요청을 받는 워크스페이스의 경우 키가 적용되는 데 최대 하루 걸릴 수 있어요.

    ```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) 배포의 경우 동일한 단계가 azurermazuread 제공자로 매핑돼요.

더 알아보기 (Learn more)