From 284164c40c57fa14f3d99b18dca017478e4933c9 Mon Sep 17 00:00:00 2001 From: xinjiexu Date: Wed, 12 Dec 2018 19:06:40 +0800 Subject: [PATCH] Add feature of encrypt and decrypt helm charts Signed-off-by: xinjiexu --- cmd/helm/package.go | 10 +++ cmd/tiller/tiller.go | 10 ++- docs/helm/helm_package.md | 3 +- pkg/aesutil/aesutil.go | 65 +++++++++++++++ pkg/chartutil/cipher.go | 112 ++++++++++++++++++++++++++ pkg/tiller/environment/environment.go | 2 + pkg/tiller/release_install.go | 8 ++ pkg/tiller/release_update.go | 8 ++ 8 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 pkg/aesutil/aesutil.go create mode 100644 pkg/chartutil/cipher.go diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 05fdf02f8..da953f260 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -59,6 +59,7 @@ type packageCmd struct { version string appVersion string destination string + cipherKey string dependencyUpdate bool out io.Writer @@ -103,6 +104,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { f.StringVar(&pkg.version, "version", "", "set the version on the chart to this semver version") f.StringVar(&pkg.appVersion, "app-version", "", "set the appVersion on the chart to this version") f.StringVarP(&pkg.destination, "destination", "d", ".", "location to write the chart.") + f.StringVar(&pkg.cipherKey, "cipher-key", "", "the cipher key to encrypt this package") f.BoolVarP(&pkg.dependencyUpdate, "dependency-update", "u", false, `update dependencies from "requirements.yaml" to dir "charts/" before packaging`) return cmd @@ -173,6 +175,14 @@ func (p *packageCmd) run() error { dest = p.destination } + // encrypt this chart + if len(p.cipherKey) > 0 { + ch, err = chartutil.Encrypt(ch, p.cipherKey) + if err != nil { + return fmt.Errorf("Failed to encrypt: %s", err) + } + } + name, err := chartutil.Save(ch, dest) if err == nil { fmt.Fprintf(p.out, "Successfully packaged chart and saved it to: %s\n", name) diff --git a/cmd/tiller/tiller.go b/cmd/tiller/tiller.go index 478ca92f4..1b4e2dc0b 100644 --- a/cmd/tiller/tiller.go +++ b/cmd/tiller/tiller.go @@ -61,6 +61,8 @@ const ( tlsCertsEnvVar = "TILLER_TLS_CERTS" // historyMaxEnvVar is the name of the env var for setting max history. historyMaxEnvVar = "TILLER_HISTORY_MAX" + // cipherKeyEnvVar is the name of the env var for the cipher key to decrypt chart. + cipherKeyEnvVar = "CIPHER_KEY" storageMemory = "memory" storageConfigMap = "configmap" @@ -84,6 +86,7 @@ var ( certFile = flag.String("tls-cert", tlsDefaultsFromEnv("tls-cert"), "path to TLS certificate file") caCertFile = flag.String("tls-ca-cert", tlsDefaultsFromEnv("tls-ca-cert"), "trust certificates signed by this CA") maxHistory = flag.Int("history-max", historyMaxFromEnv(), "maximum number of releases kept in release history, with 0 meaning no limit") + cipherKey = flag.String("cipher-key", cipherKeyDefaultsFromEnv(), "the cipher key to decrypt chart") printVersion = flag.Bool("version", false, "print the version number") // rootServer is the root gRPC server. @@ -151,6 +154,8 @@ func start() { kubeClient.Log = newLogger("kube").Printf env.KubeClient = kubeClient + env.CipherKey = *cipherKey + if *tlsEnable || *tlsVerify { opts := tlsutil.Options{CertFile: *certFile, KeyFile: *keyFile} if *tlsVerify { @@ -287,5 +292,6 @@ func historyMaxFromEnv() int { return ret } -func tlsEnableEnvVarDefault() bool { return os.Getenv(tlsEnableEnvVar) != "" } -func tlsVerifyEnvVarDefault() bool { return os.Getenv(tlsVerifyEnvVar) != "" } +func tlsEnableEnvVarDefault() bool { return os.Getenv(tlsEnableEnvVar) != "" } +func tlsVerifyEnvVarDefault() bool { return os.Getenv(tlsVerifyEnvVar) != "" } +func cipherKeyDefaultsFromEnv() string { return os.Getenv(cipherKeyEnvVar) } diff --git a/docs/helm/helm_package.md b/docs/helm/helm_package.md index b772fa70c..0f5508005 100644 --- a/docs/helm/helm_package.md +++ b/docs/helm/helm_package.md @@ -23,6 +23,7 @@ helm package [flags] [CHART_PATH] [...] ``` --app-version string set the appVersion on the chart to this version + --cipher-key string the cipher key to encrypt this package -u, --dependency-update update dependencies from "requirements.yaml" to dir "charts/" before packaging -d, --destination string location to write the chart. (default ".") -h, --help help for package @@ -49,4 +50,4 @@ helm package [flags] [CHART_PATH] [...] * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 1-Aug-2018 +###### Auto generated by spf13/cobra on 12-Dec-2018 diff --git a/pkg/aesutil/aesutil.go b/pkg/aesutil/aesutil.go new file mode 100644 index 000000000..f68729867 --- /dev/null +++ b/pkg/aesutil/aesutil.go @@ -0,0 +1,65 @@ +/* +Copyright The Helm 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 aesutil + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" +) + +// Padding ciphertext to multiples of blockSize +func PKCS7Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +// UnPadding origData +func PKCS7UnPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +} + +// AES encrypt orig data with key, and return encrypted data +func AesEncrypt(origData, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + origData = PKCS7Padding(origData, blockSize) + blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) + crypted := make([]byte, len(origData)) + blockMode.CryptBlocks(crypted, origData) + return crypted, nil +} + +// AES decrypt crypted with key, and return orig data +func AesDecrypt(crypted, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) + origData := make([]byte, len(crypted)) + blockMode.CryptBlocks(origData, crypted) + origData = PKCS7UnPadding(origData) + return origData, nil +} diff --git a/pkg/chartutil/cipher.go b/pkg/chartutil/cipher.go new file mode 100644 index 000000000..a84ca0bef --- /dev/null +++ b/pkg/chartutil/cipher.go @@ -0,0 +1,112 @@ +/* +Copyright The Helm 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 chartutil + +import ( + "k8s.io/helm/pkg/aesutil" + "k8s.io/helm/pkg/proto/hapi/chart" +) + +const ENCRYPTED_ANNOTATION_KEY = "encrypted" + +// Encrypt Data of templates and values in Chart with cipherKey +func Encrypt(ch *chart.Chart, cipherKey string) (*chart.Chart, error) { + cipherBytes := []byte(cipherKey) + // encrypt templates and values + ch, err := encryptChart(ch, cipherBytes) + if err != nil { + return ch, err + } + // encrypt dependencies + for _, c := range ch.Dependencies { + _, err := encryptChart(c, cipherBytes) + if err != nil { + return ch, err + } + } + return ch, nil +} + +func encryptChart(ch *chart.Chart, cipherBytes []byte) (*chart.Chart, error) { + shouldEncrypt := false + if len(ch.Metadata.Annotations) == 0 { + shouldEncrypt = true + } else if v, ok := ch.Metadata.Annotations[ENCRYPTED_ANNOTATION_KEY]; !ok || v != "true" { + shouldEncrypt = true + } + if shouldEncrypt { + for _, template := range ch.Templates { + data, err := aesutil.AesEncrypt(template.Data, cipherBytes) + if err != nil { + return ch, err + } + template.Data = data + } + if ch.Values != nil { + data, err := aesutil.AesEncrypt([]byte(ch.Values.Raw), cipherBytes) + if err != nil { + return ch, err + } + ch.Values.Raw = string(data) + } + if ch.Metadata.Annotations == nil { + ch.Metadata.Annotations = map[string]string{} + } + ch.Metadata.Annotations[ENCRYPTED_ANNOTATION_KEY] = "true" + } + return ch, nil +} + +// Decrypt Data of templates and values in Chart with cipherKey +func Decrypt(ch *chart.Chart, cipherKey string) (*chart.Chart, error) { + cipherBytes := []byte(cipherKey) + // decrypt templates and values + ch, err := decryptChart(ch, cipherBytes) + if err != nil { + return ch, err + } + // decrypt dependencies + for _, c := range ch.Dependencies { + _, err := decryptChart(c, cipherBytes) + if err != nil { + return ch, err + } + } + return ch, nil +} + +func decryptChart(ch *chart.Chart, cipherBytes []byte) (*chart.Chart, error) { + if len(ch.Metadata.Annotations) > 0 { + if v, ok := ch.Metadata.Annotations[ENCRYPTED_ANNOTATION_KEY]; ok && v == "true" { + for _, template := range ch.Templates { + data, err := aesutil.AesDecrypt(template.Data, cipherBytes) + if err != nil { + return ch, err + } + template.Data = data + } + if ch.Values != nil { + data, err := aesutil.AesDecrypt([]byte(ch.Values.Raw), cipherBytes) + if err != nil { + return ch, err + } + ch.Values.Raw = string(data) + } + ch.Metadata.Annotations[ENCRYPTED_ANNOTATION_KEY] = "false" + } + } + return ch, nil +} diff --git a/pkg/tiller/environment/environment.go b/pkg/tiller/environment/environment.go index 86d077b89..cb3dafe6a 100644 --- a/pkg/tiller/environment/environment.go +++ b/pkg/tiller/environment/environment.go @@ -207,6 +207,8 @@ type Environment struct { Releases *storage.Storage // KubeClient is a Kubernetes API client. KubeClient KubeClient + // the cipher key to decrypt chart + CipherKey string } // New returns an environment initialized with the defaults. diff --git a/pkg/tiller/release_install.go b/pkg/tiller/release_install.go index 973da3581..54e4b24b1 100644 --- a/pkg/tiller/release_install.go +++ b/pkg/tiller/release_install.go @@ -32,6 +32,14 @@ import ( // InstallRelease installs a release and stores the release record. func (s *ReleaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { + // decrypt this chart if possible + if len(s.env.CipherKey) > 0 { + ch, err := chartutil.Decrypt(req.Chart, s.env.CipherKey) + if err != nil { + return nil, fmt.Errorf("Failed to decrypt: %s", err) + } + req.Chart = ch + } s.Log("preparing install for %s", req.Name) rel, err := s.prepareRelease(req) if err != nil { diff --git a/pkg/tiller/release_update.go b/pkg/tiller/release_update.go index 8f3cc4e8e..83e7db442 100644 --- a/pkg/tiller/release_update.go +++ b/pkg/tiller/release_update.go @@ -31,6 +31,14 @@ import ( // UpdateRelease takes an existing release and new information, and upgrades the release. func (s *ReleaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { + // decrypt this chart if possible + if len(s.env.CipherKey) > 0 { + ch, err := chartutil.Decrypt(req.Chart, s.env.CipherKey) + if err != nil { + return nil, fmt.Errorf("Failed to decrypt: %s", err) + } + req.Chart = ch + } if err := validateReleaseName(req.Name); err != nil { s.Log("updateRelease: Release name is invalid: %s", req.Name) return nil, err