Merge pull request #55684 from wu-qiang/kms-plugin-grpc-api

Automatic merge from submit-queue (batch tested with PRs 58437, 59490, 55684). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

gRPC-based KMS plugin service

**What this PR does / why we need it**:
Implement for issue https://github.com/kubernetes/kubernetes/issues/51965
**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #51965

**Special notes for your reviewer**:
@destijl @sakshamsharma @deads2k @ericchiang
The implementation based on the document https://docs.google.com/document/d/1S_Wgn-psI0Z7SYGvp-83ePte5oUNMr4244uanGLYUmw/edit
**Release note**:

```release-note
Implement envelope service with gRPC, so that KMS providers can be pulled out from API server.
```
This commit is contained in:
Kubernetes Submit Queue 2018-02-08 21:34:41 -08:00 committed by GitHub
commit 53207e1be6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 978 additions and 163 deletions

View File

@ -594,6 +594,7 @@ staging/src/k8s.io/apiserver/pkg/storage/storagebackend
staging/src/k8s.io/apiserver/pkg/storage/testing
staging/src/k8s.io/apiserver/pkg/storage/tests
staging/src/k8s.io/apiserver/pkg/storage/value
staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1
staging/src/k8s.io/apiserver/pkg/util/feature
staging/src/k8s.io/apiserver/pkg/util/flag
staging/src/k8s.io/apiserver/pkg/util/proxy

View File

@ -0,0 +1,62 @@
#!/bin/bash
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -o errexit
set -o nounset
set -o pipefail
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/..
KUBE_KMS_GRPC_ROOT="${KUBE_ROOT}/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1/"
source "${KUBE_ROOT}/hack/lib/init.sh"
kube::golang::setup_env
BINS=(
vendor/k8s.io/code-generator/cmd/go-to-protobuf/protoc-gen-gogo
)
make -C "${KUBE_ROOT}" WHAT="${BINS[*]}"
if [[ -z "$(which protoc)" || "$(protoc --version)" != "libprotoc 3."* ]]; then
echo "Generating protobuf requires protoc 3.0.0-beta1 or newer. Please download and"
echo "install the platform appropriate Protobuf package for your OS: "
echo
echo " https://github.com/google/protobuf/releases"
echo
echo "WARNING: Protobuf changes are not being validated"
exit 1
fi
function cleanup {
rm -f ${KUBE_KMS_GRPC_ROOT}/service.pb.go.bak
}
trap cleanup EXIT
gogopath=$(dirname $(kube::util::find-binary "protoc-gen-gogo"))
PATH="${gogopath}:${PATH}" \
protoc \
--proto_path="${KUBE_KMS_GRPC_ROOT}" \
--proto_path="${KUBE_ROOT}/vendor" \
--gogo_out=plugins=grpc:${KUBE_KMS_GRPC_ROOT} ${KUBE_KMS_GRPC_ROOT}/service.proto
# Update boilerplate for the generated file.
echo "$(cat hack/boilerplate/boilerplate.go.txt ${KUBE_KMS_GRPC_ROOT}/service.pb.go)" > ${KUBE_KMS_GRPC_ROOT}/service.pb.go
sed -i".bak" "s/Copyright YEAR/Copyright $(date '+%Y')/g" ${KUBE_KMS_GRPC_ROOT}/service.pb.go
# Run gofmt to clean up the generated code.
kube::golang::verify_go_version
gofmt -l -s -w ${KUBE_KMS_GRPC_ROOT}/service.pb.go

29
hack/update-generated-kms.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -o errexit
set -o nounset
set -o pipefail
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/..
# NOTE: All output from this script needs to be copied back to the calling
# source tree. This is managed in kube::build::copy_output in build/common.sh.
# If the output set is changed update that function.
${KUBE_ROOT}/build/run.sh hack/update-generated-kms-dockerized.sh "$@"
# ex: ts=2 sw=2 et filetype=sh

45
hack/verify-generated-kms.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -o errexit
set -o nounset
set -o pipefail
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/..
KUBE_KMS_GRPC_ROOT="${KUBE_ROOT}/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1/"
source "${KUBE_ROOT}/hack/lib/init.sh"
kube::golang::setup_env
function cleanup {
rm -rf ${KUBE_KMS_GRPC_ROOT}/_tmp/
}
trap cleanup EXIT
mkdir -p ${KUBE_KMS_GRPC_ROOT}/_tmp
cp ${KUBE_KMS_GRPC_ROOT}/service.pb.go ${KUBE_KMS_GRPC_ROOT}/_tmp/
ret=0
KUBE_VERBOSE=3 "${KUBE_ROOT}/hack/update-generated-kms.sh"
diff -I "gzipped FileDescriptorProto" -I "0x" -Naupr ${KUBE_KMS_GRPC_ROOT}/_tmp/service.pb.go ${KUBE_KMS_GRPC_ROOT}/service.pb.go || ret=$?
if [[ $ret -eq 0 ]]; then
echo "Generated KMS gRPC is up to date."
cp ${KUBE_KMS_GRPC_ROOT}/_tmp/service.pb.go ${KUBE_KMS_GRPC_ROOT}/
else
echo "Generated KMS gRPC is out of date. Please run hack/update-generated-kms.sh"
exit 1
fi

