From 791c2909e50ee284ac7ba089d51f8cc60d360209 Mon Sep 17 00:00:00 2001 From: Ben Bardin Date: Fri, 27 Jan 2023 10:12:16 -0500 Subject: [PATCH] pkg/cloud: Expand Azure backups functionality Add KMS support for Azure KeyVaults. Add App Registration authentication for Azure Storage and Azure KeyVault support. Note that ASSUME ROLE is not offered here, as Azure does not support it at this time. It will be available "soon." Release note (enterprise change): Add KMS support for Azure KeyVaults. Add App Registration authentication for Azure Storage and Azure KeyVault support. Fixes: https://github.com/cockroachdb/cockroach/issues/86903 --- DEPS.bzl | 32 +++- build/bazelutil/distdir_files.bzl | 6 +- .../cockroach/nightlies/cloud_unit_tests.sh | 2 +- .../nightlies/cloud_unit_tests_impl.sh | 8 +- go.mod | 8 +- go.sum | 12 +- pkg/ccl/backupccl/backup_cloud_test.go | 56 +++++- pkg/ccl/backupccl/backup_test.go | 52 +++-- pkg/ccl/backupccl/utils_test.go | 7 +- .../testdata/create_drop_external_connection | 2 +- pkg/cloud/azure/BUILD.bazel | 10 +- pkg/cloud/azure/azure_kms.go | 180 ++++++++++++++++++ pkg/cloud/azure/azure_kms_connection.go | 43 +++++ pkg/cloud/azure/azure_kms_test.go | 96 ++++++++++ pkg/cloud/azure/azure_storage.go | 94 +++++++-- pkg/cloud/azure/azure_storage_test.go | 75 +++++++- pkg/cloud/cloudpb/external_storage.proto | 4 + .../externalconn/connectionpb/connection.go | 2 +- .../connectionpb/connection.proto | 1 + pkg/cloud/gcp/gcp_kms.go | 2 +- 20 files changed, 625 insertions(+), 67 deletions(-) create mode 100644 pkg/cloud/azure/azure_kms.go create mode 100644 pkg/cloud/azure/azure_kms_connection.go create mode 100644 pkg/cloud/azure/azure_kms_test.go diff --git a/DEPS.bzl b/DEPS.bzl index 5763daa75831..07b03a66bf2c 100644 --- a/DEPS.bzl +++ b/DEPS.bzl @@ -604,6 +604,26 @@ def go_deps(): "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/internal/com_github_azure_azure_sdk_for_go_sdk_internal-v1.1.1.zip", ], ) + go_repository( + name = "com_github_azure_azure_sdk_for_go_sdk_keyvault_azkeys", + build_file_proto_mode = "disable_global", + importpath = "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys", + sha256 = "8f29c576ee07c3b8f7ca821927ceec97573479c882285ca71c2a13d92d4b9927", + strip_prefix = "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys@v0.9.0", + urls = [ + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys/com_github_azure_azure_sdk_for_go_sdk_keyvault_azkeys-v0.9.0.zip", + ], + ) + go_repository( + name = "com_github_azure_azure_sdk_for_go_sdk_keyvault_internal", + build_file_proto_mode = "disable_global", + importpath = "github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal", + sha256 = "a3a79250f250d01abd0b402649ce9baf7eeebbbad186dc602eb011692fdbec24", + strip_prefix = "github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal@v0.7.0", + urls = [ + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal/com_github_azure_azure_sdk_for_go_sdk_keyvault_internal-v0.7.0.zip", + ], + ) go_repository( name = "com_github_azure_azure_sdk_for_go_sdk_storage_azblob", build_file_proto_mode = "disable_global", @@ -3550,10 +3570,10 @@ def go_deps(): name = "com_github_golang_jwt_jwt_v4", build_file_proto_mode = "disable_global", importpath = "github.com/golang-jwt/jwt/v4", - sha256 = "51d00fb75dfa0f4ff7b5597d842c43f53573eca8c58f8bc89b229b9b4853a4ab", - strip_prefix = "github.com/golang-jwt/jwt/v4@v4.0.0", + sha256 = "bea2e7c045b07f50b60211bee94b62c442322ded7fa893e3fda49dcdce0e2908", + strip_prefix = "github.com/golang-jwt/jwt/v4@v4.2.0", urls = [ - "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-jwt/jwt/v4/com_github_golang_jwt_jwt_v4-v4.0.0.zip", + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-jwt/jwt/v4/com_github_golang_jwt_jwt_v4-v4.2.0.zip", ], ) go_repository( @@ -6127,10 +6147,10 @@ def go_deps(): name = "com_github_montanaflynn_stats", build_file_proto_mode = "disable_global", importpath = "github.com/montanaflynn/stats", - sha256 = "25069347054502d9ab97531f0757b916124ba9966ead38f36f98812b37a6acd9", - strip_prefix = "github.com/montanaflynn/stats@v0.6.3", + sha256 = "fac4308cc66d568256e7aafe694ae58603ddeb9bb39965caa550dbe3fbd77ddc", + strip_prefix = "github.com/montanaflynn/stats@v0.6.6", urls = [ - "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/montanaflynn/stats/com_github_montanaflynn_stats-v0.6.3.zip", + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/montanaflynn/stats/com_github_montanaflynn_stats-v0.6.6.zip", ], ) go_repository( diff --git a/build/bazelutil/distdir_files.bzl b/build/bazelutil/distdir_files.bzl index d8ac51caeb57..48efbe65baa4 100644 --- a/build/bazelutil/distdir_files.bzl +++ b/build/bazelutil/distdir_files.bzl @@ -21,6 +21,8 @@ DISTDIR_FILES = { "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/azcore/com_github_azure_azure_sdk_for_go_sdk_azcore-v1.3.0.zip": "cf80995c85451a7990c4d68dfbfd7de89536d319df9502ba9dfd38eb84501810", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/azidentity/com_github_azure_azure_sdk_for_go_sdk_azidentity-v1.1.0.zip": "27947f13cb64475fd59e5d9f8b9c042b3d1e8603f49c54fc42820001c33d5f78", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/internal/com_github_azure_azure_sdk_for_go_sdk_internal-v1.1.1.zip": "10f2a543b9e000a988722c8210d30d377c2306b042e5de1bfea4b3ec730d0319", + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys/com_github_azure_azure_sdk_for_go_sdk_keyvault_azkeys-v0.9.0.zip": "8f29c576ee07c3b8f7ca821927ceec97573479c882285ca71c2a13d92d4b9927", + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal/com_github_azure_azure_sdk_for_go_sdk_keyvault_internal-v0.7.0.zip": "a3a79250f250d01abd0b402649ce9baf7eeebbbad186dc602eb011692fdbec24", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/com_github_azure_azure_sdk_for_go_sdk_storage_azblob-v0.6.1.zip": "c2539d189b22bdb6eb67c4682ded4e070d6cf0f52c8bd6899f7eb1408045783f", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/azure-storage-blob-go/com_github_azure_azure_storage_blob_go-v0.8.0.zip": "3b02b720c25bbb6cdaf77f45a29a21e374e087081dedfeac2700aed6147b4b35", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/Azure/go-ansiterm/com_github_azure_go_ansiterm-v0.0.0-20210617225240-d185dfc1b5a1.zip": "631ff4b167a4360e10911e475933ecb3bd327c58974c17877d0d4cf6fbef6c96", @@ -392,7 +394,7 @@ DISTDIR_FILES = { "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-commonmark/mdurl/com_github_golang_commonmark_mdurl-v0.0.0-20180910110917-8d018c6567d6.zip": "b7386081771d71200f34972369cf4bcea3eb1dc6dcb1cf3906b692c586c22557", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-commonmark/puny/com_github_golang_commonmark_puny-v0.0.0-20180910110745-050be392d8b8.zip": "1296aef61f597df70d851197dacc258fc8f3e80d0a7180e7470bd1a2f2dcbd08", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-jwt/jwt/com_github_golang_jwt_jwt-v3.2.2+incompatible.zip": "28d6dd7cc77d0a960699196e9c2170731f65d624d675888d2ababe7e8a422955", - "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-jwt/jwt/v4/com_github_golang_jwt_jwt_v4-v4.0.0.zip": "51d00fb75dfa0f4ff7b5597d842c43f53573eca8c58f8bc89b229b9b4853a4ab", + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-jwt/jwt/v4/com_github_golang_jwt_jwt_v4-v4.2.0.zip": "bea2e7c045b07f50b60211bee94b62c442322ded7fa893e3fda49dcdce0e2908", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-sql/civil/com_github_golang_sql_civil-v0.0.0-20190719163853-cb61b32ac6fe.zip": "22fcd1e01cabf6ec75c6b6c8e443de029611c9dd5cc4673818d52dac465ac688", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang-sql/sqlexp/com_github_golang_sql_sqlexp-v0.0.0-20170517235910-f1bb20e5a188.zip": "edcfe6a0d8da7796b82de5d44c70d6d0e1b7a433d5b267c2895c30c4ce445342", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/golang/freetype/com_github_golang_freetype-v0.0.0-20170609003504-e2365dfdc4a0.zip": "cdcb9e6a14933dcbf167b44dcd5083fc6a2e52c4fae8fb79747c691efeb7d84e", @@ -638,7 +640,7 @@ DISTDIR_FILES = { "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/modern-go/concurrent/com_github_modern_go_concurrent-v0.0.0-20180306012644-bacd9c7ef1dd.zip": "91ef49599bec459869d94ff3dec128871ab66bd2dfa61041f1e1169f9b4a8073", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/modern-go/reflect2/com_github_modern_go_reflect2-v1.0.2.zip": "f46f41409c2e74293f82cfe6c70b5d582bff8ada0106a7d3ff5706520c50c21c", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/modocache/gover/com_github_modocache_gover-v0.0.0-20171022184752-b58185e213c5.zip": "4a96d0a90331d92074e902cf2772d22c1a067438bc627713b80bba4d5509e3d3", - "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/montanaflynn/stats/com_github_montanaflynn_stats-v0.6.3.zip": "25069347054502d9ab97531f0757b916124ba9966ead38f36f98812b37a6acd9", + "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/montanaflynn/stats/com_github_montanaflynn_stats-v0.6.6.zip": "fac4308cc66d568256e7aafe694ae58603ddeb9bb39965caa550dbe3fbd77ddc", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/morikuni/aec/com_github_morikuni_aec-v1.0.0.zip": "c14eeff6945b854edd8b91a83ac760fbd95068f33dc17d102c18f2e8e86bcced", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/mostynb/go-grpc-compression/com_github_mostynb_go_grpc_compression-v1.1.12.zip": "a260a65018fbde39f8b3b996bbb1b6f76f1ea5db26f8892842b249ba7cd5f318", "https://storage.googleapis.com/cockroach-godeps/gomod/github.com/moul/http2curl/com_github_moul_http2curl-v1.0.0.zip": "3600be3621038727f856bf7403d3ef0ffcc2a6729716bab67b592dcd19b3fee2", diff --git a/build/teamcity/cockroach/nightlies/cloud_unit_tests.sh b/build/teamcity/cockroach/nightlies/cloud_unit_tests.sh index 5e4a18c91f55..1c1052c859bf 100755 --- a/build/teamcity/cockroach/nightlies/cloud_unit_tests.sh +++ b/build/teamcity/cockroach/nightlies/cloud_unit_tests.sh @@ -8,6 +8,6 @@ source "$dir/teamcity-support.sh" # For $root source "$dir/teamcity-bazel-support.sh" # For run_bazel tc_start_block "Run cloud unit tests" -BAZEL_SUPPORT_EXTRA_DOCKER_ARGS="-e GITHUB_API_TOKEN -e GITHUB_REPO -e TC_BUILDTYPE_ID -e TC_BUILD_BRANCH -e TC_BUILD_ID -e TC_SERVER_URL -e GOOGLE_EPHEMERAL_CREDENTIALS -e GOOGLE_KMS_KEY_NAME -e GOOGLE_LIMITED_KEY_ID -e ASSUME_SERVICE_ACCOUNT -e GOOGLE_LIMITED_BUCKET -e ASSUME_SERVICE_ACCOUNT_CHAIN -e AWS_DEFAULT_REGION -e AWS_SHARED_CREDENTIALS_FILE -e AWS_CONFIG_FILE -e AWS_S3_BUCKET -e AWS_ASSUME_ROLE -e AWS_ROLE_ARN_CHAIN -e AWS_KMS_KEY_ARN -e AWS_S3_ENDPOINT -e AWS_KMS_ENDPOINT -e AWS_KMS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AZURE_ACCOUNT_KEY -e AZURE_ACCOUNT_NAME -e AZURE_CONTAINER" \ +BAZEL_SUPPORT_EXTRA_DOCKER_ARGS="-e GITHUB_API_TOKEN -e GITHUB_REPO -e TC_BUILDTYPE_ID -e TC_BUILD_BRANCH -e TC_BUILD_ID -e TC_SERVER_URL -e GOOGLE_EPHEMERAL_CREDENTIALS -e GOOGLE_KMS_KEY_NAME -e GOOGLE_LIMITED_KEY_ID -e ASSUME_SERVICE_ACCOUNT -e GOOGLE_LIMITED_BUCKET -e ASSUME_SERVICE_ACCOUNT_CHAIN -e AWS_DEFAULT_REGION -e AWS_SHARED_CREDENTIALS_FILE -e AWS_CONFIG_FILE -e AWS_S3_BUCKET -e AWS_ASSUME_ROLE -e AWS_ROLE_ARN_CHAIN -e AWS_KMS_KEY_ARN -e AWS_S3_ENDPOINT -e AWS_KMS_ENDPOINT -e AWS_KMS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AZURE_ACCOUNT_KEY -e AZURE_ACCOUNT_NAME -e AZURE_CONTAINER -e AZURE_CLIENT_ID -e AZURE_CLIENT_SECRET -e AZURE_TENANT_ID -e AZURE_VAULT_NAME -e AZURE_KMS_KEY_NAME -e AZURE_KMS_KEY_VERSION" \ run_bazel build/teamcity/cockroach/nightlies/cloud_unit_tests_impl.sh "$@" tc_end_block "Run cloud unit tests" diff --git a/build/teamcity/cockroach/nightlies/cloud_unit_tests_impl.sh b/build/teamcity/cockroach/nightlies/cloud_unit_tests_impl.sh index 806b414b592f..56272ee63481 100755 --- a/build/teamcity/cockroach/nightlies/cloud_unit_tests_impl.sh +++ b/build/teamcity/cockroach/nightlies/cloud_unit_tests_impl.sh @@ -46,7 +46,13 @@ bazel_test_env=(--test_env=GO_TEST_WRAP_TESTV=1 \ --test_env=AWS_CONFIG_FILE="$AWS_CONFIG_FILE" \ --test_env=AZURE_ACCOUNT_NAME="$AZURE_ACCOUNT_NAME" \ --test_env=AZURE_ACCOUNT_KEY="$AZURE_ACCOUNT_KEY" \ - --test_env=AZURE_CONTAINER="$AZURE_CONTAINER") + --test_env=AZURE_CONTAINER="$AZURE_CONTAINER" \ + --test_env=AZURE_CLIENT_ID="$AZURE_CLIENT_ID" \ + --test_env=AZURE_CLIENT_SECRET="$AZURE_CLIENT_SECRET" \ + --test_env=AZURE_TENANT_ID="$AZURE_TENANT_ID" \ + --test_env=AZURE_VAULT_NAME="$AZURE_VAULT_NAME" \ + --test_env=AZURE_KMS_KEY_NAME="$AZURE_KMS_KEY_NAME" \ + --test_env=AZURE_KMS_KEY_VERSION="$AZURE_KMS_KEY_VERSION") exit_status=0 $BAZEL_BIN/pkg/cmd/bazci/bazci_/bazci -- test --config=ci \ diff --git a/go.mod b/go.mod index 55a7d0d49559..5a8eddf2a345 100644 --- a/go.mod +++ b/go.mod @@ -79,6 +79,8 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 github.com/BurntSushi/toml v0.4.1 github.com/Masterminds/semver/v3 v3.1.1 @@ -177,7 +179,7 @@ require ( github.com/mibk/dupl v1.0.0 github.com/mitchellh/reflectwalk v1.0.0 github.com/mmatczuk/go_generics v0.0.0-20181212143635-0aaa050f9bab - github.com/montanaflynn/stats v0.6.3 + github.com/montanaflynn/stats v0.6.6 github.com/mozillazg/go-slugify v0.2.0 github.com/nightlyone/lockfile v1.0.0 github.com/olekukonko/tablewriter v0.0.5-0.20200416053754-163badb3bac6 @@ -229,6 +231,7 @@ require ( cloud.google.com/go/compute v1.6.1 // indirect cloud.google.com/go/iam v0.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect @@ -236,6 +239,7 @@ require ( github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect github.com/Masterminds/goutils v1.1.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect @@ -283,7 +287,7 @@ require ( github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.0.0 // indirect + github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/googleapis/gax-go/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 1f5f69b057c6..e4b1021a1b05 100644 --- a/go.sum +++ b/go.sum @@ -88,9 +88,14 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 h1:VuHAcMq8pU1IWNT/m5yRaGqbK github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0/go.mod h1:tZoQYdDZNOiIjdSn0dVWVfl0NEPGOJqVLzSrcFk4Is0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 h1:TOFrNxfjslms5nLLIMjW7N0+zSALX4KiGsptmpb16AA= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0/go.mod h1:EAyXOW1F6BTJPiK2pDvmnvxOHPxoTYWoqBeIlql+QhI= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 h1:Lg6BW0VPmCwcMlvOviL3ruHFO+H9tZNqscK0AeuFjGM= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 h1:YvQv9Mz6T8oR5ypQOL6erY0Z5t71ak1uHV4QFokCOZk= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1/go.mod h1:c6WvOhtmjNUWbLfOG1qxM/q0SPvQNSVJvolm+C52dIU= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= @@ -152,6 +157,7 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE= +github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -1017,8 +1023,9 @@ github.com/golang-commonmark/puny v0.0.0-20180910110745-050be392d8b8/go.mod h1:/ github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -1699,8 +1706,9 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/montanaflynn/stats v0.6.3 h1:F8446DrvIF5V5smZfZ8K9nrmmix0AFgevPdLruGOmzk= github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mostynb/go-grpc-compression v1.1.4/go.mod h1:rUYnd4NcUyZqHgjjISqjBMa6qTxMFjFu5+fHLI3k3Ho= diff --git a/pkg/ccl/backupccl/backup_cloud_test.go b/pkg/ccl/backupccl/backup_cloud_test.go index fea4b2b69869..65ba506604a9 100644 --- a/pkg/ccl/backupccl/backup_cloud_test.go +++ b/pkg/ccl/backupccl/backup_cloud_test.go @@ -57,7 +57,7 @@ func TestCloudBackupRestoreS3(t *testing.T) { defer cleanupFn() prefix := fmt.Sprintf("TestBackupRestoreS3-%d", timeutil.Now().UnixNano()) uri := setupS3URI(t, db, bucket, prefix, creds) - backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts) + backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts, nil) } // TestCloudBackupRestoreS3WithLegacyPut tests that backup/restore works when @@ -75,7 +75,7 @@ func TestCloudBackupRestoreS3WithLegacyPut(t *testing.T) { prefix := fmt.Sprintf("TestBackupRestoreS3-%d", timeutil.Now().UnixNano()) db.Exec(t, "SET CLUSTER SETTING cloudstorage.s3.buffer_and_put_uploads.enabled=true") uri := setupS3URI(t, db, bucket, prefix, creds) - backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts) + backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts, nil) } func requiredS3CredsAndBucket(t *testing.T) (credentials.Value, string) { @@ -136,12 +136,12 @@ func TestCloudBackupRestoreGoogleCloudStorage(t *testing.T) { values := uri.Query() values.Add(cloud.AuthParam, cloud.AuthParamImplicit) uri.RawQuery = values.Encode() - backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts) + backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts, nil) } // TestBackupRestoreAzure hits the real Azure Blob Storage and so could -// occasionally be flaky. It's only run if the AZURE_ACCOUNT_NAME and -// AZURE_ACCOUNT_KEY environment vars are set. +// occasionally be flaky. It's only run if the AZURE_ACCOUNT_NAME, +// AZURE_ACCOUNT_KEY, and AZURE_CONTAINER environment vars are set. func TestCloudBackupRestoreAzure(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -169,5 +169,49 @@ func TestCloudBackupRestoreAzure(t *testing.T) { values.Add(azure.AzureAccountKeyParam, accountKey) uri.RawQuery = values.Encode() - backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts) + backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts, nil) +} + +// TestBackupRestoreAzureWithKMS hits real Azure services and so could +// occasionally be flaky. +// +// It's only run if the AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY, and +// AZURE_CONTAINER environment vars are set. This is for consistency with the +// non-KMS Azure test. In point of fact, many variables are required. These +// are listed in cloud_unit_tests_impl.sh, and the test will fail without them +// rather than skipping. +func TestCloudBackupRestoreAzureWithKMS(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + accountName := os.Getenv("AZURE_ACCOUNT_NAME") + + // NB: the Azure Account key must not be url encoded. + accountKey := os.Getenv("AZURE_ACCOUNT_KEY") + if accountName == "" || accountKey == "" { + skip.IgnoreLint(t, "AZURE_ACCOUNT_NAME and AZURE_ACCOUNT_KEY env vars must be set") + } + bucket := os.Getenv("AZURE_CONTAINER") + if bucket == "" { + skip.IgnoreLint(t, "AZURE_CONTAINER env var must be set") + } + + const numAccounts = 1000 + + ctx := context.Background() + tc, _, _, cleanupFn := backupRestoreTestSetup(t, 1, numAccounts, InitManualReplication) + defer cleanupFn() + prefix := fmt.Sprintf("TestBackupRestoreAzureWithKMS-%d", timeutil.Now().UnixNano()) + uri := url.URL{Scheme: "azure", Host: bucket, Path: prefix} + values := uri.Query() + values.Add(azure.AzureAccountNameParam, accountName) + values.Add(azure.AzureAccountKeyParam, accountKey) + uri.RawQuery = values.Encode() + + kmsParams := make(url.Values) + for _, k := range []string{"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID", "AZURE_VAULT_NAME"} { + v := os.Getenv(k) + kmsParams.Add(k, v) + } + kmsURI := fmt.Sprintf("azure-kms:///%s/%s?%s", os.Getenv("AZURE_KMS_KEY_NAME"), os.Getenv("AZURE_KMS_KEY_VERSION"), kmsParams.Encode()) + backupAndRestore(ctx, t, tc, []string{uri.String()}, []string{uri.String()}, numAccounts, []string{kmsURI}) } diff --git a/pkg/ccl/backupccl/backup_test.go b/pkg/ccl/backupccl/backup_test.go index b4386a05d0bb..e1f7a3b297dd 100644 --- a/pkg/ccl/backupccl/backup_test.go +++ b/pkg/ccl/backupccl/backup_test.go @@ -171,7 +171,7 @@ func TestBackupRestoreSingleUserfile(t *testing.T) { tc, _, _, cleanupFn := backupRestoreTestSetup(t, singleNode, numAccounts, InitManualReplication) defer cleanupFn() - backupAndRestore(ctx, t, tc, []string{"userfile:///a"}, []string{"userfile:///a"}, numAccounts) + backupAndRestore(ctx, t, tc, []string{"userfile:///a"}, []string{"userfile:///a"}, numAccounts, nil) } func TestBackupRestoreSingleNodeLocal(t *testing.T) { @@ -183,7 +183,7 @@ func TestBackupRestoreSingleNodeLocal(t *testing.T) { tc, _, _, cleanupFn := backupRestoreTestSetup(t, singleNode, numAccounts, InitManualReplication) defer cleanupFn() - backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts) + backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts, nil) } func TestBackupRestoreMultiNodeLocal(t *testing.T) { @@ -195,7 +195,7 @@ func TestBackupRestoreMultiNodeLocal(t *testing.T) { tc, _, _, cleanupFn := backupRestoreTestSetup(t, multiNode, numAccounts, InitManualReplication) defer cleanupFn() - backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts) + backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts, nil) } func TestBackupRestoreMultiNodeRemote(t *testing.T) { @@ -209,7 +209,7 @@ func TestBackupRestoreMultiNodeRemote(t *testing.T) { // Backing up to node2's local file system remoteFoo := "nodelocal://2/foo" - backupAndRestore(ctx, t, tc, []string{remoteFoo}, []string{localFoo}, numAccounts) + backupAndRestore(ctx, t, tc, []string{remoteFoo}, []string{localFoo}, numAccounts, nil) } // TestBackupRestoreJobTagAndLabel runs a backup and restore and verifies that @@ -271,7 +271,7 @@ func TestBackupRestoreJobTagAndLabel(t *testing.T) { ) defer cleanupFn() - backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts) + backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts, nil) require.True(t, found) } @@ -382,7 +382,7 @@ func TestBackupRestorePartitioned(t *testing.T) { } runBackupRestore := func(t *testing.T, sqlDB *sqlutils.SQLRunner, backupURIs []string) { - locationFmtString, locationURIArgs := uriFmtStringAndArgs(backupURIs) + locationFmtString, locationURIArgs := uriFmtStringAndArgs(backupURIs, 0) backupQuery := fmt.Sprintf("BACKUP DATABASE data TO %s", locationFmtString) sqlDB.Exec(t, backupQuery, locationURIArgs...) @@ -476,7 +476,7 @@ func TestBackupRestorePartitioned(t *testing.T) { } // Specifying multiple tiers is not supported. - locationFmtString, locationURIArgs := uriFmtStringAndArgs(backupURIs) + locationFmtString, locationURIArgs := uriFmtStringAndArgs(backupURIs, 0) backupQuery := fmt.Sprintf("BACKUP DATABASE data TO %s", locationFmtString) sqlDB.ExpectErr(t, `tier must be in the form "key=value" not "region=east,az=az1"`, backupQuery, locationURIArgs...) }) @@ -838,7 +838,7 @@ func TestBackupRestorePartitionedMergeDirectories(t *testing.T) { restoreURIs := []string{ localFoo1, } - backupAndRestore(ctx, t, tc, backupURIs, restoreURIs, numAccounts) + backupAndRestore(ctx, t, tc, backupURIs, restoreURIs, numAccounts, nil) } func TestBackupRestoreEmpty(t *testing.T) { @@ -850,7 +850,7 @@ func TestBackupRestoreEmpty(t *testing.T) { tc, _, _, cleanupFn := backupRestoreTestSetup(t, singleNode, numAccounts, InitManualReplication) defer cleanupFn() - backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts) + backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts, nil) } // Regression test for #16008. In short, the way RESTORE constructed split keys @@ -875,7 +875,7 @@ func TestBackupRestoreNegativePrimaryKey(t *testing.T) { -numAccounts/2, numAccounts/backupRestoreDefaultRanges/2, ) - backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts) + backupAndRestore(ctx, t, tc, []string{localFoo}, []string{localFoo}, numAccounts, nil) sqlDB.Exec(t, `CREATE UNIQUE INDEX id2 ON data.bank (id)`) @@ -900,6 +900,7 @@ func backupAndRestore( backupURIs []string, restoreURIs []string, numAccounts int, + kmsURIs []string, ) { conn := tc.Conns[0] sqlDB := sqlutils.MakeSQLRunner(conn) @@ -922,9 +923,16 @@ func backupAndRestore( rows, idx, bytes int64 } - backupURIFmtString, backupURIArgs := uriFmtStringAndArgs(backupURIs) + backupURIFmtString, backupURIArgs := uriFmtStringAndArgs(backupURIs, 0) backupQuery := fmt.Sprintf("BACKUP DATABASE data INTO %s", backupURIFmtString) - sqlDB.QueryRow(t, backupQuery, backupURIArgs...).Scan( + kmsURIArgs := make([]interface{}, 0) + var kmsURIFmtString string + if len(kmsURIs) > 0 { + kmsURIFmtString, kmsURIArgs = uriFmtStringAndArgs(kmsURIs, len(backupURIs)) + backupQuery = fmt.Sprintf("%s WITH kms = %s", backupQuery, kmsURIFmtString) + } + queryArgs := append(backupURIArgs, kmsURIArgs...) + sqlDB.QueryRow(t, backupQuery, queryArgs...).Scan( &unused, &unused, &unused, &exported.rows, &exported.idx, &exported.bytes, ) // When numAccounts == 0, our approxBytes formula breaks down because @@ -979,8 +987,13 @@ func backupAndRestore( // Create an incremental backup to exercise incremental destination code that captures a new // table sqlDB.Exec(t, `CREATE TABLE data.empty (a INT PRIMARY KEY)`) - sqlDB.Exec(t, fmt.Sprintf(`BACKUP DATABASE data INTO LATEST IN %s`, backupURIFmtString), - backupURIArgs...) + + incBackupQuery := fmt.Sprintf(`BACKUP DATABASE data INTO LATEST IN %s`, backupURIFmtString) + if len(kmsURIs) > 0 { + incBackupQuery = fmt.Sprintf("%s WITH kms = %s", incBackupQuery, kmsURIFmtString) + } + + sqlDB.Exec(t, incBackupQuery, queryArgs...) } sqlDB.Exec(t, `DROP DATABASE data CASCADE`) @@ -993,9 +1006,16 @@ func backupAndRestore( // Force the ID of the restored bank table to be different. sqlDB.Exec(t, `CREATE TABLE other.empty (a INT PRIMARY KEY)`) - restoreURIFmtString, restoreURIArgs := uriFmtStringAndArgs(restoreURIs) + restoreURIFmtString, restoreURIArgs := uriFmtStringAndArgs(restoreURIs, 0) restoreQuery := fmt.Sprintf("RESTORE DATABASE DATA FROM LATEST IN %s", restoreURIFmtString) - verifyRestoreData(t, sqlDB, storageSQLDB, restoreQuery, restoreURIArgs, numAccounts) + kmsURIArgs := make([]interface{}, 0) + if len(kmsURIs) > 0 { + var kmsURIFmtString string + kmsURIFmtString, kmsURIArgs = uriFmtStringAndArgs(kmsURIs, len(backupURIs)) + restoreQuery = fmt.Sprintf("%s WITH kms = %s", restoreQuery, kmsURIFmtString) + } + queryArgs := append(restoreURIArgs, kmsURIArgs...) + verifyRestoreData(t, sqlDB, storageSQLDB, restoreQuery, queryArgs, numAccounts) } func verifyRestoreData( diff --git a/pkg/ccl/backupccl/utils_test.go b/pkg/ccl/backupccl/utils_test.go index cfab32342133..0d28f6bc9499 100644 --- a/pkg/ccl/backupccl/utils_test.go +++ b/pkg/ccl/backupccl/utils_test.go @@ -378,7 +378,10 @@ func getKVCount( // uriFmtStringAndArgs returns format strings like "$1" or "($1, $2, $3)" and // an []interface{} of URIs for the BACKUP/RESTORE queries. -func uriFmtStringAndArgs(uris []string) (string, []interface{}) { +// +// Passing startIndex=i will start the fmt strings at $i+1. This can be useful +// when formatting different blocks of strings/args in the same query. +func uriFmtStringAndArgs(uris []string, startIndex int) (string, []interface{}) { urisForFormat := make([]interface{}, len(uris)) var fmtString strings.Builder if len(uris) > 1 { @@ -388,7 +391,7 @@ func uriFmtStringAndArgs(uris []string) (string, []interface{}) { if i > 0 { fmtString.WriteString(", ") } - fmtString.WriteString(fmt.Sprintf("$%d", i+1)) + fmtString.WriteString(fmt.Sprintf("$%d", startIndex+i+1)) urisForFormat[i] = uri } if len(uris) > 1 { diff --git a/pkg/ccl/cloudccl/externalconn/testdata/create_drop_external_connection b/pkg/ccl/cloudccl/externalconn/testdata/create_drop_external_connection index cf0505e08bba..2d254ee0c7a9 100644 --- a/pkg/ccl/cloudccl/externalconn/testdata/create_drop_external_connection +++ b/pkg/ccl/cloudccl/externalconn/testdata/create_drop_external_connection @@ -70,7 +70,7 @@ CREATE EXTERNAL CONNECTION "foo-kms" AS 'gcp-kms:///cmk?AUTH=specified&BEARER_TO exec-sql CREATE EXTERNAL CONNECTION "missing-cmk-kms" AS 'gcp-kms:///?AUTH=implicit&CREDENTIALS=baz&ASSUME_ROLE=ronaldo,rashford,bruno&BEARER_TOKEN=foo'; ---- -pq: failed to construct External Connection details: failed to create GCP KMS external connection: host component of the KMS cannot be empty; must contain the Customer Managed Key +pq: failed to construct External Connection details: failed to create GCP KMS external connection: path component of the KMS cannot be empty; must contain the Customer Managed Key exec-sql CREATE EXTERNAL CONNECTION "invalid-params-kms" AS 'gcp-kms:///cmk?AUTH=implicit&INVALIDPARAM=baz'; diff --git a/pkg/cloud/azure/BUILD.bazel b/pkg/cloud/azure/BUILD.bazel index 9808127a6d0e..d4530a369481 100644 --- a/pkg/cloud/azure/BUILD.bazel +++ b/pkg/cloud/azure/BUILD.bazel @@ -5,6 +5,8 @@ go_library( name = "azure", srcs = [ "azure_connection.go", + "azure_kms.go", + "azure_kms_connection.go", "azure_storage.go", ], importpath = "github.com/cockroachdb/cockroach/pkg/cloud/azure", @@ -24,6 +26,8 @@ go_library( "//pkg/util/ioctx", "//pkg/util/tracing", "@com_github_azure_azure_sdk_for_go_sdk_azcore//:azcore", + "@com_github_azure_azure_sdk_for_go_sdk_azidentity//:azidentity", + "@com_github_azure_azure_sdk_for_go_sdk_keyvault_azkeys//:azkeys", "@com_github_azure_azure_sdk_for_go_sdk_storage_azblob//:azblob", "@com_github_azure_azure_sdk_for_go_sdk_storage_azblob//blob", "@com_github_azure_azure_sdk_for_go_sdk_storage_azblob//blockblob", @@ -37,10 +41,14 @@ go_library( go_test( name = "azure_test", - srcs = ["azure_storage_test.go"], + srcs = [ + "azure_kms_test.go", + "azure_storage_test.go", + ], args = ["-test.timeout=295s"], embed = [":azure"], deps = [ + "//pkg/base", "//pkg/cloud", "//pkg/cloud/cloudpb", "//pkg/cloud/cloudtestutils", diff --git a/pkg/cloud/azure/azure_kms.go b/pkg/cloud/azure/azure_kms.go new file mode 100644 index 000000000000..fa251d22a205 --- /dev/null +++ b/pkg/cloud/azure/azure_kms.go @@ -0,0 +1,180 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package azure + +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + kms "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/cockroachdb/cockroach/pkg/cloud" + "github.com/cockroachdb/errors" +) + +const ( + kmsScheme = "azure-kms" + + AzureVaultName = "AZURE_VAULT_NAME" +) + +type azureKMS struct { + kms *kms.Client + customerMasterKeyID string + customerMasterKeyVersion string +} + +var _ cloud.KMS = &azureKMS{} + +// At time of writing, Azure KeyVault supports three encryption algorithms: +// https://learn.microsoft.com/en-us/azure/key-vault/keys/about-keys-details +// All are fine choices, but this is the most modern algorithm. +var encryptionAlgorithm = kms.JSONWebKeyEncryptionAlgorithmRSAOAEP256 + +func init() { + cloud.RegisterKMSFromURIFactory(MakeAzureKMS, kmsScheme) +} + +type kmsURIParams struct { + vaultName string + + // Documented in azure_storage.go + environment string + clientID string + clientSecret string + tenantID string +} + +// resolveKMSURIParams parses the `kmsURI` for all the supported KMS parameters. +func resolveKMSURIParams(kmsURI cloud.ConsumeURL) (kmsURIParams, error) { + params := kmsURIParams{ + vaultName: kmsURI.ConsumeParam(AzureVaultName), + environment: kmsURI.ConsumeParam(AzureEnvironmentKeyParam), + clientID: kmsURI.ConsumeParam(AzureClientIDParam), + clientSecret: kmsURI.ConsumeParam(AzureClientSecretParam), + tenantID: kmsURI.ConsumeParam(AzureTenantIDParam), + } + + // Validate that all the passed in parameters are supported. + if unknownParams := kmsURI.RemainingQueryParams(); len(unknownParams) > 0 { + return kmsURIParams{}, errors.Errorf( + `unknown KMS query parameters: %s`, strings.Join(unknownParams, ", ")) + } + + return params, nil +} + +func MakeAzureKMS(ctx context.Context, uri string, env cloud.KMSEnv) (cloud.KMS, error) { + if env.KMSConfig().DisableOutbound { + return nil, errors.New("external IO must be enabled to use KMS") + } + kmsURI, err := url.ParseRequestURI(uri) + if err != nil { + return nil, err + } + if kmsURI.Path == "/" { + return nil, errors.Newf("path component of the KMS cannot be empty; must contain the Customer Managed Key") + } + + kmsConsumeURL := cloud.ConsumeURL{URL: kmsURI} + // Extract the URI parameters required to setup the Azure KMS session. + kmsURIParams, err := resolveKMSURIParams(kmsConsumeURL) + if err != nil { + return nil, err + } + + missingParams := make([]string, 0) + if kmsURIParams.vaultName == "" { + missingParams = append(missingParams, AzureVaultName) + } + if kmsURIParams.clientID == "" { + missingParams = append(missingParams, AzureClientIDParam) + } + if kmsURIParams.clientSecret == "" { + missingParams = append(missingParams, AzureClientSecretParam) + } + if kmsURIParams.tenantID == "" { + missingParams = append(missingParams, AzureTenantIDParam) + } + if len(missingParams) != 0 { + return nil, errors.Errorf("kms URI expected but did not receive: %s", strings.Join(missingParams, ", ")) + } + + if kmsURIParams.environment == "" { + // Default to AzurePublicCloud if not specified for consistency with Azure Storage, + // which itself defaults to this for backwards compatibility. + kmsURIParams.environment = azure.PublicCloud.Name + } + + //TODO(benbardin): Implicit auth. + credential, err := azidentity.NewClientSecretCredential(kmsURIParams.tenantID, kmsURIParams.clientID, kmsURIParams.clientSecret, nil) + if err != nil { + return nil, errors.Wrap(err, "azure kms client secret credential") + } + + azureEnv, err := azure.EnvironmentFromName(kmsURIParams.environment) + if err != nil { + return nil, errors.Wrap(err, "azure kms environment") + } + + u, err := url.Parse(fmt.Sprintf("https://%s.%s", kmsURIParams.vaultName, azureEnv.KeyVaultDNSSuffix)) + if err != nil { + return nil, errors.Wrap(err, "azure kms vault url") + } + client, err := kms.NewClient(u.String(), credential, nil) + if err != nil { + return nil, errors.Wrap(err, "azure kms vault client") + } + keyTokens := strings.Split(strings.TrimPrefix(kmsURI.Path, "/"), "/") + if len(keyTokens) != 2 { + return nil, errors.New("azure kms key must be of form 'id/version'") + } + + return &azureKMS{ + kms: client, + customerMasterKeyID: keyTokens[0], + customerMasterKeyVersion: keyTokens[1], + }, nil +} + +func (k *azureKMS) MasterKeyID() (string, error) { + return k.customerMasterKeyID, nil +} + +func (k *azureKMS) Encrypt(ctx context.Context, data []byte) ([]byte, error) { + val, err := k.kms.Encrypt(ctx, k.customerMasterKeyID, k.customerMasterKeyVersion, kms.KeyOperationsParameters{ + Value: data, + Algorithm: &encryptionAlgorithm, + }, nil) + if err != nil { + return nil, err + } + return val.Result, nil +} + +func (k *azureKMS) Decrypt(ctx context.Context, data []byte) ([]byte, error) { + val, err := k.kms.Decrypt(ctx, k.customerMasterKeyID, k.customerMasterKeyVersion, kms.KeyOperationsParameters{ + Value: data, + Algorithm: &encryptionAlgorithm, + }, nil) + if err != nil { + return nil, err + } + return val.Result, nil +} + +func (k *azureKMS) Close() error { + // Azure KMS client does not implement Close. + return nil +} diff --git a/pkg/cloud/azure/azure_kms_connection.go b/pkg/cloud/azure/azure_kms_connection.go new file mode 100644 index 000000000000..085aad8cd5ff --- /dev/null +++ b/pkg/cloud/azure/azure_kms_connection.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package azure + +import ( + "context" + + "github.com/cockroachdb/cockroach/pkg/cloud/externalconn" + "github.com/cockroachdb/cockroach/pkg/cloud/externalconn/connectionpb" + "github.com/cockroachdb/cockroach/pkg/cloud/externalconn/utils" + "github.com/cockroachdb/cockroach/pkg/security/username" + "github.com/cockroachdb/errors" +) + +func validateAzureKMSConnectionURI( + ctx context.Context, execCfg interface{}, user username.SQLUsername, uri string, +) error { + if err := utils.CheckKMSConnection(ctx, execCfg, user, uri); err != nil { + return errors.Wrap(err, "failed to create Azure KMS external connection") + } + + return nil +} + +func init() { + externalconn.RegisterConnectionDetailsFromURIFactory( + scheme, + connectionpb.ConnectionProvider_azure_kms, + externalconn.SimpleURIFactory, + ) + externalconn.RegisterDefaultValidation( + scheme, + validateAzureKMSConnectionURI, + ) +} diff --git a/pkg/cloud/azure/azure_kms_test.go b/pkg/cloud/azure/azure_kms_test.go new file mode 100644 index 000000000000..6dcc1d6cae02 --- /dev/null +++ b/pkg/cloud/azure/azure_kms_test.go @@ -0,0 +1,96 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. +package azure + +import ( + "context" + "fmt" + "net/url" + "os" + "testing" + + "github.com/Azure/go-autorest/autorest/azure" + "github.com/cockroachdb/cockroach/pkg/base" + "github.com/cockroachdb/cockroach/pkg/cloud" + "github.com/cockroachdb/cockroach/pkg/settings/cluster" + "github.com/cockroachdb/cockroach/pkg/testutils/skip" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" +) + +var azureKMSTestSettings *cluster.Settings + +func init() { + azureKMSTestSettings = cluster.MakeTestingClusterSettings() +} + +type azureKMSConfig struct { + keyName, keyVersion, clientID, clientSecret, tenantID, vaultName, environment string +} + +func getAzureKMSConfig() (azureKMSConfig, error) { + cfg := azureKMSConfig{ + keyName: os.Getenv("AZURE_KMS_KEY_NAME"), + keyVersion: os.Getenv("AZURE_KMS_KEY_VERSION"), + clientID: os.Getenv("AZURE_CLIENT_ID"), + clientSecret: os.Getenv("AZURE_CLIENT_SECRET"), + tenantID: os.Getenv("AZURE_TENANT_ID"), + vaultName: os.Getenv("AZURE_VAULT_NAME"), + environment: azure.PublicCloud.Name, + } + + if cfg.keyName == "" || cfg.keyVersion == "" || cfg.clientID == "" || cfg.clientSecret == "" || cfg.tenantID == "" { + return azureKMSConfig{}, errors.New( + "AZURE_KMS_KEY_NAME, AZURE_KMS_KEY_VERSION, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID must all be set") + } + if v, ok := os.LookupEnv(AzureEnvironmentKeyParam); ok { + cfg.environment = v + } + return cfg, nil +} + +func TestEncryptDecryptAzure(t *testing.T) { + defer leaktest.AfterTest(t)() + ctx := context.Background() + + cfg, err := getAzureKMSConfig() + if err != nil { + skip.IgnoreLint(t, "Test not configured for Azure") + return + } + params := make(url.Values) + params.Add(AzureEnvironmentKeyParam, cfg.environment) + params.Add(AzureClientIDParam, cfg.clientID) + params.Add(AzureClientSecretParam, cfg.clientSecret) + params.Add(AzureTenantIDParam, cfg.tenantID) + params.Add(AzureVaultName, cfg.vaultName) + + t.Run("fails without credentials", func(t *testing.T) { + redactedParams := make(url.Values) + for k, v := range params { + redactedParams[k] = v + } + redactedParams.Del(AzureClientSecretParam) + + uri := fmt.Sprintf("azure-kms:///%s/%s?%s", cfg.keyName, cfg.keyVersion, redactedParams.Encode()) + + _, err := cloud.KMSFromURI(ctx, uri, &cloud.TestKMSEnv{ExternalIOConfig: &base.ExternalIODirConfig{}}) + require.Error(t, err) + }) + + t.Run("succeeds", func(t *testing.T) { + uri := fmt.Sprintf("azure-kms:///%s/%s?%s", cfg.keyName, cfg.keyVersion, params.Encode()) + cloud.KMSEncryptDecrypt(t, uri, &cloud.TestKMSEnv{ + Settings: azureKMSTestSettings, + ExternalIOConfig: &base.ExternalIODirConfig{}, + }) + }) +} diff --git a/pkg/cloud/azure/azure_storage.go b/pkg/cloud/azure/azure_storage.go index a54f1a3ea150..b0d83cb8d109 100644 --- a/pkg/cloud/azure/azure_storage.go +++ b/pkg/cloud/azure/azure_storage.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob" @@ -46,14 +47,51 @@ var maxConcurrentUploadBuffers = settings.RegisterIntSetting( 1, ).WithPublic() +// A note on Azure authentication: +// +// The standardized way to authenticate a third-party identity to the Azure +// Cloud is via an "App Registration." This is the equivalent of an ID/Secret +// pair on other providers, and uses the Azure-wide RBAC authentication +// system. +// +// Azure RBAC is supported across all Azure products, but is often not the +// only way to attach permissions. Individual Azure products often each +// provide their _own_ permissions systems, likely for legacy reasons. +// +// In the case of Azure storage, one can also authenticate using a "key" tied +// specifically to that storage account. (This key grants no other access, +// and cannot be modified or restricted in scope.) +// +// Were we building Azure support in CRDB from scratch, we probably would not +// support this access method. For backwards compatibility, however, we retain +// support for these keys on Storage. +// +// So to authenticate to Azure Storage, a CRDB user must provide EITHER +// 1. AZURE_ACCOUNT_KEY (legacy storage-key access), OR +// 2. All three of AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID (RBAC +// access). const ( // AzureAccountNameParam is the query parameter for account_name in an azure URI. AzureAccountNameParam = "AZURE_ACCOUNT_NAME" - // AzureAccountKeyParam is the query parameter for account_key in an azure URI. - AzureAccountKeyParam = "AZURE_ACCOUNT_KEY" // AzureEnvironmentKeyParam is the query parameter for the environment name in an azure URI. AzureEnvironmentKeyParam = "AZURE_ENVIRONMENT" + // Storage Key identifiers: + + // AzureAccountKeyParam is the query parameter for account_key in an azure URI. + AzureAccountKeyParam = "AZURE_ACCOUNT_KEY" + + // App Registration identifiers: + + // AzureClientIDParam is the query parameter for client_id in an azure URI. + AzureClientIDParam = "AZURE_CLIENT_ID" + // AzureClientSecretParam is the query parameter for client_secret in an azure URI. + AzureClientSecretParam = "AZURE_CLIENT_SECRET" + // AzureTenantIDParam is the query parameter for tenant_id in an azure URI. + // Note that tenant ID here refers to the Azure Active Directory tenant, + // _not_ to any CRDB tenant. + AzureTenantIDParam = "AZURE_TENANT_ID" + scheme = "azure-blob" deprecatedScheme = "azure" @@ -67,11 +105,14 @@ func parseAzureURL( conf := cloudpb.ExternalStorage{} conf.Provider = cloudpb.ExternalStorageProvider_azure conf.AzureConfig = &cloudpb.ExternalStorage_Azure{ - Container: uri.Host, - Prefix: uri.Path, - AccountName: azureURL.ConsumeParam(AzureAccountNameParam), - AccountKey: azureURL.ConsumeParam(AzureAccountKeyParam), - Environment: azureURL.ConsumeParam(AzureEnvironmentKeyParam), + Container: uri.Host, + Prefix: uri.Path, + AccountName: azureURL.ConsumeParam(AzureAccountNameParam), + AccountKey: azureURL.ConsumeParam(AzureAccountKeyParam), + Environment: azureURL.ConsumeParam(AzureEnvironmentKeyParam), + ClientID: azureURL.ConsumeParam(AzureClientIDParam), + ClientSecret: azureURL.ConsumeParam(AzureClientSecretParam), + TenantID: azureURL.ConsumeParam(AzureTenantIDParam), } // Validate that all the passed in parameters are supported. @@ -83,9 +124,16 @@ func parseAzureURL( if conf.AzureConfig.AccountName == "" { return conf, errors.Errorf("azure uri missing %q parameter", AzureAccountNameParam) } - if conf.AzureConfig.AccountKey == "" { - return conf, errors.Errorf("azure uri missing %q parameter", AzureAccountKeyParam) + + hasKeyCreds := conf.AzureConfig.AccountKey != "" + + hasRoleCreds := conf.AzureConfig.TenantID != "" && conf.AzureConfig.ClientID != "" && conf.AzureConfig.ClientSecret != "" + noRoleCreds := conf.AzureConfig.TenantID == "" && conf.AzureConfig.ClientID == "" && conf.AzureConfig.ClientSecret == "" + + if hasRoleCreds == hasKeyCreds || hasRoleCreds == noRoleCreds { + return conf, errors.Errorf("azure uri requires exactly one authentication method: %q OR all three of %q, %q, and %q", AzureAccountKeyParam, AzureTenantIDParam, AzureClientIDParam, AzureClientSecretParam) } + if conf.AzureConfig.Environment == "" { // Default to AzurePublicCloud if not specified for backwards compatibility conf.AzureConfig.Environment = azure.PublicCloud.Name @@ -112,10 +160,6 @@ func makeAzureStorage( if conf == nil { return nil, errors.Errorf("azure upload requested but info missing") } - credential, err := azblob.NewSharedKeyCredential(conf.AccountName, conf.AccountKey) - if err != nil { - return nil, errors.Wrap(err, "azure credential") - } env, err := azure.EnvironmentFromName(conf.Environment) if err != nil { return nil, errors.Wrap(err, "azure environment") @@ -125,9 +169,27 @@ func makeAzureStorage( return nil, errors.Wrap(err, "azure: account name is not valid") } - azClient, err := service.NewClientWithSharedKeyCredential(u.String(), credential, nil) - if err != nil { - return nil, err + //TODO(benbardin): Implicit auth. + var azClient *service.Client + if conf.ClientID != "" { + credential, err := azidentity.NewClientSecretCredential(conf.TenantID, conf.ClientID, conf.ClientSecret, nil) + if err != nil { + return nil, errors.Wrap(err, "azure client secret credential") + } + + azClient, err = service.NewClient(u.String(), credential, nil) + if err != nil { + return nil, err + } + } else { + credential, err := azblob.NewSharedKeyCredential(conf.AccountName, conf.AccountKey) + if err != nil { + return nil, errors.Wrap(err, "azure shared key credential") + } + azClient, err = service.NewClientWithSharedKeyCredential(u.String(), credential, nil) + if err != nil { + return nil, err + } } return &azureStorage{ diff --git a/pkg/cloud/azure/azure_storage_test.go b/pkg/cloud/azure/azure_storage_test.go index 99de291d39da..c223505de0f4 100644 --- a/pkg/cloud/azure/azure_storage_test.go +++ b/pkg/cloud/azure/azure_storage_test.go @@ -30,14 +30,14 @@ import ( ) type azureConfig struct { - account, key, bucket, environment string + account, key, bucket, environment, clientID, clientSecret, tenantID string } -func (a azureConfig) filePath(f string) string { +func (a *azureConfig) filePath(f string) string { return a.filePathWithScheme("azure", f) } -func (a azureConfig) filePathWithScheme(scheme string, f string) string { +func (a *azureConfig) filePathWithScheme(scheme string, f string) string { uri := url.URL{Scheme: scheme, Host: a.bucket, Path: f} values := uri.Query() values.Add(AzureAccountNameParam, a.account) @@ -47,16 +47,36 @@ func (a azureConfig) filePathWithScheme(scheme string, f string) string { return uri.String() } +func (a *azureConfig) filePathClientAuth(f string) string { + return a.filePathWithSchemeClientAuth("azure", f) +} + +func (a *azureConfig) filePathWithSchemeClientAuth(scheme string, f string) string { + uri := url.URL{Scheme: scheme, Host: a.bucket, Path: f} + values := uri.Query() + values.Add(AzureAccountNameParam, a.account) + values.Add(AzureClientIDParam, a.clientID) + values.Add(AzureClientSecretParam, a.clientSecret) + values.Add(AzureTenantIDParam, a.tenantID) + values.Add(AzureEnvironmentKeyParam, a.environment) + uri.RawQuery = values.Encode() + return uri.String() +} + func getAzureConfig() (azureConfig, error) { // NB: the Azure Account key must not be url encoded. cfg := azureConfig{ - account: os.Getenv("AZURE_ACCOUNT_NAME"), - key: os.Getenv("AZURE_ACCOUNT_KEY"), - bucket: os.Getenv("AZURE_CONTAINER"), - environment: azure.PublicCloud.Name, + account: os.Getenv("AZURE_ACCOUNT_NAME"), + key: os.Getenv("AZURE_ACCOUNT_KEY"), + bucket: os.Getenv("AZURE_CONTAINER"), + clientID: os.Getenv("AZURE_CLIENT_ID"), + clientSecret: os.Getenv("AZURE_CLIENT_SECRET"), + tenantID: os.Getenv("AZURE_TENANT_ID"), + environment: azure.PublicCloud.Name, } - if cfg.account == "" || cfg.key == "" || cfg.bucket == "" { - return azureConfig{}, errors.New("AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY, AZURE_CONTAINER must all be set") + if cfg.account == "" || cfg.key == "" || cfg.bucket == "" || cfg.clientID == "" || cfg.clientSecret == "" || cfg.tenantID == "" { + return azureConfig{}, errors.New( + "AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY, AZURE_CONTAINER, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID must all be set") } if v, ok := os.LookupEnv(AzureEnvironmentKeyParam); ok { cfg.environment = v @@ -82,6 +102,16 @@ func TestAzure(t *testing.T) { nil, /* db */ testSettings, ) + cloudtestutils.CheckExportStore(t, cfg.filePathClientAuth("backup-test"), + false, username.RootUserName(), + nil, /* db */ + testSettings, + ) + cloudtestutils.CheckListFiles(t, cfg.filePathClientAuth("listing-test"), + username.RootUserName(), + nil, /* db */ + testSettings, + ) } func TestAzureSchemes(t *testing.T) { @@ -95,6 +125,10 @@ func TestAzureSchemes(t *testing.T) { uri := cfg.filePathWithScheme(scheme, "not-used") _, err := cloud.ExternalStorageConfFromURI(uri, username.RootUserName()) require.NoError(t, err) + + uriClientAuth := cfg.filePathWithSchemeClientAuth(scheme, "not-used") + _, err = cloud.ExternalStorageConfFromURI(uriClientAuth, username.RootUserName()) + require.NoError(t, err) } } @@ -113,6 +147,12 @@ func TestAntagonisticAzureRead(t *testing.T) { require.NoError(t, err) cloudtestutils.CheckAntagonisticRead(t, conf, testSettings) + + clientAuthConf, err := cloud.ExternalStorageConfFromURI( + cfg.filePathClientAuth("antagonistic-read"), username.RootUserName()) + require.NoError(t, err) + + cloudtestutils.CheckAntagonisticRead(t, clientAuthConf, testSettings) } func TestParseAzureURL(t *testing.T) { @@ -126,6 +166,23 @@ func TestParseAzureURL(t *testing.T) { require.Equal(t, azure.PublicCloud.Name, sut.AzureConfig.Environment) }) + t.Run("Parses client-secret auth params", func(t *testing.T) { + u, err := url.Parse("azure://container/path?AZURE_ACCOUNT_NAME=account&AZURE_CLIENT_ID=client&AZURE_CLIENT_SECRET=secret&AZURE_TENANT_ID=tenant") + require.NoError(t, err) + + _, err = parseAzureURL(cloud.ExternalStorageURIContext{}, u) + require.NoError(t, err) + }) + + t.Run("Rejects combined client-secret auth params and ACCOUNT_KEY", func(t *testing.T) { + u, err := url.Parse("azure://container/path?AZURE_ACCOUNT_NAME=account&AZURE_ACCOUNT_KEY=key&AZURE_CLIENT_ID=client&AZURE_CLIENT_SECRET=secret&AZURE_TENANT_ID=tenant") + require.NoError(t, err) + + _, err = parseAzureURL(cloud.ExternalStorageURIContext{}, u) + require.Error(t, err) + + }) + t.Run("Can Override AZURE_ENVIRONMENT", func(t *testing.T) { u, err := url.Parse("azure-storage://container/path?AZURE_ACCOUNT_NAME=account&AZURE_ACCOUNT_KEY=key&AZURE_ENVIRONMENT=AzureUSGovernmentCloud") require.NoError(t, err) diff --git a/pkg/cloud/cloudpb/external_storage.proto b/pkg/cloud/cloudpb/external_storage.proto index 04bdf09249a3..5d19846b299f 100644 --- a/pkg/cloud/cloudpb/external_storage.proto +++ b/pkg/cloud/cloudpb/external_storage.proto @@ -126,6 +126,10 @@ message ExternalStorage { string account_name = 3; string account_key = 4; string environment = 5; + + string client_id = 6 [(gogoproto.customname) = "ClientID"]; + string client_secret = 7; + string tenant_id = 8 [(gogoproto.customname) = "TenantID"]; } message FileTable { // User interacting with the external storage. This is used to check access diff --git a/pkg/cloud/externalconn/connectionpb/connection.go b/pkg/cloud/externalconn/connectionpb/connection.go index 07eec7df7404..390dc22fba71 100644 --- a/pkg/cloud/externalconn/connectionpb/connection.go +++ b/pkg/cloud/externalconn/connectionpb/connection.go @@ -18,7 +18,7 @@ func (d *ConnectionDetails) Type() ConnectionType { case ConnectionProvider_nodelocal, ConnectionProvider_s3, ConnectionProvider_userfile, ConnectionProvider_gs, ConnectionProvider_azure_storage: return TypeStorage - case ConnectionProvider_gcp_kms, ConnectionProvider_aws_kms: + case ConnectionProvider_gcp_kms, ConnectionProvider_aws_kms, ConnectionProvider_azure_kms: return TypeKMS case ConnectionProvider_kafka, ConnectionProvider_http, ConnectionProvider_https, ConnectionProvider_sql, ConnectionProvider_webhookhttp, ConnectionProvider_webhookhttps, ConnectionProvider_gcpubsub: diff --git a/pkg/cloud/externalconn/connectionpb/connection.proto b/pkg/cloud/externalconn/connectionpb/connection.proto index 4c9401bf10b0..a0034b734d95 100644 --- a/pkg/cloud/externalconn/connectionpb/connection.proto +++ b/pkg/cloud/externalconn/connectionpb/connection.proto @@ -27,6 +27,7 @@ enum ConnectionProvider { // KMS providers. gcp_kms = 2; aws_kms = 8; + azure_kms = 15; // Sink providers. kafka = 3; diff --git a/pkg/cloud/gcp/gcp_kms.go b/pkg/cloud/gcp/gcp_kms.go index e20c183a623c..e4da13a3dcf5 100644 --- a/pkg/cloud/gcp/gcp_kms.go +++ b/pkg/cloud/gcp/gcp_kms.go @@ -80,7 +80,7 @@ func MakeGCSKMS(ctx context.Context, uri string, env cloud.KMSEnv) (cloud.KMS, e return nil, err } if kmsURI.Path == "/" { - return nil, errors.Newf("host component of the KMS cannot be empty; must contain the Customer Managed Key") + return nil, errors.Newf("path component of the KMS cannot be empty; must contain the Customer Managed Key") } kmsConsumeURL := cloud.ConsumeURL{URL: kmsURI}