diff --git a/Gopkg.lock b/Gopkg.lock index 2c81852b0e..ccaa7777ae 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -7,8 +7,8 @@ name = "bitbucket.org/ww/goautoneg" packages = ["."] pruneopts = "UT" - source = "https://github.com/munnerz/goautoneg.git" revision = "a547fc61f48d567d5b4ec6f8aee5573d8efce11d" + source = "https://github.com/munnerz/goautoneg.git" [[projects]] branch = "master" @@ -279,7 +279,7 @@ [[projects]] branch = "master" - digest = "1:507e473e2eed514d471611c2384649571f2b2fc5604705b651d99d8071dedc8c" + digest = "1:560d4d5c6f31baa5c79d6921a8cbf6056a0e89da5c7bb6558ca6d1b5c640d268" name = "github.com/gophercloud/gophercloud" packages = [ ".", @@ -300,6 +300,7 @@ "openstack/identity/v2/tokens", "openstack/identity/v3/extensions/trusts", "openstack/identity/v3/tokens", + "openstack/keymanager/v1/secrets", "openstack/loadbalancer/v2/l7policies", "openstack/loadbalancer/v2/listeners", "openstack/loadbalancer/v2/loadbalancers", @@ -1083,7 +1084,7 @@ version = "kubernetes-1.13.0" [[projects]] - digest = "1:7d553606675df9570ff207add7c6df78a6f8059c5df14406fd4ed34ac3906f4c" + digest = "1:cdb0ccc40b1879c8e1826eb9a7461272e7dcd0577739b35a22d87a78d5cca051" name = "k8s.io/apiserver" packages = [ "pkg/admission", @@ -1166,6 +1167,7 @@ "pkg/storage/storagebackend", "pkg/storage/storagebackend/factory", "pkg/storage/value", + "pkg/storage/value/encrypt/envelope/v1beta1", "pkg/util/dryrun", "pkg/util/feature", "pkg/util/flag", @@ -1516,6 +1518,7 @@ analyzer-version = 1 input-imports = [ "github.com/container-storage-interface/spec/lib/go/csi/v0", + "github.com/golang/glog", "github.com/gophercloud/gophercloud", "github.com/gophercloud/gophercloud/openstack", "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions", @@ -1529,6 +1532,7 @@ "github.com/gophercloud/gophercloud/openstack/compute/v2/servers", "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts", "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens", + "github.com/gophercloud/gophercloud/openstack/keymanager/v1/secrets", "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies", "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners", "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers", @@ -1569,6 +1573,7 @@ "golang.org/x/crypto/ssh/terminal", "golang.org/x/net/context", "golang.org/x/sys/unix", + "google.golang.org/grpc", "google.golang.org/grpc/codes", "google.golang.org/grpc/status", "gopkg.in/gcfg.v1", @@ -1590,6 +1595,7 @@ "k8s.io/apiserver/pkg/authentication/user", "k8s.io/apiserver/pkg/authorization/authorizer", "k8s.io/apiserver/pkg/server/healthz", + "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1", "k8s.io/apiserver/pkg/util/feature", "k8s.io/apiserver/pkg/util/flag", "k8s.io/apiserver/pkg/util/logs", diff --git a/Makefile b/Makefile index 146ea2af0d..cb198d1eb0 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ endif depend-update: work dep ensure -update -v -build: openstack-cloud-controller-manager cinder-provisioner cinder-flex-volume-driver cinder-csi-plugin k8s-keystone-auth client-keystone-auth octavia-ingress-controller manila-provisioner +build: openstack-cloud-controller-manager cinder-provisioner cinder-flex-volume-driver cinder-csi-plugin k8s-keystone-auth client-keystone-auth octavia-ingress-controller manila-provisioner barbican-kms-plugin openstack-cloud-controller-manager: depend $(SOURCES) CGO_ENABLED=0 GOOS=$(GOOS) go build \ @@ -109,6 +109,12 @@ manila-provisioner: depend $(SOURCES) -o manila-provisioner \ cmd/manila-provisioner/main.go +barbican-kms-plugin: depend $(SOURCES) + cd $(DEST) && CGO_ENABLED=0 GOOS=$(GOOS) go build \ + -ldflags $(LDFLAGS) \ + -o barbican-kms-plugin \ + cmd/barbican-kms-plugin/main.go + test: unit functional check: depend fmt vet lint import-boss @@ -193,7 +199,7 @@ realclean: clean shell: $(SHELL) -i -images: image-controller-manager image-flex-volume-driver image-provisioner image-csi-plugin image-k8s-keystone-auth image-octavia-ingress-controller image-manila-provisioner +images: image-controller-manager image-flex-volume-driver image-provisioner image-csi-plugin image-k8s-keystone-auth image-octavia-ingress-controller image-manila-provisioner image-kms-plugin image-controller-manager: depend openstack-cloud-controller-manager ifeq ($(GOOS),linux) @@ -258,6 +264,15 @@ else $(error Please set GOOS=linux for building the image) endif +image-kms-plugin: depend barbican-kms-plugin +ifeq ($(GOOS), linux) + cp barbican-kms-plugin cluster/images/barbican-kms-plugin + docker build -t $(REGISTRY)/barbican-kms-plugin:$(VERSION) cluster/images/barbican-kms-plugin + rm cluster/images/barbican-kms-plugin/barbican-kms-plugin +else + $(error Please set GOOS=linux for building the image) +endif + upload-images: images @echo "push images to $(REGISTRY)" docker login -u="$(DOCKER_USERNAME)" -p="$(DOCKER_PASSWORD)"; diff --git a/cluster/images/barbican-kms-plugin/Dockerfile b/cluster/images/barbican-kms-plugin/Dockerfile new file mode 100644 index 0000000000..f00e7caded --- /dev/null +++ b/cluster/images/barbican-kms-plugin/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:3.7 +LABEL maintainers="Kubernetes Authors" +LABEL description="Barbican KMS Plugin" + +ADD barbican-kms-plugin /bin/ + +CMD ["sh", "-c", "/bin/barbican-kms-plugin --socketpath ${socketpath} --cloud-config ${cloudconfig}"] diff --git a/cmd/barbican-kms-plugin/main.go b/cmd/barbican-kms-plugin/main.go new file mode 100644 index 0000000000..7af9440260 --- /dev/null +++ b/cmd/barbican-kms-plugin/main.go @@ -0,0 +1,67 @@ +/* +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 main + +import ( + "flag" + "fmt" + "os" + "os/signal" + + "github.com/spf13/cobra" + "golang.org/x/sys/unix" + "k8s.io/cloud-provider-openstack/pkg/kms/server" +) + +var ( + socketpath string + cloudconfig string +) + +func init() { + flag.Set("logtostderr", "true") +} + +func main() { + flag.CommandLine.Parse([]string{}) + + cmd := &cobra.Command{ + Use: "barbican-kms-plugin", + Short: "Barbican KMS plugin for kubernetes", + RunE: func(cmd *cobra.Command, args []string) error { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, unix.SIGTERM, unix.SIGINT) + err := server.Run(cloudconfig, socketpath, sigchan) + return err + }, + } + + cmd.Flags().AddGoFlagSet(flag.CommandLine) + + cmd.PersistentFlags().StringVar(&socketpath, "socketpath", "", "Barbican KMS Plugin unix socket endpoint") + cmd.MarkPersistentFlagRequired("socketpath") + + cmd.PersistentFlags().StringVar(&cloudconfig, "cloud-config", "", "Barbican KMS Plugin cloud config") + cmd.MarkPersistentFlagRequired("cloud-config") + + if err := cmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(1) + } + + os.Exit(0) +} diff --git a/manifests/barbican-kms/encryption-config.yaml b/manifests/barbican-kms/encryption-config.yaml new file mode 100755 index 0000000000..7719750564 --- /dev/null +++ b/manifests/barbican-kms/encryption-config.yaml @@ -0,0 +1,11 @@ +kind: EncryptionConfig +apiVersion: v1 +resources: + - resources: + - secrets + providers: + - kms: + name : barbican + endpoint: unix:///var/lib/kms/kms.sock + cachesize: 20 + - identity: {} diff --git a/manifests/barbican-kms/pod.yaml b/manifests/barbican-kms/pod.yaml new file mode 100644 index 0000000000..b721862e25 --- /dev/null +++ b/manifests/barbican-kms/pod.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Pod +metadata: + name: barbican-kms +spec: + containers: + - name: barbican-kms + image: docker.io/k8scloudprovider/barbican-kms-plugin:latest + args: + - "--socketpath=/kms/kms.sock" + - "--cloud-config=/etc/kubernetes/cloud-config" + volumeMounts: + - name: cloud-config + mountPath: /etc/kubernetes/ + - name: socket-dir + mountPath: /kms/ + volumes: + - name: config + hostPath: + path: /etc/kubernetes + - name: socket-dir + hostPath: + path: /var/lib/kms/ + type: DirectoryOrCreate diff --git a/pkg/kms/barbican/barbican.go b/pkg/kms/barbican/barbican.go new file mode 100644 index 0000000000..4b164c78ee --- /dev/null +++ b/pkg/kms/barbican/barbican.go @@ -0,0 +1,95 @@ +package barbican + +import ( + "github.com/golang/glog" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/keymanager/v1/secrets" +) + +type BarbicanService interface { + GetSecret(cfg Config) ([]byte, error) +} + +type KMSOpts struct { + KeyID string `gcfg:"key-id"` +} + +//Config to read config options +type Config struct { + Global struct { + AuthURL string `gcfg:"auth-url"` + Username string + UserID string `gcfg:"user-id"` + Password string + TenantID string `gcfg:"tenant-id"` + TenantName string `gcfg:"tenant-name"` + DomainID string `gcfg:"domain-id"` + DomainName string `gcfg:"domain-name"` + Region string + } + KeyManager KMSOpts +} + +// Barbican is gophercloud service client +type Barbican struct { +} + +func (cfg Config) toAuthOptions() gophercloud.AuthOptions { + return gophercloud.AuthOptions{ + IdentityEndpoint: cfg.Global.AuthURL, + Username: cfg.Global.Username, + UserID: cfg.Global.UserID, + Password: cfg.Global.Password, + TenantID: cfg.Global.TenantID, + TenantName: cfg.Global.TenantName, + DomainID: cfg.Global.DomainID, + DomainName: cfg.Global.DomainName, + + // Persistent service, so we need to be able to renew tokens. + AllowReauth: true, + } +} + +// NewBarbicanClient creates new BarbicanClient +func newBarbicanClient(cfg Config) (client *gophercloud.ServiceClient, err error) { + + provider, err := openstack.AuthenticatedClient(cfg.toAuthOptions()) + + if err != nil { + return nil, err + } + + client, err = openstack.NewKeyManagerV1(provider, gophercloud.EndpointOpts{ + Region: cfg.Global.Region, + }) + if err != nil { + return nil, err + } + + return client, nil +} + +// GetSecret gets unencrypted secret +func (barbican *Barbican) GetSecret(cfg Config) ([]byte, error) { + + client, err := newBarbicanClient(cfg) + + keyID := cfg.KeyManager.KeyID + + if err != nil { + glog.V(4).Infof("Failed to get Barbican client %v: ", err) + return nil, err + } + + opts := secrets.GetPayloadOpts{ + PayloadContentType: "application/octet-stream", + } + + key, err := secrets.GetPayload(client, keyID, opts).Extract() + if err != nil { + return nil, err + } + + return key, nil +} diff --git a/pkg/kms/barbican/fake_barbican.go b/pkg/kms/barbican/fake_barbican.go new file mode 100644 index 0000000000..2293625d61 --- /dev/null +++ b/pkg/kms/barbican/fake_barbican.go @@ -0,0 +1,11 @@ +package barbican + +import "encoding/hex" + +type FakeBarbican struct { +} + +func (client *FakeBarbican) GetSecret(cfg Config) ([]byte, error) { + return hex.DecodeString("6368616e676520746869732070617373") + +} diff --git a/pkg/kms/client/client.go b/pkg/kms/client/client.go new file mode 100644 index 0000000000..ace69eca1b --- /dev/null +++ b/pkg/kms/client/client.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "golang.org/x/net/context" + "google.golang.org/grpc" + pb "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1" + "os" +) + +//This client is for test purpose only, Kubernetes api server will call to kms plugin grpc server + +func main() { + + connection, err := grpc.Dial("unix:///var/lib/kms/kms.sock", grpc.WithInsecure()) + defer connection.Close() + if err != nil { + fmt.Printf("\nConnection to KMS plugin failed, error: %v", err) + } + + kmsClient := pb.NewKeyManagementServiceClient(connection) + request := &pb.VersionRequest{Version: "v1beta1"} + _, err = kmsClient.Version(context.TODO(), request) + + if err != nil { + fmt.Printf("\nError in getting version from KMS Plugin: %v", err) + } + + secretBytes := []byte("mypassword") + + //Encryption Request to KMS Plugin + encRequest := &pb.EncryptRequest{ + Version: "v1beta1", + Plain: secretBytes} + encResponse, err := kmsClient.Encrypt(context.TODO(), encRequest) + + if err != nil { + fmt.Printf("\nEncrypt Request Failed: %v", err) + os.Exit(1) + } + + cipher := string(encResponse.Cipher) + fmt.Println("cipher:", cipher) + + //Decryption Request to KMS plugin + decRequest := &pb.DecryptRequest{ + Version: "v1beta1", + Cipher: encResponse.Cipher, + } + + decResponse, err := kmsClient.Decrypt(context.TODO(), decRequest) + + fmt.Printf("\n\ndecryption response %v", decResponse) +} diff --git a/pkg/kms/encryption/aescbc/aescbc.go b/pkg/kms/encryption/aescbc/aescbc.go new file mode 100644 index 0000000000..25dca49d21 --- /dev/null +++ b/pkg/kms/encryption/aescbc/aescbc.go @@ -0,0 +1,72 @@ +package aescbc + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" + + "github.com/golang/glog" +) + +// Encrypt plain text +func Encrypt(data, key []byte) (ciphertext []byte, err error) { + + glog.V(3).Infof("aescbc encrypt") + + // NewCipher returns a new cipher block, the key argument should be AES key + // either 16, 24 or 32 bytes to select AES-128, AES-192, AES-256, 32 byte is preferred + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // determine the padding size for block cipher + paddingSize := aes.BlockSize - (len(data) % aes.BlockSize) + plaintext := make([]byte, len(data)+paddingSize) + // copy data and padding + copy(plaintext, data) + copy(plaintext[len(data):], bytes.Repeat([]byte{byte(paddingSize)}, paddingSize)) + + // create slice to hold ciphertext, iv + ciphertext = make([]byte, aes.BlockSize+len(plaintext)) + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) + + glog.V(3).Infof("aescbc encrypt %s", string(ciphertext)) + + return +} + +// Decrypt plaintext +func Decrypt(data, key []byte) (plaintext []byte, err error) { + glog.V(3).Infof("aescbc decrypt") + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + iv := data[:aes.BlockSize] + ciphertext := data[aes.BlockSize:] + + if len(ciphertext)%aes.BlockSize != 0 { + return nil, errors.New("Invalid Data, not multiple of block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ciphertext, ciphertext) + + paddingLength := int(ciphertext[len(ciphertext)-1]) + dataLength := len(ciphertext) - paddingLength + plaintext = ciphertext[:dataLength] + glog.V(3).Infof("aescbc decrypt %s", string(plaintext)) + + return +} diff --git a/pkg/kms/encryption/aescbc/aescbc_test.go b/pkg/kms/encryption/aescbc/aescbc_test.go new file mode 100644 index 0000000000..f1d04c5759 --- /dev/null +++ b/pkg/kms/encryption/aescbc/aescbc_test.go @@ -0,0 +1,40 @@ +package aescbc + +import ( + "bytes" + "crypto/rand" + "testing" +) + +var key []byte + +func init() { + // genereate key for encrypt decrypt operation + genKey() +} + +func TestEncryptDecrypt(t *testing.T) { + data := []byte("mypassword") + cipher, _ := Encrypt((data), key) + plain, _ := Decrypt(cipher, key) + if !bytes.Equal((data), plain) { + t.FailNow() + } +} + +// testKeyerror +func TestEncryptDecryptInvalidData(t *testing.T) { + data := []byte("mypassword") + cipher, err := Encrypt(data, key) + _, err = Decrypt(cipher[1:], key) + if err == nil { + t.FailNow() + } + t.Log(err) +} + +func genKey() { + key = make([]byte, 32) + _, _ = rand.Read(key) + +} diff --git a/pkg/kms/server/server.go b/pkg/kms/server/server.go new file mode 100644 index 0000000000..05cae097fc --- /dev/null +++ b/pkg/kms/server/server.go @@ -0,0 +1,136 @@ +package server + +import ( + "fmt" + "net" + "os" + + "github.com/golang/glog" + "golang.org/x/net/context" + "golang.org/x/sys/unix" + "google.golang.org/grpc" + gcfg "gopkg.in/gcfg.v1" + pb "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1" + "k8s.io/cloud-provider-openstack/pkg/kms/barbican" + "k8s.io/cloud-provider-openstack/pkg/kms/encryption/aescbc" +) + +const ( + netProtocol = "unix" + version = "v1beta1" + runtimename = "barbican" + runtimeversion = "0.0.1" +) + +// KMSserver struct +type KMSserver struct { + cfg barbican.Config + barbican barbican.BarbicanService +} + +func initConfig(configFilePath string, cfg *barbican.Config) error { + + config, err := os.Open(configFilePath) + defer config.Close() + if err != nil { + return err + } + err = gcfg.FatalOnly(gcfg.ReadInto(cfg, config)) + if err != nil { + return err + } + return nil +} + +// Run Grpc server for barbican KMS +func Run(configFilePath string, socketpath string, sigchan <-chan os.Signal) (err error) { + + glog.Infof("Barbican KMS Plugin Starting Version: %s, RunTimeVersion: %s", version, runtimeversion) + s := new(KMSserver) + err = initConfig(configFilePath, &s.cfg) + s.barbican = &barbican.Barbican{} + if err != nil { + glog.V(4).Infof("Error in Getting Config File: %v", err) + return err + } + + // unlink the unix socket + if err = unix.Unlink(socketpath); err != nil { + glog.V(4).Infof("Error to unlink unix socket: %v", err) + } + + listener, err := net.Listen(netProtocol, socketpath) + if err != nil { + glog.Fatalf("Failed to Listen: %v", err) + return err + } + + gServer := grpc.NewServer() + pb.RegisterKeyManagementServiceServer(gServer, s) + + go gServer.Serve(listener) + + for { + sig := <-sigchan + if sig == unix.SIGINT || sig == unix.SIGTERM { + fmt.Println("force stop, shutting down grpc server") + gServer.GracefulStop() + return nil + } + } +} + +// Version returns KMS service version +func (s *KMSserver) Version(ctx context.Context, req *pb.VersionRequest) (*pb.VersionResponse, error) { + + glog.V(4).Infof("Version Information Requested by Kubernetes api server") + + res := &pb.VersionResponse{ + Version: version, + RuntimeName: runtimename, + RuntimeVersion: runtimeversion, + } + + return res, nil +} + +// Decrypt decrypts the cipher +func (s *KMSserver) Decrypt(ctx context.Context, req *pb.DecryptRequest) (*pb.DecryptResponse, error) { + + glog.V(4).Infof("Decrypt Request by Kubernetes api server") + + key, err := s.barbican.GetSecret(s.cfg) + if err != nil { + glog.V(4).Infof("Failed to get key %v: ", err) + return nil, err + } + + plain, err := aescbc.Decrypt(req.Cipher, key) + if err != nil { + glog.V(4).Infof("Failed to decrypt data %v: ", err) + return nil, err + } + + return &pb.DecryptResponse{Plain: plain}, nil +} + +// Encrypt encrypts DEK +func (s *KMSserver) Encrypt(ctx context.Context, req *pb.EncryptRequest) (*pb.EncryptResponse, error) { + + glog.V(4).Infof("Encrypt Request by Kubernetes api server") + + key, err := s.barbican.GetSecret(s.cfg) + + if err != nil { + glog.V(4).Infof("Failed to get key %v: ", err) + return nil, err + } + + cipher, err := aescbc.Encrypt(req.Plain, key) + + if err != nil { + glog.V(4).Infof("Failed to encrypt data %v: ", err) + return nil, err + } + return &pb.EncryptResponse{Cipher: cipher}, nil +} diff --git a/pkg/kms/server/server_test.go b/pkg/kms/server/server_test.go new file mode 100644 index 0000000000..284474ddae --- /dev/null +++ b/pkg/kms/server/server_test.go @@ -0,0 +1,42 @@ +package server + +import ( + "bytes" + "testing" + + "golang.org/x/net/context" + pb "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1" + "k8s.io/cloud-provider-openstack/pkg/kms/barbican" +) + +var s = new(KMSserver) + +func TestInitConfig(t *testing.T) { + +} + +func TestVersion(t *testing.T) { + req := &pb.VersionRequest{Version: "v1beta1"} + _, err := s.Version(context.TODO(), req) + if err != nil { + t.FailNow() + } +} + +func TestEncryptDecrypt(t *testing.T) { + s.barbican = &barbican.FakeBarbican{} + fakeData := []byte("fakedata") + encreq := &pb.EncryptRequest{Version: "v1beta1", Plain: fakeData} + encresp, err := s.Encrypt(context.TODO(), encreq) + if err != nil { + t.Log(err) + t.FailNow() + } + decreq := &pb.DecryptRequest{Version: "v1beta1", Cipher: encresp.Cipher} + decresp, err := s.Decrypt(context.TODO(), decreq) + if err != nil || !bytes.Equal(decresp.Plain, fakeData) { + t.Log(err) + t.FailNow() + } + +}