plugin/v2/plugin.go (123 lines of code) (raw):

// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Implementation of the KMS Plugin API v2. package v2 import ( "regexp" "sync" "google.golang.org/api/cloudkms/v1" grpc "google.golang.org/grpc" "context" "encoding/base64" "fmt" "time" "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" "github.com/golang/glog" ) const ( apiVersion = "v2beta1" ok = "ok" ping = "cGluZw==" keyNotReachable = "Cloud KMS key is not reachable" keyDisabled = "Cloud KMS key is not enabled or no cloudkms.cryptoKeys.get permission" ) // Regex to extract Cloud KMS key resource name from the key version resource name var keyResourceRegEx = regexp.MustCompile(`projects\/[^/]+\/locations\/[^/]+\/keyRings\/[^/]+\/cryptoKeys\/[^/:]+`) var _ plugin.Plugin = (*Plugin)(nil) type Plugin struct { keyService *cloudkms.ProjectsLocationsKeyRingsCryptoKeysService keyURI string keySuffix string // lastKeyID stores the last known primary key version resource name to return // as KeyId in case when the Cloud KMS service is not reachable because KeyId // in StatusResponse cannot be empty and shouldn't trigger key migration in // case of transient remote service unavailability. lastKeyID string lastKeyIDLock sync.RWMutex } // New constructs Plugin. func NewPlugin(keyService *cloudkms.ProjectsLocationsKeyRingsCryptoKeysService, keyURI, keySuffix string) *Plugin { p := &Plugin{ keyService: keyService, keyURI: keyURI, keySuffix: keySuffix, } p.setKeyID(keyURI) return p } // Register registers the plugin as a service management service. func (g *Plugin) Register(s *grpc.Server) { RegisterKeyManagementServiceServer(s, g) } // Status returns the version of KMS API version that plugin supports. // Response also contains the status of the plugin, which is calculated as availability of the // encryption key that the plugin is confinged with, and the current primary key version. // kube-apiserver will provide this key version in Encrypt and Decrypt calls and will be able // to know whether the remote CLoud KMS key has been rotated or not. func (g *Plugin) Status(ctx context.Context, request *StatusRequest) (*StatusResponse, error) { defer plugin.RecordCloudKMSOperation("encrypt", time.Now().UTC()) keyID := g.keyID() statusResp := &StatusResponse{ Version: apiVersion, KeyId: keyID, Healthz: ok, } resp, err := g.keyService.Encrypt(g.keyURI, &cloudkms.EncryptRequest{ Plaintext: ping, }).Context(ctx).Do() if err != nil { plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("encrypt").Inc() statusResp.Healthz = keyNotReachable } else { g.setKeyID(resp.Name) } glog.V(4).Infof("Status response: %s", statusResp.Healthz) return statusResp, nil } // Encrypt encrypts payload provided by K8S API Server. func (g *Plugin) Encrypt(ctx context.Context, request *EncryptRequest) (*EncryptResponse, error) { glog.V(4).Infof("Processing request for encryption %s using %s", request.Uid, g.keyURI) defer plugin.RecordCloudKMSOperation("encrypt", time.Now().UTC()) resp, err := g.keyService.Encrypt(g.keyURI, &cloudkms.EncryptRequest{ Plaintext: base64.StdEncoding.EncodeToString(request.Plaintext), }).Context(ctx).Do() if err != nil { plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("encrypt").Inc() return nil, err } cipher, err := base64.StdEncoding.DecodeString(resp.Ciphertext) if err != nil { return nil, err } keyID := g.setKeyID(resp.Name) glog.V(4).Infof("Processed request for encryption %s using %s", request.Uid, keyID) return &EncryptResponse{ Ciphertext: cipher, KeyId: keyID, }, nil } // Decrypt decrypts payload supplied by K8S API Server. func (g *Plugin) Decrypt(ctx context.Context, request *DecryptRequest) (*DecryptResponse, error) { glog.V(4).Infof("Processing request for decryption %s using %s", request.Uid, request.KeyId) defer plugin.RecordCloudKMSOperation("decrypt", time.Now().UTC()) keyResourceName := g.keyURI if request.KeyId != "" { // request.KeyId is empty when health checker calls this method from PingKMS() keyResourceName = extractKeyName(request.KeyId) } resp, err := g.keyService.Decrypt(keyResourceName, &cloudkms.DecryptRequest{ Ciphertext: base64.StdEncoding.EncodeToString(request.Ciphertext), }).Context(ctx).Do() if err != nil { plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("decrypt").Inc() return nil, err } plain, err := base64.StdEncoding.DecodeString(resp.Plaintext) if err != nil { return nil, fmt.Errorf("failed to decode from base64, error: %w", err) } return &DecryptResponse{ Plaintext: plain, }, nil } // keyID is a threadsafe way to get the current key ID. func (g *Plugin) keyID() string { g.lastKeyIDLock.RLock() defer g.lastKeyIDLock.RUnlock() return g.lastKeyID } // If the key id suffix has been passed in the command line parameters this // function will return a key id value constructed from the Cloud KMS key // version with appended suffix separated by ":" // // This is to return a unique key id to Kubernetes in case if the plugin is // reconfigured to use a Cloud KMS key version which has been already in use // before func (g *Plugin) setKeyID(name string) string { result := name if v := g.keySuffix; v != "" { result = result + ":" + v } g.lastKeyIDLock.Lock() defer g.lastKeyIDLock.Unlock() g.lastKeyID = result return result } // Extracts the Cloud KMS key resource name from the key version resource name func extractKeyName(keyVersionId string) string { return keyResourceRegEx.FindString(keyVersionId) }