├── .gitignore ├── OWNERS ├── testutils ├── test_seal.sh ├── download_from_gcs.sh ├── upload_to_gcs.sh ├── kmspluginclient │ └── kmspluginclient.go ├── fakekubeapi │ └── fakekubeapi.go └── fakekms │ └── fakekms.go ├── Dockerfile ├── cloudbuild.yaml ├── Makefile ├── cmd ├── fakekms │ └── main.go ├── tpmunseal │ └── main.go ├── tpmseal │ └── main.go ├── fakekubeapi │ └── main.go └── k8s-cloudkms-plugin │ └── main.go ├── CONTRIBUTING.md ├── plugin ├── plugin_test.go ├── v1 │ ├── healthz.go │ ├── plugin.go │ ├── healthz_test.go │ ├── plugin_test.go │ └── service.pb.go ├── v2 │ ├── healthz.go │ ├── healthz_test.go │ ├── plugin.go │ ├── plugin_test.go │ └── api.pb.go ├── token_source.go ├── http_client_test.go ├── metrics.go ├── plugin.go ├── http_client.go └── healthz.go ├── go.mod ├── tpm └── tpm.go ├── README.md ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .ijwb 2 | .idea 3 | build/ 4 | .vscode/ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - immutablet 3 | - ihmccreery 4 | reviewers: 5 | - ihmccreery -------------------------------------------------------------------------------- /testutils/test_seal.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o xtrace 18 | set -o errexit 19 | set -o nounset 20 | 21 | SA_EXPORT=sa.json 22 | SA_OUT=cleartext.json 23 | 24 | ./tpmseal --path-to-plaintext="${SA_EXPORT}" 25 | ./tpmunseal --path-to-output "${SA_OUT}" 26 | cat "${SA_OUT}" 27 | -------------------------------------------------------------------------------- /testutils/download_from_gcs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o xtrace 18 | set -o errexit 19 | set -o nounset 20 | 21 | GCS_BUCKET='tpm-lab' 22 | 23 | gsutil cp gs://"${GCS_BUCKET}"/tpmseal . 24 | gsutil cp gs://"${GCS_BUCKET}"/tpmunseal . 25 | chmod +x tpmseal 26 | chmod +x tpmunseal 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google, LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM golang:1.22 AS builder 16 | 17 | ENV GO111MODULE=on \ 18 | CGO_ENABLED=0 \ 19 | GOOS=linux \ 20 | GOARCH=amd64 21 | 22 | WORKDIR /src 23 | COPY . . 24 | 25 | RUN make build 26 | 27 | 28 | 29 | FROM alpine:latest 30 | RUN apk --no-cache add ca-certificates && \ 31 | update-ca-certificates 32 | 33 | COPY --from=builder /src/build/k8s-cloudkms-plugin /bin/k8s-cloudkms-plugin 34 | ENTRYPOINT ["/bin/k8s-cloudkms-plugin"] 35 | -------------------------------------------------------------------------------- /testutils/upload_to_gcs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o xtrace 18 | set -o errexit 19 | set -o nounset 20 | 21 | BAZEL_BIN="$(bazel info bazel-bin)" 22 | GCS_BUCKET='tpm-lab' 23 | 24 | bazel build //cmd/...:all 25 | 26 | $HOME/google-cloud-sdk/bin/gsutil cp "${BAZEL_BIN}"/cmd/tpmseal/linux_amd64_stripped/tpmseal gs://"${GCS_BUCKET}" 27 | $HOME/google-cloud-sdk/bin/gsutil cp "${BAZEL_BIN}"/cmd/tpmunseal/linux_amd64_stripped/tpmunseal gs://"${GCS_BUCKET}" 28 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google, LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - id: test 17 | name: golang:1.22 18 | entrypoint: make 19 | args: ['test-acc'] 20 | 21 | - id: build 22 | name: golang:1.22 23 | entrypoint: make 24 | args: ['build'] 25 | waitFor: ['test'] 26 | 27 | - id: docker 28 | name: gcr.io/cloud-builders/docker 29 | entrypoint: /bin/bash 30 | args: 31 | - -c 32 | - |- 33 | REF="${TAG_NAME//v}" 34 | [[ -z "$${REF}" && "${BRANCH_NAME}" == "master" ]] && REF="latest" 35 | [[ -z "$${REF}" ]] && echo "not a tag or master branch" && exit 0 36 | docker build -t gcr.io/${PROJECT_ID}/k8s-cloudkms-plugin:$${REF} . && \ 37 | docker push gcr.io/${PROJECT_ID}/k8s-cloudkms-plugin:$${REF} 38 | waitFor: ['test'] 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google, LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | export GO111MODULE = on 16 | 17 | # build 18 | build: 19 | @GOOS=linux GOARCH=amd64 go build \ 20 | -trimpath \ 21 | -a \ 22 | -ldflags "-s -w -extldflags 'static'" \ 23 | -installsuffix cgo \ 24 | -tags netgo \ 25 | -o build/k8s-cloudkms-plugin \ 26 | ./cmd/k8s-cloudkms-plugin/... 27 | .PHONY: build 28 | 29 | # deps updates all dependencies to their latest version 30 | deps: 31 | @go get -u all ./... 32 | @go mod tidy 33 | .PHONY: deps 34 | 35 | # dev installs the plugin for local development 36 | dev: 37 | @go install -i ./cmd/k8s-cloudkms-plugin/... 38 | .PHONY: dev 39 | 40 | # test runs the tests 41 | test: 42 | @go test -shuffle=on -short ./... 43 | .PHONY: test 44 | 45 | # test runs the tests, including integration tests 46 | test-acc: 47 | @go test -count=1 -shuffle=on -race ./... 48 | .PHONY: test-acc 49 | -------------------------------------------------------------------------------- /cmd/fakekms/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Command fakekms simulates CloudKMS service - use only in integration tests. 16 | package main 17 | 18 | import ( 19 | "flag" 20 | "os" 21 | "os/signal" 22 | "syscall" 23 | 24 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/testutils/fakekms" 25 | "github.com/golang/glog" 26 | ) 27 | 28 | var ( 29 | port = flag.Int("port", 8085, "Port on which to listen.") 30 | keyName = flag.String("key-name", "", "Name of CloudKMS key.") 31 | ) 32 | 33 | func main() { 34 | if *keyName == "" { 35 | glog.Exit("key-name is a mandatory argument.") 36 | } 37 | 38 | s, err := fakekms.NewWithPipethrough(*keyName, *port) 39 | if err != nil { 40 | glog.Exitf("failed to start FakeKMS, error %v", err) 41 | } 42 | defer s.Close() 43 | glog.Infof("FakeKMS is listening on port: %d", *port) 44 | 45 | signalsChan := make(chan os.Signal, 1) 46 | signal.Notify(signalsChan, syscall.SIGINT, syscall.SIGTERM) 47 | 48 | sig := <-signalsChan 49 | glog.Exitf("captured %v, shutting down fakeKMS", sig) 50 | } 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement 9 | (CLA). 10 | 11 | * If you are an individual writing original source code and you're sure you 12 | own the intellectual property, then you'll need to sign an [individual CLA] 13 | (https://developers.google.com/open-source/cla/individual). 14 | * If you work for a company that wants to allow you to contribute your work, 15 | then you'll need to sign a [corporate CLA] 16 | (https://developers.google.com/open-source/cla/corporate). 17 | 18 | Follow either of the two links above to access the appropriate CLA and 19 | instructions for how to sign and return it. Once we receive it, we'll be able to 20 | accept your pull requests. 21 | 22 | ## Contributing A Patch 23 | 24 | 1. Submit an issue describing your proposed change to the repo in question. 25 | 1. The repo owner will respond to your issue promptly. 26 | 1. If your proposed change is accepted, and you haven't already done so, sign a 27 | Contributor License Agreement (see details above). 28 | 1. Fork the desired repo, develop and test your code changes. 29 | 1. Ensure that your code adheres to the existing style in the sample to which 30 | you are contributing. Refer to the 31 | [Google Cloud Platform Samples Style Guide] 32 | (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the 33 | recommended coding standards for this organization. 34 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 35 | 1. Submit a pull request. -------------------------------------------------------------------------------- /cmd/tpmunseal/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "io/ioutil" 20 | 21 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/tpm" 22 | "github.com/golang/glog" 23 | ) 24 | 25 | var ( 26 | pathToTPM = flag.String("path-to-tpm", "/dev/tpmrm0", "Path to tpm device or tpm resource manager.") 27 | pcrToMeasure = flag.Int("pcr-to-measure", 7, "PCR to measure.") 28 | out = flag.String("path-to-output", "", "Path to output.") 29 | pathToPrivateArea = flag.String("path-to-priv-area", "priv.bin", "Path to the private area.") 30 | pathToPublicArea = flag.String("path-to-pub-area", "pub.bin", "Path to the public area.") 31 | ) 32 | 33 | func main() { 34 | flag.Parse() 35 | 36 | privateArea, err := ioutil.ReadFile(*pathToPrivateArea) 37 | if err != nil { 38 | glog.Fatal(err) 39 | } 40 | 41 | publicArea, err := ioutil.ReadFile(*pathToPublicArea) 42 | if err != nil { 43 | glog.Fatal(err) 44 | } 45 | 46 | c, err := tpm.Unseal(*pathToTPM, *pcrToMeasure, "", "", privateArea, publicArea) 47 | if err != nil { 48 | glog.Fatal(err) 49 | } 50 | 51 | if err := ioutil.WriteFile(*out, c, 0644); err != nil { 52 | glog.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cmd/tpmseal/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "io/ioutil" 20 | 21 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/tpm" 22 | "github.com/golang/glog" 23 | ) 24 | 25 | var ( 26 | pathToTPM = flag.String("path-to-tpm", "/dev/tpmrm0", "Path to tpm device or tpm resource manager.") 27 | pcrToMeasure = flag.Int("pcr-to-measure", 7, "PCR to measure.") 28 | pathToPlaintext = flag.String("path-to-plaintext", "", "Data to seal.") 29 | privateAreaOutput = flag.String("path-to-priv-area", "priv.bin", "Path to where to place the private area.") 30 | publicAreaOutput = flag.String("path-to-pub-area", "pub.bin", "Path to where to place the public area.") 31 | ) 32 | 33 | func main() { 34 | flag.Parse() 35 | 36 | d, err := ioutil.ReadFile(*pathToPlaintext) 37 | if err != nil { 38 | glog.Fatal(err) 39 | } 40 | 41 | privateArea, publicArea, err := tpm.Seal(*pathToTPM, *pcrToMeasure, "", "", d) 42 | if err != nil { 43 | glog.Fatal(err) 44 | } 45 | 46 | if err := ioutil.WriteFile(*privateAreaOutput, privateArea, 0644); err != nil { 47 | glog.Fatal(err) 48 | } 49 | 50 | if err := ioutil.WriteFile(*publicAreaOutput, publicArea, 0600); err != nil { 51 | glog.Fatal(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugin 16 | 17 | import ( 18 | "errors" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | "time" 23 | 24 | "google.golang.org/grpc" 25 | ) 26 | 27 | type fakePlugin struct{} 28 | 29 | func (p *fakePlugin) Register(s *grpc.Server) {} 30 | 31 | func TestSocket(t *testing.T) { 32 | t.Parallel() 33 | 34 | dir, err := os.MkdirTemp(os.TempDir(), "") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | t.Cleanup(func() { 39 | if err := os.RemoveAll(dir); err != nil { 40 | t.Fatal(err) 41 | } 42 | }) 43 | f := filepath.Join(dir, "listener.sock") 44 | 45 | pluginManager := NewManager(&fakePlugin{}, f) 46 | server, errCh := pluginManager.Start() 47 | 48 | select { 49 | case err := <-errCh: 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | default: 54 | } 55 | 56 | fileInfo, err := os.Stat(f) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | if (fileInfo.Mode() & os.ModeSocket) != os.ModeSocket { 62 | t.Fatalf("got %v, wanted Srwxr-xr-x", fileInfo.Mode()) 63 | } 64 | 65 | server.GracefulStop() 66 | 67 | select { 68 | case err := <-errCh: 69 | if err != nil && !errors.Is(err, grpc.ErrServerStopped) { 70 | t.Fatal(err) 71 | } 72 | case <-time.After(5 * time.Second): 73 | } 74 | 75 | if _, err := os.Stat(f); err == nil { 76 | t.Fatal("expected socket to be cleaned-up by now") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /plugin/v1/healthz.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 22 | "github.com/golang/glog" 23 | grpc "google.golang.org/grpc" 24 | ) 25 | 26 | var _ plugin.HealthChecker = (*HealthChecker)(nil) 27 | 28 | type HealthChecker struct{} 29 | 30 | func NewHealthChecker() *HealthChecker { 31 | return &HealthChecker{} 32 | } 33 | 34 | func (h *HealthChecker) PingRPC(ctx context.Context, conn *grpc.ClientConn) error { 35 | client := NewKeyManagementServiceClient(conn) 36 | 37 | if _, err := client.Version(ctx, &VersionRequest{ 38 | Version: apiVersion, 39 | }); err != nil { 40 | return fmt.Errorf("failed to retrieve version from gRPC endpoint: %w", err) 41 | } 42 | 43 | glog.V(4).Infof("Successfully pinged gRPC") 44 | return nil 45 | } 46 | 47 | func (h *HealthChecker) PingKMS(ctx context.Context, conn *grpc.ClientConn) error { 48 | client := NewKeyManagementServiceClient(conn) 49 | 50 | encryptResponse, err := client.Encrypt(ctx, &EncryptRequest{ 51 | Version: apiVersion, 52 | Plain: []byte("secret"), 53 | }) 54 | if err != nil { 55 | return fmt.Errorf("failed to ping KMS: %w", err) 56 | } 57 | 58 | if _, err = client.Decrypt(ctx, &DecryptRequest{ 59 | Version: apiVersion, 60 | Cipher: []byte(encryptResponse.Cipher), 61 | }); err != nil { 62 | return fmt.Errorf("failed to ping KMS: %w", err) 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /plugin/v2/healthz.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 22 | "github.com/golang/glog" 23 | "github.com/google/uuid" 24 | grpc "google.golang.org/grpc" 25 | ) 26 | 27 | var _ plugin.HealthChecker = (*HealthChecker)(nil) 28 | 29 | type HealthChecker struct{} 30 | 31 | func NewHealthChecker() *HealthChecker { 32 | return &HealthChecker{} 33 | } 34 | 35 | func (h *HealthChecker) PingRPC(ctx context.Context, conn *grpc.ClientConn) error { 36 | client := NewKeyManagementServiceClient(conn) 37 | 38 | if _, err := client.Status(ctx, &StatusRequest{}); err != nil { 39 | return fmt.Errorf("failed to retrieve version from gRPC endpoint: %w", err) 40 | } 41 | 42 | glog.V(4).Infof("Successfully pinged gRPC") 43 | return nil 44 | } 45 | 46 | func (h *HealthChecker) PingKMS(ctx context.Context, conn *grpc.ClientConn) error { 47 | client := NewKeyManagementServiceClient(conn) 48 | 49 | encryptResponse, err := client.Encrypt(ctx, &EncryptRequest{ 50 | Uid: uuid.NewString(), 51 | Plaintext: []byte("secret"), 52 | }) 53 | if err != nil { 54 | return fmt.Errorf("failed to ping KMS: %w", err) 55 | } 56 | 57 | if _, err = client.Decrypt(ctx, &DecryptRequest{ 58 | Uid: uuid.NewString(), 59 | Ciphertext: []byte(encryptResponse.Ciphertext), 60 | }); err != nil { 61 | return fmt.Errorf("failed to ping KMS: %w", err) 62 | } 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /plugin/token_source.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Kubernetes Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package plugin 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | "net/http" 20 | "strings" 21 | "time" 22 | 23 | "golang.org/x/oauth2" 24 | "golang.org/x/oauth2/google" 25 | "google.golang.org/api/googleapi" 26 | ) 27 | 28 | // altTokenSource is the structure holding the data for the functionality needed to generates tokens 29 | type altTokenSource struct { 30 | oauthClient *http.Client 31 | tokenURL string 32 | tokenBody string 33 | } 34 | 35 | // Token returns a token which may be used for authentication 36 | func (a *altTokenSource) Token() (*oauth2.Token, error) { 37 | req, err := http.NewRequest("POST", a.tokenURL, strings.NewReader(a.tokenBody)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | res, err := a.oauthClient.Do(req) 42 | if err != nil { 43 | return nil, err 44 | } 45 | defer res.Body.Close() 46 | if err := googleapi.CheckResponse(res); err != nil { 47 | return nil, err 48 | } 49 | var tok struct { 50 | AccessToken string `json:"accessToken"` 51 | ExpireTime time.Time `json:"expireTime"` 52 | } 53 | if err := json.NewDecoder(res.Body).Decode(&tok); err != nil { 54 | return nil, err 55 | } 56 | return &oauth2.Token{ 57 | AccessToken: tok.AccessToken, 58 | Expiry: tok.ExpireTime, 59 | }, nil 60 | } 61 | 62 | // newAltTokenSource constructs a new alternate token source for generating tokens. 63 | func newAltTokenSource(ctx context.Context, tokenURL, tokenBody string) oauth2.TokenSource { 64 | return &altTokenSource{ 65 | oauthClient: oauth2.NewClient(ctx, google.ComputeTokenSource("")), 66 | tokenURL: tokenURL, 67 | tokenBody: tokenBody, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plugin/http_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugin 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | ) 21 | 22 | const gceConfOnGKE = `[global] 23 | token-url = https://my-project.googleapis.com/v1/masterProjects/722785932522/locations/us-central1-c/tokens 24 | token-body = "{\"projectNumber\":722785932522,\"clusterId\":\"sandbox\"}" 25 | project-id = iam-gke-custom-roles-master 26 | network-name = default 27 | subnetwork-name = default 28 | node-instance-prefix = gke-sandbox 29 | node-tags = gke-sandbox-ac7f9ea0-node` 30 | 31 | const gceConfOnGCE = `[global] 32 | project-id = alextc-k8s-lab 33 | network-project-id = alextc-k8s-lab 34 | network-name = default 35 | subnetwork-name = default 36 | node-instance-prefix = kubernetes-minion 37 | node-tags = kubernetes-minion` 38 | 39 | func TestExtractTokenConfigOnHostedMaster(t *testing.T) { 40 | t.Parallel() 41 | 42 | r := strings.NewReader(gceConfOnGKE) 43 | c, err := readConfig(r) 44 | if c == nil || err != nil { 45 | t.Fatalf("Failed to read gce.conf, err: %s", err) 46 | } 47 | 48 | if c.Global.TokenURL != "https://my-project.googleapis.com/v1/masterProjects/722785932522/locations/us-central1-c/tokens" { 49 | t.Fatalf("Failed to extract TokenURL") 50 | } 51 | 52 | if c.Global.TokenBody != "{\"projectNumber\":722785932522,\"clusterId\":\"sandbox\"}" { 53 | t.Fatalf("Failed to extract TokenBody") 54 | } 55 | } 56 | 57 | func TestShouldReturnNilOnGCEMaster(t *testing.T) { 58 | t.Parallel() 59 | 60 | r := strings.NewReader(gceConfOnGCE) 61 | c, err := readConfig(r) 62 | if c == nil { 63 | t.Fatalf("Failed to read gce.conf, err: %s", err) 64 | } 65 | 66 | if (tokenConfig{}) != *c { 67 | t.Fatalf("Alt Token Info should be empty on GCE.") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plugin/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugin 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | "net/url" 21 | "time" 22 | 23 | "github.com/golang/glog" 24 | 25 | "github.com/prometheus/client_golang/prometheus" 26 | "github.com/prometheus/client_golang/prometheus/promhttp" 27 | ) 28 | 29 | // Metrics encapsulates functionality related to serving Prometheus metrics for kms-plugin. 30 | type Metrics struct { 31 | ServingURL *url.URL 32 | } 33 | 34 | var ( 35 | CloudKMSOperationalLatencies = prometheus.NewHistogramVec( 36 | prometheus.HistogramOpts{ 37 | Name: "roundtrip_latencies", 38 | Help: "Latencies in milliseconds of cloud kms operations.", 39 | // When calling CloudKMS latencies may climb into milliseconds. 40 | Buckets: prometheus.ExponentialBuckets(5, 2, 14), 41 | }, 42 | []string{"operation_type"}, 43 | ) 44 | 45 | CloudKMSOperationalFailuresTotal = prometheus.NewCounterVec( 46 | prometheus.CounterOpts{ 47 | Name: "failures_count", 48 | Help: "Total number of failed kms operations.", 49 | }, 50 | []string{"operation_type"}, 51 | ) 52 | ) 53 | 54 | func init() { 55 | prometheus.MustRegister(CloudKMSOperationalLatencies) 56 | prometheus.MustRegister(CloudKMSOperationalFailuresTotal) 57 | } 58 | 59 | func RecordCloudKMSOperation(operationType string, start time.Time) { 60 | CloudKMSOperationalLatencies.WithLabelValues(operationType).Observe(sinceInMilliseconds(start)) 61 | } 62 | 63 | func sinceInMilliseconds(start time.Time) float64 { 64 | return float64(time.Since(start) / time.Millisecond) 65 | } 66 | 67 | // Serve creates http server for hosting Prometheus metrics. 68 | func (m *Metrics) Serve() chan error { 69 | errorChan := make(chan error) 70 | mux := http.NewServeMux() 71 | mux.Handle(fmt.Sprintf("/%s", m.ServingURL.EscapedPath()), promhttp.Handler()) 72 | 73 | go func() { 74 | defer close(errorChan) 75 | glog.Infof("Registering Metrics listener on port %s", m.ServingURL.Port()) 76 | errorChan <- http.ListenAndServe(m.ServingURL.Host, mux) 77 | }() 78 | 79 | return errorChan 80 | } 81 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/k8s-cloudkms-plugin 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/gogo/protobuf v1.3.2 9 | github.com/golang/glog v1.2.4 10 | github.com/golang/protobuf v1.5.3 11 | github.com/google/go-cmp v0.6.0 12 | github.com/google/go-tpm v0.9.0 13 | github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 14 | github.com/prometheus/client_golang v1.19.0 15 | github.com/prometheus/client_model v0.6.0 16 | github.com/stretchr/testify v1.9.0 17 | golang.org/x/net v0.33.0 18 | golang.org/x/oauth2 v0.27.0 19 | google.golang.org/api v0.167.0 20 | google.golang.org/grpc v1.62.0 21 | gopkg.in/gcfg.v1 v1.2.3 22 | k8s.io/api v0.29.2 23 | k8s.io/apimachinery v0.29.2 24 | ) 25 | 26 | require ( 27 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 30 | github.com/felixge/httpsnoop v1.0.4 // indirect 31 | github.com/go-logr/logr v1.4.1 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 34 | github.com/google/gofuzz v1.2.0 // indirect 35 | github.com/google/s2a-go v0.1.7 // indirect 36 | github.com/google/uuid v1.6.0 37 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 38 | github.com/googleapis/gax-go/v2 v2.12.2 // indirect 39 | github.com/json-iterator/go v1.1.12 // indirect 40 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 41 | github.com/modern-go/reflect2 v1.0.2 // indirect 42 | github.com/prometheus/common v0.49.0 // indirect 43 | github.com/prometheus/procfs v0.12.0 // indirect 44 | go.opencensus.io v0.24.0 // indirect 45 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 46 | go.opentelemetry.io/otel v1.24.0 // indirect 47 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 48 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 49 | golang.org/x/crypto v0.35.0 // indirect 50 | golang.org/x/sys v0.30.0 // indirect 51 | golang.org/x/text v0.22.0 // indirect 52 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect 53 | google.golang.org/protobuf v1.33.0 // indirect 54 | gopkg.in/inf.v0 v0.9.1 // indirect 55 | gopkg.in/warnings.v0 v0.1.2 // indirect 56 | gopkg.in/yaml.v2 v2.4.0 // indirect 57 | k8s.io/klog/v2 v2.120.1 // indirect 58 | k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect 59 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 60 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 61 | ) 62 | 63 | require ( 64 | github.com/davecgh/go-spew v1.1.1 // indirect 65 | github.com/pmezard/go-difflib v1.0.0 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package plugin implements CloudKMS plugin for GKE as described in go/gke-secrets-encryption-design. 16 | package plugin 17 | 18 | import ( 19 | "fmt" 20 | "net" 21 | "os" 22 | "strings" 23 | 24 | "github.com/golang/glog" 25 | "google.golang.org/grpc" 26 | ) 27 | 28 | const ( 29 | netProtocol = "unix" 30 | ) 31 | 32 | // Plugin is a CloudKMS plugin for K8S. 33 | type Plugin interface { 34 | Register(s *grpc.Server) 35 | } 36 | 37 | type PluginManager struct { 38 | unixSocketFilePath string 39 | 40 | // Embedding these only to shorten access to fields. 41 | net.Listener 42 | server *grpc.Server 43 | 44 | plugin Plugin 45 | } 46 | 47 | // NewManager creates a new plugin manager. 48 | func NewManager(plugin Plugin, unixSocketFilePath string) *PluginManager { 49 | return &PluginManager{ 50 | unixSocketFilePath: unixSocketFilePath, 51 | plugin: plugin, 52 | } 53 | } 54 | 55 | // ServeKMSRequests starts gRPC server or dies. 56 | func (m *PluginManager) Start() (*grpc.Server, <-chan error) { 57 | errCh := make(chan error, 1) 58 | sendError := func(err error) { 59 | defer close(errCh) 60 | select { 61 | case errCh <- err: 62 | default: 63 | } 64 | } 65 | 66 | if err := m.cleanSockFile(); err != nil { 67 | sendError(fmt.Errorf("failed to cleanup socket file: %w", err)) 68 | return nil, errCh 69 | } 70 | 71 | listener, err := net.Listen(netProtocol, m.unixSocketFilePath) 72 | if err != nil { 73 | sendError(fmt.Errorf("failed to create listener: %w", err)) 74 | return nil, errCh 75 | } 76 | m.Listener = listener 77 | glog.Infof("Listening on unix domain socket: %s", m.unixSocketFilePath) 78 | 79 | m.server = grpc.NewServer() 80 | m.plugin.Register(m.server) 81 | 82 | go func() { 83 | defer m.cleanSockFile() 84 | sendError(m.server.Serve(m.Listener)) 85 | }() 86 | 87 | return m.server, errCh 88 | } 89 | 90 | func (m *PluginManager) cleanSockFile() error { 91 | // @ implies the use of Linux socket namespace - no file on disk and nothing to clean-up. 92 | if strings.HasPrefix(m.unixSocketFilePath, "@") { 93 | return nil 94 | } 95 | 96 | err := os.Remove(m.unixSocketFilePath) 97 | if err != nil && !os.IsNotExist(err) { 98 | return fmt.Errorf("failed to delete the socket file, error: %w", err) 99 | } 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /plugin/http_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugin 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "net/http" 22 | "os" 23 | 24 | "github.com/golang/glog" 25 | 26 | "golang.org/x/oauth2" 27 | "golang.org/x/oauth2/google" 28 | 29 | "google.golang.org/api/cloudkms/v1" 30 | 31 | "gopkg.in/gcfg.v1" 32 | ) 33 | 34 | // tokenConfig represents attributes found in gce.conf - only attributes of the interest of this plugin are listed. 35 | type tokenConfig struct { 36 | Global struct { 37 | TokenURL string `gcfg:"token-url"` 38 | TokenBody string `gcfg:"token-body"` 39 | } 40 | } 41 | 42 | func NewHTTPClient(ctx context.Context, pathToGCEConf string) (*http.Client, error) { 43 | if pathToGCEConf != "" { 44 | r, err := os.Open(pathToGCEConf) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to open GCE Config: %s", pathToGCEConf) 47 | } 48 | defer r.Close() 49 | 50 | c, err := readConfig(r) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if (tokenConfig{} == *c) { 56 | glog.Infof("Since TokenConfig contains neither TokenURI nor TokenBody assuming that running on GCE (ex. via kube-up.sh)") 57 | return getDefaultClient(ctx) 58 | } 59 | 60 | // Running on GKE Hosted Master 61 | glog.Infof("TokenURI:%s, TokenBody:%s - assuming that running on a Hosted Master - GKE.", c.Global.TokenURL, c.Global.TokenBody) 62 | a := newAltTokenSource(ctx, c.Global.TokenURL, c.Global.TokenBody) 63 | 64 | // TODO: Do I need to call a.Token to get access token here? 65 | if _, err := a.Token(); err != nil { 66 | glog.Errorf("error fetching initial token: %v", err) 67 | return nil, err 68 | } 69 | 70 | return oauth2.NewClient(ctx, a), nil 71 | } 72 | 73 | glog.Infof("Path to gce.conf was not supplied - assuming that need to rely on exported service account key.") 74 | return getDefaultClient(ctx) 75 | } 76 | 77 | func readConfig(reader io.Reader) (*tokenConfig, error) { 78 | cfg := &tokenConfig{} 79 | if err := gcfg.FatalOnly(gcfg.ReadInto(cfg, reader)); err != nil { 80 | glog.Errorf("Couldn't read GCE Config: %v", err) 81 | return nil, err 82 | } 83 | return cfg, nil 84 | } 85 | 86 | func getDefaultClient(ctx context.Context) (*http.Client, error) { 87 | client, err := google.DefaultClient(ctx, cloudkms.CloudPlatformScope) 88 | if err != nil { 89 | return nil, fmt.Errorf("failed to instantiate cloud sdk client: %v", err) 90 | } 91 | return client, nil 92 | } 93 | -------------------------------------------------------------------------------- /testutils/kmspluginclient/kmspluginclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pluginclient contains logic for interacting with K8S KMS Plugin. 16 | package kmspluginclient 17 | 18 | import ( 19 | "fmt" 20 | "net" 21 | "net/url" 22 | "strings" 23 | "time" 24 | 25 | plugin "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin/v1" 26 | "google.golang.org/grpc" 27 | ) 28 | 29 | // Client interacts with KMS Plugin via gRPC. 30 | type Client struct { 31 | plugin.KeyManagementServiceClient 32 | connection *grpc.ClientConn 33 | } 34 | 35 | // Close closes the underlying gRPC connection to KMS Plugin. 36 | func (k *Client) Close() { 37 | k.connection.Close() 38 | } 39 | 40 | // New constructs Client. 41 | func New(endpoint string) (*Client, error) { 42 | addr, err := parseEndpoint(endpoint) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | connection, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.FailFast(false)), grpc.WithDialer( 48 | func(string, time.Duration) (net.Conn, error) { 49 | // Ignoring addr and timeout arguments: 50 | // addr - comes from the closure 51 | // timeout - is ignored since we are connecting in a non-blocking configuration 52 | c, err := net.DialTimeout("unix", addr, 0) 53 | if err != nil { 54 | return nil, fmt.Errorf("failed to create connection to unix socket: %s, error: %v", addr, err) 55 | } 56 | return c, nil 57 | })) 58 | 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to create connection to %s, error: %v", addr, err) 61 | } 62 | 63 | kmsClient := plugin.NewKeyManagementServiceClient(connection) 64 | return &Client{ 65 | KeyManagementServiceClient: kmsClient, 66 | connection: connection, 67 | }, nil 68 | } 69 | 70 | // Parse the endpoint to extract schema, host or path. 71 | func parseEndpoint(endpoint string) (string, error) { 72 | if len(endpoint) == 0 { 73 | return "", fmt.Errorf("remote KMS provider can't use empty string as endpoint") 74 | } 75 | 76 | u, err := url.Parse(endpoint) 77 | if err != nil { 78 | return "", fmt.Errorf("invalid endpoint %q for remote KMS provider, error: %v", endpoint, err) 79 | } 80 | 81 | if u.Scheme != "unix" { 82 | return "", fmt.Errorf("unsupported scheme %q for remote KMS provider", u.Scheme) 83 | } 84 | 85 | // Linux abstract namespace socket - no physical file required 86 | // Warning: Linux Abstract sockets have not concept of ACL (unlike traditional file based sockets). 87 | // However, Linux Abstract sockets are subject to Linux networking namespace, so will only be accessible to 88 | // containers within the same pod (unless host networking is used). 89 | if strings.HasPrefix(u.Path, "/@") { 90 | return strings.TrimPrefix(u.Path, "/"), nil 91 | } 92 | 93 | return u.Path, nil 94 | } 95 | -------------------------------------------------------------------------------- /cmd/fakekubeapi/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Command fake-kube-apiserver simulates k8s kube-apiserver - use only in integration tests. 16 | package main 17 | 18 | import ( 19 | "flag" 20 | "fmt" 21 | "os" 22 | "os/signal" 23 | "syscall" 24 | "time" 25 | 26 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/testutils/fakekubeapi" 27 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/testutils/kmspluginclient" 28 | "github.com/golang/glog" 29 | 30 | corev1 "k8s.io/api/core/v1" 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | ) 33 | 34 | var ( 35 | port = flag.Int("port", 8080, "Port on which to listen.") 36 | timeout = flag.Duration("kms-plugin-timeout", 3*time.Second, "Timeout for calls to kms-plugin in seconds.") 37 | kmsSocketPath = flag.String("path-to-kms-socket", "", "Path to Unix Domain socket for communicating with KMS Plugin.") 38 | 39 | // TODO(immutableT) Supply option to consume resources from yaml files. 40 | namespaces = corev1.NamespaceList{ 41 | Items: []corev1.Namespace{ 42 | { 43 | ObjectMeta: metav1.ObjectMeta{ 44 | Name: "default", 45 | }, 46 | }, 47 | { 48 | ObjectMeta: metav1.ObjectMeta{ 49 | Name: "system", 50 | }, 51 | }, 52 | }, 53 | } 54 | 55 | secrets = map[string][]corev1.Secret{ 56 | "default": { 57 | { 58 | TypeMeta: metav1.TypeMeta{ 59 | Kind: "Secret", 60 | APIVersion: "v1", 61 | }, 62 | ObjectMeta: metav1.ObjectMeta{ 63 | Name: "my-default-secret", 64 | Namespace: "default", 65 | }, 66 | Data: map[string][]byte{"token": []byte("fakeTokenDefault")}, 67 | }, 68 | }, 69 | "system": { 70 | { 71 | TypeMeta: metav1.TypeMeta{ 72 | Kind: "Secret", 73 | APIVersion: "v1", 74 | }, 75 | ObjectMeta: metav1.ObjectMeta{ 76 | Name: "my-system-secret", 77 | Namespace: "system", 78 | }, 79 | Data: map[string][]byte{"token": []byte("fakeTokenSystem")}, 80 | }, 81 | }, 82 | } 83 | ) 84 | 85 | func main() { 86 | if *kmsSocketPath == "" { 87 | glog.Exitln("path-to-kms-socket is mandatory argument") 88 | } 89 | 90 | k, err := kmspluginclient.New(fmt.Sprintf("unix://%s", *kmsSocketPath)) 91 | if err != nil { 92 | glog.Exitf("Failed to initialize KMS Client, error: %v", err) 93 | } 94 | 95 | s, err := fakekubeapi.New(namespaces, secrets, *port, k, *timeout) 96 | if err != nil { 97 | glog.Exitf("failed to start fake kube-apiserver, error %v", err) 98 | } 99 | defer s.Close() 100 | glog.Infof("kube-apiserver %v is listening on port: %d", s.URL(), *port) 101 | 102 | signalsChan := make(chan os.Signal, 1) 103 | signal.Notify(signalsChan, syscall.SIGINT, syscall.SIGTERM) 104 | 105 | sig := <-signalsChan 106 | glog.Exitf("captured %v, shutting down fake kube-apiserver", sig) 107 | } 108 | -------------------------------------------------------------------------------- /plugin/v1/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Implementation of the KMS Plugin API v1. 16 | package v1 17 | 18 | import ( 19 | "context" 20 | "encoding/base64" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/golang/glog" 25 | 26 | "google.golang.org/api/cloudkms/v1" 27 | grpc "google.golang.org/grpc" 28 | 29 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 30 | ) 31 | 32 | const ( 33 | apiVersion = "v1beta1" 34 | runtimeName = "CloudKMS" 35 | runtimeVersion = "0.0.1" 36 | ) 37 | 38 | var _ plugin.Plugin = (*Plugin)(nil) 39 | 40 | // Plugin is the v1 implementation of a plugin. 41 | type Plugin struct { 42 | keyService *cloudkms.ProjectsLocationsKeyRingsCryptoKeysService 43 | keyURI string 44 | } 45 | 46 | // NewPlugin creates a new v1 plugin 47 | func NewPlugin(keyService *cloudkms.ProjectsLocationsKeyRingsCryptoKeysService, keyURI string) *Plugin { 48 | return &Plugin{ 49 | keyService: keyService, 50 | keyURI: keyURI, 51 | } 52 | } 53 | 54 | // Register registers the plugin as a service management service. 55 | func (g *Plugin) Register(s *grpc.Server) { 56 | RegisterKeyManagementServiceServer(s, g) 57 | } 58 | 59 | // Version returns the version of KMS Plugin. 60 | func (g *Plugin) Version(ctx context.Context, request *VersionRequest) (*VersionResponse, error) { 61 | return &VersionResponse{ 62 | Version: apiVersion, 63 | RuntimeName: runtimeName, 64 | RuntimeVersion: runtimeVersion, 65 | }, nil 66 | } 67 | 68 | // Encrypt encrypts payload provided by K8S API Server. 69 | func (g *Plugin) Encrypt(ctx context.Context, request *EncryptRequest) (*EncryptResponse, error) { 70 | glog.V(4).Infoln("Processing request for encryption.") 71 | defer plugin.RecordCloudKMSOperation("encrypt", time.Now().UTC()) 72 | 73 | resp, err := g.keyService.Encrypt(g.keyURI, &cloudkms.EncryptRequest{ 74 | Plaintext: base64.StdEncoding.EncodeToString(request.Plain), 75 | }).Context(ctx).Do() 76 | if err != nil { 77 | plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("encrypt").Inc() 78 | return nil, err 79 | } 80 | 81 | cipher, err := base64.StdEncoding.DecodeString(resp.Ciphertext) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return &EncryptResponse{ 87 | Cipher: cipher, 88 | }, nil 89 | } 90 | 91 | // Decrypt decrypts payload supplied by K8S API Server. 92 | func (g *Plugin) Decrypt(ctx context.Context, request *DecryptRequest) (*DecryptResponse, error) { 93 | glog.V(4).Infoln("Processing request for decryption.") 94 | defer plugin.RecordCloudKMSOperation("decrypt", time.Now().UTC()) 95 | 96 | resp, err := g.keyService.Decrypt(g.keyURI, &cloudkms.DecryptRequest{ 97 | Ciphertext: base64.StdEncoding.EncodeToString(request.Cipher), 98 | }).Context(ctx).Do() 99 | if err != nil { 100 | plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("decrypt").Inc() 101 | return nil, err 102 | } 103 | 104 | plain, err := base64.StdEncoding.DecodeString(resp.Plaintext) 105 | if err != nil { 106 | return nil, fmt.Errorf("failed to decode from base64, error: %w", err) 107 | } 108 | 109 | return &DecryptResponse{ 110 | Plain: plain, 111 | }, nil 112 | } 113 | -------------------------------------------------------------------------------- /plugin/v1/healthz_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | "net/http" 22 | "net/url" 23 | "testing" 24 | "time" 25 | 26 | "google.golang.org/api/cloudkms/v1" 27 | "google.golang.org/api/googleapi" 28 | 29 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 30 | "github.com/phayes/freeport" 31 | ) 32 | 33 | func TestHealthzServer(t *testing.T) { 34 | t.Parallel() 35 | 36 | var ( 37 | positiveTestIAMResponse = &cloudkms.TestIamPermissionsResponse{ 38 | Permissions: []string{ 39 | "cloudkms.cryptoKeyVersions.useToDecrypt", 40 | "cloudkms.cryptoKeyVersions.useToEncrypt", 41 | }, 42 | ServerResponse: googleapi.ServerResponse{ 43 | HTTPStatusCode: http.StatusOK, 44 | }, 45 | } 46 | negativeTestIAMResponse = &cloudkms.TestIamPermissionsResponse{ 47 | Permissions: []string{}, 48 | ServerResponse: googleapi.ServerResponse{ 49 | HTTPStatusCode: http.StatusOK, 50 | }, 51 | } 52 | ) 53 | 54 | testCases := []struct { 55 | desc string 56 | query string 57 | response []json.Marshaler 58 | want int 59 | }{ 60 | { 61 | desc: "Positive response for TestIAM, not pinging CloudKMS", 62 | response: []json.Marshaler{positiveTestIAMResponse}, 63 | want: http.StatusOK, 64 | }, 65 | { 66 | desc: "Negative response for TestIAM, not pinging CloudKMS", 67 | response: []json.Marshaler{negativeTestIAMResponse}, 68 | want: http.StatusForbidden, 69 | }, 70 | { 71 | desc: "Positive response for TestIAM, Positive ping from CloudKMS", 72 | query: "ping-kms=true", 73 | response: []json.Marshaler{positiveTestIAMResponse, positiveEncryptResponse, positiveDecryptResponse}, 74 | want: http.StatusOK, 75 | }, 76 | { 77 | desc: "Positive response for TestIAM, Negative ping from CloudKMS", 78 | query: "ping-kms=true", 79 | response: []json.Marshaler{positiveTestIAMResponse, negativeEncryptResponse}, 80 | want: http.StatusServiceUnavailable, 81 | }, 82 | } 83 | 84 | for _, testCase := range testCases { 85 | t.Run(testCase.desc, func(t *testing.T) { 86 | t.Parallel() 87 | 88 | tt := setUpWithResponses(t, keyName, 0, testCase.response...) 89 | t.Cleanup(func() { 90 | tt.tearDown() 91 | }) 92 | 93 | // Ensure that serving both Metrics and Healthz 94 | mustServeMetrics(t) 95 | 96 | healthzPort := mustServeHealthz(t, tt) 97 | 98 | u := url.URL{ 99 | Scheme: "http", 100 | Host: fmt.Sprintf("localhost:%d", healthzPort), 101 | Path: "healthz", 102 | RawQuery: testCase.query, 103 | } 104 | gotStatus, gotBody := mustGetHealthz(t, u) 105 | if gotStatus != testCase.want { 106 | t.Fatalf("Got %d for healthz status, want %d, response: %q", gotStatus, testCase.want, gotBody) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func mustGetHealthz(t *testing.T, url url.URL) (int, []byte) { 113 | t.Helper() 114 | req, err := http.NewRequest(http.MethodGet, url.String(), nil) 115 | if err != nil { 116 | t.Fatalf("Unable to create http request: %v", err) 117 | } 118 | 119 | resp, err := http.DefaultClient.Do(req) 120 | if err != nil { 121 | t.Fatalf("Unable to contact healthz endpoint of master: %v", err) 122 | } 123 | t.Cleanup(func() { resp.Body.Close() }) 124 | b, err := io.ReadAll(resp.Body) 125 | if err != nil { 126 | t.Fatalf("Failed to read the Body of request for %v, error %v", url, err) 127 | } 128 | return resp.StatusCode, b 129 | } 130 | 131 | func mustServeHealthz(t *testing.T, tt *pluginTestCase) int { 132 | t.Helper() 133 | 134 | p, err := freeport.GetFreePort() 135 | if err != nil { 136 | t.Fatalf("Failed to allocate a free port for healthz server, err: %v", err) 137 | } 138 | 139 | u := &url.URL{ 140 | Host: fmt.Sprintf("localhost:%d", p), 141 | Path: "healthz", 142 | } 143 | 144 | healthChecker := NewHealthChecker() 145 | healthCheckerManager := plugin.NewHealthChecker(healthChecker, tt.plugin.keyURI, tt.plugin.keyService, tt.socket, 5*time.Second, u) 146 | 147 | c := healthCheckerManager.Serve() 148 | 149 | // Giving some time for healthz server to start while listening on the error channel. 150 | select { 151 | case err := <-c: 152 | t.Fatalf("received an error while starting healthz server, error channel: %v", err) 153 | case <-time.After(3 * time.Second): 154 | } 155 | 156 | return p 157 | } 158 | -------------------------------------------------------------------------------- /plugin/v2/healthz_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "encoding/json" 19 | fmt "fmt" 20 | "io" 21 | "net" 22 | "net/http" 23 | "net/url" 24 | "strconv" 25 | "testing" 26 | "time" 27 | 28 | "google.golang.org/api/cloudkms/v1" 29 | "google.golang.org/api/googleapi" 30 | 31 | "github.com/phayes/freeport" 32 | 33 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 34 | ) 35 | 36 | func TestHealthzServer(t *testing.T) { 37 | t.Parallel() 38 | 39 | var ( 40 | positiveTestIAMResponse = &cloudkms.TestIamPermissionsResponse{ 41 | Permissions: []string{ 42 | "cloudkms.cryptoKeyVersions.useToDecrypt", 43 | "cloudkms.cryptoKeyVersions.useToEncrypt", 44 | }, 45 | ServerResponse: googleapi.ServerResponse{ 46 | HTTPStatusCode: http.StatusOK, 47 | }, 48 | } 49 | negativeTestIAMResponse = &cloudkms.TestIamPermissionsResponse{ 50 | Permissions: []string{}, 51 | ServerResponse: googleapi.ServerResponse{ 52 | HTTPStatusCode: http.StatusOK, 53 | }, 54 | } 55 | ) 56 | 57 | testCases := []struct { 58 | desc string 59 | query string 60 | response []json.Marshaler 61 | want int 62 | keySuffix string 63 | }{ 64 | { 65 | desc: "Positive response for TestIAM, not pinging CloudKMS", 66 | response: []json.Marshaler{positiveTestIAMResponse}, 67 | want: http.StatusOK, 68 | }, 69 | { 70 | desc: "Negative response for TestIAM, not pinging CloudKMS", 71 | response: []json.Marshaler{negativeTestIAMResponse}, 72 | want: http.StatusForbidden, 73 | keySuffix: "test", 74 | }, 75 | { 76 | desc: "Positive response for TestIAM, Positive ping from CloudKMS", 77 | query: "ping-kms=true", 78 | response: []json.Marshaler{positiveTestIAMResponse, positiveEncryptResponse, positiveDecryptResponse}, 79 | want: http.StatusOK, 80 | }, 81 | { 82 | desc: "Positive response for TestIAM, Negative ping from CloudKMS", 83 | query: "ping-kms=true", 84 | response: []json.Marshaler{positiveTestIAMResponse, negativeEncryptResponse}, 85 | want: http.StatusServiceUnavailable, 86 | }, 87 | } 88 | 89 | for _, testCase := range testCases { 90 | t.Run(testCase.desc, func(t *testing.T) { 91 | t.Parallel() 92 | 93 | tt := setUpWithResponses(t, keyName, testCase.keySuffix, 0, testCase.response...) 94 | t.Cleanup(func() { 95 | tt.tearDown() 96 | }) 97 | 98 | // Ensure that serving both Metrics and Healthz 99 | mustServeMetrics(t) 100 | 101 | healthzPort := mustServeHealthz(t, tt) 102 | 103 | u := url.URL{ 104 | Scheme: "http", 105 | Host: net.JoinHostPort("localhost", strconv.FormatUint(uint64(healthzPort), 10)), 106 | Path: "healthz", 107 | RawQuery: testCase.query, 108 | } 109 | gotStatus, gotBody := mustGetHealthz(t, u) 110 | if gotStatus != testCase.want { 111 | t.Fatalf("Got %d for healthz status, want %d, response: %q", gotStatus, testCase.want, gotBody) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func mustGetHealthz(t *testing.T, url url.URL) (int, []byte) { 118 | t.Helper() 119 | req, err := http.NewRequest(http.MethodGet, url.String(), nil) 120 | if err != nil { 121 | t.Fatalf("Unable to create http request: %v", err) 122 | } 123 | 124 | resp, err := http.DefaultClient.Do(req) 125 | if err != nil { 126 | t.Fatalf("Unable to contact healthz endpoint of master: %v", err) 127 | } 128 | t.Cleanup(func() { resp.Body.Close() }) 129 | b, err := io.ReadAll(resp.Body) 130 | if err != nil { 131 | t.Fatalf("Failed to read the Body of request for %v, error %v", url, err) 132 | } 133 | return resp.StatusCode, b 134 | } 135 | 136 | func mustServeHealthz(t *testing.T, tt *pluginTestCase) int { 137 | t.Helper() 138 | 139 | p, err := freeport.GetFreePort() 140 | if err != nil { 141 | t.Fatalf("Failed to allocate a free port for healthz server, err: %v", err) 142 | } 143 | 144 | u := &url.URL{ 145 | Host: fmt.Sprintf("localhost:%d", p), 146 | Path: "healthz", 147 | } 148 | 149 | healthChecker := NewHealthChecker() 150 | healthCheckerManager := plugin.NewHealthChecker(healthChecker, tt.plugin.keyURI, tt.plugin.keyService, tt.socket, 5*time.Second, u) 151 | 152 | c := healthCheckerManager.Serve() 153 | 154 | // Giving some time for healthz server to start while listening on the error channel. 155 | select { 156 | case err := <-c: 157 | t.Fatalf("received an error while starting healthz server, error channel: %v", err) 158 | case <-time.After(3 * time.Second): 159 | } 160 | 161 | return p 162 | } 163 | -------------------------------------------------------------------------------- /plugin/healthz.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugin 16 | 17 | import ( 18 | "net/url" 19 | "time" 20 | 21 | "context" 22 | "fmt" 23 | 24 | kmspb "google.golang.org/api/cloudkms/v1" 25 | "google.golang.org/grpc/credentials/insecure" 26 | "k8s.io/apimachinery/pkg/util/sets" 27 | 28 | "net" 29 | "net/http" 30 | 31 | "github.com/golang/glog" 32 | "google.golang.org/grpc" 33 | ) 34 | 35 | // HealthCheckerManager types that encapsulates healthz functionality of kms-plugin. 36 | // The following health checks are performed: 37 | // 1. Getting version of the plugin - validates gRPC connectivity. 38 | // 2. Asserting that the caller has encrypt and decrypt permissions on the crypto key. 39 | type HealthCheckerManager struct { 40 | keyName string 41 | KeyService *kmspb.ProjectsLocationsKeyRingsCryptoKeysService 42 | unixSocketPath string 43 | callTimeout time.Duration 44 | servingURL *url.URL 45 | 46 | plugin HealthChecker 47 | } 48 | 49 | type HealthChecker interface { 50 | PingRPC(context.Context, *grpc.ClientConn) error 51 | PingKMS(context.Context, *grpc.ClientConn) error 52 | } 53 | 54 | func NewHealthChecker(plugin HealthChecker, keyName string, keyService *kmspb.ProjectsLocationsKeyRingsCryptoKeysService, 55 | unixSocketPath string, callTimeout time.Duration, servingURL *url.URL) *HealthCheckerManager { 56 | 57 | return &HealthCheckerManager{ 58 | keyName: keyName, 59 | KeyService: keyService, 60 | unixSocketPath: unixSocketPath, 61 | callTimeout: callTimeout, 62 | servingURL: servingURL, 63 | plugin: plugin, 64 | } 65 | } 66 | 67 | // Serve creates http server for hosting healthz. 68 | func (m *HealthCheckerManager) Serve() chan error { 69 | errorCh := make(chan error) 70 | mux := http.NewServeMux() 71 | mux.HandleFunc(fmt.Sprintf("/%s", m.servingURL.EscapedPath()), m.HandlerFunc) 72 | 73 | go func() { 74 | defer close(errorCh) 75 | glog.Infof("Registering healthz listener at %v", m.servingURL) 76 | select { 77 | case errorCh <- http.ListenAndServe(m.servingURL.Host, mux): 78 | default: 79 | } 80 | }() 81 | 82 | return errorCh 83 | } 84 | 85 | func (m *HealthCheckerManager) HandlerFunc(w http.ResponseWriter, r *http.Request) { 86 | ctx, cancel := context.WithTimeout(r.Context(), m.callTimeout) 87 | defer cancel() 88 | 89 | conn, err := dialUnix(m.unixSocketPath) 90 | if err != nil { 91 | http.Error(w, err.Error(), http.StatusServiceUnavailable) 92 | return 93 | } 94 | defer conn.Close() 95 | 96 | if err := m.plugin.PingRPC(ctx, conn); err != nil { 97 | http.Error(w, err.Error(), http.StatusServiceUnavailable) 98 | return 99 | } 100 | 101 | if err := m.TestIAMPermissions(); err != nil { 102 | http.Error(w, err.Error(), http.StatusForbidden) 103 | return 104 | } 105 | 106 | if r.FormValue("ping-kms") == "true" { 107 | if err := m.plugin.PingKMS(ctx, conn); err != nil { 108 | http.Error(w, err.Error(), http.StatusServiceUnavailable) 109 | return 110 | } 111 | } 112 | 113 | w.WriteHeader(http.StatusOK) 114 | w.Write([]byte("ok")) 115 | } 116 | 117 | func (h *HealthCheckerManager) TestIAMPermissions() error { 118 | want := sets.NewString("cloudkms.cryptoKeyVersions.useToEncrypt", "cloudkms.cryptoKeyVersions.useToDecrypt") 119 | glog.Infof("Testing IAM permissions, want %v", want.List()) 120 | 121 | req := &kmspb.TestIamPermissionsRequest{ 122 | Permissions: want.List(), 123 | } 124 | 125 | resp, err := h.KeyService.TestIamPermissions(h.keyName, req).Do() 126 | if err != nil { 127 | return fmt.Errorf("failed to test IAM Permissions on %s, %v", h.keyName, err) 128 | } 129 | glog.Infof("Got permissions: %v from CloudKMS for key:%s", resp.Permissions, h.keyName) 130 | 131 | got := sets.NewString(resp.Permissions...) 132 | diff := want.Difference(got) 133 | 134 | if diff.Len() != 0 { 135 | glog.Errorf("Failed to validate IAM Permissions on %s, diff: %v", h.keyName, diff) 136 | return fmt.Errorf("missing %v IAM permissions on CryptoKey:%s", diff, h.keyName) 137 | } 138 | 139 | glog.Infof("Successfully validated IAM Permissions on %s.", h.keyName) 140 | return nil 141 | } 142 | 143 | func dialUnix(unixSocketPath string) (*grpc.ClientConn, error) { 144 | protocol, addr := "unix", unixSocketPath 145 | dialer := func(ctx context.Context, addr string) (net.Conn, error) { 146 | if deadline, ok := ctx.Deadline(); ok { 147 | return net.DialTimeout(protocol, addr, time.Until(deadline)) 148 | } 149 | return net.DialTimeout(protocol, addr, 0) 150 | } 151 | 152 | return grpc.Dial(addr, 153 | grpc.WithTransportCredentials(insecure.NewCredentials()), 154 | grpc.WithContextDialer(dialer)) 155 | } 156 | -------------------------------------------------------------------------------- /cmd/k8s-cloudkms-plugin/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Binary kmsplugin - entry point into kms-plugin. See go/gke-secrets-encryption-design for details. 16 | package main 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "fmt" 22 | "net/http" 23 | "net/url" 24 | "os" 25 | "os/signal" 26 | "path/filepath" 27 | "syscall" 28 | "time" 29 | 30 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 31 | v1 "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin/v1" 32 | v2 "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin/v2" 33 | "github.com/golang/glog" 34 | "google.golang.org/api/cloudkms/v1" 35 | "google.golang.org/api/option" 36 | ) 37 | 38 | var ( 39 | healthzPort = flag.Int("healthz-port", 8081, "Port on which to publish healthz") 40 | healthzPath = flag.String("healthz-path", "healthz", "Path at which to publish healthz") 41 | healthzTimeout = flag.Duration("healthz-timeout", 5*time.Second, "timeout in seconds for communicating with the unix socket") 42 | 43 | metricsPort = flag.Int("metrics-port", 8082, "Port on which to publish metrics") 44 | metricsPath = flag.String("metrics-path", "metrics", "Path at which to publish metrics") 45 | 46 | gceConf = flag.String("gce-config", "", "Path to gce.conf, if running on GKE.") 47 | keyURI = flag.String("key-uri", "", "Uri of the key use for crypto operations (ex. projects/my-project/locations/my-location/keyRings/my-key-ring/cryptoKeys/my-key)") 48 | pathToUnixSocket = flag.String("path-to-unix-socket", "/var/run/kmsplugin/socket.sock", "Full path to Unix socket that is used for communicating with KubeAPI Server, or Linux socket namespace object - must start with @") 49 | kmsVersion = flag.String("kms", "v2", "Kubernetes KMS API version. Possible values: v1, v2. Default value is v2.") 50 | keySuffix = flag.String("key-suffix", "", "Set to a unique value in case if plugin is reconfigured to use Cloud KMS key version that was already in use before. Applicable only in KMS API v2 mode") 51 | 52 | // Integration testing arguments. 53 | integrationTest = flag.Bool("integration-test", false, "When set to true, http.DefaultClient will be used, as opposed callers identity acquired with a TokenService.") 54 | fakeKMSPort = flag.Int("fake-kms-port", 8085, "Port for Fake KMS, only use in integration tests.") 55 | ) 56 | 57 | func main() { 58 | ctx, cancel := signal.NotifyContext(context.Background(), 59 | syscall.SIGINT, syscall.SIGTERM) 60 | defer cancel() 61 | 62 | flag.Parse() 63 | mustValidateFlags() 64 | 65 | var ( 66 | httpClient = http.DefaultClient 67 | err error 68 | ) 69 | 70 | if !*integrationTest { 71 | // httpClient should be constructed with context.Background. Sending a context with 72 | // timeout or deadline will cause subsequent calls via the client to fail once the timeout or 73 | // deadline is triggered. Instead, the plugin supplies a context per individual calls. 74 | httpClient, err = plugin.NewHTTPClient(ctx, *gceConf) 75 | if err != nil { 76 | glog.Exitf("failed to instantiate http httpClient: %v", err) 77 | } 78 | } 79 | 80 | kms, err := cloudkms.NewService(ctx, option.WithHTTPClient(httpClient)) 81 | if err != nil { 82 | glog.Exitf("failed to instantiate cloud kms httpClient: %v", err) 83 | } 84 | 85 | if *integrationTest { 86 | kms.BasePath = fmt.Sprintf("http://localhost:%d", *fakeKMSPort) 87 | } 88 | 89 | metrics := &plugin.Metrics{ 90 | ServingURL: &url.URL{ 91 | Host: fmt.Sprintf("localhost:%d", *metricsPort), 92 | Path: *metricsPath, 93 | }, 94 | } 95 | 96 | var p plugin.Plugin 97 | var healthChecker plugin.HealthChecker 98 | switch *kmsVersion { 99 | case "v1": 100 | p = v1.NewPlugin(kms.Projects.Locations.KeyRings.CryptoKeys, *keyURI) 101 | healthChecker = v1.NewHealthChecker() 102 | glog.Info("Kubernetes KMS API v1beta1") 103 | case "v2": 104 | p = v2.NewPlugin(kms.Projects.Locations.KeyRings.CryptoKeys, *keyURI, *keySuffix) 105 | healthChecker = v2.NewHealthChecker() 106 | glog.Info("Kubernetes KMS API v2") 107 | default: 108 | glog.Exitf("invalid value %q for --kms", *kmsVersion) 109 | } 110 | 111 | hc := plugin.NewHealthChecker(healthChecker, *keyURI, kms.Projects.Locations.KeyRings.CryptoKeys, *pathToUnixSocket, *healthzTimeout, &url.URL{ 112 | Host: fmt.Sprintf("localhost:%d", *healthzPort), 113 | Path: *healthzPath, 114 | }) 115 | 116 | pluginManager := plugin.NewManager(p, *pathToUnixSocket) 117 | 118 | glog.Exit(run(pluginManager, hc, metrics)) 119 | } 120 | 121 | func run(pluginManager *plugin.PluginManager, h *plugin.HealthCheckerManager, m *plugin.Metrics) error { 122 | signalsChan := make(chan os.Signal, 1) 123 | signal.Notify(signalsChan, syscall.SIGINT, syscall.SIGTERM) 124 | 125 | metricsErrCh := m.Serve() 126 | healthzErrCh := h.Serve() 127 | 128 | gRPCSrv, kmsErrorCh := pluginManager.Start() 129 | defer gRPCSrv.GracefulStop() 130 | 131 | for { 132 | select { 133 | case sig := <-signalsChan: 134 | return fmt.Errorf("captured %v, shutting down kms-plugin", sig) 135 | case kmsError := <-kmsErrorCh: 136 | return kmsError 137 | case metricsErr := <-metricsErrCh: 138 | // Limiting this to warning only - will run without metrics. 139 | glog.Warning(metricsErr) 140 | metricsErrCh = nil 141 | case healthzErr := <-healthzErrCh: 142 | // Limiting this to warning only - will run without healthz. 143 | glog.Warning(healthzErr) 144 | healthzErrCh = nil 145 | } 146 | } 147 | } 148 | 149 | func mustValidateFlags() { 150 | if *kmsVersion == "v1" && *keySuffix != "" { 151 | glog.Exitf("--key-suffix argument cannot be used in v1 mode (--kms=v1)") 152 | } 153 | glog.Infof("Checking socket path %q", *pathToUnixSocket) 154 | socketDir := filepath.Dir(*pathToUnixSocket) 155 | glog.Infof("Unix Socket directory is %q", socketDir) 156 | if _, err := os.Stat(socketDir); err != nil { 157 | glog.Exitf(" Directory %q portion of path-to-unix-socket flag:%q does not seem to exist.", socketDir, *pathToUnixSocket) 158 | } 159 | glog.Infof("Communication between KUBE API and KMS Plugin containers will be via %q", *pathToUnixSocket) 160 | } 161 | -------------------------------------------------------------------------------- /tpm/tpm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google, LLC. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package tpm implements TPM2 API to seal and Unseal data. 16 | package tpm 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | 22 | "github.com/golang/glog" 23 | "github.com/google/go-tpm/legacy/tpm2" 24 | "github.com/google/go-tpm/tpmutil" 25 | ) 26 | 27 | var ( 28 | // Default EK template defined in: 29 | // https://trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf 30 | // Shared SRK template based off of EK template and specified in: 31 | // https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-v2.0-Provisioning-Guidance-Published-v1r1.pdf 32 | srkTemplate = tpm2.Public{ 33 | Type: tpm2.AlgRSA, 34 | NameAlg: tpm2.AlgSHA256, 35 | Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagRestricted | tpm2.FlagDecrypt | tpm2.FlagNoDA, 36 | AuthPolicy: nil, 37 | RSAParameters: &tpm2.RSAParams{ 38 | Symmetric: &tpm2.SymScheme{ 39 | Alg: tpm2.AlgAES, 40 | KeyBits: 128, 41 | Mode: tpm2.AlgCFB, 42 | }, 43 | KeyBits: 2048, 44 | ExponentRaw: 0, 45 | ModulusRaw: make([]byte, 256), 46 | }, 47 | } 48 | ) 49 | 50 | // Seal seals supplied data to TPM using PCRs and password. 51 | func Seal(tpmPath string, pcr int, srkPassword, objectPassword string, dataToSeal []byte) ([]byte, []byte, error) { 52 | rwc, err := tpm2.OpenTPM(tpmPath) 53 | if err != nil { 54 | return nil, nil, fmt.Errorf("can't open TPM %q: %v", tpmPath, err) 55 | } 56 | defer rwc.Close() 57 | 58 | // Create the parent key against which to seal the data 59 | srkHandle, _, err := tpm2.CreatePrimary(rwc, tpm2.HandleOwner, tpm2.PCRSelection{}, "", srkPassword, srkTemplate) 60 | if err != nil { 61 | return nil, nil, fmt.Errorf("can't create primary key: %v", err) 62 | } 63 | defer tpm2.FlushContext(rwc, srkHandle) 64 | 65 | glog.Infof("Created parent key with handle: 0x%x\n", srkHandle) 66 | 67 | // Note the value of the pcr against which we will seal the data 68 | pcrVal, err := tpm2.ReadPCR(rwc, pcr, tpm2.AlgSHA256) 69 | if err != nil { 70 | return nil, nil, fmt.Errorf("unable to read PCR: %v", err) 71 | } 72 | glog.Infof("PCR %v value: 0x%x\n", pcr, pcrVal) 73 | 74 | // Get the authorization policy that will protect the data to be sealed 75 | sessHandle, policy, err := policyPCRPasswordSession(rwc, pcr, objectPassword) 76 | if err != nil { 77 | return nil, nil, fmt.Errorf("unable to get policy: %v", err) 78 | } 79 | if err := tpm2.FlushContext(rwc, sessHandle); err != nil { 80 | return nil, nil, fmt.Errorf("unable to flush session: %v", err) 81 | } 82 | glog.Infof("Created authorization policy: 0x%x\n", policy) 83 | 84 | // Seal the data to the parent key and the policy 85 | privateArea, publicArea, err := tpm2.Seal(rwc, srkHandle, srkPassword, objectPassword, policy, dataToSeal) 86 | if err != nil { 87 | return nil, nil, fmt.Errorf("unable to seal data: %v", err) 88 | } 89 | glog.Infof("Sealed data: 0x%x\n", privateArea) 90 | 91 | return privateArea, publicArea, nil 92 | } 93 | 94 | // Unseal unseals data from TPM based on PCRs and password defined by the polic attached to the protected object. 95 | func Unseal(tpmPath string, pcr int, srkPassword, objectPassword string, privateArea, publicArea []byte) ([]byte, error) { 96 | rwc, err := tpm2.OpenTPM(tpmPath) 97 | if err != nil { 98 | return nil, fmt.Errorf("can't open TPM %q: %v", tpmPath, err) 99 | } 100 | defer rwc.Close() 101 | 102 | // Create the parent key against which to seal the data 103 | srkHandle, _, err := tpm2.CreatePrimary(rwc, tpm2.HandleOwner, tpm2.PCRSelection{}, "", srkPassword, srkTemplate) 104 | if err != nil { 105 | return nil, fmt.Errorf("can't create primary key: %v", err) 106 | } 107 | defer tpm2.FlushContext(rwc, srkHandle) 108 | 109 | glog.Infof("Created parent key with handle: 0x%x\n", srkHandle) 110 | 111 | // Load the sealed data into the TPM. 112 | objectHandle, _, err := tpm2.Load(rwc, srkHandle, srkPassword, publicArea, privateArea) 113 | if err != nil { 114 | return nil, fmt.Errorf("unable to load data: %v", err) 115 | } 116 | defer tpm2.FlushContext(rwc, objectHandle) 117 | 118 | glog.Infof("Loaded sealed data with handle: 0x%x\n", objectHandle) 119 | 120 | // Create the authorization session 121 | sessHandle, _, err := policyPCRPasswordSession(rwc, pcr, objectPassword) 122 | if err != nil { 123 | return nil, fmt.Errorf("unable to get auth session: %v", err) 124 | } 125 | defer tpm2.FlushContext(rwc, sessHandle) 126 | 127 | // Unseal the data 128 | unsealedData, err := tpm2.UnsealWithSession(rwc, sessHandle, objectHandle, objectPassword) 129 | if err != nil { 130 | return nil, fmt.Errorf("unable to Unseal data: %v", err) 131 | } 132 | return unsealedData, nil 133 | } 134 | 135 | // Returns session handle and policy digest. 136 | func policyPCRPasswordSession(rwc io.ReadWriteCloser, pcr int, password string) (tpmutil.Handle, []byte, error) { 137 | // FYI, this is not a very secure session. 138 | sessHandle, _, err := tpm2.StartAuthSession( 139 | rwc, 140 | tpm2.HandleNull, /*tpmKey*/ 141 | tpm2.HandleNull, /*bindKey*/ 142 | make([]byte, 16), /*nonceCaller*/ 143 | nil, /*secret*/ 144 | tpm2.SessionPolicy, 145 | tpm2.AlgNull, 146 | tpm2.AlgSHA256) 147 | if err != nil { 148 | return tpm2.HandleNull, nil, fmt.Errorf("unable to start session: %v", err) 149 | } 150 | 151 | pcrSelection := tpm2.PCRSelection{ 152 | Hash: tpm2.AlgSHA256, 153 | PCRs: []int{pcr}, 154 | } 155 | 156 | // An empty expected digest means that digest verification is skipped. 157 | if err := tpm2.PolicyPCR(rwc, sessHandle, nil /*expectedDigest*/, pcrSelection); err != nil { 158 | return sessHandle, nil, fmt.Errorf("unable to bind PCRs to auth policy: %v", err) 159 | } 160 | 161 | if err := tpm2.PolicyPassword(rwc, sessHandle); err != nil { 162 | return sessHandle, nil, fmt.Errorf("unable to require password for auth policy: %v", err) 163 | } 164 | 165 | policy, err := tpm2.PolicyGetDigest(rwc, sessHandle) 166 | if err != nil { 167 | return sessHandle, nil, fmt.Errorf("unable to get policy digest: %v", err) 168 | } 169 | return sessHandle, policy, nil 170 | } 171 | -------------------------------------------------------------------------------- /plugin/v2/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Implementation of the KMS Plugin API v2. 16 | package v2 17 | 18 | import ( 19 | "regexp" 20 | "sync" 21 | 22 | "google.golang.org/api/cloudkms/v1" 23 | grpc "google.golang.org/grpc" 24 | 25 | "context" 26 | "encoding/base64" 27 | "fmt" 28 | "time" 29 | 30 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 31 | "github.com/golang/glog" 32 | ) 33 | 34 | const ( 35 | apiVersion = "v2beta1" 36 | ok = "ok" 37 | ping = "cGluZw==" 38 | keyNotReachable = "Cloud KMS key is not reachable" 39 | keyDisabled = "Cloud KMS key is not enabled or no cloudkms.cryptoKeys.get permission" 40 | ) 41 | 42 | // Regex to extract Cloud KMS key resource name from the key version resource name 43 | var keyResourceRegEx = regexp.MustCompile(`projects\/[^/]+\/locations\/[^/]+\/keyRings\/[^/]+\/cryptoKeys\/[^/:]+`) 44 | 45 | var _ plugin.Plugin = (*Plugin)(nil) 46 | 47 | type Plugin struct { 48 | keyService *cloudkms.ProjectsLocationsKeyRingsCryptoKeysService 49 | keyURI string 50 | keySuffix string 51 | 52 | // lastKeyID stores the last known primary key version resource name to return 53 | // as KeyId in case when the Cloud KMS service is not reachable because KeyId 54 | // in StatusResponse cannot be empty and shouldn't trigger key migration in 55 | // case of transient remote service unavailability. 56 | lastKeyID string 57 | lastKeyIDLock sync.RWMutex 58 | } 59 | 60 | // New constructs Plugin. 61 | func NewPlugin(keyService *cloudkms.ProjectsLocationsKeyRingsCryptoKeysService, keyURI, keySuffix string) *Plugin { 62 | p := &Plugin{ 63 | keyService: keyService, 64 | keyURI: keyURI, 65 | keySuffix: keySuffix, 66 | } 67 | p.setKeyID(keyURI) 68 | 69 | return p 70 | } 71 | 72 | // Register registers the plugin as a service management service. 73 | func (g *Plugin) Register(s *grpc.Server) { 74 | RegisterKeyManagementServiceServer(s, g) 75 | } 76 | 77 | // Status returns the version of KMS API version that plugin supports. 78 | // Response also contains the status of the plugin, which is calculated as availability of the 79 | // encryption key that the plugin is confinged with, and the current primary key version. 80 | // kube-apiserver will provide this key version in Encrypt and Decrypt calls and will be able 81 | // to know whether the remote CLoud KMS key has been rotated or not. 82 | func (g *Plugin) Status(ctx context.Context, request *StatusRequest) (*StatusResponse, error) { 83 | defer plugin.RecordCloudKMSOperation("encrypt", time.Now().UTC()) 84 | 85 | keyID := g.keyID() 86 | 87 | statusResp := &StatusResponse{ 88 | Version: apiVersion, 89 | KeyId: keyID, 90 | Healthz: ok, 91 | } 92 | resp, err := g.keyService.Encrypt(g.keyURI, &cloudkms.EncryptRequest{ 93 | Plaintext: ping, 94 | }).Context(ctx).Do() 95 | if err != nil { 96 | plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("encrypt").Inc() 97 | statusResp.Healthz = keyNotReachable 98 | } else { 99 | g.setKeyID(resp.Name) 100 | } 101 | 102 | glog.V(4).Infof("Status response: %s", statusResp.Healthz) 103 | return statusResp, nil 104 | } 105 | 106 | // Encrypt encrypts payload provided by K8S API Server. 107 | func (g *Plugin) Encrypt(ctx context.Context, request *EncryptRequest) (*EncryptResponse, error) { 108 | glog.V(4).Infof("Processing request for encryption %s using %s", request.Uid, g.keyURI) 109 | defer plugin.RecordCloudKMSOperation("encrypt", time.Now().UTC()) 110 | 111 | resp, err := g.keyService.Encrypt(g.keyURI, &cloudkms.EncryptRequest{ 112 | Plaintext: base64.StdEncoding.EncodeToString(request.Plaintext), 113 | }).Context(ctx).Do() 114 | if err != nil { 115 | plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("encrypt").Inc() 116 | return nil, err 117 | } 118 | 119 | cipher, err := base64.StdEncoding.DecodeString(resp.Ciphertext) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | keyID := g.setKeyID(resp.Name) 125 | 126 | glog.V(4).Infof("Processed request for encryption %s using %s", 127 | request.Uid, keyID) 128 | 129 | return &EncryptResponse{ 130 | Ciphertext: cipher, 131 | KeyId: keyID, 132 | }, nil 133 | } 134 | 135 | // Decrypt decrypts payload supplied by K8S API Server. 136 | func (g *Plugin) Decrypt(ctx context.Context, request *DecryptRequest) (*DecryptResponse, error) { 137 | glog.V(4).Infof("Processing request for decryption %s using %s", request.Uid, request.KeyId) 138 | defer plugin.RecordCloudKMSOperation("decrypt", time.Now().UTC()) 139 | 140 | keyResourceName := g.keyURI 141 | if request.KeyId != "" { // request.KeyId is empty when health checker calls this method from PingKMS() 142 | keyResourceName = extractKeyName(request.KeyId) 143 | } 144 | resp, err := g.keyService.Decrypt(keyResourceName, &cloudkms.DecryptRequest{ 145 | Ciphertext: base64.StdEncoding.EncodeToString(request.Ciphertext), 146 | }).Context(ctx).Do() 147 | if err != nil { 148 | plugin.CloudKMSOperationalFailuresTotal.WithLabelValues("decrypt").Inc() 149 | return nil, err 150 | } 151 | 152 | plain, err := base64.StdEncoding.DecodeString(resp.Plaintext) 153 | if err != nil { 154 | return nil, fmt.Errorf("failed to decode from base64, error: %w", err) 155 | } 156 | 157 | return &DecryptResponse{ 158 | Plaintext: plain, 159 | }, nil 160 | } 161 | 162 | // keyID is a threadsafe way to get the current key ID. 163 | func (g *Plugin) keyID() string { 164 | g.lastKeyIDLock.RLock() 165 | defer g.lastKeyIDLock.RUnlock() 166 | return g.lastKeyID 167 | } 168 | 169 | // If the key id suffix has been passed in the command line parameters this 170 | // function will return a key id value constructed from the Cloud KMS key 171 | // version with appended suffix separated by ":" 172 | // 173 | // This is to return a unique key id to Kubernetes in case if the plugin is 174 | // reconfigured to use a Cloud KMS key version which has been already in use 175 | // before 176 | func (g *Plugin) setKeyID(name string) string { 177 | result := name 178 | if v := g.keySuffix; v != "" { 179 | result = result + ":" + v 180 | } 181 | 182 | g.lastKeyIDLock.Lock() 183 | defer g.lastKeyIDLock.Unlock() 184 | g.lastKeyID = result 185 | return result 186 | } 187 | 188 | // Extracts the Cloud KMS key resource name from the key version resource name 189 | func extractKeyName(keyVersionId string) string { 190 | return keyResourceRegEx.FindString(keyVersionId) 191 | } 192 | -------------------------------------------------------------------------------- /testutils/fakekubeapi/fakekubeapi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fakekubeapi supports integration testing of kms-plugin by faking K8S kube-apiserver. 16 | package fakekubeapi 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | "net" 24 | "net/http" 25 | "net/http/httptest" 26 | "regexp" 27 | "strings" 28 | "sync" 29 | "time" 30 | 31 | msgspb "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin/v1" 32 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/testutils/kmspluginclient" 33 | "github.com/golang/glog" 34 | "github.com/google/go-cmp/cmp" 35 | "github.com/phayes/freeport" 36 | corev1 "k8s.io/api/core/v1" 37 | ) 38 | 39 | var ( 40 | secretsURLRegex = regexp.MustCompile(`/api/v1/namespaces/[a-z-]*/secrets/[a-z-]*`) 41 | ) 42 | 43 | // Server fakes kube-apiserver. 44 | type Server struct { 45 | srv *httptest.Server 46 | port int 47 | namespaces corev1.NamespaceList 48 | secrets map[string][]corev1.Secret 49 | kms *kmspluginclient.Client 50 | timeout time.Duration 51 | 52 | mux sync.Mutex 53 | secretsListLog []corev1.Secret 54 | secretsPutLog []corev1.Secret 55 | } 56 | 57 | // Client returns *http.Client for the fake. 58 | func (f *Server) Client() *http.Client { 59 | return f.srv.Client() 60 | } 61 | 62 | // URL returns URL on which the fake is expecting requests. 63 | func (f *Server) URL() string { 64 | return f.srv.URL 65 | } 66 | 67 | // Close closes the underlying httptest.Server. 68 | func (f *Server) Close() { 69 | f.srv.Close() 70 | } 71 | 72 | // ListSecretsRequestsEquals validates that the supplied Secrets are equal to all secrets 73 | // processed by the server via http.Get. 74 | func (f *Server) ListSecretsRequestsEquals(r []corev1.Secret) error { 75 | f.mux.Lock() 76 | defer f.mux.Unlock() 77 | 78 | if diff := cmp.Diff(f.secretsListLog, r); diff != "" { 79 | return fmt.Errorf("list log differs from expected: (-want +got)\n%s", diff) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // PutSecretsEquals validates that the supplied Secrets are equal to all 86 | // secrets processed by the server via http.Put. 87 | func (f *Server) PutSecretsEquals(r []corev1.Secret) error { 88 | f.mux.Lock() 89 | defer f.mux.Unlock() 90 | 91 | if diff := cmp.Diff(f.secretsPutLog, r); diff != "" { 92 | return fmt.Errorf("put log differs from expected: (-want +got)\n%s", diff) 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // New constructs kube-apiserver fake. 99 | // It is the responsibility of the caller to call Close. 100 | func New(namespaces corev1.NamespaceList, secrets map[string][]corev1.Secret, port int, kmsClient *kmspluginclient.Client, timeout time.Duration) (*Server, error) { 101 | var err error 102 | if port == 0 { 103 | port, err = freeport.GetFreePort() 104 | if err != nil { 105 | return nil, fmt.Errorf("failed to allocate port for fake kube-apiserver, error: %v", err) 106 | } 107 | } 108 | 109 | s := &Server{ 110 | namespaces: namespaces, 111 | secrets: secrets, 112 | kms: kmsClient, 113 | timeout: timeout, 114 | } 115 | 116 | s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 | switch r.Method { 118 | case http.MethodGet: 119 | s.processGet(r.URL.EscapedPath(), w) 120 | case http.MethodPut: 121 | s.processPut(r, w) 122 | default: 123 | http.Error(w, fmt.Sprintf("unexpected http method %v", r.Method), http.StatusBadRequest) 124 | } 125 | })) 126 | 127 | l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) 128 | if err != nil { 129 | return nil, fmt.Errorf("failed to listen on port %d, error: %v", port, err) 130 | } 131 | s.srv.Listener = l 132 | s.srv.Start() 133 | return s, nil 134 | } 135 | 136 | func (f *Server) recordSecretList(s []corev1.Secret) { 137 | f.mux.Lock() 138 | defer f.mux.Unlock() 139 | f.secretsListLog = append(f.secretsListLog, s...) 140 | } 141 | 142 | func (f *Server) recordSecretPut(s corev1.Secret) { 143 | f.mux.Lock() 144 | defer f.mux.Unlock() 145 | f.secretsPutLog = append(f.secretsPutLog, s) 146 | } 147 | 148 | func (f *Server) processPut(r *http.Request, w http.ResponseWriter) { 149 | ctx, cancel := context.WithTimeout(r.Context(), f.timeout) 150 | defer cancel() 151 | 152 | glog.Infof("Processing PUT request %v", r) 153 | if !secretsURLRegex.MatchString(r.URL.EscapedPath()) { 154 | http.Error(w, fmt.Sprintf("unexpected uri: %s", r.URL.EscapedPath()), http.StatusNotFound) 155 | return 156 | } 157 | 158 | b, err := ioutil.ReadAll(r.Body) 159 | if err != nil { 160 | http.Error(w, fmt.Sprintf("failed to read the body of the request, error: %v", err), http.StatusBadRequest) 161 | return 162 | } 163 | 164 | s := &corev1.Secret{} 165 | if err := json.Unmarshal(b, s); err != nil { 166 | http.Error(w, fmt.Sprintf("failed to unmarshal request, error: %v", err), http.StatusBadRequest) 167 | return 168 | } 169 | 170 | f.recordSecretPut(*s) 171 | 172 | glog.Infoln("Sending secret for encryption to kms-plugin.") 173 | if _, err := f.kms.Encrypt(ctx, &msgspb.EncryptRequest{Version: "v1beta1", Plain: b}); err != nil { 174 | m := fmt.Sprintf("failed to transform secret, error: %v", err) 175 | glog.Warning(m) 176 | http.Error(w, m, http.StatusServiceUnavailable) 177 | return 178 | } 179 | glog.Info("kms-plugin processed the encryption request.") 180 | 181 | w.Header().Set("Content-Type", "application/json") 182 | if err := json.NewEncoder(w).Encode(s); err != nil { 183 | http.Error(w, fmt.Sprintf("failed to write response for secret put, error: %v", err), http.StatusBadRequest) 184 | return 185 | } 186 | } 187 | 188 | func (f *Server) processGet(url string, w http.ResponseWriter) { 189 | glog.Infof("Processing Get request %s", url) 190 | // TODO(alextc) Check URL - is it actually a get/list request for a Secret? 191 | var response interface{} 192 | switch { 193 | case url == "/api/v1/namespaces": 194 | response = f.namespaces 195 | 196 | // Expect url to be of the following format: /api/v1/namespaces/default/secrets. 197 | case strings.HasSuffix(url, "/secrets"): 198 | urlParts := strings.Split(url, "/") 199 | if len(urlParts) != 6 { 200 | http.Error(w, fmt.Sprintf("unexpected format of url: %q, wanted len of 4, got %d, parts: %#v", url, len(urlParts), urlParts), http.StatusBadRequest) 201 | return 202 | } 203 | s, ok := f.secrets[urlParts[4]] 204 | if !ok { 205 | http.Error(w, fmt.Sprintf("invalid test data, request for %q, but namespace %s was not provided", url, urlParts[4]), http.StatusNotFound) 206 | return 207 | } 208 | 209 | response = corev1.SecretList{ 210 | Items: s, 211 | } 212 | f.recordSecretList(s) 213 | 214 | default: 215 | http.Error(w, fmt.Sprintf("Was not expecting call to %q", url), http.StatusNotFound) 216 | return 217 | } 218 | 219 | w.Header().Set("Content-Type", "application/json") 220 | if err := json.NewEncoder(w).Encode(response); err != nil { 221 | http.Error(w, fmt.Sprintf("failed to write response for request:%s, err: %v", url, err), http.StatusInternalServerError) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /testutils/fakekms/fakekms.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fakekms supports integration testing of kms-plugin by faking CloudKMS. 16 | package fakekms 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io/ioutil" 23 | "net" 24 | "net/http" 25 | "net/http/httptest" 26 | "sync" 27 | "time" 28 | 29 | "github.com/golang/glog" 30 | "github.com/google/go-cmp/cmp" 31 | "github.com/phayes/freeport" 32 | "google.golang.org/api/cloudkms/v1" 33 | ) 34 | 35 | // Server fakes CloudKMS. 36 | type Server struct { 37 | srv *httptest.Server 38 | mux sync.Mutex 39 | encryptRequestLog []*cloudkms.EncryptRequest 40 | decryptRequestLog []*cloudkms.DecryptRequest 41 | iamTestRequestLog []*cloudkms.TestIamPermissionsRequest 42 | port int 43 | } 44 | 45 | // Client returns *http.Client for the fake. 46 | func (f *Server) Client() *http.Client { 47 | return f.srv.Client() 48 | } 49 | 50 | // URL returns URL on which the fake is expecting requests. 51 | func (f *Server) URL() string { 52 | return f.srv.URL 53 | } 54 | 55 | // Close closes the underlying httptest.Server. 56 | func (f *Server) Close() { 57 | f.srv.Close() 58 | } 59 | 60 | // EncryptRequestsEqual validates that the supplied EncryptRequests are equal to all 61 | // EncryptRequests processed by the server. 62 | func (f *Server) EncryptRequestsEqual(r []*cloudkms.EncryptRequest) error { 63 | f.mux.Lock() 64 | defer f.mux.Unlock() 65 | 66 | if diff := cmp.Diff(f.encryptRequestLog, r); diff != "" { 67 | return fmt.Errorf("EncryptRequests differs from expected:(-want +got)\n%s", diff) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // DecryptRequestsEqual validates that the supplied DecryptRequests are equal to the all 74 | // DecryptRequests processed by the server. 75 | func (f *Server) DecryptRequestsEqual(r []*cloudkms.DecryptRequest) error { 76 | f.mux.Lock() 77 | defer f.mux.Unlock() 78 | 79 | if diff := cmp.Diff(f.decryptRequestLog, r); diff != "" { 80 | return fmt.Errorf("DecryptRequests differ from expected: (-want +got)\n%s", diff) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // TestIAMRequestsEqual validates that the supplied TestIamPermissionsRequests are equal to the all 87 | // TestIamPermissionsRequests processed by the server. 88 | func (f *Server) TestIAMRequestsEqual(r []*cloudkms.TestIamPermissionsRequest) error { 89 | f.mux.Lock() 90 | defer f.mux.Unlock() 91 | 92 | if diff := cmp.Diff(f.iamTestRequestLog[len(f.iamTestRequestLog)-1], r); diff != "" { 93 | return fmt.Errorf("Last TestIAMPermissionsRequest differs from expected: (-want +got)\n%s", diff) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (f *Server) recordEncryptRequest(r *cloudkms.EncryptRequest) { 100 | f.mux.Lock() 101 | defer f.mux.Unlock() 102 | f.encryptRequestLog = append(f.encryptRequestLog, r) 103 | } 104 | 105 | func (f *Server) recordDecryptRequest(r *cloudkms.DecryptRequest) { 106 | f.mux.Lock() 107 | defer f.mux.Unlock() 108 | f.decryptRequestLog = append(f.decryptRequestLog, r) 109 | } 110 | 111 | func (f *Server) recordTestIAMRequest(r *cloudkms.TestIamPermissionsRequest) { 112 | f.mux.Lock() 113 | defer f.mux.Unlock() 114 | f.iamTestRequestLog = append(f.iamTestRequestLog, r) 115 | } 116 | 117 | // NewWithPipethrough creates and returns *Server that simply passed through the requests, by 118 | // replacing ciphertext to cleartext and vice versa. 119 | // Callers are also responsible for calling Close after completing tests. 120 | // keyName simulates CloudKMS' keyName and is taken into account when calculating expected URL endpoints. 121 | func NewWithPipethrough(keyName string, port int) (*Server, error) { 122 | handle := func(req json.Marshaler) (json.Marshaler, int, error) { 123 | glog.Infof("Processing request: %#v", req) 124 | 125 | switch r := req.(type) { 126 | case *cloudkms.EncryptRequest: 127 | return &cloudkms.EncryptResponse{ 128 | Name: keyName, 129 | Ciphertext: r.Plaintext, 130 | }, http.StatusOK, nil 131 | case *cloudkms.DecryptRequest: 132 | return &cloudkms.DecryptResponse{ 133 | Plaintext: r.Ciphertext, 134 | }, http.StatusOK, nil 135 | case *cloudkms.TestIamPermissionsRequest: 136 | return &cloudkms.TestIamPermissionsResponse{ 137 | Permissions: []string{ 138 | "cloudkms.cryptoKeyVersions.useToEncrypt", 139 | "cloudkms.cryptoKeyVersions.useToDecrypt", 140 | }, 141 | }, http.StatusOK, nil 142 | default: 143 | return nil, http.StatusInternalServerError, fmt.Errorf("was not expecting request type:%T", r) 144 | } 145 | } 146 | 147 | return newWithCallback(keyName, port, 0, handle) 148 | } 149 | 150 | // NewWithResponses creates and returns *Server. 151 | // It is the responsibility of the caller to supply the expected number of Responses. 152 | // When the provided Responses are exhausted an error will be returned. 153 | // Callers are also responsible for calling Close after completing tests. 154 | // keyName simulates CloudKMS' keyName and is taken into account when calculating expected URL endpoints. 155 | // delay allows the caller to simulate delayed responses from KMS. 156 | func NewWithResponses(keyName string, port int, delay time.Duration, responses ...json.Marshaler) (*Server, error) { 157 | handle := func(req json.Marshaler) (json.Marshaler, int, error) { 158 | if len(responses) == 0 { 159 | return nil, http.StatusServiceUnavailable, errors.New("list of responses is empty") 160 | } 161 | 162 | status := http.StatusInternalServerError 163 | switch req.(type) { 164 | case *cloudkms.EncryptRequest: 165 | e, ok := responses[0].(*cloudkms.EncryptResponse) 166 | if !ok { 167 | return nil, status, errors.New("request for encrypt does not have a corresponding response of cloudkms.EncryptResponse") 168 | } 169 | status = e.HTTPStatusCode 170 | case *cloudkms.DecryptRequest: 171 | d, ok := responses[0].(*cloudkms.DecryptResponse) 172 | if !ok { 173 | return nil, status, errors.New("request for decrypt does not have a corresponding response of cloudkms.DecryptResponse") 174 | } 175 | status = d.HTTPStatusCode 176 | case *cloudkms.TestIamPermissionsRequest: 177 | t, ok := responses[0].(*cloudkms.TestIamPermissionsResponse) 178 | if !ok { 179 | return nil, status, errors.New("request for testIamPermissions does not have a corresponding response of cloudkms.TestIAMPermissionResponse") 180 | } 181 | status = t.HTTPStatusCode 182 | } 183 | r := responses[0] 184 | responses = responses[1:] 185 | return r, status, nil 186 | } 187 | 188 | return newWithCallback(keyName, port, delay, handle) 189 | } 190 | 191 | func newWithCallback(keyName string, port int, delay time.Duration, handle func(req json.Marshaler) (json.Marshaler, int, error)) (*Server, error) { 192 | var err error 193 | if port == 0 { 194 | port, err = freeport.GetFreePort() 195 | if err != nil { 196 | return nil, fmt.Errorf("failed to allocate port for fake kms, error: %v", err) 197 | } 198 | } 199 | 200 | s := &Server{port: port} 201 | s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 202 | time.Sleep(delay) 203 | 204 | body, err := ioutil.ReadAll(r.Body) 205 | if err != nil { 206 | http.Error(w, fmt.Sprintf("can't read the body of the request, error: %v", err), http.StatusBadRequest) 207 | return 208 | } 209 | 210 | var ( 211 | response json.Marshaler 212 | status = http.StatusInternalServerError 213 | ) 214 | switch r.URL.EscapedPath() { 215 | case fmt.Sprintf("/v1/%s:encrypt", keyName): 216 | e := &cloudkms.EncryptRequest{} 217 | if err := json.Unmarshal(body, e); err != nil { 218 | http.Error(w, err.Error(), status) 219 | return 220 | } 221 | s.recordEncryptRequest(e) 222 | 223 | response, status, err = handle(e) 224 | if err != nil { 225 | http.Error(w, err.Error(), status) 226 | return 227 | } 228 | case fmt.Sprintf("/v1/%s:decrypt", keyName): 229 | d := &cloudkms.DecryptRequest{} 230 | if err := json.Unmarshal(body, d); err != nil { 231 | http.Error(w, err.Error(), status) 232 | return 233 | } 234 | s.recordDecryptRequest(d) 235 | 236 | response, status, err = handle(d) 237 | if err != nil { 238 | http.Error(w, err.Error(), status) 239 | return 240 | } 241 | case fmt.Sprintf("/v1/%s:testIamPermissions", keyName): 242 | t := &cloudkms.TestIamPermissionsRequest{} 243 | if err := json.Unmarshal(body, t); err != nil { 244 | http.Error(w, err.Error(), status) 245 | return 246 | } 247 | s.recordTestIAMRequest(t) 248 | 249 | response, status, err = handle(t) 250 | if err != nil { 251 | http.Error(w, err.Error(), status) 252 | return 253 | } 254 | default: 255 | http.Error(w, fmt.Sprintf("Was not expecting call to %q", r.URL.EscapedPath()), status) 256 | return 257 | } 258 | 259 | w.WriteHeader(status) 260 | if err := json.NewEncoder(w).Encode(response); err != nil { 261 | http.Error(w, fmt.Sprintf("failed to marshal response, error %v", err), http.StatusInternalServerError) 262 | return 263 | } 264 | })) 265 | 266 | l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) 267 | if err != nil { 268 | return nil, fmt.Errorf("failed to listen on port %d, error: %v", port, err) 269 | } 270 | s.srv.Listener = l 271 | s.srv.Start() 272 | 273 | return s, nil 274 | } 275 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes KMS Plugin for Cloud KMS 2 | 3 | This repo contains an implementation of a [Kubernetes KMS Plugin][k8s-kms-plugin] for [Cloud KMS][gcp-kms]. 4 | 5 | **If you are running on Kubernetes Engine (GKE), you do not need this plugin. You can enable [Application-layer Secrets Encryption][gke-secrets-docs] and GKE will manage the communication between GKE and KMS automatically.** 6 | 7 | 8 | ## Use with Compute Engine 9 | 10 | If you are running Kubernetes on VMs, the configuration is logically divided into the following stages. These steps are specific to Compute Engine (GCE), but could be expanded to any VM-based machine. 11 | 12 | 1. Create Cloud KMS Keys 13 | 1. Grant service account IAM permissions on Cloud KMS keys 14 | 1. Deploy the KMS plugin binary to the Kubernetes master nodes 15 | 1. Create Kubernetes encryption configuration 16 | 17 | ### Assumptions 18 | 19 | This guide makes a few assumptions: 20 | 21 | * You have gcloud and the [Cloud SDK][cloud-sdk] installed locally. Alternatively you can run in [Cloud Shell][cloud-shell] where these tools are already installed. 22 | 23 | * You have [billing enabled][gcp-billing] on your project. This project uses Cloud KMS, and you will not be able to use Cloud KMS without billing enabled on your project. Even if you are on a free trial, you will need to enable billing (your trial credits will still be used first). 24 | 25 | * The Cloud KMS keys exist in the same project where the GCE VMs are running. This is not a hard requirement (in fact, you may want to separate them depending on your security posture and threat model adhering to the principle of separation of duties), but this guide assumes they are in the same project for simplicity. 26 | 27 | * The VMs that run the Kubernetes masters run with [dedicated Service Account][dedicated-sa] to follow the principle of least privilege. The dedicated service account will be given permission to encrypt/decrypt data using a Cloud KMS key. 28 | 29 | * The KMS plugin will share its security context with the underlying VM. Even though principle of least privilege advocates for a dedicated service account for the KMS plugin, doing so breaks the threat model since it forces you to store a service account key on disk. An attacker with access to an offline VM image would therefore decrypt the contents of etcd offline by leveraging the service account stored on the image. GCE VM service accounts are not stored on disk and therefore do not share this same threat vector. 30 | 31 | ### Set environment variables 32 | 33 | Set the following environment variables to your values: 34 | 35 | ```sh 36 | # The ID of the project. Please note that this is the ID, not the NAME of the 37 | # project. In some cases they are the same, but they can be different. Run 38 | # `gcloud projects list` and choose the value in "project id" column. 39 | PROJECT_ID="" 40 | 41 | # The FQDN email of the service account that will be attached to the VMs which 42 | # run the Kubernetes master nodes. 43 | SERVICE_ACCOUNT_EMAIL="" 44 | 45 | # These values correspond to the KMS key. KMS crypto keys belong to a key ring 46 | # which belong to a location. For a full list of locations, run 47 | # `gcloud kms locations list`. 48 | KMS_LOCATION="" 49 | KMS_KEY_RING="" 50 | KMS_CRYPTO_KEY="" 51 | ``` 52 | 53 | Here is an example (replace with your values): 54 | 55 | ```sh 56 | PROJECT_ID="my-gce-project23" 57 | SERVICE_ACCOUNT_EMAIL="kms-plugin@my-gce-project23@iam.gserviceaccount.com" 58 | KMS_LOCATION="us-east4" 59 | KMS_KEY_RING="my-keyring" 60 | KMS_CRYPTO_KEY="my-key" 61 | ``` 62 | 63 | ### Create Cloud KMS key 64 | 65 | Create the key in Cloud KMS. 66 | 67 | ```sh 68 | # Enable the Cloud KMS API (this only needs to be done once per project 69 | # where KMS is being used). 70 | $ gcloud services enable --project "${PROJECT_ID}" \ 71 | cloudkms.googleapis.com 72 | 73 | # Create the Cloud KMS key ring in the specified location. 74 | $ gcloud kms keyrings create "${KMS_KEY_RING}" \ 75 | --project "${PROJECT_ID}" \ 76 | --location "${KMS_LOCATION}" 77 | 78 | # Create the Cloud KMS crypto key inside the key ring. 79 | $ gcloud kms keys create "${KMS_KEY_NAME}" \ 80 | --project "${PROJECT_ID}" \ 81 | --location "${KMS_LOCATION}" \ 82 | --keyring "${KMS_KEY_RING}" 83 | ``` 84 | 85 | ### Grant Service Account key permissions 86 | 87 | Grant the dedicated service account permission to encrypt/decrypt data using the Cloud KMS crypto key we just created: 88 | 89 | ```sh 90 | $ gcloud kms keys add-iam-policy-binding "${KMS_KEY_NAME}" \ 91 | --project "${PROJECT_ID}" \ 92 | --location "${KMS_LOCATION}" \ 93 | --keyring "${KMS_KEY_RING}" \ 94 | --member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \ 95 | --role "roles/cloudkms.cryptoKeyEncrypterDecrypter" 96 | ``` 97 | 98 | In addition to the IAM permissions, you also need to increase the oauth scopes on the VM. The following command assumes your VM master nodes require the `gke-default` scopes. If that is not the case, alter the command accordingly. Replace "my-master-instance" with the name of your VM: 99 | 100 | ```sh 101 | $ gcloud compute instances set-service-account "my-master-instance" \ 102 | --service-account "${SERVICE_ACCOUNT_EMAIL}" \ 103 | --scopes "gke-default, https://www.googleapis.com/auth/cloudkms" 104 | ``` 105 | 106 | Restart any instances to pickup the scope changes: 107 | 108 | ```sh 109 | $ gcloud compute instances stop "my-master-instance" 110 | $ gcloud compute instances start "my-master-instance" 111 | ``` 112 | 113 | 114 | ### Deploy the KMS plugin 115 | 116 | There are a few options for deploying the KMS plugin into a Kubernetes cluster: 117 | 118 | 1. As a Docker image 119 | 120 | A pre-built image is available at: 121 | 122 | ```text 123 | gcr.io/cloud-kms-lab/k8s-cloudkms-plugin:$TAG 124 | ``` 125 | 126 | where `TAG` refers to a published git tag or "latest" for builds off the master branch. You can also build and publish your own image by cloning this repository and running: 127 | 128 | ```sh 129 | $ gcloud builds submit \ 130 | --tag gcr.io/$PROJECT_ID/k8s-cloudkms-plugin \ 131 | . 132 | ``` 133 | 134 | 1. As a Go binary 135 | 1. As a [static pod][k8s-static-pod] 136 | 137 | 138 | #### Pull image onto master VMs 139 | 140 | **On the Kubernetes master VM**, run the following command to pull the Docker image. Replace the URL with your repository's URL: 141 | 142 | ```sh 143 | # Pick a tag and pin to that tag 144 | $ docker pull "gcr.io/cloud-kms-lab/k8s-cloudkms-plugin:1.2.3" 145 | ``` 146 | 147 | #### Test the interaction between KMS Plugin and Cloud KMS 148 | 149 | **On the Kubernetes master VM**, instruct the KMS plugin to perform a self-test. First, set some environment variables: 150 | 151 | ```sh 152 | KMS_FULL_KEY="projects/${PROJECT_ID}/locations/${KMS_LOCATION}/keyRings/${KMS_KEY_RING}/cryptoKeys/${KMS_CRYPTO_KEY}" 153 | SOCKET_DIR="/var/kms-plugin" 154 | SOCKET_PATH="${SOCKET_DIR}/socket.sock" 155 | PLUGIN_HEALTHZ_PORT="8081" 156 | PLUGIN_IMAGE="gcr.io/cloud-kms-lab/k8s-cloudkms-plugin:1.2.3" # Pick a tag 157 | ``` 158 | 159 | Start the container: 160 | 161 | ```sh 162 | $ docker run \ 163 | --name="kms-plugin" \ 164 | --network="host" \ 165 | --detach \ 166 | --rm \ 167 | --volume "${SOCKET_DIR}:/var/run/kmsplugin:rw" \ 168 | "${PLUGIN_IMAGE}" \ 169 | /bin/k8s-cloudkms-plugin \ 170 | --logtostderr \ 171 | --integration-test="true" \ 172 | --path-to-unix-socket="${SOCKET_PATH}" \ 173 | --key-uri="${KMS_FULL_KEY}" 174 | ``` 175 | 176 | Probe the plugin on it's healthz port. You should expect the command to return "OK": 177 | 178 | ```sh 179 | $ curl "http://localhost:${PLUGIN_HEALTHZ_PORT}/healthz?ping-kms=true" 180 | ``` 181 | 182 | Stop the container: 183 | 184 | ```sh 185 | $ docker kill --signal="SIGHUP" kms-plugin 186 | ``` 187 | 188 | Finally, depending on your deployment strategy, configure the KMS plugin container to automatically boot at-startup. This can be done with systemd or an orchestration/configuration management tool. 189 | 190 | 191 | ### Configure kube-apiserver 192 | 193 | Update the kube-apiserver's encryption configuration to point to the shared socket from the KMS plugin. If you changed the `SOCKET_DIR` or `SOCKET_PATH` variables above, update the `endpoint` in the configuration below accordingly: 194 | 195 | ```yaml 196 | kind: EncryptionConfiguration 197 | apiVersion: apiserver.config.k8s.io/v1 198 | resources: 199 | - resources: 200 | - secrets 201 | providers: 202 | - kms: 203 | apiVersion: v2 204 | name: myKmsPlugin 205 | endpoint: unix:///var/kms-plugin/socket.sock 206 | - identity: {} 207 | ``` 208 | 209 | More information about this file and configuration options can be found in the [Kubernetes KMS plugin documentation][k8s-kms-plugin]. 210 | 211 | 212 | ## Learn more 213 | 214 | * Read [Encrypting Kubernetes Secrets with Cloud KMS][blog-container-security] 215 | * Read [GKE documentation for encrypting secrets][gke-secrets-docs] 216 | * Watch [Turtles all the way down: Managing Kubernetes Secrets][video-turtles] 217 | 218 | 219 | 220 | [gcp-kms]: https://cloud.google.com/kms 221 | [k8s-kms-plugin]: https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/ 222 | [blog-container-security]: https://cloud.google.com/blog/products/containers-kubernetes/exploring-container-security-encrypting-kubernetes-secrets-with-cloud-kms 223 | [gke-secrets-docs]: https://cloud.google.com/kubernetes-engine/docs/how-to/encrypting-secrets 224 | [video-turtles]: https://www.youtube.com/watch?v=rLHJZE2XKl8 225 | [cloud-sdk]: https://cloud.google.com/sdk 226 | [cloud-shell]: https://cloud.google.com/shell 227 | [gcp-billing]: https://cloud.google.com/billing/docs/how-to/modify-project#enable_billing_for_a_new_project 228 | [k8s-static-pod]: https://kubernetes.io/docs/tasks/administer-cluster/static-pod/ 229 | [dedicated-sa]: https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /plugin/v1/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "net" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | "path/filepath" 27 | "strconv" 28 | "testing" 29 | "time" 30 | 31 | "google.golang.org/api/cloudkms/v1" 32 | "google.golang.org/api/googleapi" 33 | "google.golang.org/api/option" 34 | "google.golang.org/grpc" 35 | 36 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/testutils/fakekms" 37 | "github.com/golang/protobuf/proto" 38 | "github.com/phayes/freeport" 39 | "github.com/prometheus/client_golang/prometheus" 40 | prometheuspb "github.com/prometheus/client_model/go" 41 | 42 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 43 | ) 44 | 45 | const ( 46 | keyName = "testKey" 47 | // Tests fake encryption by assuming that "foo" decrypts to "bar" 48 | // Zm9v is base64 encoded foo 49 | // YmFy is base64 encoded "bar" 50 | ciphertext = "YmFy" 51 | plaintext = "Zm9v" 52 | ) 53 | 54 | var ( 55 | positiveEncryptResponse = &cloudkms.EncryptResponse{ 56 | Ciphertext: ciphertext, 57 | Name: keyName, 58 | ServerResponse: googleapi.ServerResponse{ 59 | HTTPStatusCode: http.StatusOK, 60 | }, 61 | } 62 | positiveDecryptResponse = &cloudkms.DecryptResponse{ 63 | Plaintext: plaintext, 64 | ServerResponse: googleapi.ServerResponse{ 65 | HTTPStatusCode: http.StatusOK, 66 | }, 67 | } 68 | negativeEncryptResponse = &cloudkms.EncryptResponse{ 69 | ServerResponse: googleapi.ServerResponse{ 70 | HTTPStatusCode: http.StatusInternalServerError, 71 | }, 72 | } 73 | ) 74 | 75 | type pluginTestCase struct { 76 | plugin *Plugin 77 | socket string 78 | pluginRPCSrv *grpc.Server 79 | fakeKMSSrv *fakekms.Server 80 | } 81 | 82 | func (p *pluginTestCase) tearDown() { 83 | p.pluginRPCSrv.GracefulStop() 84 | p.fakeKMSSrv.Close() 85 | } 86 | 87 | func setUp(t *testing.T, fakeKMSSrv *fakekms.Server, keyName string) *pluginTestCase { 88 | t.Helper() 89 | 90 | ctx := context.Background() 91 | 92 | dir, err := os.MkdirTemp(os.TempDir(), "") 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | t.Cleanup(func() { 97 | if err := os.RemoveAll(dir); err != nil { 98 | t.Fatal(err) 99 | } 100 | }) 101 | socket := filepath.Join(dir, "listener.sock") 102 | 103 | waitForPluginStart := 3 * time.Second 104 | fakeKMSKeyService, err := cloudkms.NewService(ctx, 105 | option.WithHTTPClient(fakeKMSSrv.Client())) 106 | if err != nil { 107 | t.Fatalf("failed to instantiate cloud kms httpClient: %v", err) 108 | } 109 | fakeKMSKeyService.BasePath = fakeKMSSrv.URL() 110 | p := NewPlugin(fakeKMSKeyService.Projects.Locations.KeyRings.CryptoKeys, keyName) 111 | pluginManager := plugin.NewManager(p, socket) 112 | pluginRPCSrv, errChan := pluginManager.Start() 113 | 114 | // Giving some time for plugin to start while listening on the error channel. 115 | select { 116 | case err := <-errChan: 117 | t.Fatalf("received an error on plugin's error channel: %v", err) 118 | case <-time.After(waitForPluginStart): 119 | } 120 | 121 | return &pluginTestCase{ 122 | plugin: p, 123 | pluginRPCSrv: pluginRPCSrv, 124 | fakeKMSSrv: fakeKMSSrv, 125 | socket: socket, 126 | } 127 | } 128 | 129 | func setUpWithResponses(t *testing.T, keyName string, delay time.Duration, responses ...json.Marshaler) *pluginTestCase { 130 | t.Helper() 131 | fakeKMSSrv, err := fakekms.NewWithResponses(keyName, 0, delay, responses...) 132 | if err != nil { 133 | t.Fatalf("Failed to construct FakeKMS, error: %v", err) 134 | } 135 | return setUp(t, fakeKMSSrv, keyName) 136 | } 137 | 138 | func TestMain(m *testing.M) { 139 | os.Exit(m.Run()) 140 | } 141 | 142 | func TestEncrypt(t *testing.T) { 143 | t.Parallel() 144 | 145 | testCases := []struct { 146 | desc string 147 | wantEncryptRequests []*cloudkms.EncryptRequest 148 | wantDecryptRequests []*cloudkms.DecryptRequest 149 | testFn func(t *testing.T, p *Plugin) 150 | response json.Marshaler 151 | }{ 152 | { 153 | desc: "Encrypt", 154 | wantEncryptRequests: []*cloudkms.EncryptRequest{{Plaintext: plaintext}}, 155 | testFn: func(t *testing.T, p *Plugin) { 156 | encryptRequest := EncryptRequest{Version: apiVersion, Plain: []byte("foo")} 157 | if _, err := p.Encrypt(context.Background(), &encryptRequest); err != nil { 158 | t.Fatalf("Failure while submitting request %v, error %v", encryptRequest, err) 159 | } 160 | }, 161 | response: positiveEncryptResponse, 162 | }, 163 | { 164 | desc: "Decrypt", 165 | wantDecryptRequests: []*cloudkms.DecryptRequest{{Ciphertext: ciphertext}}, 166 | testFn: func(t *testing.T, p *Plugin) { 167 | decryptRequest := DecryptRequest{Version: apiVersion, Cipher: []byte("bar")} 168 | if _, err := p.Decrypt(context.Background(), &decryptRequest); err != nil { 169 | t.Fatalf("Failure while submitting request %v, error %v", decryptRequest, err) 170 | } 171 | }, 172 | response: positiveDecryptResponse, 173 | }, 174 | } 175 | 176 | for _, testCase := range testCases { 177 | 178 | t.Run(testCase.desc, func(t *testing.T) { 179 | t.Parallel() 180 | 181 | tt := setUpWithResponses(t, keyName, 0, testCase.response) 182 | t.Cleanup(func() { 183 | tt.tearDown() 184 | }) 185 | 186 | testCase.testFn(t, tt.plugin) 187 | 188 | if err := tt.fakeKMSSrv.EncryptRequestsEqual(testCase.wantEncryptRequests); err != nil { 189 | t.Fatalf("Failed to compare last processed request on KMS Server, error: %v", err) 190 | } 191 | 192 | if err := tt.fakeKMSSrv.DecryptRequestsEqual(testCase.wantDecryptRequests); err != nil { 193 | t.Fatalf("Failed to compare last processed request on KMS Server, error: %v", err) 194 | } 195 | }) 196 | } 197 | } 198 | 199 | func TestGatherMetrics(t *testing.T) { 200 | t.Parallel() 201 | 202 | testCases := []struct { 203 | desc string 204 | testFn func(t *testing.T, p *Plugin) 205 | want []string 206 | response json.Marshaler 207 | }{ 208 | { 209 | desc: "Decrypt", 210 | testFn: func(t *testing.T, p *Plugin) { 211 | decryptRequest := DecryptRequest{Version: apiVersion, Cipher: []byte("foo")} 212 | _, err := p.Decrypt(context.Background(), &decryptRequest) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | }, 217 | want: []string{ 218 | "roundtrip_latencies", 219 | }, 220 | response: positiveDecryptResponse, 221 | }, 222 | { 223 | desc: "Decrypt Failure", 224 | testFn: func(t *testing.T, p *Plugin) { 225 | decryptRequest := DecryptRequest{Version: apiVersion, Cipher: []byte("foo")} 226 | _, err := p.Decrypt(context.Background(), &decryptRequest) 227 | if err == nil { 228 | t.Fatal("expected Decrypt to fail") 229 | } 230 | }, 231 | want: []string{ 232 | "roundtrip_latencies", 233 | "failures_count", 234 | }, 235 | response: &cloudkms.DecryptResponse{ 236 | ServerResponse: googleapi.ServerResponse{ 237 | HTTPStatusCode: http.StatusInternalServerError, 238 | }, 239 | }, 240 | }, 241 | { 242 | desc: "Encrypt", 243 | testFn: func(t *testing.T, p *Plugin) { 244 | encryptRequest := EncryptRequest{Version: apiVersion, Plain: []byte("foo")} 245 | _, err := p.Encrypt(context.Background(), &encryptRequest) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | }, 250 | want: []string{ 251 | "roundtrip_latencies", 252 | }, 253 | response: positiveEncryptResponse, 254 | }, 255 | { 256 | desc: "Encrypt Failure", 257 | testFn: func(t *testing.T, p *Plugin) { 258 | encryptRequest := EncryptRequest{Version: apiVersion, Plain: []byte("foo")} 259 | _, err := p.Encrypt(context.Background(), &encryptRequest) 260 | if err == nil { 261 | t.Fatal("expected Encrypt to fail") 262 | } 263 | }, 264 | want: []string{ 265 | "roundtrip_latencies", 266 | "failures_count", 267 | }, 268 | response: negativeEncryptResponse, 269 | }, 270 | } 271 | 272 | for _, testCase := range testCases { 273 | 274 | t.Run(testCase.desc, func(t *testing.T) { 275 | t.Parallel() 276 | 277 | tt := setUpWithResponses(t, keyName, 0, testCase.response) 278 | t.Cleanup(func() { 279 | tt.tearDown() 280 | }) 281 | testCase.testFn(t, tt.plugin) 282 | 283 | got, err := prometheus.DefaultGatherer.Gather() 284 | if err != nil { 285 | t.Fatalf("failed to gather metrics: %s", err) 286 | } 287 | checkForExpectedMetrics(t, got, testCase.want) 288 | }) 289 | } 290 | } 291 | 292 | func TestKMSTimeout(t *testing.T) { 293 | t.Parallel() 294 | 295 | testCases := []struct { 296 | desc string 297 | response json.Marshaler 298 | responseDelay time.Duration 299 | pluginTimeout time.Duration 300 | testFn func(ctx context.Context, t *testing.T, p *Plugin) 301 | }{ 302 | { 303 | desc: "Encrypt", 304 | response: positiveEncryptResponse, 305 | pluginTimeout: 1 * time.Second, 306 | responseDelay: 3 * time.Second, 307 | testFn: func(ctx context.Context, t *testing.T, p *Plugin) { 308 | encryptRequest := EncryptRequest{Version: apiVersion, Plain: []byte("foo")} 309 | if _, err := p.Encrypt(ctx, &encryptRequest); err == nil { 310 | t.Fatal("exected to timeout") 311 | } 312 | }, 313 | }, 314 | { 315 | desc: "Decrypt", 316 | response: positiveDecryptResponse, 317 | pluginTimeout: 1 * time.Second, 318 | responseDelay: 3 * time.Second, 319 | testFn: func(ctx context.Context, t *testing.T, p *Plugin) { 320 | decryptRequest := DecryptRequest{Version: apiVersion, Cipher: []byte("bar")} 321 | if _, err := p.Decrypt(ctx, &decryptRequest); err == nil { 322 | t.Fatal("exected to timeout") 323 | } 324 | }, 325 | }, 326 | } 327 | 328 | for _, testCase := range testCases { 329 | 330 | t.Run(testCase.desc, func(t *testing.T) { 331 | t.Parallel() 332 | 333 | tt := setUpWithResponses(t, keyName, testCase.responseDelay, testCase.response) 334 | t.Cleanup(func() { 335 | tt.tearDown() 336 | }) 337 | 338 | ctx, cancel := context.WithTimeout(context.Background(), testCase.pluginTimeout) 339 | defer cancel() 340 | testCase.testFn(ctx, t, tt.plugin) 341 | }) 342 | } 343 | } 344 | 345 | func TestMetricsServer(t *testing.T) { 346 | t.Parallel() 347 | 348 | ctx := context.Background() 349 | metricsPort := mustServeMetrics(t) 350 | 351 | tt := setUpWithResponses(t, keyName, 0, positiveEncryptResponse) 352 | t.Cleanup(func() { 353 | tt.tearDown() 354 | }) 355 | 356 | encryptRequest := EncryptRequest{Version: apiVersion, Plain: []byte("foo")} 357 | if _, err := tt.plugin.Encrypt(ctx, &encryptRequest); err != nil { 358 | t.Fatalf("Failed to submit encrypt request to plugin, error %v", err) 359 | } 360 | 361 | m, err := scrapeMetrics(metricsPort) 362 | if err != nil { 363 | t.Fatalf("Failed to scrape metrics, %v", err) 364 | } 365 | checkForExpectedMetrics(t, m, []string{"roundtrip_latencies"}) 366 | } 367 | 368 | func mustServeMetrics(t *testing.T) int { 369 | t.Helper() 370 | 371 | // TODO(sethvargo): switch to using port 0 372 | p, err := freeport.GetFreePort() 373 | if err != nil { 374 | t.Fatalf("Failed to allocate a free port for metrics server, err: %v", err) 375 | } 376 | 377 | m := &plugin.Metrics{ 378 | ServingURL: &url.URL{ 379 | Host: fmt.Sprintf("localhost:%d", p), 380 | Path: "metrics", 381 | }, 382 | } 383 | 384 | c := m.Serve() 385 | 386 | // Giving some time for metrics server to start while listening on the error 387 | // channel. 388 | select { 389 | case err := <-c: 390 | t.Fatalf("received an error while starting metrics server, error channel: %v", err) 391 | // TODO (alextc): Instead of waiting re-try scrapeMetrics call. 392 | case <-time.After(5 * time.Second): 393 | } 394 | 395 | return p 396 | } 397 | 398 | // scrapeMetrics scrapes Prometheus metrics. 399 | // From https://github.com/kubernetes/kubernetes/blob/master/test/integration/metrics/metrics_test.go#L40 400 | func scrapeMetrics(port int) ([]*prometheuspb.MetricFamily, error) { 401 | var scrapeRequestHeader = "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text" 402 | u := url.URL{ 403 | Scheme: "http", 404 | Host: net.JoinHostPort("localhost", strconv.FormatUint(uint64(port), 10)), 405 | Path: "metrics", 406 | } 407 | 408 | req, err := http.NewRequest(http.MethodGet, u.String(), nil) 409 | if err != nil { 410 | return nil, fmt.Errorf("unable to create http request: %v", err) 411 | } 412 | // Ask the prometheus exporter for its text protocol buffer format, since it's 413 | // much easier to parse than its plain-text format. Don't use the serialized 414 | // proto representation since it uses a non-standard varint delimiter between 415 | // metric families. 416 | req.Header.Add("Accept", scrapeRequestHeader) 417 | resp, err := http.DefaultClient.Do(req) 418 | if err != nil { 419 | return nil, fmt.Errorf("unable to contact metrics endpoint of the master: %v", err) 420 | } 421 | defer resp.Body.Close() 422 | if resp.StatusCode != http.StatusOK { 423 | return nil, fmt.Errorf("non-200 response trying to scrape metrics from the master: %v", resp.Status) 424 | } 425 | 426 | // Each line in the response body should contain all the data for a single metric. 427 | var metrics []*prometheuspb.MetricFamily 428 | scanner := bufio.NewScanner(resp.Body) 429 | for scanner.Scan() { 430 | var metric prometheuspb.MetricFamily 431 | if err := proto.UnmarshalText(scanner.Text(), &metric); err != nil { 432 | return nil, fmt.Errorf("failed to unmarshal line of metrics response: %v", err) 433 | } 434 | metrics = append(metrics, &metric) 435 | } 436 | return metrics, scanner.Err() 437 | } 438 | 439 | func checkForExpectedMetrics(t *testing.T, metrics []*prometheuspb.MetricFamily, expectedMetrics []string) { 440 | t.Helper() 441 | foundMetrics := make(map[string]bool) 442 | for _, metric := range metrics { 443 | foundMetrics[metric.GetName()] = true 444 | } 445 | for _, expected := range expectedMetrics { 446 | if _, found := foundMetrics[expected]; !found { 447 | t.Errorf("Master metrics did not include expected metric %q\n.Metrics:\n%v", expected, foundMetrics) 448 | } 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /plugin/v1/service.pb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 18 | // source: service.proto 19 | 20 | /* 21 | Package v1beta1 is a generated protocol buffer package. 22 | 23 | It is generated from these files: 24 | service.proto 25 | 26 | It has these top-level messages: 27 | VersionRequest 28 | VersionResponse 29 | DecryptRequest 30 | DecryptResponse 31 | EncryptRequest 32 | EncryptResponse 33 | */ 34 | package v1 35 | 36 | import ( 37 | fmt "fmt" 38 | 39 | proto "github.com/gogo/protobuf/proto" 40 | 41 | math "math" 42 | 43 | context "golang.org/x/net/context" 44 | 45 | grpc "google.golang.org/grpc" 46 | ) 47 | 48 | // Reference imports to suppress errors if they are not otherwise used. 49 | var _ = proto.Marshal 50 | var _ = fmt.Errorf 51 | var _ = math.Inf 52 | 53 | // This is a compile-time assertion to ensure that this generated file 54 | // is compatible with the proto package it is being compiled against. 55 | // A compilation error at this line likely means your copy of the 56 | // proto package needs to be updated. 57 | const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package 58 | 59 | type VersionRequest struct { 60 | // Version of the KMS plugin API. 61 | Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` 62 | } 63 | 64 | func (m *VersionRequest) Reset() { *m = VersionRequest{} } 65 | func (m *VersionRequest) String() string { return proto.CompactTextString(m) } 66 | func (*VersionRequest) ProtoMessage() {} 67 | func (*VersionRequest) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{0} } 68 | 69 | func (m *VersionRequest) GetVersion() string { 70 | if m != nil { 71 | return m.Version 72 | } 73 | return "" 74 | } 75 | 76 | type VersionResponse struct { 77 | // Version of the KMS plugin API. 78 | Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` 79 | // Name of the KMS provider. 80 | RuntimeName string `protobuf:"bytes,2,opt,name=runtime_name,json=runtimeName,proto3" json:"runtime_name,omitempty"` 81 | // Version of the KMS provider. The string must be semver-compatible. 82 | RuntimeVersion string `protobuf:"bytes,3,opt,name=runtime_version,json=runtimeVersion,proto3" json:"runtime_version,omitempty"` 83 | } 84 | 85 | func (m *VersionResponse) Reset() { *m = VersionResponse{} } 86 | func (m *VersionResponse) String() string { return proto.CompactTextString(m) } 87 | func (*VersionResponse) ProtoMessage() {} 88 | func (*VersionResponse) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{1} } 89 | 90 | func (m *VersionResponse) GetVersion() string { 91 | if m != nil { 92 | return m.Version 93 | } 94 | return "" 95 | } 96 | 97 | func (m *VersionResponse) GetRuntimeName() string { 98 | if m != nil { 99 | return m.RuntimeName 100 | } 101 | return "" 102 | } 103 | 104 | func (m *VersionResponse) GetRuntimeVersion() string { 105 | if m != nil { 106 | return m.RuntimeVersion 107 | } 108 | return "" 109 | } 110 | 111 | type DecryptRequest struct { 112 | // Version of the KMS plugin API. 113 | Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` 114 | // The data to be decrypted. 115 | Cipher []byte `protobuf:"bytes,2,opt,name=cipher,proto3" json:"cipher,omitempty"` 116 | } 117 | 118 | func (m *DecryptRequest) Reset() { *m = DecryptRequest{} } 119 | func (m *DecryptRequest) String() string { return proto.CompactTextString(m) } 120 | func (*DecryptRequest) ProtoMessage() {} 121 | func (*DecryptRequest) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{2} } 122 | 123 | func (m *DecryptRequest) GetVersion() string { 124 | if m != nil { 125 | return m.Version 126 | } 127 | return "" 128 | } 129 | 130 | func (m *DecryptRequest) GetCipher() []byte { 131 | if m != nil { 132 | return m.Cipher 133 | } 134 | return nil 135 | } 136 | 137 | type DecryptResponse struct { 138 | // The decrypted data. 139 | Plain []byte `protobuf:"bytes,1,opt,name=plain,proto3" json:"plain,omitempty"` 140 | } 141 | 142 | func (m *DecryptResponse) Reset() { *m = DecryptResponse{} } 143 | func (m *DecryptResponse) String() string { return proto.CompactTextString(m) } 144 | func (*DecryptResponse) ProtoMessage() {} 145 | func (*DecryptResponse) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{3} } 146 | 147 | func (m *DecryptResponse) GetPlain() []byte { 148 | if m != nil { 149 | return m.Plain 150 | } 151 | return nil 152 | } 153 | 154 | type EncryptRequest struct { 155 | // Version of the KMS plugin API. 156 | Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` 157 | // The data to be encrypted. 158 | Plain []byte `protobuf:"bytes,2,opt,name=plain,proto3" json:"plain,omitempty"` 159 | } 160 | 161 | func (m *EncryptRequest) Reset() { *m = EncryptRequest{} } 162 | func (m *EncryptRequest) String() string { return proto.CompactTextString(m) } 163 | func (*EncryptRequest) ProtoMessage() {} 164 | func (*EncryptRequest) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{4} } 165 | 166 | func (m *EncryptRequest) GetVersion() string { 167 | if m != nil { 168 | return m.Version 169 | } 170 | return "" 171 | } 172 | 173 | func (m *EncryptRequest) GetPlain() []byte { 174 | if m != nil { 175 | return m.Plain 176 | } 177 | return nil 178 | } 179 | 180 | type EncryptResponse struct { 181 | // The encrypted data. 182 | Cipher []byte `protobuf:"bytes,1,opt,name=cipher,proto3" json:"cipher,omitempty"` 183 | } 184 | 185 | func (m *EncryptResponse) Reset() { *m = EncryptResponse{} } 186 | func (m *EncryptResponse) String() string { return proto.CompactTextString(m) } 187 | func (*EncryptResponse) ProtoMessage() {} 188 | func (*EncryptResponse) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{5} } 189 | 190 | func (m *EncryptResponse) GetCipher() []byte { 191 | if m != nil { 192 | return m.Cipher 193 | } 194 | return nil 195 | } 196 | 197 | func init() { 198 | proto.RegisterType((*VersionRequest)(nil), "v1beta1.VersionRequest") 199 | proto.RegisterType((*VersionResponse)(nil), "v1beta1.VersionResponse") 200 | proto.RegisterType((*DecryptRequest)(nil), "v1beta1.DecryptRequest") 201 | proto.RegisterType((*DecryptResponse)(nil), "v1beta1.DecryptResponse") 202 | proto.RegisterType((*EncryptRequest)(nil), "v1beta1.EncryptRequest") 203 | proto.RegisterType((*EncryptResponse)(nil), "v1beta1.EncryptResponse") 204 | } 205 | 206 | // Reference imports to suppress errors if they are not otherwise used. 207 | var _ context.Context 208 | var _ grpc.ClientConn 209 | 210 | // This is a compile-time assertion to ensure that this generated file 211 | // is compatible with the grpc package it is being compiled against. 212 | const _ = grpc.SupportPackageIsVersion4 213 | 214 | // Client API for KeyManagementService service 215 | 216 | type KeyManagementServiceClient interface { 217 | // Version returns the runtime name and runtime version of the KMS provider. 218 | Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error) 219 | // Execute decryption operation in KMS provider. 220 | Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) 221 | // Execute encryption operation in KMS provider. 222 | Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) 223 | } 224 | 225 | type keyManagementServiceClient struct { 226 | cc *grpc.ClientConn 227 | } 228 | 229 | func NewKeyManagementServiceClient(cc *grpc.ClientConn) KeyManagementServiceClient { 230 | return &keyManagementServiceClient{cc} 231 | } 232 | 233 | func (c *keyManagementServiceClient) Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error) { 234 | out := new(VersionResponse) 235 | err := grpc.Invoke(ctx, "/v1beta1.KeyManagementService/Version", in, out, c.cc, opts...) 236 | if err != nil { 237 | return nil, err 238 | } 239 | return out, nil 240 | } 241 | 242 | func (c *keyManagementServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) { 243 | out := new(DecryptResponse) 244 | err := grpc.Invoke(ctx, "/v1beta1.KeyManagementService/Decrypt", in, out, c.cc, opts...) 245 | if err != nil { 246 | return nil, err 247 | } 248 | return out, nil 249 | } 250 | 251 | func (c *keyManagementServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) { 252 | out := new(EncryptResponse) 253 | err := grpc.Invoke(ctx, "/v1beta1.KeyManagementService/Encrypt", in, out, c.cc, opts...) 254 | if err != nil { 255 | return nil, err 256 | } 257 | return out, nil 258 | } 259 | 260 | // Server API for KeyManagementService service 261 | 262 | type KeyManagementServiceServer interface { 263 | // Version returns the runtime name and runtime version of the KMS provider. 264 | Version(context.Context, *VersionRequest) (*VersionResponse, error) 265 | // Execute decryption operation in KMS provider. 266 | Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) 267 | // Execute encryption operation in KMS provider. 268 | Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) 269 | } 270 | 271 | func RegisterKeyManagementServiceServer(s *grpc.Server, srv KeyManagementServiceServer) { 272 | s.RegisterService(&_KeyManagementService_serviceDesc, srv) 273 | } 274 | 275 | func _KeyManagementService_Version_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 276 | in := new(VersionRequest) 277 | if err := dec(in); err != nil { 278 | return nil, err 279 | } 280 | if interceptor == nil { 281 | return srv.(KeyManagementServiceServer).Version(ctx, in) 282 | } 283 | info := &grpc.UnaryServerInfo{ 284 | Server: srv, 285 | FullMethod: "/v1beta1.KeyManagementService/Version", 286 | } 287 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 288 | return srv.(KeyManagementServiceServer).Version(ctx, req.(*VersionRequest)) 289 | } 290 | return interceptor(ctx, in, info, handler) 291 | } 292 | 293 | func _KeyManagementService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 294 | in := new(DecryptRequest) 295 | if err := dec(in); err != nil { 296 | return nil, err 297 | } 298 | if interceptor == nil { 299 | return srv.(KeyManagementServiceServer).Decrypt(ctx, in) 300 | } 301 | info := &grpc.UnaryServerInfo{ 302 | Server: srv, 303 | FullMethod: "/v1beta1.KeyManagementService/Decrypt", 304 | } 305 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 306 | return srv.(KeyManagementServiceServer).Decrypt(ctx, req.(*DecryptRequest)) 307 | } 308 | return interceptor(ctx, in, info, handler) 309 | } 310 | 311 | func _KeyManagementService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 312 | in := new(EncryptRequest) 313 | if err := dec(in); err != nil { 314 | return nil, err 315 | } 316 | if interceptor == nil { 317 | return srv.(KeyManagementServiceServer).Encrypt(ctx, in) 318 | } 319 | info := &grpc.UnaryServerInfo{ 320 | Server: srv, 321 | FullMethod: "/v1beta1.KeyManagementService/Encrypt", 322 | } 323 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 324 | return srv.(KeyManagementServiceServer).Encrypt(ctx, req.(*EncryptRequest)) 325 | } 326 | return interceptor(ctx, in, info, handler) 327 | } 328 | 329 | var _KeyManagementService_serviceDesc = grpc.ServiceDesc{ 330 | ServiceName: "v1beta1.KeyManagementService", 331 | HandlerType: (*KeyManagementServiceServer)(nil), 332 | Methods: []grpc.MethodDesc{ 333 | { 334 | MethodName: "Version", 335 | Handler: _KeyManagementService_Version_Handler, 336 | }, 337 | { 338 | MethodName: "Decrypt", 339 | Handler: _KeyManagementService_Decrypt_Handler, 340 | }, 341 | { 342 | MethodName: "Encrypt", 343 | Handler: _KeyManagementService_Encrypt_Handler, 344 | }, 345 | }, 346 | Streams: []grpc.StreamDesc{}, 347 | Metadata: "service.proto", 348 | } 349 | 350 | func init() { proto.RegisterFile("service.proto", fileDescriptorService) } 351 | 352 | var fileDescriptorService = []byte{ 353 | // 287 bytes of a gzipped FileDescriptorProto 354 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x52, 0xcd, 0x4a, 0xc4, 0x30, 355 | 0x10, 0xde, 0xae, 0xb8, 0xc5, 0xb1, 0xb6, 0x10, 0x16, 0x2d, 0x9e, 0x34, 0x97, 0x55, 0x0f, 0x85, 356 | 0xd5, 0xbb, 0x88, 0xe8, 0x49, 0xf4, 0x50, 0xc1, 0xab, 0x64, 0xcb, 0xa0, 0x05, 0x9b, 0xc6, 0x24, 357 | 0x5b, 0xd9, 0x17, 0xf5, 0x79, 0xc4, 0x66, 0x5a, 0xd3, 0x15, 0x71, 0x8f, 0x33, 0x99, 0xef, 0x6f, 358 | 0x26, 0xb0, 0x67, 0x50, 0x37, 0x65, 0x81, 0x99, 0xd2, 0xb5, 0xad, 0x59, 0xd8, 0xcc, 0x17, 0x68, 359 | 0xc5, 0x9c, 0x9f, 0x41, 0xfc, 0x84, 0xda, 0x94, 0xb5, 0xcc, 0xf1, 0x7d, 0x89, 0xc6, 0xb2, 0x14, 360 | 0xc2, 0xc6, 0x75, 0xd2, 0xe0, 0x28, 0x38, 0xd9, 0xc9, 0xbb, 0x92, 0x7f, 0x40, 0xd2, 0xcf, 0x1a, 361 | 0x55, 0x4b, 0x83, 0x7f, 0x0f, 0xb3, 0x63, 0x88, 0xf4, 0x52, 0xda, 0xb2, 0xc2, 0x67, 0x29, 0x2a, 362 | 0x4c, 0xc7, 0xed, 0xf3, 0x2e, 0xf5, 0x1e, 0x44, 0x85, 0x6c, 0x06, 0x49, 0x37, 0xd2, 0x91, 0x6c, 363 | 0xb5, 0x53, 0x31, 0xb5, 0x49, 0x8d, 0x5f, 0x43, 0x7c, 0x83, 0x85, 0x5e, 0x29, 0xfb, 0xaf, 0x49, 364 | 0xb6, 0x0f, 0x93, 0xa2, 0x54, 0xaf, 0xa8, 0x5b, 0xc5, 0x28, 0xa7, 0x8a, 0xcf, 0x20, 0xe9, 0x39, 365 | 0xc8, 0xfc, 0x14, 0xb6, 0xd5, 0x9b, 0x28, 0x1d, 0x45, 0x94, 0xbb, 0x82, 0x5f, 0x41, 0x7c, 0x2b, 366 | 0x37, 0x14, 0xeb, 0x19, 0xc6, 0x3e, 0xc3, 0x29, 0x24, 0x3d, 0x03, 0x49, 0xfd, 0xb8, 0x0a, 0x7c, 367 | 0x57, 0xe7, 0x9f, 0x01, 0x4c, 0xef, 0x70, 0x75, 0x2f, 0xa4, 0x78, 0xc1, 0x0a, 0xa5, 0x7d, 0x74, 368 | 0x67, 0x62, 0x97, 0x10, 0x52, 0x7a, 0x76, 0x90, 0xd1, 0xb1, 0xb2, 0xe1, 0xa5, 0x0e, 0xd3, 0xdf, 369 | 0x0f, 0x4e, 0x8e, 0x8f, 0xbe, 0xf1, 0x14, 0xd7, 0xc3, 0x0f, 0x97, 0xe8, 0xe1, 0xd7, 0x36, 0xe3, 370 | 0xf0, 0x94, 0xc1, 0xc3, 0x0f, 0xf7, 0xe2, 0xe1, 0xd7, 0xe2, 0xf2, 0xd1, 0x62, 0xd2, 0xfe, 0xb3, 371 | 0x8b, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x33, 0x8d, 0x09, 0xe1, 0x78, 0x02, 0x00, 0x00, 372 | } 373 | -------------------------------------------------------------------------------- /plugin/v2/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "net" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | "path/filepath" 27 | "strconv" 28 | "testing" 29 | "time" 30 | 31 | "google.golang.org/api/cloudkms/v1" 32 | "google.golang.org/api/googleapi" 33 | "google.golang.org/api/option" 34 | "google.golang.org/grpc" 35 | 36 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin" 37 | "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/testutils/fakekms" 38 | "github.com/golang/protobuf/proto" 39 | "github.com/phayes/freeport" 40 | "github.com/prometheus/client_golang/prometheus" 41 | prometheuspb "github.com/prometheus/client_model/go" 42 | 43 | "github.com/stretchr/testify/assert" 44 | ) 45 | 46 | const ( 47 | keyName = "projects/my-project/locations/us-east1/keyRings/my-key-ring/cryptoKeys/my-key" 48 | keyVersionName = keyName + "/cryptoKeyVersions/1" 49 | keySuffix = "test" 50 | // Tests fake encryption by assuming that "foo" decrypts to "bar" 51 | // Zm9v is base64 encoded foo 52 | // YmFy is base64 encoded "bar" 53 | ciphertext = "YmFy" 54 | plaintext = "Zm9v" 55 | ) 56 | 57 | var ( 58 | positiveEncryptResponse = &cloudkms.EncryptResponse{ 59 | Ciphertext: ciphertext, 60 | Name: keyName, 61 | ServerResponse: googleapi.ServerResponse{ 62 | HTTPStatusCode: http.StatusOK, 63 | }, 64 | } 65 | positiveDecryptResponse = &cloudkms.DecryptResponse{ 66 | Plaintext: plaintext, 67 | ServerResponse: googleapi.ServerResponse{ 68 | HTTPStatusCode: http.StatusOK, 69 | }, 70 | } 71 | negativeEncryptResponse = &cloudkms.EncryptResponse{ 72 | ServerResponse: googleapi.ServerResponse{ 73 | HTTPStatusCode: http.StatusInternalServerError, 74 | }, 75 | } 76 | ) 77 | 78 | type pluginTestCase struct { 79 | plugin *Plugin 80 | pluginRPCSrv *grpc.Server 81 | fakeKMSSrv *fakekms.Server 82 | socket string 83 | } 84 | 85 | func (p *pluginTestCase) tearDown() { 86 | p.pluginRPCSrv.GracefulStop() 87 | p.fakeKMSSrv.Close() 88 | } 89 | 90 | func setUp(t *testing.T, fakeKMSSrv *fakekms.Server, keyName string, keySuffix string) *pluginTestCase { 91 | t.Helper() 92 | 93 | dir, err := os.MkdirTemp(os.TempDir(), "") 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | t.Cleanup(func() { 98 | if err := os.RemoveAll(dir); err != nil { 99 | t.Fatal(err) 100 | } 101 | }) 102 | socket := filepath.Join(dir, "listener.sock") 103 | 104 | ctx := context.Background() 105 | waitForPluginStart := 3 * time.Second 106 | fakeKMSKeyService, err := cloudkms.NewService(ctx, 107 | option.WithHTTPClient(fakeKMSSrv.Client())) 108 | if err != nil { 109 | t.Fatalf("failed to instantiate cloud kms httpClient: %v", err) 110 | } 111 | fakeKMSKeyService.BasePath = fakeKMSSrv.URL() 112 | p := NewPlugin(fakeKMSKeyService.Projects.Locations.KeyRings.CryptoKeys, keyName, keySuffix) 113 | pluginManager := plugin.NewManager(p, socket) 114 | pluginRPCSrv, errCh := pluginManager.Start() 115 | // Giving some time for plugin to start while listening on the error channel. 116 | select { 117 | case err := <-errCh: 118 | t.Fatalf("received an error on plugin's error channel: %v", err) 119 | case <-time.After(waitForPluginStart): 120 | } 121 | 122 | return &pluginTestCase{ 123 | plugin: p, 124 | pluginRPCSrv: pluginRPCSrv, 125 | fakeKMSSrv: fakeKMSSrv, 126 | socket: socket, 127 | } 128 | } 129 | 130 | func setUpWithResponses(t *testing.T, keyName string, keySuffix string, delay time.Duration, responses ...json.Marshaler) *pluginTestCase { 131 | t.Helper() 132 | fakeKMSSrv, err := fakekms.NewWithResponses(keyName, 0, delay, responses...) 133 | if err != nil { 134 | t.Fatalf("Failed to construct FakeKMS, error: %v", err) 135 | } 136 | return setUp(t, fakeKMSSrv, keyName, keySuffix) 137 | } 138 | 139 | func TestMain(m *testing.M) { 140 | os.Exit(m.Run()) 141 | } 142 | 143 | func TestEncrypt(t *testing.T) { 144 | t.Parallel() 145 | 146 | testCases := []struct { 147 | desc string 148 | wantEncryptRequests []*cloudkms.EncryptRequest 149 | wantDecryptRequests []*cloudkms.DecryptRequest 150 | testFn func(t *testing.T, p *Plugin) 151 | response json.Marshaler 152 | keySuffix string 153 | }{ 154 | { 155 | desc: "Encrypt", 156 | wantEncryptRequests: []*cloudkms.EncryptRequest{{Plaintext: plaintext}}, 157 | testFn: func(t *testing.T, p *Plugin) { 158 | encryptRequest := EncryptRequest{Plaintext: []byte("foo")} 159 | if _, err := p.Encrypt(context.Background(), &encryptRequest); err != nil { 160 | t.Fatalf("Failure while submitting request %v, error %v", encryptRequest, err) 161 | } 162 | }, 163 | response: positiveEncryptResponse, 164 | }, 165 | { 166 | desc: "Decrypt", 167 | wantDecryptRequests: []*cloudkms.DecryptRequest{{Ciphertext: ciphertext}}, 168 | testFn: func(t *testing.T, p *Plugin) { 169 | decryptRequest := DecryptRequest{Ciphertext: []byte("bar"), KeyId: keyVersionName} 170 | if _, err := p.Decrypt(context.Background(), &decryptRequest); err != nil { 171 | t.Fatalf("Failure while submitting request %v, error %v", decryptRequest, err) 172 | } 173 | }, 174 | response: positiveDecryptResponse, 175 | }, 176 | { 177 | desc: "Encrypt", 178 | wantEncryptRequests: []*cloudkms.EncryptRequest{{Plaintext: plaintext}}, 179 | testFn: func(t *testing.T, p *Plugin) { 180 | encryptRequest := EncryptRequest{Plaintext: []byte("foo")} 181 | if _, err := p.Encrypt(context.Background(), &encryptRequest); err != nil { 182 | t.Fatalf("Failure while submitting request %v, error %v", encryptRequest, err) 183 | } 184 | }, 185 | response: positiveEncryptResponse, 186 | keySuffix: "test", 187 | }, 188 | { 189 | desc: "Decrypt", 190 | wantDecryptRequests: []*cloudkms.DecryptRequest{{Ciphertext: ciphertext}}, 191 | testFn: func(t *testing.T, p *Plugin) { 192 | decryptRequest := DecryptRequest{Ciphertext: []byte("bar"), KeyId: keyVersionName} 193 | if _, err := p.Decrypt(context.Background(), &decryptRequest); err != nil { 194 | t.Fatalf("Failure while submitting request %v, error %v", decryptRequest, err) 195 | } 196 | }, 197 | response: positiveDecryptResponse, 198 | keySuffix: "test", 199 | }, 200 | } 201 | 202 | for _, testCase := range testCases { 203 | 204 | t.Run(testCase.desc, func(t *testing.T) { 205 | t.Parallel() 206 | 207 | tt := setUpWithResponses(t, keyName, testCase.keySuffix, 0, testCase.response) 208 | t.Cleanup(func() { 209 | tt.tearDown() 210 | }) 211 | 212 | testCase.testFn(t, tt.plugin) 213 | 214 | if err := tt.fakeKMSSrv.EncryptRequestsEqual(testCase.wantEncryptRequests); err != nil { 215 | t.Fatalf("Failed to compare last processed request on KMS Server, error: %v", err) 216 | } 217 | 218 | if err := tt.fakeKMSSrv.DecryptRequestsEqual(testCase.wantDecryptRequests); err != nil { 219 | t.Fatalf("Failed to compare last processed request on KMS Server, error: %v", err) 220 | } 221 | }) 222 | } 223 | } 224 | 225 | func TestGatherMetrics(t *testing.T) { 226 | t.Parallel() 227 | 228 | testCases := []struct { 229 | desc string 230 | testFn func(t *testing.T, p *Plugin) 231 | want []string 232 | response json.Marshaler 233 | keySuffix string 234 | }{ 235 | { 236 | desc: "Decrypt", 237 | testFn: func(t *testing.T, p *Plugin) { 238 | decryptRequest := DecryptRequest{Ciphertext: []byte("foo"), KeyId: keyVersionName} 239 | _, err := p.Decrypt(context.Background(), &decryptRequest) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | }, 244 | want: []string{ 245 | "roundtrip_latencies", 246 | }, 247 | response: positiveDecryptResponse, 248 | }, 249 | { 250 | desc: "Decrypt Failure", 251 | testFn: func(t *testing.T, p *Plugin) { 252 | decryptRequest := DecryptRequest{Ciphertext: []byte("foo"), KeyId: keyVersionName} 253 | _, err := p.Decrypt(context.Background(), &decryptRequest) 254 | if err == nil { 255 | t.Fatal("expected Decrypt to fail") 256 | } 257 | }, 258 | want: []string{ 259 | "roundtrip_latencies", 260 | "failures_count", 261 | }, 262 | response: &cloudkms.DecryptResponse{ 263 | ServerResponse: googleapi.ServerResponse{ 264 | HTTPStatusCode: http.StatusInternalServerError, 265 | }, 266 | }, 267 | }, 268 | { 269 | desc: "Encrypt", 270 | testFn: func(t *testing.T, p *Plugin) { 271 | encryptRequest := EncryptRequest{Plaintext: []byte("foo")} 272 | _, err := p.Encrypt(context.Background(), &encryptRequest) 273 | if err != nil { 274 | t.Fatal(err) 275 | } 276 | }, 277 | want: []string{ 278 | "roundtrip_latencies", 279 | }, 280 | response: positiveEncryptResponse, 281 | }, 282 | { 283 | desc: "Encrypt Failure", 284 | testFn: func(t *testing.T, p *Plugin) { 285 | encryptRequest := EncryptRequest{Plaintext: []byte("foo")} 286 | _, err := p.Encrypt(context.Background(), &encryptRequest) 287 | if err == nil { 288 | t.Fatal("expected Encrypt to fail") 289 | } 290 | }, 291 | want: []string{ 292 | "roundtrip_latencies", 293 | "failures_count", 294 | }, 295 | response: negativeEncryptResponse, 296 | }, 297 | } 298 | 299 | for _, testCase := range testCases { 300 | 301 | t.Run(testCase.desc, func(t *testing.T) { 302 | t.Parallel() 303 | 304 | tt := setUpWithResponses(t, keyName, testCase.keySuffix, 0, testCase.response) 305 | t.Cleanup(func() { 306 | tt.tearDown() 307 | }) 308 | testCase.testFn(t, tt.plugin) 309 | 310 | got, err := prometheus.DefaultGatherer.Gather() 311 | if err != nil { 312 | t.Fatalf("failed to gather metrics: %s", err) 313 | } 314 | checkForExpectedMetrics(t, got, testCase.want) 315 | }) 316 | } 317 | } 318 | 319 | func TestKMSTimeout(t *testing.T) { 320 | t.Parallel() 321 | 322 | testCases := []struct { 323 | desc string 324 | response json.Marshaler 325 | responseDelay time.Duration 326 | pluginTimeout time.Duration 327 | testFn func(ctx context.Context, t *testing.T, p *Plugin) 328 | keySuffix string 329 | }{ 330 | { 331 | desc: "Encrypt", 332 | response: positiveEncryptResponse, 333 | pluginTimeout: 1 * time.Second, 334 | responseDelay: 3 * time.Second, 335 | testFn: func(ctx context.Context, t *testing.T, p *Plugin) { 336 | encryptRequest := EncryptRequest{Plaintext: []byte("foo")} 337 | if _, err := p.Encrypt(ctx, &encryptRequest); err == nil { 338 | t.Fatal("exected to timeout") 339 | } 340 | }, 341 | keySuffix: "test", 342 | }, 343 | { 344 | desc: "Decrypt", 345 | response: positiveDecryptResponse, 346 | pluginTimeout: 1 * time.Second, 347 | responseDelay: 3 * time.Second, 348 | testFn: func(ctx context.Context, t *testing.T, p *Plugin) { 349 | decryptRequest := DecryptRequest{Ciphertext: []byte("bar")} 350 | if _, err := p.Decrypt(ctx, &decryptRequest); err == nil { 351 | t.Fatal("exected to timeout") 352 | } 353 | }, 354 | keySuffix: "test", 355 | }, 356 | } 357 | 358 | for _, testCase := range testCases { 359 | 360 | t.Run(testCase.desc, func(t *testing.T) { 361 | t.Parallel() 362 | 363 | tt := setUpWithResponses(t, keyName, testCase.keySuffix, testCase.responseDelay, testCase.response) 364 | t.Cleanup(func() { 365 | tt.tearDown() 366 | }) 367 | 368 | ctx, cancel := context.WithTimeout(context.Background(), testCase.pluginTimeout) 369 | t.Cleanup(func() { 370 | cancel() 371 | }) 372 | testCase.testFn(ctx, t, tt.plugin) 373 | }) 374 | } 375 | } 376 | 377 | func TestMetricsServer(t *testing.T) { 378 | t.Parallel() 379 | 380 | ctx := context.Background() 381 | metricsPort := mustServeMetrics(t) 382 | 383 | tt := setUpWithResponses(t, keyName, keySuffix, 0, positiveEncryptResponse) 384 | t.Cleanup(func() { 385 | tt.tearDown() 386 | }) 387 | 388 | encryptRequest := EncryptRequest{Plaintext: []byte("foo")} 389 | if _, err := tt.plugin.Encrypt(ctx, &encryptRequest); err != nil { 390 | t.Fatalf("Failed to submit encrypt request to plugin, error %v", err) 391 | } 392 | 393 | m, err := scrapeMetrics(metricsPort) 394 | if err != nil { 395 | t.Fatalf("Failed to scrape metrics, %v", err) 396 | } 397 | checkForExpectedMetrics(t, m, []string{"roundtrip_latencies"}) 398 | } 399 | 400 | func mustServeMetrics(t *testing.T) int { 401 | t.Helper() 402 | p, err := freeport.GetFreePort() 403 | if err != nil { 404 | t.Fatalf("Failed to allocate a free port for metrics server, err: %v", err) 405 | } 406 | 407 | m := &plugin.Metrics{ 408 | ServingURL: &url.URL{ 409 | Host: fmt.Sprintf("localhost:%d", p), 410 | Path: "metrics", 411 | }, 412 | } 413 | 414 | c := m.Serve() 415 | // Giving some time for metrics server to start while listening on the error channel. 416 | select { 417 | case err := <-c: 418 | t.Fatalf("received an error while starting metrics server, error channel: %v", err) 419 | // TODO (alextc): Instead of waiting re-try scrapeMetrics call. 420 | case <-time.After(5 * time.Second): 421 | } 422 | 423 | return p 424 | } 425 | 426 | // scrapeMetrics scrapes Prometheus metrics. 427 | // From https://github.com/kubernetes/kubernetes/blob/master/test/integration/metrics/metrics_test.go#L40 428 | func scrapeMetrics(port int) ([]*prometheuspb.MetricFamily, error) { 429 | var scrapeRequestHeader = "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text" 430 | u := url.URL{ 431 | Scheme: "http", 432 | Host: net.JoinHostPort("localhost", strconv.FormatUint(uint64(port), 10)), 433 | Path: "metrics", 434 | } 435 | 436 | req, err := http.NewRequest(http.MethodGet, u.String(), nil) 437 | if err != nil { 438 | return nil, fmt.Errorf("unable to create http request: %v", err) 439 | } 440 | // Ask the prometheus exporter for its text protocol buffer format, since it's 441 | // much easier to parse than its plain-text format. Don't use the serialized 442 | // proto representation since it uses a non-standard varint delimiter between 443 | // metric families. 444 | req.Header.Add("Accept", scrapeRequestHeader) 445 | resp, err := http.DefaultClient.Do(req) 446 | if err != nil { 447 | return nil, fmt.Errorf("unable to contact metrics endpoint of the master: %v", err) 448 | } 449 | defer resp.Body.Close() 450 | if resp.StatusCode != http.StatusOK { 451 | return nil, fmt.Errorf("non-200 response trying to scrape metrics from the master: %v", resp.Status) 452 | } 453 | 454 | // Each line in the response body should contain all the data for a single metric. 455 | var metrics []*prometheuspb.MetricFamily 456 | scanner := bufio.NewScanner(resp.Body) 457 | for scanner.Scan() { 458 | var metric prometheuspb.MetricFamily 459 | if err := proto.UnmarshalText(scanner.Text(), &metric); err != nil { 460 | return nil, fmt.Errorf("failed to unmarshal line of metrics response: %v", err) 461 | } 462 | metrics = append(metrics, &metric) 463 | } 464 | return metrics, scanner.Err() 465 | } 466 | 467 | func checkForExpectedMetrics(t *testing.T, metrics []*prometheuspb.MetricFamily, expectedMetrics []string) { 468 | t.Helper() 469 | foundMetrics := make(map[string]bool) 470 | for _, metric := range metrics { 471 | foundMetrics[metric.GetName()] = true 472 | } 473 | for _, expected := range expectedMetrics { 474 | if _, found := foundMetrics[expected]; !found { 475 | t.Errorf("Master metrics did not include expected metric %q\n.Metrics:\n%v", expected, foundMetrics) 476 | } 477 | } 478 | } 479 | 480 | func TestExtractKeyVersion(t *testing.T) { 481 | tests := []struct { 482 | keyVersionId string 483 | expectedKey string 484 | }{ 485 | { 486 | keyVersionId: keyName + "/cryptoKeyVersions/123", 487 | expectedKey: keyName, 488 | }, 489 | { 490 | keyVersionId: keyName + "/cryptoKeyVersions/456", 491 | expectedKey: keyName, 492 | }, 493 | { 494 | keyVersionId: keyName + "/cryptoKeyVersions/123:test", 495 | expectedKey: keyName, 496 | }, 497 | { 498 | keyVersionId: keyName + ":test", 499 | expectedKey: keyName, 500 | }, 501 | { 502 | keyVersionId: keyName + "/cryptoKeyVersions/123:", 503 | expectedKey: keyName, 504 | }, 505 | { 506 | keyVersionId: keyName + ":", 507 | expectedKey: keyName, 508 | }, 509 | { 510 | keyVersionId: "projects/my-project", 511 | expectedKey: "", 512 | }, 513 | { 514 | keyVersionId: keyName, 515 | expectedKey: keyName, 516 | }, 517 | { 518 | keyVersionId: "1234567", 519 | expectedKey: "", 520 | }, 521 | } 522 | 523 | for _, test := range tests { 524 | actualKey := extractKeyName(test.keyVersionId) 525 | assert.Equal(t, test.expectedKey, actualKey) 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 3 | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 8 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 9 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 16 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 17 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 20 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 21 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 22 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 23 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 24 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 25 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 26 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 27 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 28 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 29 | github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= 30 | github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 31 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 32 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 33 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 34 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 35 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 36 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 38 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 39 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 40 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 41 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 42 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 43 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 44 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 45 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 46 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 47 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 48 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 49 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 50 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 54 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 55 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 56 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 57 | github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= 58 | github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= 59 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 60 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 61 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 62 | github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= 63 | github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= 64 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 65 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 66 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 67 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= 68 | github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= 69 | github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= 70 | github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= 71 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 72 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 73 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 74 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 75 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 76 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 77 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 78 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 79 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 80 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 81 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 82 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 83 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 84 | github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= 85 | github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 86 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 87 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 88 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 89 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 90 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 91 | github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= 92 | github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= 93 | github.com/prometheus/common v0.49.0 h1:ToNTdK4zSnPVJmh698mGFkDor9wBI/iGaJy5dbH1EgI= 94 | github.com/prometheus/common v0.49.0/go.mod h1:Kxm+EULxRbUkjGU6WFsQqo3ORzB4tyKvlWFOE9mB2sE= 95 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 96 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 97 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 98 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 99 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 100 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 101 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 102 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 103 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 104 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 105 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 106 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 107 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 108 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 109 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 110 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 111 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 112 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 113 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 114 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 115 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 116 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 117 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 118 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 119 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 120 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 121 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 122 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 124 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 125 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 126 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 127 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 128 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 129 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 130 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 131 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 132 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 133 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 134 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 135 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 136 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 137 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 138 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 139 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 140 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 141 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 142 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 143 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 144 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 145 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 146 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 147 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 148 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 149 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 150 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 151 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 152 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 153 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 154 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 155 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 156 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 157 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 158 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 159 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 160 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 161 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 162 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 163 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 164 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 165 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 166 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 167 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 168 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 169 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 170 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 171 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 172 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 173 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 174 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 175 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 176 | google.golang.org/api v0.167.0 h1:CKHrQD1BLRii6xdkatBDXyKzM0mkawt2QP+H3LtPmSE= 177 | google.golang.org/api v0.167.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= 178 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 179 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 180 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 181 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 182 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 183 | google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 h1:g/4bk7P6TPMkAUbUhquq98xey1slwvuVJPosdBqYJlU= 184 | google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M= 185 | google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A= 186 | google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= 187 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 h1:DKU1r6Tj5s1vlU/moGhuGz7E3xRfwjdAfDzbsaQJtEY= 188 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= 189 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 190 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 191 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 192 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 193 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 194 | google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= 195 | google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= 196 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 197 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 198 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 199 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 200 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 201 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 202 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 203 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 204 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 205 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 206 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 207 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 208 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 211 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 212 | gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= 213 | gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= 214 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 215 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 216 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 217 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 218 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 219 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 220 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 221 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 222 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 223 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 224 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 225 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 226 | k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= 227 | k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= 228 | k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= 229 | k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= 230 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 231 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 232 | k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= 233 | k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 234 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 235 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 236 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 237 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 238 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 239 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 240 | -------------------------------------------------------------------------------- /plugin/v2/api.pb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 18 | // source: api.proto 19 | 20 | package v2 21 | 22 | import ( 23 | context "context" 24 | fmt "fmt" 25 | proto "github.com/gogo/protobuf/proto" 26 | grpc "google.golang.org/grpc" 27 | codes "google.golang.org/grpc/codes" 28 | status "google.golang.org/grpc/status" 29 | math "math" 30 | ) 31 | 32 | // Reference imports to suppress errors if they are not otherwise used. 33 | var _ = proto.Marshal 34 | var _ = fmt.Errorf 35 | var _ = math.Inf 36 | 37 | // This is a compile-time assertion to ensure that this generated file 38 | // is compatible with the proto package it is being compiled against. 39 | // A compilation error at this line likely means your copy of the 40 | // proto package needs to be updated. 41 | const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package 42 | 43 | type StatusRequest struct { 44 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 45 | XXX_unrecognized []byte `json:"-"` 46 | XXX_sizecache int32 `json:"-"` 47 | } 48 | 49 | func (m *StatusRequest) Reset() { *m = StatusRequest{} } 50 | func (m *StatusRequest) String() string { return proto.CompactTextString(m) } 51 | func (*StatusRequest) ProtoMessage() {} 52 | func (*StatusRequest) Descriptor() ([]byte, []int) { 53 | return fileDescriptor_00212fb1f9d3bf1c, []int{0} 54 | } 55 | func (m *StatusRequest) XXX_Unmarshal(b []byte) error { 56 | return xxx_messageInfo_StatusRequest.Unmarshal(m, b) 57 | } 58 | func (m *StatusRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 59 | return xxx_messageInfo_StatusRequest.Marshal(b, m, deterministic) 60 | } 61 | func (m *StatusRequest) XXX_Merge(src proto.Message) { 62 | xxx_messageInfo_StatusRequest.Merge(m, src) 63 | } 64 | func (m *StatusRequest) XXX_Size() int { 65 | return xxx_messageInfo_StatusRequest.Size(m) 66 | } 67 | func (m *StatusRequest) XXX_DiscardUnknown() { 68 | xxx_messageInfo_StatusRequest.DiscardUnknown(m) 69 | } 70 | 71 | var xxx_messageInfo_StatusRequest proto.InternalMessageInfo 72 | 73 | type StatusResponse struct { 74 | // Version of the KMS gRPC plugin API. Must equal v2 to v2beta1 (v2 is recommended, but both are equivalent). 75 | Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` 76 | // Any value other than "ok" is failing healthz. On failure, the associated API server healthz endpoint will contain this value as part of the error message. 77 | Healthz string `protobuf:"bytes,2,opt,name=healthz,proto3" json:"healthz,omitempty"` 78 | // the current write key, used to determine staleness of data updated via value.Transformer.TransformFromStorage. 79 | // keyID must satisfy the following constraints: 80 | // 1. The keyID is not empty. 81 | // 2. The size of keyID is less than 1 kB. 82 | KeyId string `protobuf:"bytes,3,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` 83 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 84 | XXX_unrecognized []byte `json:"-"` 85 | XXX_sizecache int32 `json:"-"` 86 | } 87 | 88 | func (m *StatusResponse) Reset() { *m = StatusResponse{} } 89 | func (m *StatusResponse) String() string { return proto.CompactTextString(m) } 90 | func (*StatusResponse) ProtoMessage() {} 91 | func (*StatusResponse) Descriptor() ([]byte, []int) { 92 | return fileDescriptor_00212fb1f9d3bf1c, []int{1} 93 | } 94 | func (m *StatusResponse) XXX_Unmarshal(b []byte) error { 95 | return xxx_messageInfo_StatusResponse.Unmarshal(m, b) 96 | } 97 | func (m *StatusResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 98 | return xxx_messageInfo_StatusResponse.Marshal(b, m, deterministic) 99 | } 100 | func (m *StatusResponse) XXX_Merge(src proto.Message) { 101 | xxx_messageInfo_StatusResponse.Merge(m, src) 102 | } 103 | func (m *StatusResponse) XXX_Size() int { 104 | return xxx_messageInfo_StatusResponse.Size(m) 105 | } 106 | func (m *StatusResponse) XXX_DiscardUnknown() { 107 | xxx_messageInfo_StatusResponse.DiscardUnknown(m) 108 | } 109 | 110 | var xxx_messageInfo_StatusResponse proto.InternalMessageInfo 111 | 112 | func (m *StatusResponse) GetVersion() string { 113 | if m != nil { 114 | return m.Version 115 | } 116 | return "" 117 | } 118 | 119 | func (m *StatusResponse) GetHealthz() string { 120 | if m != nil { 121 | return m.Healthz 122 | } 123 | return "" 124 | } 125 | 126 | func (m *StatusResponse) GetKeyId() string { 127 | if m != nil { 128 | return m.KeyId 129 | } 130 | return "" 131 | } 132 | 133 | type DecryptRequest struct { 134 | // The data to be decrypted. 135 | Ciphertext []byte `protobuf:"bytes,1,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` 136 | // UID is a unique identifier for the request. 137 | Uid string `protobuf:"bytes,2,opt,name=uid,proto3" json:"uid,omitempty"` 138 | // The keyID that was provided to the apiserver during encryption. 139 | // This represents the KMS KEK that was used to encrypt the data. 140 | KeyId string `protobuf:"bytes,3,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` 141 | // Additional metadata that was sent by the KMS plugin during encryption. 142 | Annotations map[string][]byte `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` 143 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 144 | XXX_unrecognized []byte `json:"-"` 145 | XXX_sizecache int32 `json:"-"` 146 | } 147 | 148 | func (m *DecryptRequest) Reset() { *m = DecryptRequest{} } 149 | func (m *DecryptRequest) String() string { return proto.CompactTextString(m) } 150 | func (*DecryptRequest) ProtoMessage() {} 151 | func (*DecryptRequest) Descriptor() ([]byte, []int) { 152 | return fileDescriptor_00212fb1f9d3bf1c, []int{2} 153 | } 154 | func (m *DecryptRequest) XXX_Unmarshal(b []byte) error { 155 | return xxx_messageInfo_DecryptRequest.Unmarshal(m, b) 156 | } 157 | func (m *DecryptRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 158 | return xxx_messageInfo_DecryptRequest.Marshal(b, m, deterministic) 159 | } 160 | func (m *DecryptRequest) XXX_Merge(src proto.Message) { 161 | xxx_messageInfo_DecryptRequest.Merge(m, src) 162 | } 163 | func (m *DecryptRequest) XXX_Size() int { 164 | return xxx_messageInfo_DecryptRequest.Size(m) 165 | } 166 | func (m *DecryptRequest) XXX_DiscardUnknown() { 167 | xxx_messageInfo_DecryptRequest.DiscardUnknown(m) 168 | } 169 | 170 | var xxx_messageInfo_DecryptRequest proto.InternalMessageInfo 171 | 172 | func (m *DecryptRequest) GetCiphertext() []byte { 173 | if m != nil { 174 | return m.Ciphertext 175 | } 176 | return nil 177 | } 178 | 179 | func (m *DecryptRequest) GetUid() string { 180 | if m != nil { 181 | return m.Uid 182 | } 183 | return "" 184 | } 185 | 186 | func (m *DecryptRequest) GetKeyId() string { 187 | if m != nil { 188 | return m.KeyId 189 | } 190 | return "" 191 | } 192 | 193 | func (m *DecryptRequest) GetAnnotations() map[string][]byte { 194 | if m != nil { 195 | return m.Annotations 196 | } 197 | return nil 198 | } 199 | 200 | type DecryptResponse struct { 201 | // The decrypted data. 202 | Plaintext []byte `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` 203 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 204 | XXX_unrecognized []byte `json:"-"` 205 | XXX_sizecache int32 `json:"-"` 206 | } 207 | 208 | func (m *DecryptResponse) Reset() { *m = DecryptResponse{} } 209 | func (m *DecryptResponse) String() string { return proto.CompactTextString(m) } 210 | func (*DecryptResponse) ProtoMessage() {} 211 | func (*DecryptResponse) Descriptor() ([]byte, []int) { 212 | return fileDescriptor_00212fb1f9d3bf1c, []int{3} 213 | } 214 | func (m *DecryptResponse) XXX_Unmarshal(b []byte) error { 215 | return xxx_messageInfo_DecryptResponse.Unmarshal(m, b) 216 | } 217 | func (m *DecryptResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 218 | return xxx_messageInfo_DecryptResponse.Marshal(b, m, deterministic) 219 | } 220 | func (m *DecryptResponse) XXX_Merge(src proto.Message) { 221 | xxx_messageInfo_DecryptResponse.Merge(m, src) 222 | } 223 | func (m *DecryptResponse) XXX_Size() int { 224 | return xxx_messageInfo_DecryptResponse.Size(m) 225 | } 226 | func (m *DecryptResponse) XXX_DiscardUnknown() { 227 | xxx_messageInfo_DecryptResponse.DiscardUnknown(m) 228 | } 229 | 230 | var xxx_messageInfo_DecryptResponse proto.InternalMessageInfo 231 | 232 | func (m *DecryptResponse) GetPlaintext() []byte { 233 | if m != nil { 234 | return m.Plaintext 235 | } 236 | return nil 237 | } 238 | 239 | type EncryptRequest struct { 240 | // The data to be encrypted. 241 | Plaintext []byte `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` 242 | // UID is a unique identifier for the request. 243 | Uid string `protobuf:"bytes,2,opt,name=uid,proto3" json:"uid,omitempty"` 244 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 245 | XXX_unrecognized []byte `json:"-"` 246 | XXX_sizecache int32 `json:"-"` 247 | } 248 | 249 | func (m *EncryptRequest) Reset() { *m = EncryptRequest{} } 250 | func (m *EncryptRequest) String() string { return proto.CompactTextString(m) } 251 | func (*EncryptRequest) ProtoMessage() {} 252 | func (*EncryptRequest) Descriptor() ([]byte, []int) { 253 | return fileDescriptor_00212fb1f9d3bf1c, []int{4} 254 | } 255 | func (m *EncryptRequest) XXX_Unmarshal(b []byte) error { 256 | return xxx_messageInfo_EncryptRequest.Unmarshal(m, b) 257 | } 258 | func (m *EncryptRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 259 | return xxx_messageInfo_EncryptRequest.Marshal(b, m, deterministic) 260 | } 261 | func (m *EncryptRequest) XXX_Merge(src proto.Message) { 262 | xxx_messageInfo_EncryptRequest.Merge(m, src) 263 | } 264 | func (m *EncryptRequest) XXX_Size() int { 265 | return xxx_messageInfo_EncryptRequest.Size(m) 266 | } 267 | func (m *EncryptRequest) XXX_DiscardUnknown() { 268 | xxx_messageInfo_EncryptRequest.DiscardUnknown(m) 269 | } 270 | 271 | var xxx_messageInfo_EncryptRequest proto.InternalMessageInfo 272 | 273 | func (m *EncryptRequest) GetPlaintext() []byte { 274 | if m != nil { 275 | return m.Plaintext 276 | } 277 | return nil 278 | } 279 | 280 | func (m *EncryptRequest) GetUid() string { 281 | if m != nil { 282 | return m.Uid 283 | } 284 | return "" 285 | } 286 | 287 | type EncryptResponse struct { 288 | // The encrypted data. 289 | // ciphertext must satisfy the following constraints: 290 | // 1. The ciphertext is not empty. 291 | // 2. The ciphertext is less than 1 kB. 292 | Ciphertext []byte `protobuf:"bytes,1,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` 293 | // The KMS key ID used to encrypt the data. This must always refer to the KMS KEK and not any local KEKs that may be in use. 294 | // This can be used to inform staleness of data updated via value.Transformer.TransformFromStorage. 295 | // keyID must satisfy the following constraints: 296 | // 1. The keyID is not empty. 297 | // 2. The size of keyID is less than 1 kB. 298 | KeyId string `protobuf:"bytes,2,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` 299 | // Additional metadata to be stored with the encrypted data. 300 | // This data is stored in plaintext in etcd. KMS plugin implementations are responsible for pre-encrypting any sensitive data. 301 | // Annotations must satisfy the following constraints: 302 | // 1. Annotation key must be a fully qualified domain name that conforms to the definition in DNS (RFC 1123). 303 | // 2. The size of annotations keys + values is less than 32 kB. 304 | Annotations map[string][]byte `protobuf:"bytes,3,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` 305 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 306 | XXX_unrecognized []byte `json:"-"` 307 | XXX_sizecache int32 `json:"-"` 308 | } 309 | 310 | func (m *EncryptResponse) Reset() { *m = EncryptResponse{} } 311 | func (m *EncryptResponse) String() string { return proto.CompactTextString(m) } 312 | func (*EncryptResponse) ProtoMessage() {} 313 | func (*EncryptResponse) Descriptor() ([]byte, []int) { 314 | return fileDescriptor_00212fb1f9d3bf1c, []int{5} 315 | } 316 | func (m *EncryptResponse) XXX_Unmarshal(b []byte) error { 317 | return xxx_messageInfo_EncryptResponse.Unmarshal(m, b) 318 | } 319 | func (m *EncryptResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 320 | return xxx_messageInfo_EncryptResponse.Marshal(b, m, deterministic) 321 | } 322 | func (m *EncryptResponse) XXX_Merge(src proto.Message) { 323 | xxx_messageInfo_EncryptResponse.Merge(m, src) 324 | } 325 | func (m *EncryptResponse) XXX_Size() int { 326 | return xxx_messageInfo_EncryptResponse.Size(m) 327 | } 328 | func (m *EncryptResponse) XXX_DiscardUnknown() { 329 | xxx_messageInfo_EncryptResponse.DiscardUnknown(m) 330 | } 331 | 332 | var xxx_messageInfo_EncryptResponse proto.InternalMessageInfo 333 | 334 | func (m *EncryptResponse) GetCiphertext() []byte { 335 | if m != nil { 336 | return m.Ciphertext 337 | } 338 | return nil 339 | } 340 | 341 | func (m *EncryptResponse) GetKeyId() string { 342 | if m != nil { 343 | return m.KeyId 344 | } 345 | return "" 346 | } 347 | 348 | func (m *EncryptResponse) GetAnnotations() map[string][]byte { 349 | if m != nil { 350 | return m.Annotations 351 | } 352 | return nil 353 | } 354 | 355 | func init() { 356 | proto.RegisterType((*StatusRequest)(nil), "v2.StatusRequest") 357 | proto.RegisterType((*StatusResponse)(nil), "v2.StatusResponse") 358 | proto.RegisterType((*DecryptRequest)(nil), "v2.DecryptRequest") 359 | proto.RegisterMapType((map[string][]byte)(nil), "v2.DecryptRequest.AnnotationsEntry") 360 | proto.RegisterType((*DecryptResponse)(nil), "v2.DecryptResponse") 361 | proto.RegisterType((*EncryptRequest)(nil), "v2.EncryptRequest") 362 | proto.RegisterType((*EncryptResponse)(nil), "v2.EncryptResponse") 363 | proto.RegisterMapType((map[string][]byte)(nil), "v2.EncryptResponse.AnnotationsEntry") 364 | } 365 | 366 | func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } 367 | 368 | var fileDescriptor_00212fb1f9d3bf1c = []byte{ 369 | // 403 bytes of a gzipped FileDescriptorProto 370 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x93, 0xcd, 0x6e, 0xda, 0x40, 371 | 0x10, 0xc7, 0xb1, 0x5d, 0x40, 0x0c, 0x14, 0xe8, 0x96, 0x4a, 0x16, 0xaa, 0x2a, 0xb4, 0xed, 0x81, 372 | 0x93, 0xad, 0xba, 0x3d, 0xa0, 0x1e, 0xaa, 0xb6, 0x2a, 0x95, 0xaa, 0xaa, 0x17, 0x73, 0x6b, 0x0f, 373 | 0xd1, 0x06, 0x46, 0x61, 0x65, 0x58, 0x3b, 0xde, 0xb5, 0x15, 0xe7, 0xbd, 0xf2, 0x1e, 0x79, 0x84, 374 | 0x3c, 0x4a, 0x64, 0x7b, 0x01, 0x1b, 0x94, 0xe4, 0x94, 0x9b, 0xe7, 0xf3, 0x3f, 0xf3, 0xdb, 0x31, 375 | 0x74, 0x58, 0xc4, 0x9d, 0x28, 0x0e, 0x55, 0x48, 0xcc, 0xd4, 0xa3, 0x03, 0x78, 0xb9, 0x50, 0x4c, 376 | 0x25, 0xd2, 0xc7, 0xcb, 0x04, 0xa5, 0xa2, 0xff, 0xa1, 0xbf, 0x73, 0xc8, 0x28, 0x14, 0x12, 0x89, 377 | 0x0d, 0xed, 0x14, 0x63, 0xc9, 0x43, 0x61, 0x1b, 0x13, 0x63, 0xda, 0xf1, 0x77, 0x66, 0x1e, 0x59, 378 | 0x23, 0xdb, 0xa8, 0xf5, 0xb5, 0x6d, 0x96, 0x11, 0x6d, 0x92, 0x37, 0xd0, 0x0a, 0x30, 0x3b, 0xe3, 379 | 0x2b, 0xdb, 0x2a, 0x02, 0xcd, 0x00, 0xb3, 0xdf, 0x2b, 0x7a, 0x67, 0x40, 0xff, 0x27, 0x2e, 0xe3, 380 | 0x2c, 0x52, 0x5a, 0x8f, 0xbc, 0x03, 0x58, 0xf2, 0x68, 0x8d, 0xb1, 0xc2, 0x2b, 0x55, 0x08, 0xf4, 381 | 0xfc, 0x8a, 0x87, 0x0c, 0xc1, 0x4a, 0xf8, 0x4a, 0xf7, 0xcf, 0x3f, 0x1f, 0xe8, 0x4d, 0xe6, 0xd0, 382 | 0x65, 0x42, 0x84, 0x8a, 0x29, 0x1e, 0x0a, 0x69, 0xbf, 0x98, 0x58, 0xd3, 0xae, 0xf7, 0xde, 0x49, 383 | 0x3d, 0xa7, 0xae, 0xe8, 0x7c, 0x3f, 0x64, 0xcd, 0x85, 0x8a, 0x33, 0xbf, 0x5a, 0x37, 0xfe, 0x0a, 384 | 0xc3, 0xe3, 0x84, 0x7c, 0x86, 0x00, 0x33, 0xbd, 0x7d, 0xfe, 0x49, 0x46, 0xd0, 0x4c, 0xd9, 0x26, 385 | 0xc1, 0x62, 0xae, 0x9e, 0x5f, 0x1a, 0x5f, 0xcc, 0x99, 0x41, 0x5d, 0x18, 0xec, 0xf5, 0x34, 0xc0, 386 | 0xb7, 0xd0, 0x89, 0x36, 0x8c, 0x8b, 0xca, 0x86, 0x07, 0x07, 0xfd, 0x06, 0xfd, 0xb9, 0xa8, 0x21, 387 | 0x79, 0x34, 0xff, 0x14, 0x08, 0xbd, 0x35, 0x60, 0xb0, 0x6f, 0xa1, 0x35, 0x9f, 0xc2, 0x7a, 0x80, 388 | 0x68, 0x56, 0x21, 0xfe, 0xaa, 0x43, 0xb4, 0x0a, 0x88, 0x1f, 0x72, 0x88, 0x47, 0x02, 0xcf, 0x4b, 389 | 0xd1, 0xbb, 0x31, 0x60, 0xf4, 0x07, 0xb3, 0xbf, 0x4c, 0xb0, 0x0b, 0xdc, 0xa2, 0x50, 0x0b, 0x8c, 390 | 0x53, 0xbe, 0x44, 0xf2, 0x11, 0x5a, 0xe5, 0x79, 0x92, 0x57, 0xf9, 0x54, 0xb5, 0xdb, 0x1d, 0x93, 391 | 0xaa, 0xab, 0x9c, 0x93, 0x36, 0xc8, 0x67, 0x68, 0xeb, 0x17, 0x21, 0xe4, 0xf4, 0x1c, 0xc6, 0xaf, 392 | 0x6b, 0xbe, 0x6a, 0x95, 0x5e, 0xb9, 0xac, 0xaa, 0xbf, 0x51, 0x59, 0x75, 0xc4, 0x84, 0x36, 0x7e, 393 | 0x8c, 0xfe, 0x91, 0x60, 0x26, 0x1d, 0x1e, 0xba, 0xc1, 0x56, 0xba, 0x2c, 0xe2, 0xd2, 0x4d, 0xbd, 394 | 0xf3, 0x56, 0xf1, 0xbf, 0x7d, 0xba, 0x0f, 0x00, 0x00, 0xff, 0xff, 0x5f, 0xf8, 0x49, 0x17, 0x7c, 395 | 0x03, 0x00, 0x00, 396 | } 397 | 398 | // Reference imports to suppress errors if they are not otherwise used. 399 | var _ context.Context 400 | var _ grpc.ClientConn 401 | 402 | // This is a compile-time assertion to ensure that this generated file 403 | // is compatible with the grpc package it is being compiled against. 404 | const _ = grpc.SupportPackageIsVersion4 405 | 406 | // KeyManagementServiceClient is the client API for KeyManagementService service. 407 | // 408 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 409 | type KeyManagementServiceClient interface { 410 | // this API is meant to be polled 411 | Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) 412 | // Execute decryption operation in KMS provider. 413 | Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) 414 | // Execute encryption operation in KMS provider. 415 | Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) 416 | } 417 | 418 | type keyManagementServiceClient struct { 419 | cc *grpc.ClientConn 420 | } 421 | 422 | func NewKeyManagementServiceClient(cc *grpc.ClientConn) KeyManagementServiceClient { 423 | return &keyManagementServiceClient{cc} 424 | } 425 | 426 | func (c *keyManagementServiceClient) Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) { 427 | out := new(StatusResponse) 428 | err := c.cc.Invoke(ctx, "/v2.KeyManagementService/Status", in, out, opts...) 429 | if err != nil { 430 | return nil, err 431 | } 432 | return out, nil 433 | } 434 | 435 | func (c *keyManagementServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) { 436 | out := new(DecryptResponse) 437 | err := c.cc.Invoke(ctx, "/v2.KeyManagementService/Decrypt", in, out, opts...) 438 | if err != nil { 439 | return nil, err 440 | } 441 | return out, nil 442 | } 443 | 444 | func (c *keyManagementServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) { 445 | out := new(EncryptResponse) 446 | err := c.cc.Invoke(ctx, "/v2.KeyManagementService/Encrypt", in, out, opts...) 447 | if err != nil { 448 | return nil, err 449 | } 450 | return out, nil 451 | } 452 | 453 | // KeyManagementServiceServer is the server API for KeyManagementService service. 454 | type KeyManagementServiceServer interface { 455 | // this API is meant to be polled 456 | Status(context.Context, *StatusRequest) (*StatusResponse, error) 457 | // Execute decryption operation in KMS provider. 458 | Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) 459 | // Execute encryption operation in KMS provider. 460 | Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) 461 | } 462 | 463 | // UnimplementedKeyManagementServiceServer can be embedded to have forward compatible implementations. 464 | type UnimplementedKeyManagementServiceServer struct { 465 | } 466 | 467 | func (*UnimplementedKeyManagementServiceServer) Status(ctx context.Context, req *StatusRequest) (*StatusResponse, error) { 468 | return nil, status.Errorf(codes.Unimplemented, "method Status not implemented") 469 | } 470 | func (*UnimplementedKeyManagementServiceServer) Decrypt(ctx context.Context, req *DecryptRequest) (*DecryptResponse, error) { 471 | return nil, status.Errorf(codes.Unimplemented, "method Decrypt not implemented") 472 | } 473 | func (*UnimplementedKeyManagementServiceServer) Encrypt(ctx context.Context, req *EncryptRequest) (*EncryptResponse, error) { 474 | return nil, status.Errorf(codes.Unimplemented, "method Encrypt not implemented") 475 | } 476 | 477 | func RegisterKeyManagementServiceServer(s *grpc.Server, srv KeyManagementServiceServer) { 478 | s.RegisterService(&_KeyManagementService_serviceDesc, srv) 479 | } 480 | 481 | func _KeyManagementService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 482 | in := new(StatusRequest) 483 | if err := dec(in); err != nil { 484 | return nil, err 485 | } 486 | if interceptor == nil { 487 | return srv.(KeyManagementServiceServer).Status(ctx, in) 488 | } 489 | info := &grpc.UnaryServerInfo{ 490 | Server: srv, 491 | FullMethod: "/v2.KeyManagementService/Status", 492 | } 493 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 494 | return srv.(KeyManagementServiceServer).Status(ctx, req.(*StatusRequest)) 495 | } 496 | return interceptor(ctx, in, info, handler) 497 | } 498 | 499 | func _KeyManagementService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 500 | in := new(DecryptRequest) 501 | if err := dec(in); err != nil { 502 | return nil, err 503 | } 504 | if interceptor == nil { 505 | return srv.(KeyManagementServiceServer).Decrypt(ctx, in) 506 | } 507 | info := &grpc.UnaryServerInfo{ 508 | Server: srv, 509 | FullMethod: "/v2.KeyManagementService/Decrypt", 510 | } 511 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 512 | return srv.(KeyManagementServiceServer).Decrypt(ctx, req.(*DecryptRequest)) 513 | } 514 | return interceptor(ctx, in, info, handler) 515 | } 516 | 517 | func _KeyManagementService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 518 | in := new(EncryptRequest) 519 | if err := dec(in); err != nil { 520 | return nil, err 521 | } 522 | if interceptor == nil { 523 | return srv.(KeyManagementServiceServer).Encrypt(ctx, in) 524 | } 525 | info := &grpc.UnaryServerInfo{ 526 | Server: srv, 527 | FullMethod: "/v2.KeyManagementService/Encrypt", 528 | } 529 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 530 | return srv.(KeyManagementServiceServer).Encrypt(ctx, req.(*EncryptRequest)) 531 | } 532 | return interceptor(ctx, in, info, handler) 533 | } 534 | 535 | var _KeyManagementService_serviceDesc = grpc.ServiceDesc{ 536 | ServiceName: "v2.KeyManagementService", 537 | HandlerType: (*KeyManagementServiceServer)(nil), 538 | Methods: []grpc.MethodDesc{ 539 | { 540 | MethodName: "Status", 541 | Handler: _KeyManagementService_Status_Handler, 542 | }, 543 | { 544 | MethodName: "Decrypt", 545 | Handler: _KeyManagementService_Decrypt_Handler, 546 | }, 547 | { 548 | MethodName: "Encrypt", 549 | Handler: _KeyManagementService_Encrypt_Handler, 550 | }, 551 | }, 552 | Streams: []grpc.StreamDesc{}, 553 | Metadata: "api.proto", 554 | } 555 | --------------------------------------------------------------------------------