feat: Add --hide-secrets flag

Signed-off-by: Szymon Gibała <szymongib@gmail.com>
pull/9417/head
Szymon Gibała 5 years ago
parent e71d38b414
commit 9119f84cbe

@ -59,7 +59,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return tpl(template, data, out)
}
return output.Table.Write(out, &statusPrinter{res, true, false})
return output.Table.Write(out, &statusPrinter{res, true, false, settings.HideSecrets})
},
}

@ -122,7 +122,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return err
}
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false})
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, settings.HideSecrets})
},
}

@ -61,7 +61,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
return runErr
}
if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}); err != nil {
if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, settings.HideSecrets}); err != nil {
return err
}

@ -29,6 +29,7 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/sanitize"
"helm.sh/helm/v3/pkg/release"
)
@ -70,7 +71,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
// strip chart metadata from the output
rel.Chart = nil
return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription})
return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription, false})
},
}
@ -99,6 +100,7 @@ type statusPrinter struct {
release *release.Release
debug bool
showDescription bool
hideSecrets bool
}
func (s statusPrinter) WriteJSON(out io.Writer) error {
@ -170,7 +172,16 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
for _, h := range s.release.Hooks {
fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest)
}
fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest)
var err error
manifest := s.release.Manifest
if s.hideSecrets {
manifest, err = sanitize.HideSecrets(manifest)
if err != nil {
return err
}
}
fmt.Fprintf(out, "MANIFEST:\n%s\n", manifest)
}
if len(s.release.Info.Notes) > 0 {

@ -115,7 +115,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil {
return err
}
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false})
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, settings.HideSecrets})
} else if err != nil {
return err
}
@ -160,7 +160,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0])
}
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false})
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, settings.HideSecrets})
},
}

