Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bigtable): Customer Managed Encryption (CMEK) #3899

Merged
merged 33 commits into from Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
98ccaff
feat(bigtable): add EncryptionInfo method to admin surface
crwilcox Mar 19, 2021
2c97163
feat(bigtable: add EncryptionInfo to backups
crwilcox Mar 22, 2021
84206e6
test(bigtable): add bit of testing for new encryption info
crwilcox Mar 24, 2021
42a9437
chore(): add some notes for self
crwilcox Mar 31, 2021
b7008bf
test: add additional tests for methods
crwilcox Apr 1, 2021
9d89cdc
test: bring back some test bits
crwilcox Apr 6, 2021
366a695
test: wait for cmek to become 'ready', before evaluating other fields
crwilcox Apr 7, 2021
e9cebaa
chore: cleanup
crwilcox Apr 7, 2021
2aa4d06
chore: some pr feedback cleanup
crwilcox Apr 7, 2021
095d606
cleanup
crwilcox Apr 9, 2021
c9f9abf
tritone feedback, move backup info to have pointer to encryption info…
crwilcox Apr 9, 2021
1b45af1
create custom type instead of map
crwilcox Apr 14, 2021
71dcf18
iota
crwilcox Apr 14, 2021
4818177
cleanup
crwilcox Apr 14, 2021
bf94324
Comment improvements
crwilcox Apr 15, 2021
843d988
use same key specified in contributing
crwilcox Apr 16, 2021
8117c7e
Merge branch 'master' into bigtable-cmek
crwilcox Apr 16, 2021
b5a7efd
Code cleanup
crwilcox Apr 16, 2021
ee8f42a
Merge branch 'master' into bigtable-cmek
crwilcox Apr 16, 2021
09a4ef0
Merge branch 'master' into bigtable-cmek
crwilcox Apr 19, 2021
f413fc8
gofmt
crwilcox Apr 19, 2021
4f400f2
Address nits from codyoss
crwilcox Apr 20, 2021
749ae3a
Merge branch 'master' into bigtable-cmek
crwilcox Apr 20, 2021
4a0010b
Merge branch 'master' into bigtable-cmek
crwilcox Apr 21, 2021
20c839d
docs
crwilcox Apr 21, 2021
533fe8c
Merge branch 'master' into bigtable-cmek
crwilcox Apr 22, 2021
bdb52a7
fix: address kolea2 comments
crwilcox Apr 26, 2021
60e210e
Merge branch 'bigtable-cmek' of github.com:crwilcox/google-cloud-go i…
crwilcox Apr 26, 2021
22957b5
fix: narrow view for getting tables
crwilcox Apr 28, 2021
19a5bb1
Merge branch 'master' into bigtable-cmek
crwilcox Apr 28, 2021
6c492a6
refactor: remove repeated Encryption keyword
crwilcox Apr 28, 2021
b699e1a
Merge branch 'master' into bigtable-cmek
crwilcox Apr 29, 2021
891e52f
fix: infer service account email for contributing guide
crwilcox Apr 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -182,6 +182,13 @@ $ gcloud kms keys create key2 --keyring $MY_KEYRING --location $MY_LOCATION --pu
$ export GCLOUD_TESTS_GOLANG_KEYRING=projects/$GCLOUD_TESTS_GOLANG_PROJECT_ID/locations/$MY_LOCATION/keyRings/$MY_KEYRING
# Authorizes Google Cloud Storage to encrypt and decrypt using key1.
$ gsutil kms authorize -p $GCLOUD_TESTS_GOLANG_PROJECT_ID -k $GCLOUD_TESTS_GOLANG_KEYRING/cryptoKeys/key1
# Authorizes Google Cloud Bigtable to encrypt and decrypt using key1
gcloud kms keys add-iam-policy-binding key1 \
--keyring $MY_KEYRING \
--location $MY_LOCATION \
--role roles/cloudkms.cryptoKeyEncrypterDecrypter \
--member allAuthenticatedUsers \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this true? Wouldn't you want this to point to a service account?

Copy link
Contributor Author

@crwilcox crwilcox Apr 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that, as this encryption key isn't really a secret, opening it up to all authn'd users was fine. I could be mistaken but I also think adding a service account here would be another dependency/setup step to being a contributor.

@tritone do you know what gsutil kms is doing behind the scenes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm not exactly sure what the question is here but here's the code: https://github.com/GoogleCloudPlatform/gsutil/blob/7845859b924004d16db0408335fd434424f6c8d7/gslib/commands/kms.py

Looks like it requests a service account and creates one if it doesn't exist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you re-use the service account used elsewhere in the setup? GCLOUD_TESTS_GOLANG_KEY should already point to a service account json; is there a gcloud command to grab the email from that? I see https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/list . I just don't know enough about IAM to be confident that adding this binding isn't some kind of security risk.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the complication here is we store the path to the key, but the email you need (the member id) for the service account is within the json.

  "client_email": "{PROJECT}@{PROJECT}.iam.gserviceaccount.com",

I could infer this as it generally follows that form, but it may not. I was avoiding adding another EnvVar.

--project $GCLOUD_TESTS_GOLANG_PROJECT_ID
```

It may be useful to add exports to your shell initialization for future use.
Expand Down
185 changes: 158 additions & 27 deletions bigtable/admin.go
Expand Up @@ -39,6 +39,7 @@ import (
"google.golang.org/api/option"
gtransport "google.golang.org/api/transport/grpc"
btapb "google.golang.org/genproto/googleapis/bigtable/admin/v2"
"google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/metadata"
)
Expand Down Expand Up @@ -119,6 +120,72 @@ func (ac *AdminClient) backupPath(cluster, backup string) string {
return fmt.Sprintf("projects/%s/instances/%s/clusters/%s/backups/%s", ac.project, ac.instance, cluster, backup)
}

// EncryptionInfo represents the encryption info of a table.
type EncryptionInfo struct {
EncryptionStatus *Status
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
EncryptionType EncryptionType
KMSKeyVersion string
}

func newEncryptionInfo(pbInfo *btapb.EncryptionInfo) *EncryptionInfo {
return &EncryptionInfo{
EncryptionStatus: pbInfo.EncryptionStatus,
EncryptionType: EncryptionType(pbInfo.EncryptionType.Number()),
KMSKeyVersion: pbInfo.KmsKeyVersion,
}
}

// Status references google.golang.org/grpc/status.
// It represents an RPC status code, message, and details of EncryptionInfo.
// https://pkg.go.dev/google.golang.org/grpc/internal/status
type Status = status.Status

// EncryptionType is the type of encryption for an instance.
type EncryptionType int32

const (
// EncryptionTypeUnspecified is the type was not specified, though data at rest remains encrypted.
EncryptionTypeUnspecified EncryptionType = iota
// GoogleDefaultEncryption represents that data backing this resource is
// encrypted at rest with a key that is fully managed by Google. No key
// version or status will be populated. This is the default state.
GoogleDefaultEncryption
codyoss marked this conversation as resolved.
Show resolved Hide resolved
// CustomerManagedEncryption represents that data backing this resource is
// encrypted at rest with a key that is managed by the customer.
// The in-use version of the key and its status are populated for
// CMEK-protected tables.
// CMEK-protected backups are pinned to the key version that was in use at
// the time the backup was taken. This key version is populated but its
// status is not tracked and is reported as `UNKNOWN`.
CustomerManagedEncryption
)

// EncryptionInfoByCluster is a map of cluster name to EncryptionInfo
type EncryptionInfoByCluster map[string][]*EncryptionInfo

// EncryptionInfo gets the current encryption info for the table across all of the clusters.
// The returned map will be keyed by cluster id and contain a status for all of the keys in use.
func (ac *AdminClient) EncryptionInfo(ctx context.Context, table string) (EncryptionInfoByCluster, error) {
ctx = mergeOutgoingMetadata(ctx, ac.md)

res, err := ac.getTable(ctx, table)
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
encryptionInfo := EncryptionInfoByCluster{}
for key, cs := range res.ClusterStates {
for _, pbInfo := range cs.EncryptionInfo {
info := EncryptionInfo{}
info.EncryptionStatus = pbInfo.EncryptionStatus
info.EncryptionType = EncryptionType(pbInfo.EncryptionType.Number())
info.KMSKeyVersion = pbInfo.KmsKeyVersion
encryptionInfo[key] = append(encryptionInfo[key], &info)
}
}

return encryptionInfo, nil
}

// Tables returns a list of the tables in the instance.
func (ac *AdminClient) Tables(ctx context.Context) ([]string, error) {
ctx = mergeOutgoingMetadata(ctx, ac.md)
Expand Down Expand Up @@ -247,12 +314,12 @@ type FamilyInfo struct {
GCPolicy string
}

// TableInfo retrieves information about a table.
func (ac *AdminClient) TableInfo(ctx context.Context, table string) (*TableInfo, error) {
func (ac *AdminClient) getTable(ctx context.Context, table string) (*btapb.Table, error) {
ctx = mergeOutgoingMetadata(ctx, ac.md)
prefix := ac.instancePrefix()
req := &btapb.GetTableRequest{
Name: prefix + "/tables/" + table,
View: btapb.Table_FULL,
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
}

var res *btapb.Table
Expand All @@ -265,6 +332,17 @@ func (ac *AdminClient) TableInfo(ctx context.Context, table string) (*TableInfo,
if err != nil {
return nil, err
}
return res, nil
}

// TableInfo retrieves information about a table.
func (ac *AdminClient) TableInfo(ctx context.Context, table string) (*TableInfo, error) {
ctx = mergeOutgoingMetadata(ctx, ac.md)

res, err := ac.getTable(ctx, table)
if err != nil {
return nil, err
}

ti := &TableInfo{}
for name, fam := range res.ColumnFamilies {
Expand Down Expand Up @@ -957,26 +1035,71 @@ func (iac *InstanceAdminClient) InstanceInfo(ctx context.Context, instanceID str

// ClusterConfig contains the information necessary to create a cluster
type ClusterConfig struct {
InstanceID, ClusterID, Zone string
NumNodes int32
StorageType StorageType
// InstanceID specifies the unique name of the instance. Required.
InstanceID string

// ClusterID specifies the unique name of the cluster. Required.
ClusterID string

// Zone specifies the location where this cluster's nodes and storage reside.
// For best performance, clients should be located as close as possible to this
// cluster. Required.
Zone string

// NumNodes specifies the number of nodes allocated to this cluster. More
// nodes enable higher throughput and more consistent performance. Required.
NumNodes int32

// StorageType specifies the type of storage used by this cluster to serve
// its parent instance's tables, unless explicitly overridden. Required.
StorageType StorageType

// KMSKeyName is the name of the KMS customer managed encryption key (CMEK)
// to use for at-rest encryption of data in this cluster. If omitted,
// Google's default encryption will be used. If specified, the requirements
// for this key are:
// 1) The Cloud Bigtable service account associated with the
// project that contains the cluster must be granted the
// ``cloudkms.cryptoKeyEncrypterDecrypter`` role on the
// CMEK.
// 2) Only regional keys can be used and the region of the
// CMEK key must match the region of the cluster.
// 3) All clusters within an instance must use the same CMEK
// key.
// Optional. Immutable.
KMSKeyName string
}

func (cc *ClusterConfig) proto(project string) *btapb.Cluster {
ec := btapb.Cluster_EncryptionConfig{}
ec.KmsKeyName = cc.KMSKeyName
return &btapb.Cluster{
ServeNodes: cc.NumNodes,
DefaultStorageType: cc.StorageType.proto(),
Location: "projects/" + project + "/locations/" + cc.Zone,
EncryptionConfig: &ec,
}
}

// ClusterInfo represents information about a cluster.
type ClusterInfo struct {
Name string // name of the cluster
Zone string // GCP zone of the cluster (e.g. "us-central1-a")
ServeNodes int // number of allocated serve nodes
State string // state of the cluster
StorageType StorageType // the storage type of the cluster
// Name is the name of the cluster.
Name string

// Zone is the GCP zone of the cluster (e.g. "us-central1-a").
Zone string

// ServeNodes is the number of allocated serve nodes.
ServeNodes int

// State is the state of the cluster.
State string

// StorageType is the storage type of the cluster.
StorageType StorageType

// KMSKeyName is the customer managed encryption key for the cluster.
KMSKeyName string
}

// CreateCluster creates a new cluster in an instance.
Expand Down Expand Up @@ -1045,6 +1168,7 @@ func (iac *InstanceAdminClient) Clusters(ctx context.Context, instanceID string)
ServeNodes: int(c.ServeNodes),
State: c.State.String(),
StorageType: storageTypeFromProto(c.DefaultStorageType),
KMSKeyName: c.EncryptionConfig.KmsKeyName,
})
}
if len(res.FailedLocations) > 0 {
Expand All @@ -1058,7 +1182,9 @@ func (iac *InstanceAdminClient) Clusters(ctx context.Context, instanceID string)
// GetCluster fetches a cluster in an instance
func (iac *InstanceAdminClient) GetCluster(ctx context.Context, instanceID, clusterID string) (*ClusterInfo, error) {
ctx = mergeOutgoingMetadata(ctx, iac.md)
req := &btapb.GetClusterRequest{Name: "projects/" + iac.project + "/instances/" + instanceID + "/clusters/" + clusterID}
req := &btapb.GetClusterRequest{
Name: fmt.Sprintf("projects/%s/instances/%s/clusters/%s", iac.project, instanceID, clusterID),
}
var c *btapb.Cluster
err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
var err error
Expand All @@ -1077,6 +1203,7 @@ func (iac *InstanceAdminClient) GetCluster(ctx context.Context, instanceID, clus
ServeNodes: int(c.ServeNodes),
State: c.State.String(),
StorageType: storageTypeFromProto(c.DefaultStorageType),
KMSKeyName: c.EncryptionConfig.KmsKeyName,
}
return cis, nil
}
Expand Down Expand Up @@ -1569,16 +1696,19 @@ func newBackupInfo(backup *btapb.Backup) (*BackupInfo, error) {
if err != nil {
return nil, fmt.Errorf("invalid expireTime: %v", err)
}
encryptionInfo := newEncryptionInfo(backup.EncryptionInfo)
bi := BackupInfo{
Name: name,
SourceTable: tableID,
SizeBytes: backup.SizeBytes,
StartTime: startTime,
EndTime: endTime,
ExpireTime: expireTime,
State: backup.State.String(),
EncryptionInfo: encryptionInfo,
}

return &BackupInfo{
Name: name,
SourceTable: tableID,
SizeBytes: backup.SizeBytes,
StartTime: startTime,
EndTime: endTime,
ExpireTime: expireTime,
State: backup.State.String(),
}, nil
return &bi, nil
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
}

// BackupIterator is an EntryIterator that iterates over log entries.
Expand Down Expand Up @@ -1607,13 +1737,14 @@ func (it *BackupIterator) Next() (*BackupInfo, error) {

// BackupInfo contains backup metadata. This struct is read-only.
type BackupInfo struct {
Name string
SourceTable string
SizeBytes int64
StartTime time.Time
EndTime time.Time
ExpireTime time.Time
State string
Name string
SourceTable string
SizeBytes int64
StartTime time.Time
EndTime time.Time
ExpireTime time.Time
State string
EncryptionInfo *EncryptionInfo
}

// BackupInfo gets backup metadata.
Expand Down