View File

@ -10,13 +10,11 @@ go_library(
name = "go_default_library",
srcs = [
"config.go",
"plugins.go",
"types.go",
],
importpath = "k8s.io/apiserver/pkg/server/options/encryptionconfig",
deps = [
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/aes:go_default_library",

View File

@ -102,6 +102,9 @@ func ParseEncryptionConfiguration(f io.Reader) (map[schema.GroupResource]value.T
return result, nil
}
// The factory to create kms service. This is to make writing test easier.
var envelopeServiceFactory = envelope.NewGRPCService
// GetPrefixTransformers constructs and returns the appropriate prefix transformers for the passed resource using its configuration
func GetPrefixTransformers(config *ResourceConfig) ([]value.PrefixTransformer, error) {
var result []value.PrefixTransformer
@ -150,18 +153,18 @@ func GetPrefixTransformers(config *ResourceConfig) ([]value.PrefixTransformer, e
if found == true {
return nil, fmt.Errorf("more than one provider specified in a single element, should split into different list elements")
}
f, err := os.Open(provider.KMS.ConfigFile)
// Ensure the endpoint is provided.
if len(provider.KMS.Endpoint) == 0 {
return nil, fmt.Errorf("remote KMS provider can't use empty string as endpoint")
}
// Get gRPC client service with endpoint.
envelopeService, err := envelopeServiceFactory(provider.KMS.Endpoint)
if err != nil {
return nil, fmt.Errorf("error opening KMS provider configuration file %q: %v", provider.KMS.ConfigFile, err)
}
defer f.Close()
envelopeService, pluginFound, err := KMSPluginRegistry.getPlugin(provider.KMS.Name, f)
if err != nil {
return nil, fmt.Errorf("could not configure KMS plugin %q, %v", provider.KMS.Name, err)
}
if pluginFound == false {
return nil, fmt.Errorf("KMS plugin %q not found", provider.KMS.Name)
return nil, fmt.Errorf("could not configure KMS plugin %q, error: %v", provider.KMS.Name, err)
}
transformer, err = getEnvelopePrefixTransformer(provider.KMS, envelopeService, kmsTransformerPrefixV1)
found = true
}

View File

@ -19,9 +19,6 @@ package encryptionconfig
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"testing"
@ -35,10 +32,6 @@ const (
sampleContextText = "0123456789"
// Modify these in all configurations if changed
testEnvelopeServiceConfigPath = "testproviderconfig"
testEnvelopeServiceProviderName = "testprovider"
correctConfigWithIdentityFirst = `
kind: EncryptionConfig
apiVersion: v1
@ -56,7 +49,7 @@ resources:
secret: dGhpcyBpcyBwYXNzd29yZA==
- kms:
name: testprovider
configfile: testproviderconfig
endpoint: unix:///tmp/testprovider.sock
cachesize: 10
- aescbc:
keys:
@ -89,7 +82,7 @@ resources:
secret: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
- kms:
name: testprovider
configfile: testproviderconfig
endpoint: unix:///tmp/testprovider.sock
cachesize: 10
- aescbc:
keys:
@ -115,7 +108,7 @@ resources:
secret: dGhpcyBpcyBwYXNzd29yZA==
- kms:
name: testprovider
configfile: testproviderconfig
endpoint: unix:///tmp/testprovider.sock
cachesize: 10
- identity: {}
- secretbox:
@ -149,7 +142,7 @@ resources:
secret: dGhpcyBpcyBwYXNzd29yZA==
- kms:
name: testprovider
configfile: testproviderconfig
endpoint: unix:///tmp/testprovider.sock
cachesize: 10
- identity: {}
- aesgcm:
@ -169,7 +162,7 @@ resources:
providers:
- kms:
name: testprovider
configfile: testproviderconfig
endpoint: unix:///tmp/testprovider.sock
cachesize: 10
- secretbox:
keys:
@ -218,40 +211,45 @@ resources:
- name: key2
secret: YSBzZWNyZXQgYSBzZWNyZXQ=
`
incorrectConfigNoEndpointForKMS = `
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- kms:
name: testprovider
cachesize: 10
`
)
// testEnvelopeService is a mock envelope service which can be used to simulate remote Envelope services
// for testing of the envelope transformer with other transformers.
type testEnvelopeService struct {
disabled bool
}
func (t *testEnvelopeService) Decrypt(data []byte) ([]byte, error) {
if t.disabled {
return nil, fmt.Errorf("Envelope service was disabled")
}
return base64.StdEncoding.DecodeString(string(data))
}
func (t *testEnvelopeService) Encrypt(data []byte) ([]byte, error) {
if t.disabled {
return nil, fmt.Errorf("Envelope service was disabled")
}
return []byte(base64.StdEncoding.EncodeToString(data)), nil
}
func (t *testEnvelopeService) SetDisabledStatus(status bool) {
t.disabled = status
// The factory method to create mock envelope service.
func newMockEnvelopeService(endpoint string) (envelope.Service, error) {
return &testEnvelopeService{}, nil
}
var _ envelope.Service = &testEnvelopeService{}
func TestEncryptionProviderConfigCorrect(t *testing.T) {
os.OpenFile(testEnvelopeServiceConfigPath, os.O_CREATE, 0666)
defer os.Remove(testEnvelopeServiceConfigPath)
KMSPluginRegistry.Register(testEnvelopeServiceProviderName, func(_ io.Reader) (envelope.Service, error) {
return &testEnvelopeService{}, nil
})
// Set factory for mock envelope service
factory := envelopeServiceFactory
envelopeServiceFactory = newMockEnvelopeService
defer func() {
envelopeServiceFactory = factory
}()
// Creates compound/prefix transformers with different ordering of available transformers.
// Transforms data using one of them, and tries to untransform using the others.
@ -337,3 +335,10 @@ func TestEncryptionProviderConfigInvalidKey(t *testing.T) {
t.Fatalf("invalid configuration file (bad AES key) got parsed:\n%s", incorrectConfigInvalidKey)
}
}
// Throw error if kms has no endpoint
func TestEncryptionProviderConfigNoEndpointForKMS(t *testing.T) {
if _, err := ParseEncryptionConfiguration(strings.NewReader(incorrectConfigNoEndpointForKMS)); err == nil {
t.Fatalf("invalid configuration file (kms has no endpoint) got parsed:\n%s", incorrectConfigNoEndpointForKMS)
}
}

View File

@ -1,118 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package encryptionconfig
import (
"bytes"
"io"
"io/ioutil"
"reflect"
"sync"
"github.com/golang/glog"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope"
)
// Factory is a function that returns an envelope Service for encryption providers.
// The config parameter provides an io.Reader handler to the factory in
// order to load specific configurations. If no configuration is provided
// the parameter is nil.
type Factory func(config io.Reader) (envelope.Service, error)
// KMSPlugins contains all registered KMS options.
type KMSPlugins struct {
lock sync.RWMutex
registry map[string]Factory
}
var (
// PluginEnabledFn checks whether a plugin is enabled. By default, if you ask about it, it's enabled.
PluginEnabledFn = func(name string, config io.Reader) bool {
return true
}
// KMSPluginRegistry contains the registered KMS plugins which can be used for configuring
// encryption providers.
KMSPluginRegistry = KMSPlugins{}
)
// PluginEnabledFunc is a function type that can provide an external check on whether an admission plugin may be enabled
type PluginEnabledFunc func(name string, config io.Reader) bool
// Register registers a plugin Factory by name. This
// is expected to happen during app startup. It does not allow
// registering a plugin by the same name twice.
func (ps *KMSPlugins) Register(name string, plugin Factory) {
ps.lock.Lock()
defer ps.lock.Unlock()
_, found := ps.registry[name]
if ps.registry == nil {
ps.registry = map[string]Factory{}
}
if found {
glog.Fatalf("KMS plugin %q was registered twice", name)
} else {
ps.registry[name] = plugin
glog.V(1).Infof("Registered KMS plugin %q", name)
}
}
// getPlugin creates an instance of the named plugin. It returns `false` if the
// the name is not known. The error is returned only when the named provider was
// known but failed to initialize. The config parameter specifies the io.Reader
// handler of the configuration file for the cloud provider, or nil for no configuration.
func (ps *KMSPlugins) getPlugin(name string, config io.Reader) (envelope.Service, bool, error) {
f, found := ps.fetchPluginFromRegistry(name)
if !found {
return nil, false, nil
}
config1, config2, err := splitStream(config)
if err != nil {
return nil, true, err
}
if !PluginEnabledFn(name, config1) {
return nil, true, nil
}
ret, err := f(config2)
return ret, true, err
}
// fetchPluginFromRegistry tries to get a registered plugin with the requested name.
func (ps *KMSPlugins) fetchPluginFromRegistry(name string) (Factory, bool) {
ps.lock.RLock()
defer ps.lock.RUnlock()
// Map lookup defaults to single value context
f, found := ps.registry[name]
return f, found
}
// splitStream reads the stream bytes and constructs two copies of it.
func splitStream(config io.Reader) (io.Reader, io.Reader, error) {
if config == nil || reflect.ValueOf(config).IsNil() {
return nil, nil, nil
}
configBytes, err := ioutil.ReadAll(config)
if err != nil {
return nil, nil, err
}
return bytes.NewBuffer(configBytes), bytes.NewBuffer(configBytes), nil
}

View File

@ -81,6 +81,6 @@ type KMSConfig struct {
// cacheSize is the maximum number of secrets which are cached in memory. The default value is 1000.
// +optional
CacheSize int `json:"cachesize,omitempty"`
// configfile is the path to the configuration file for the named KMS provider.
ConfigFile string `json:"configfile"`
// the gRPC server listening address, for example "unix:///var/run/kms-provider.sock".
Endpoint string `json:"endpoint"`
}

View File

@ -8,22 +8,35 @@ load(
go_library(
name = "go_default_library",
srcs = ["envelope.go"],
srcs = [
"envelope.go",
"grpc_service.go",
],
importpath = "k8s.io/apiserver/pkg/storage/value/encrypt/envelope",
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
"//vendor/golang.org/x/net/context:go_default_library",
"//vendor/google.golang.org/grpc:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["envelope_test.go"],
srcs = [
"envelope_test.go",
"grpc_service_unix_test.go",
],
embed = [":go_default_library"],
importpath = "k8s.io/apiserver/pkg/storage/value/encrypt/envelope",
deps = [
"//vendor/golang.org/x/sys/unix:go_default_library",
"//vendor/google.golang.org/grpc:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/aes:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1:go_default_library",
],
)
@ -36,6 +49,9 @@ filegroup(
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
srcs = [
":package-srcs",
"//staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1:all-srcs",
],
tags = ["automanaged"],
)

View File

@ -0,0 +1,144 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package envelope transforms values for storage at rest using a Envelope provider
package envelope
import (
"fmt"
"net"
"net/url"
"time"
"github.com/golang/glog"
"google.golang.org/grpc"
"golang.org/x/net/context"
kmsapi "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1"
)
const (
// Now only supportied unix domain socket.
unixProtocol = "unix"
// Current version for the protocal interface definition.
kmsapiVersion = "v1beta1"
// The timeout that communicate with KMS server.
timeout = 30 * time.Second
)
// The gRPC implementation for envelope.Service.
type gRPCService struct {
// gRPC client instance
kmsClient kmsapi.KMSServiceClient
connection *grpc.ClientConn
}
// NewGRPCService returns an envelope.Service which use gRPC to communicate the remote KMS provider.
func NewGRPCService(endpoint string) (Service, error) {
glog.V(4).Infof("Configure KMS provider with endpoint: %s", endpoint)
addr, err := parseEndpoint(endpoint)
if err != nil {
return nil, err
}
connection, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithTimeout(timeout), grpc.WithDialer(unixDial))
if err != nil {
return nil, fmt.Errorf("connect remote KMS provider %q failed, error: %v", addr, err)
}
kmsClient := kmsapi.NewKMSServiceClient(connection)
err = checkAPIVersion(kmsClient)
if err != nil {
connection.Close()
return nil, fmt.Errorf("failed check version for %q, error: %v", addr, err)
}
return &gRPCService{kmsClient: kmsClient, connection: connection}, nil
}
// This dialer explicitly ask gRPC to use unix socket as network.
func unixDial(addr string, timeout time.Duration) (net.Conn, error) {
return net.DialTimeout(unixProtocol, addr, timeout)
}
// Parse the endpoint to extract schema, host or path.
func parseEndpoint(endpoint string) (string, error) {
if len(endpoint) == 0 {
return "", fmt.Errorf("remote KMS provider can't use empty string as endpoint")
}
u, err := url.Parse(endpoint)
if err != nil {
return "", fmt.Errorf("invalid endpoint %q for remote KMS provider, error: %v", endpoint, err)
}
if u.Scheme != unixProtocol {
return "", fmt.Errorf("unsupported scheme %q for remote KMS provider", u.Scheme)
}
return u.Path, nil
}
// Check the KMS provider API version.
// Only matching kmsapiVersion is supported now.
func checkAPIVersion(kmsClient kmsapi.KMSServiceClient) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
request := &kmsapi.VersionRequest{Version: kmsapiVersion}
response, err := kmsClient.Version(ctx, request)
if err != nil {
return fmt.Errorf("failed get version from remote KMS provider: %v", err)
}
if response.Version != kmsapiVersion {
return fmt.Errorf("KMS provider api version %s is not supported, only %s is supported now",
response.Version, kmsapiVersion)
}
glog.V(4).Infof("KMS provider %s initialized, version: %s", response.RuntimeName, response.RuntimeVersion)
return nil
}
// Decrypt a given data string to obtain the original byte data.
func (g *gRPCService) Decrypt(cipher []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
request := &kmsapi.DecryptRequest{Cipher: cipher, Version: kmsapiVersion}
response, err := g.kmsClient.Decrypt(ctx, request)
if err != nil {
return nil, err
}
return response.Plain, nil
}
// Encrypt bytes to a string ciphertext.
func (g *gRPCService) Encrypt(plain []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
request := &kmsapi.EncryptRequest{Plain: plain, Version: kmsapiVersion}
response, err := g.kmsClient.Encrypt(ctx, request)
if err != nil {
return nil, err
}
return response.Cipher, nil
}

View File

@ -0,0 +1,173 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +build !windows
// Package envelope transforms values for storage at rest using a Envelope provider
package envelope
import (
"context"
"encoding/base64"
"fmt"
"net"
"os"
"reflect"
"testing"
"google.golang.org/grpc"
"golang.org/x/sys/unix"
kmsapi "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1"
)
const (
sockFile = "/tmp/kms-provider.sock"
)
// Normal encryption and decryption operation.
func TestGRPCService(t *testing.T) {
// Start a test gRPC server.
server, err := startTestKMSProvider()
if err != nil {
t.Fatalf("failed to start test KMS provider server, error: %v", err)
}
defer stopTestKMSProvider(server)
// Create the gRPC client service.
endpoint := unixProtocol + "://" + sockFile
service, err := NewGRPCService(endpoint)
if err != nil {
t.Fatalf("failed to create envelope service, error: %v", err)
}
defer destroyService(service)
// Call service to encrypt data.
data := []byte("test data")
cipher, err := service.Encrypt(data)
if err != nil {
t.Fatalf("failed when execute encrypt, error: %v", err)
}
// Call service to decrypt data.
result, err := service.Decrypt(cipher)
if err != nil {
t.Fatalf("failed when execute decrypt, error: %v", err)
}
if !reflect.DeepEqual(data, result) {
t.Errorf("expect: %v, but: %v", data, result)
}
}
func destroyService(service Service) {
s := service.(*gRPCService)
s.connection.Close()
}
// Test all those invalid configuration for KMS provider.
func TestInvalidConfiguration(t *testing.T) {
// Start a test gRPC server.
server, err := startTestKMSProvider()
if err != nil {
t.Fatalf("failed to start test KMS provider server, error: %v", err)
}
defer stopTestKMSProvider(server)
invalidConfigs := []struct {
name string
apiVersion string
endpoint string
}{
{"emptyConfiguration", kmsapiVersion, ""},
{"invalidScheme", kmsapiVersion, "tcp://localhost:6060"},
{"unavailableEndpoint", kmsapiVersion, unixProtocol + "://" + sockFile + ".nonexist"},
{"invalidAPIVersion", "invalidVersion", unixProtocol + "://" + sockFile},
}
for _, testCase := range invalidConfigs {
t.Run(testCase.name, func(t *testing.T) {
setAPIVersion(testCase.apiVersion)
defer setAPIVersion(kmsapiVersion)
_, err := NewGRPCService(testCase.endpoint)
if err == nil {
t.Fatalf("should fail to create envelope service for %s.", testCase.name)
}
})
}
}
// Start the gRPC server that listens on unix socket.
func startTestKMSProvider() (*grpc.Server, error) {
if err := cleanSockFile(); err != nil {
return nil, err
}
listener, err := net.Listen(unixProtocol, sockFile)
if err != nil {
return nil, fmt.Errorf("failed to listen on the unix socket, error: %v", err)
}
server := grpc.NewServer()
kmsapi.RegisterKMSServiceServer(server, &base64Server{})
go server.Serve(listener)
return server, nil
}
func stopTestKMSProvider(server *grpc.Server) {
server.Stop()
cleanSockFile()
}
func cleanSockFile() error {
err := unix.Unlink(sockFile)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete the socket file, error: %v", err)
}
return nil
}
// Fake gRPC sever for remote KMS provider.
// Use base64 to simulate encrypt and decrypt.
type base64Server struct{}
var testProviderAPIVersion = kmsapiVersion
func setAPIVersion(apiVersion string) {
testProviderAPIVersion = apiVersion
}
func (s *base64Server) Version(ctx context.Context, request *kmsapi.VersionRequest) (*kmsapi.VersionResponse, error) {
return &kmsapi.VersionResponse{Version: testProviderAPIVersion, RuntimeName: "testKMS", RuntimeVersion: "0.0.1"}, nil
}
func (s *base64Server) Decrypt(ctx context.Context, request *kmsapi.DecryptRequest) (*kmsapi.DecryptResponse, error) {
buf := make([]byte, base64.StdEncoding.DecodedLen(len(request.Cipher)))
n, err := base64.StdEncoding.Decode(buf, request.Cipher)
if err != nil {
return nil, err
}
return &kmsapi.DecryptResponse{Plain: buf[:n]}, nil
}
func (s *base64Server) Encrypt(ctx context.Context, request *kmsapi.EncryptRequest) (*kmsapi.EncryptResponse, error) {
buf := make([]byte, base64.StdEncoding.EncodedLen(len(request.Plain)))
base64.StdEncoding.Encode(buf, request.Plain)
return &kmsapi.EncryptResponse{Cipher: buf}, nil
}

View File

@ -0,0 +1,33 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
filegroup(
name = "go_default_library_protos",
srcs = ["service.proto"],
visibility = ["//visibility:public"],
)
go_library(
name = "go_default_library",
srcs = ["service.pb.go"],
importpath = "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gogo/protobuf/proto:go_default_library",
"//vendor/golang.org/x/net/context:go_default_library",
"//vendor/google.golang.org/grpc:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,370 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by protoc-gen-gogo.
// source: service.proto
// DO NOT EDIT!
/*
Package v1beta1 is a generated protocol buffer package.
It is generated from these files:
service.proto
It has these top-level messages:
VersionRequest
VersionResponse
DecryptRequest
DecryptResponse
EncryptRequest
EncryptResponse
*/
package v1beta1
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type VersionRequest struct {
// Version of the KMS plugin API.
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
}
func (m *VersionRequest) Reset() { *m = VersionRequest{} }
func (m *VersionRequest) String() string { return proto.CompactTextString(m) }
func (*VersionRequest) ProtoMessage() {}
func (*VersionRequest) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{0} }
func (m *VersionRequest) GetVersion() string {
if m != nil {
return m.Version
}
return ""
}
type VersionResponse struct {
// Version of the KMS plugin API.
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
// Name of the KMS provider.
RuntimeName string `protobuf:"bytes,2,opt,name=runtime_name,json=runtimeName,proto3" json:"runtime_name,omitempty"`
// Version of the KMS provider. The string must be semver-compatible.
RuntimeVersion string `protobuf:"bytes,3,opt,name=runtime_version,json=runtimeVersion,proto3" json:"runtime_version,omitempty"`
}
func (m *VersionResponse) Reset() { *m = VersionResponse{} }
func (m *VersionResponse) String() string { return proto.CompactTextString(m) }
func (*VersionResponse) ProtoMessage() {}
func (*VersionResponse) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{1} }
func (m *VersionResponse) GetVersion() string {
if m != nil {
return m.Version
}
return ""
}
func (m *VersionResponse) GetRuntimeName() string {
if m != nil {
return m.RuntimeName
}
return ""
}
func (m *VersionResponse) GetRuntimeVersion() string {
if m != nil {
return m.RuntimeVersion
}
return ""
}
type DecryptRequest struct {
// Version of the KMS plugin API.
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
// The data to be decrypted.
Cipher []byte `protobuf:"bytes,2,opt,name=cipher,proto3" json:"cipher,omitempty"`
}
func (m *DecryptRequest) Reset() { *m = DecryptRequest{} }
func (m *DecryptRequest) String() string { return proto.CompactTextString(m) }
func (*DecryptRequest) ProtoMessage() {}
func (*DecryptRequest) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{2} }
func (m *DecryptRequest) GetVersion() string {
if m != nil {
return m.Version
}
return ""
}
func (m *DecryptRequest) GetCipher() []byte {
if m != nil {
return m.Cipher
}
return nil
}
type DecryptResponse struct {
// The decrypted data.
Plain []byte `protobuf:"bytes,1,opt,name=plain,proto3" json:"plain,omitempty"`
}
func (m *DecryptResponse) Reset() { *m = DecryptResponse{} }
func (m *DecryptResponse) String() string { return proto.CompactTextString(m) }
func (*DecryptResponse) ProtoMessage() {}
func (*DecryptResponse) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{3} }
func (m *DecryptResponse) GetPlain() []byte {
if m != nil {
return m.Plain
}
return nil
}
type EncryptRequest struct {
// Version of the KMS plugin API.
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
// The data to be encrypted.
Plain []byte `protobuf:"bytes,2,opt,name=plain,proto3" json:"plain,omitempty"`
}
func (m *EncryptRequest) Reset() { *m = EncryptRequest{} }
func (m *EncryptRequest) String() string { return proto.CompactTextString(m) }
func (*EncryptRequest) ProtoMessage() {}
func (*EncryptRequest) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{4} }
func (m *EncryptRequest) GetVersion() string {
if m != nil {
return m.Version
}
return ""
}
func (m *EncryptRequest) GetPlain() []byte {
if m != nil {
return m.Plain
}
return nil
}
type EncryptResponse struct {
// The encrypted data.
Cipher []byte `protobuf:"bytes,1,opt,name=cipher,proto3" json:"cipher,omitempty"`
}
func (m *EncryptResponse) Reset() { *m = EncryptResponse{} }
func (m *EncryptResponse) String() string { return proto.CompactTextString(m) }
func (*EncryptResponse) ProtoMessage() {}
func (*EncryptResponse) Descriptor() ([]byte, []int) { return fileDescriptorService, []int{5} }
func (m *EncryptResponse) GetCipher() []byte {
if m != nil {
return m.Cipher
}
return nil
}
func init() {
proto.RegisterType((*VersionRequest)(nil), "v1beta1.VersionRequest")
proto.RegisterType((*VersionResponse)(nil), "v1beta1.VersionResponse")
proto.RegisterType((*DecryptRequest)(nil), "v1beta1.DecryptRequest")
proto.RegisterType((*DecryptResponse)(nil), "v1beta1.DecryptResponse")
proto.RegisterType((*EncryptRequest)(nil), "v1beta1.EncryptRequest")
proto.RegisterType((*EncryptResponse)(nil), "v1beta1.EncryptResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for KMSService service
type KMSServiceClient interface {
// Version returns the runtime name and runtime version of the KMS provider.
Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error)
// Execute decryption operation in KMS provider.
Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error)
// Execute encryption operation in KMS provider.
Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error)
}
type kMSServiceClient struct {
cc *grpc.ClientConn
}
func NewKMSServiceClient(cc *grpc.ClientConn) KMSServiceClient {
return &kMSServiceClient{cc}
}
func (c *kMSServiceClient) Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error) {
out := new(VersionResponse)
err := grpc.Invoke(ctx, "/v1beta1.KMSService/Version", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kMSServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) {
out := new(DecryptResponse)
err := grpc.Invoke(ctx, "/v1beta1.KMSService/Decrypt", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kMSServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) {
out := new(EncryptResponse)
err := grpc.Invoke(ctx, "/v1beta1.KMSService/Encrypt", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for KMSService service
type KMSServiceServer interface {
// Version returns the runtime name and runtime version of the KMS provider.
Version(context.Context, *VersionRequest) (*VersionResponse, error)
// Execute decryption operation in KMS provider.
Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error)
// Execute encryption operation in KMS provider.
Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error)
}
func RegisterKMSServiceServer(s *grpc.Server, srv KMSServiceServer) {
s.RegisterService(&_KMSService_serviceDesc, srv)
}
func _KMSService_Version_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(VersionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KMSServiceServer).Version(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/v1beta1.KMSService/Version",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KMSServiceServer).Version(ctx, req.(*VersionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KMSService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DecryptRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KMSServiceServer).Decrypt(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/v1beta1.KMSService/Decrypt",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KMSServiceServer).Decrypt(ctx, req.(*DecryptRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KMSService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EncryptRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KMSServiceServer).Encrypt(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/v1beta1.KMSService/Encrypt",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KMSServiceServer).Encrypt(ctx, req.(*EncryptRequest))
}
return interceptor(ctx, in, info, handler)
}
var _KMSService_serviceDesc = grpc.ServiceDesc{
ServiceName: "v1beta1.KMSService",
HandlerType: (*KMSServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Version",
Handler: _KMSService_Version_Handler,
},
{
MethodName: "Decrypt",
Handler: _KMSService_Decrypt_Handler,
},
{
MethodName: "Encrypt",
Handler: _KMSService_Encrypt_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "service.proto",
}
func init() { proto.RegisterFile("service.proto", fileDescriptorService) }
var fileDescriptorService = []byte{
// 279 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x52, 0xcd, 0x4a, 0xc4, 0x30,
0x10, 0xde, 0xae, 0xb8, 0xc5, 0xb1, 0xb6, 0x10, 0x44, 0x8b, 0x27, 0xcd, 0x65, 0xd5, 0x43, 0x61,
0xf5, 0x2e, 0x22, 0x7a, 0x12, 0x3d, 0x74, 0xc1, 0xab, 0x74, 0xcb, 0x80, 0x01, 0x9b, 0xc6, 0x24,
0x5b, 0xf1, 0x1d, 0x7d, 0x28, 0xb1, 0x99, 0xd6, 0xb4, 0x22, 0xee, 0x71, 0x26, 0xdf, 0xdf, 0xcc,
0x04, 0xf6, 0x0c, 0xea, 0x46, 0x94, 0x98, 0x29, 0x5d, 0xdb, 0x9a, 0x85, 0xcd, 0x62, 0x85, 0xb6,
0x58, 0xf0, 0x73, 0x88, 0x9f, 0x50, 0x1b, 0x51, 0xcb, 0x1c, 0xdf, 0xd6, 0x68, 0x2c, 0x4b, 0x21,
0x6c, 0x5c, 0x27, 0x0d, 0x8e, 0x83, 0xd3, 0x9d, 0xbc, 0x2b, 0xf9, 0x3b, 0x24, 0x3d, 0xd6, 0xa8,
0x5a, 0x1a, 0xfc, 0x1b, 0xcc, 0x4e, 0x20, 0xd2, 0x6b, 0x69, 0x45, 0x85, 0xcf, 0xb2, 0xa8, 0x30,
0x9d, 0xb6, 0xcf, 0xbb, 0xd4, 0x7b, 0x2c, 0x2a, 0x64, 0x73, 0x48, 0x3a, 0x48, 0x27, 0xb2, 0xd5,
0xa2, 0x62, 0x6a, 0x93, 0x1b, 0xbf, 0x81, 0xf8, 0x16, 0x4b, 0xfd, 0xa1, 0xec, 0xbf, 0x21, 0xd9,
0x01, 0xcc, 0x4a, 0xa1, 0x5e, 0x50, 0xb7, 0x8e, 0x51, 0x4e, 0x15, 0x9f, 0x43, 0xd2, 0x6b, 0x50,
0xf8, 0x7d, 0xd8, 0x56, 0xaf, 0x85, 0x70, 0x12, 0x51, 0xee, 0x0a, 0x7e, 0x0d, 0xf1, 0x9d, 0xdc,
0xd0, 0xac, 0x57, 0x98, 0xfa, 0x0a, 0x67, 0x90, 0xf4, 0x0a, 0x64, 0xf5, 0x93, 0x2a, 0xf0, 0x53,
0x5d, 0x7c, 0x06, 0x00, 0xf7, 0x0f, 0xcb, 0xa5, 0x3b, 0x0e, 0xbb, 0x82, 0x90, 0x66, 0x66, 0x87,
0x19, 0x9d, 0x28, 0x1b, 0xde, 0xe7, 0x28, 0xfd, 0xfd, 0xe0, 0x4c, 0xf8, 0xe4, 0x9b, 0x4f, 0x43,
0x7a, 0xfc, 0xe1, 0xea, 0x3c, 0xfe, 0x68, 0x1f, 0x8e, 0x4f, 0xc9, 0x3d, 0xfe, 0x70, 0x1b, 0x1e,
0x7f, 0x34, 0x24, 0x9f, 0xac, 0x66, 0xed, 0xef, 0xba, 0xfc, 0x0a, 0x00, 0x00, 0xff, 0xff, 0x28,
0x69, 0xfc, 0xea, 0x6e, 0x02, 0x00, 0x00,
}

View File

@ -0,0 +1,54 @@
// To regenerate service.pb.go run hack/update-generated-kms.sh
syntax = "proto3";
package v1beta1;
// This service defines the public APIs for remote KMS provider.
service KMSService {
// Version returns the runtime name and runtime version of the KMS provider.
rpc Version(VersionRequest) returns (VersionResponse) {}
// Execute decryption operation in KMS provider.
rpc Decrypt(DecryptRequest) returns (DecryptResponse) {}
// Execute encryption operation in KMS provider.
rpc Encrypt(EncryptRequest) returns (EncryptResponse) {}
}
message VersionRequest {
// Version of the KMS plugin API.
string version = 1;
}
message VersionResponse {
// Version of the KMS plugin API.
string version = 1;
// Name of the KMS provider.
string runtime_name = 2;
// Version of the KMS provider. The string must be semver-compatible.
string runtime_version = 3;
}
message DecryptRequest {
// Version of the KMS plugin API.
string version = 1;
// The data to be decrypted.
bytes cipher = 2;
}
message DecryptResponse {
// The decrypted data.
bytes plain = 1;
}
message EncryptRequest {
// Version of the KMS plugin API.
string version = 1;
// The data to be encrypted.
bytes plain = 2;
}
message EncryptResponse {
// The encrypted data.
bytes cipher = 1;
}