@ -35,6 +35,7 @@ require (
github.com/stretchr/testify v1.6.1
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
gopkg.in/yaml.v2 v2.3.0
k8s.io/api v0.20.0
k8s.io/apiextensions-apiserver v0.20.0
k8s.io/apimachinery v0.20.0

@ -58,6 +58,8 @@ type EnvSettings struct {
KubeCaFile string
// Debug indicates whether or not Helm is running in Debug mode.
Debug bool
// HideSecrets indicates whether Secret values should be hidden.
HideSecrets bool
// RegistryConfig is the path to the registry config file.
RegistryConfig string
// RepositoryConfig is the path to the repositories file.
@ -86,6 +88,7 @@ func New() *EnvSettings {
RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")),
}
env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG"))
env.HideSecrets, _ = strconv.ParseBool(os.Getenv("HELM_HIDE_SECRETS"))
// bind to kubernetes config flags
env.config = &genericclioptions.ConfigFlags{
@ -112,6 +115,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "the address and the port for the Kubernetes API server")
fs.StringVar(&s.KubeCaFile, "kube-ca-file", s.KubeCaFile, "the certificate authority file for the Kubernetes API server connection")
fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output")
fs.BoolVar(&s.HideSecrets, "hide-secrets", s.HideSecrets, "hide Secret values in printed manifests")
fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file")
fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs")
fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the file containing cached repository indexes")

@ -36,6 +36,7 @@ func TestEnvSettings(t *testing.T) {
// expected values
ns, kcontext string
debug bool
hideSecrets bool
maxhistory int
kAsUser string
kAsGroups []string
@ -47,35 +48,38 @@ func TestEnvSettings(t *testing.T) {
maxhistory: defaultMaxHistory,
},
{
name: "with flags set",
args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/tmp/ca.crt",
ns: "myns",
debug: true,
maxhistory: defaultMaxHistory,
kAsUser: "poro",
kAsGroups: []string{"admins", "teatime", "snackeaters"},
kCaFile: "/tmp/ca.crt",
name: "with flags set",
args: "--debug --hide-secrets --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/tmp/ca.crt",
ns: "myns",
debug: true,
hideSecrets: true,
maxhistory: defaultMaxHistory,
kAsUser: "poro",
kAsGroups: []string{"admins", "teatime", "snackeaters"},
kCaFile: "/tmp/ca.crt",
},
{
name: "with envvars set",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt"},
ns: "yourns",
maxhistory: 5,
debug: true,
kAsUser: "pikachu",
kAsGroups: []string{"operators", "snackeaters", "partyanimals"},
kCaFile: "/tmp/ca.crt",
name: "with envvars set",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_HIDE_SECRETS": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt"},
ns: "yourns",
maxhistory: 5,
debug: true,
hideSecrets: true,
kAsUser: "pikachu",
kAsGroups: []string{"operators", "snackeaters", "partyanimals"},
kCaFile: "/tmp/ca.crt",
},
{
name: "with flags and envvars set",
args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/my/ca.crt",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt"},
ns: "myns",
debug: true,
maxhistory: 5,
kAsUser: "poro",
kAsGroups: []string{"admins", "teatime", "snackeaters"},
kCaFile: "/my/ca.crt",
name: "with flags and envvars set",
args: "--debug --hide-secrets --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/my/ca.crt",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_HIDE_SECRETS": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt"},
ns: "myns",
debug: true,
hideSecrets: true,
maxhistory: 5,
kAsUser: "poro",
kAsGroups: []string{"admins", "teatime", "snackeaters"},
kCaFile: "/my/ca.crt",
},
}
@ -96,6 +100,9 @@ func TestEnvSettings(t *testing.T) {
if settings.Debug != tt.debug {
t.Errorf("expected debug %t, got %t", tt.debug, settings.Debug)
}
if settings.HideSecrets != tt.hideSecrets {
t.Errorf("expected hide-secrets %t, got %t", tt.hideSecrets, settings.HideSecrets)
}
if settings.Namespace() != tt.ns {
t.Errorf("expected namespace %q, got %q", tt.ns, settings.Namespace())
}

@ -0,0 +1,131 @@
/*
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 sanitize
import (
"fmt"
"strings"
"gopkg.in/yaml.v2"
)
const (
hiddenSecretValue = "[HIDDEN]"
)
// HideSecrets replaces values in Secrets in the chart manifest with
// `[HIDDEN]` value.
func HideSecrets(manifest string) (string, error) {
resources := strings.Split(manifest, "\n---")
outRes := make([]string, 0, len(resources))
for _, r := range resources {
var resourceMap map[string]interface{}
err := yaml.Unmarshal([]byte(r), &resourceMap)
if err != nil {
return "", err
}
if isSecret(resourceMap) {
r = hideSecretData(r, resourceMap)
}
outRes = append(outRes, r)
}
return strings.Join(outRes, "\n---"), nil
}
func isSecret(resource map[string]interface{}) bool {
kind, ok := resource["kind"].(string)
if !ok || kind != "Secret" {
return false
}
apiVersion, ok := resource["apiVersion"].(string)
if !ok || apiVersion != "v1" {
return false
}
return true
}
func hideSecretData(raw string, resource map[string]interface{}) string {
dataRaw, ok := resource["data"].(map[interface{}]interface{})
if !ok || len(dataRaw) == 0 {
return raw
}
data := toMapOfStrings(dataRaw)
lines := strings.Split(raw, "\n")
outLines := make([]string, len(lines))
for i, line := range lines {
trimmed := strings.TrimSpace(line)
// If line is part of secret.data, sanitize line by replacing the value part
if key, matches := matchKeyValPair(data, trimmed); matches {
sanitizedLine := strings.Replace(line, trimmed, formatHiddenValue(key), 1)
outLines[i] = sanitizedLine
continue
}
outLines[i] = line
}
return strings.Join(outLines, "\n")
}
func toMapOfStrings(rawMap map[interface{}]interface{}) map[string]string {
stringsMap := make(map[string]string, len(rawMap))
for k, v := range rawMap {
key, ok := k.(string)
if !ok {
continue
}
val, ok := v.(string)
if !ok {
continue
}
stringsMap[key] = val
}
return stringsMap
}
// matchKeyValPair checks if data contains joined key value pair in format
// `key: value` equal to specified string.
// Returns key with which string matched and indicator if it matched any.
func matchKeyValPair(data map[string]string, str string) (string, bool) {
for k, v := range data {
joined := joinKeyVal(k, v)
if joined == str {
return k, true
}
}
return "", false
}
func joinKeyVal(key, val string) string {
return fmt.Sprintf("%s: %s", key, val)
}
func formatHiddenValue(key string) string {
return joinKeyVal(key, hiddenSecretValue)
}

@ -0,0 +1,50 @@
/*
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 sanitize
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHideSecrets(t *testing.T) {
t.Run("hide secret values", func(t *testing.T) {
manifestRaw, err := ioutil.ReadFile("testdata/manifest-input.yaml")
require.NoError(t, err)
expectedManifestRaw, err := ioutil.ReadFile("testdata/manifest-sanitized.yaml")
require.NoError(t, err)
sanitizedManifest, err := HideSecrets(string(manifestRaw))
require.NoError(t, err)
assert.Equal(t, string(expectedManifestRaw), sanitizedManifest)
})
t.Run("do not modify, when no secret values", func(t *testing.T) {
manifestRaw, err := ioutil.ReadFile("testdata/manifest-no-secret.yaml")
require.NoError(t, err)
sanitizedManifest, err := HideSecrets(string(manifestRaw))
require.NoError(t, err)
assert.Equal(t, string(manifestRaw), sanitizedManifest)
})
}

@ -0,0 +1,63 @@
---
# Source: test/templates/sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: build-robot
---
# Source: test/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: secret-sample
data:
test: YmFyCg==
password: bXktcGFzc3dvcmQ=
---
# Source: test/templates/cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: 2016-02-18T18:52:05Z
name: game-config
namespace: default
resourceVersion: "516"
uid: b4952dc3-d670-11e5-8cd0-68f728db1985
data:
test: YmFyCg==
game.properties: |
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
ui.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
---
# Source: test/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

@ -0,0 +1,40 @@
---
# Source: test/templates/sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: build-robot
---
# Source: test/templates/secret.yaml
apiVersion: my.custom.secret.com/v1
kind: Secret
metadata:
name: secret-sample
data:
test: YmFyCg==
password: bXktcGFzc3dvcmQ=
---
# Source: test/templates/cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: 2016-02-18T18:52:05Z
name: game-config
namespace: default
resourceVersion: "516"
uid: b4952dc3-d670-11e5-8cd0-68f728db1985
data:
test: YmFyCg==
game.properties: |
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
ui.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice

@ -0,0 +1,63 @@
---
# Source: test/templates/sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: build-robot
---
# Source: test/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: secret-sample
data:
test: [HIDDEN]
password: [HIDDEN]
---
# Source: test/templates/cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: 2016-02-18T18:52:05Z
name: game-config
namespace: default
resourceVersion: "516"
uid: b4952dc3-d670-11e5-8cd0-68f728db1985
data:
test: YmFyCg==
game.properties: |
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
ui.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
---
# Source: test/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
Loading…
Cancel
Save