diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers.go index 1b4fefdb443..dd5f9180673 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers.go @@ -23,6 +23,8 @@ import ( "os" "path" "path/filepath" + "reflect" + "strings" ) func init() { @@ -81,21 +83,21 @@ func MinifyConfig(config *Config) error { } var ( - redactedBytes []byte dataOmittedBytes []byte + redactedBytes []byte ) -// Flatten redacts raw data entries from the config object for a human-readable view. +// ShortenConfig redacts raw data entries from the config object for a human-readable view. func ShortenConfig(config *Config) { - // trick json encoder into printing a human readable string in the raw data + // trick json encoder into printing a human-readable string in the raw data // by base64 decoding what we want to print. Relies on implementation of // http://golang.org/pkg/encoding/json/#Marshal using base64 to encode []byte for key, authInfo := range config.AuthInfos { if len(authInfo.ClientKeyData) > 0 { - authInfo.ClientKeyData = redactedBytes + authInfo.ClientKeyData = dataOmittedBytes } if len(authInfo.ClientCertificateData) > 0 { - authInfo.ClientCertificateData = redactedBytes + authInfo.ClientCertificateData = dataOmittedBytes } if len(authInfo.Token) > 0 { authInfo.Token = "REDACTED" @@ -110,7 +112,7 @@ func ShortenConfig(config *Config) { } } -// Flatten changes the config object into a self contained config (useful for making secrets) +// FlattenConfig changes the config object into a self-contained config (useful for making secrets) func FlattenConfig(config *Config) error { for key, authInfo := range config.AuthInfos { baseDir, err := MakeAbs(path.Dir(authInfo.LocationOfOrigin), "") @@ -188,3 +190,77 @@ func MakeAbs(path, base string) (string, error) { } return filepath.Join(base, path), nil } + +// RedactSecrets replaces any sensitive values with REDACTED +func RedactSecrets(config *Config) error { + return redactSecrets(reflect.ValueOf(config), false) +} + +func redactSecrets(curr reflect.Value, redact bool) error { + redactedBytes = []byte("REDACTED") + if !curr.IsValid() { + return nil + } + + actualCurrValue := curr + if curr.Kind() == reflect.Ptr { + actualCurrValue = curr.Elem() + } + + switch actualCurrValue.Kind() { + case reflect.Map: + for _, v := range actualCurrValue.MapKeys() { + err := redactSecrets(actualCurrValue.MapIndex(v), false) + if err != nil { + return err + } + } + return nil + + case reflect.String: + if redact { + if !actualCurrValue.IsZero() { + actualCurrValue.SetString("REDACTED") + } + } + return nil + + case reflect.Slice: + if actualCurrValue.Type() == reflect.TypeOf([]byte{}) && redact { + if !actualCurrValue.IsNil() { + actualCurrValue.SetBytes(redactedBytes) + } + return nil + } + for i := 0; i < actualCurrValue.Len(); i++ { + err := redactSecrets(actualCurrValue.Index(i), false) + if err != nil { + return err + } + } + return nil + + case reflect.Struct: + for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { + currFieldValue := actualCurrValue.Field(fieldIndex) + currFieldType := actualCurrValue.Type().Field(fieldIndex) + currYamlTag := currFieldType.Tag.Get("datapolicy") + currFieldTypeYamlName := strings.Split(currYamlTag, ",")[0] + if currFieldTypeYamlName != "" { + err := redactSecrets(currFieldValue, true) + if err != nil { + return err + } + } else { + err := redactSecrets(currFieldValue, false) + if err != nil { + return err + } + } + } + return nil + + default: + return nil + } +} diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers_test.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers_test.go index 12aab343d8a..169191f1cec 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers_test.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/helpers_test.go @@ -17,6 +17,7 @@ limitations under the License. package api import ( + "bytes" "fmt" "os" "reflect" @@ -240,8 +241,8 @@ func Example_minifyAndShorten() { // users: // red-user: // LocationOfOrigin: "" - // client-certificate-data: REDACTED - // client-key-data: REDACTED + // client-certificate-data: DATA+OMITTED + // client-key-data: DATA+OMITTED // token: REDACTED } @@ -274,7 +275,6 @@ func TestShortenSuccess(t *testing.T) { t.Errorf("expected %v, got %v", startingConfig.Contexts, mutatingConfig.Contexts) } - redacted := string(redactedBytes) dataOmitted := string(dataOmittedBytes) if len(mutatingConfig.Clusters) != 2 { t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters) @@ -292,13 +292,65 @@ func TestShortenSuccess(t *testing.T) { if !reflect.DeepEqual(startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) { t.Errorf("expected %v, got %v", startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) } - if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData) != redacted { - t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData)) + if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData) != dataOmitted { + t.Errorf("expected %v, got %v", dataOmitted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData)) } - if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) != redacted { - t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData)) + if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) != dataOmitted { + t.Errorf("expected %v, got %v", dataOmitted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData)) + } + if mutatingConfig.AuthInfos[changingAuthInfo].Token != "REDACTED" { + t.Errorf("expected REDACTED, got %q", mutatingConfig.AuthInfos[changingAuthInfo].Token) + } +} + +func TestRedactSecrets(t *testing.T) { + certFile, _ := os.CreateTemp("", "") + defer os.Remove(certFile.Name()) + keyFile, _ := os.CreateTemp("", "") + defer os.Remove(keyFile.Name()) + caFile, _ := os.CreateTemp("", "") + defer os.Remove(caFile.Name()) + + certData := "cert" + keyData := "key" + caData := "ca" + + unchangingCluster := "chicken-cluster" + unchangingAuthInfo := "blue-user" + changingAuthInfo := "red-user" + + startingConfig := newMergedConfig(certFile.Name(), certData, keyFile.Name(), keyData, caFile.Name(), caData, t) + mutatingConfig := startingConfig + + err := RedactSecrets(&mutatingConfig) + if err != nil { + t.Errorf("unexpected error redacting secrets:\n%v", err) + } + + if len(mutatingConfig.Contexts) != 2 { + t.Errorf("unexpected contexts: %v", mutatingConfig.Contexts) + } + if !reflect.DeepEqual(startingConfig.Contexts, mutatingConfig.Contexts) { + t.Errorf("expected %v, got %v", startingConfig.Contexts, mutatingConfig.Contexts) + } + + if len(mutatingConfig.Clusters) != 2 { + t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters) + } + if !reflect.DeepEqual(startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster]) { + t.Errorf("expected %v, got %v", startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster]) + } + + if len(mutatingConfig.AuthInfos) != 2 { + t.Errorf("unexpected users: %v", mutatingConfig.AuthInfos) + } + if !reflect.DeepEqual(startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) { + t.Errorf("expected %v, got %v", startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) } if mutatingConfig.AuthInfos[changingAuthInfo].Token != "REDACTED" { t.Errorf("expected REDACTED, got %v", mutatingConfig.AuthInfos[changingAuthInfo].Token) } + if !bytes.Equal(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData, []byte("REDACTED")) { + t.Errorf("expected REDACTED, got %s", mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) + } } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/view.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/view.go index 6d4c245207e..9f3392626c5 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/config/view.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/view.go @@ -18,9 +18,7 @@ package config import ( "errors" - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/tools/clientcmd" @@ -60,7 +58,7 @@ var ( # Show merged kubeconfig settings kubectl config view - # Show merged kubeconfig settings and raw certificate data + # Show merged kubeconfig settings and raw certificate data and exposed secrets kubectl config view --raw # Get the password for the e2e user @@ -93,7 +91,7 @@ func NewCmdConfigView(streams genericclioptions.IOStreams, ConfigAccess clientcm o.Merge.Default(true) mergeFlag := cmd.Flags().VarPF(&o.Merge, "merge", "", "Merge the full hierarchy of kubeconfig files") mergeFlag.NoOptDefVal = "true" - cmd.Flags().BoolVar(&o.RawByteData, "raw", o.RawByteData, "Display raw byte data") + cmd.Flags().BoolVar(&o.RawByteData, "raw", o.RawByteData, "Display raw byte data and sensitive data") cmd.Flags().BoolVar(&o.Flatten, "flatten", o.Flatten, "Flatten the resulting kubeconfig file into self-contained output (useful for creating portable kubeconfig files)") cmd.Flags().BoolVar(&o.Minify, "minify", o.Minify, "Remove all information not used by current-context from the output") return cmd @@ -150,6 +148,9 @@ func (o ViewOptions) Run() error { return err } } else if !o.RawByteData { + if err := clientcmdapi.RedactSecrets(config); err != nil { + return err + } clientcmdapi.ShortenConfig(config) } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/view_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/view_test.go index bce7768e507..de860a4f611 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/config/view_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/view_test.go @@ -47,8 +47,18 @@ func TestViewCluster(t *testing.T) { }, CurrentContext: "minikube", AuthInfos: map[string]*clientcmdapi.AuthInfo{ - "minikube": {Token: "REDACTED"}, - "mu-cluster": {Token: "REDACTED"}, + "minikube": { + ClientKeyData: []byte("notredacted"), + Token: "notredacted", + Username: "foo", + Password: "notredacted", + }, + "mu-cluster": { + ClientKeyData: []byte("notredacted"), + Token: "notredacted", + Username: "bar", + Password: "notredacted", + }, }, } @@ -78,16 +88,113 @@ preferences: {} users: - name: minikube user: + client-key-data: DATA+OMITTED + password: REDACTED token: REDACTED + username: foo - name: mu-cluster user: - token: REDACTED` + "\n", + client-key-data: DATA+OMITTED + password: REDACTED + token: REDACTED + username: bar` + "\n", } test.run(t) } +func TestViewClusterUnredacted(t *testing.T) { + conf := clientcmdapi.Config{ + Kind: "Config", + APIVersion: "v1", + Clusters: map[string]*clientcmdapi.Cluster{ + "minikube": {Server: "https://192.168.99.100:8443"}, + "my-cluster": {Server: "https://192.168.0.1:3434"}, + }, + Contexts: map[string]*clientcmdapi.Context{ + "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, + "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, + }, + CurrentContext: "minikube", + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "minikube": { + ClientKeyData: []byte("notredacted"), + ClientCertificateData: []byte("plaintext"), + Token: "notredacted", + Username: "foo", + Password: "notredacted", + }, + "mu-cluster": { + ClientKeyData: []byte("notredacted"), + ClientCertificateData: []byte("plaintext"), + Token: "notredacted", + Username: "bar", + Password: "notredacted", + }, + }, + } + + testCases := []struct { + description string + config clientcmdapi.Config + flags []string + expected string + }{ + { + description: "Testing for kubectl config view --raw=true", + config: conf, + flags: []string{"--raw=true"}, + expected: `apiVersion: v1 +clusters: +- cluster: + server: https://192.168.99.100:8443 + name: minikube +- cluster: + server: https://192.168.0.1:3434 + name: my-cluster +contexts: +- context: + cluster: minikube + user: minikube + name: minikube +- context: + cluster: my-cluster + user: mu-cluster + name: my-cluster +current-context: minikube +kind: Config +preferences: {} +users: +- name: minikube + user: + client-certificate-data: cGxhaW50ZXh0 + client-key-data: bm90cmVkYWN0ZWQ= + password: notredacted + token: notredacted + username: foo +- name: mu-cluster + user: + client-certificate-data: cGxhaW50ZXh0 + client-key-data: bm90cmVkYWN0ZWQ= + password: notredacted + token: notredacted + username: bar` + "\n", + }, + } + + for _, test := range testCases { + cmdTest := viewClusterTest{ + description: test.description, + config: test.config, + flags: test.flags, + expected: test.expected, + } + cmdTest.run(t) + } + +} + func TestViewClusterMinify(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", @@ -102,8 +209,18 @@ func TestViewClusterMinify(t *testing.T) { }, CurrentContext: "minikube", AuthInfos: map[string]*clientcmdapi.AuthInfo{ - "minikube": {Token: "REDACTED"}, - "mu-cluster": {Token: "REDACTED"}, + "minikube": { + ClientKeyData: []byte("notredacted"), + Token: "notredacted", + Username: "foo", + Password: "notredacted", + }, + "mu-cluster": { + ClientKeyData: []byte("notredacted"), + Token: "notredacted", + Username: "bar", + Password: "notredacted", + }, }, } @@ -133,7 +250,10 @@ preferences: {} users: - name: minikube user: - token: REDACTED` + "\n", + client-key-data: DATA+OMITTED + password: REDACTED + token: REDACTED + username: foo` + "\n", }, { description: "Testing for kubectl config view --minify=true --context=my-cluster", @@ -155,7 +275,10 @@ preferences: {} users: - name: mu-cluster user: - token: REDACTED` + "\n", + client-key-data: DATA+OMITTED + password: REDACTED + token: REDACTED + username: bar` + "\n", }, }