Implement fmt.Stringer on rest.Config to sanitize sensitive fields

It's very easy to add glog.Info(config) calls for debugging (or actual
logging). In some scenarios those configs will carry sensitive tokens
and those tokens will end up in logs or response bodies.
Leaking of those stringified configs compromises the cluster.

Also implement fmt.GoStringer.

Kubernetes-commit: c9ad1d7339b164dfba0846ec49fa4a52474d3e23
This commit is contained in:
Andrew Lytvynov 2018-11-02 11:39:14 -07:00 committed by Kubernetes Publisher
parent b831b8de71
commit 420f3f77fb
4 changed files with 262 additions and 1 deletions

View File

@ -31,6 +31,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/davecgh/go-spew/spew"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -73,8 +74,10 @@ func newCache() *cache {
return &cache{m: make(map[string]*Authenticator)} return &cache{m: make(map[string]*Authenticator)}
} }
var spewConfig = &spew.ConfigState{DisableMethods: true, Indent: " "}
func cacheKey(c *api.ExecConfig) string { func cacheKey(c *api.ExecConfig) string {
return fmt.Sprintf("%#v", c) return spewConfig.Sprint(c)
} }
type cache struct { type cache struct {

View File

@ -129,6 +129,47 @@ type Config struct {
// Version string // Version string
} }
var _ fmt.Stringer = new(Config)
var _ fmt.GoStringer = new(Config)
type sanitizedConfig *Config
type sanitizedAuthConfigPersister struct{ AuthProviderConfigPersister }
func (sanitizedAuthConfigPersister) GoString() string {
return "rest.AuthProviderConfigPersister(--- REDACTED ---)"
}
func (sanitizedAuthConfigPersister) String() string {
return "rest.AuthProviderConfigPersister(--- REDACTED ---)"
}
// GoString implements fmt.GoStringer and sanitizes sensitive fields of Config
// to prevent accidental leaking via logs.
func (c *Config) GoString() string {
return c.String()
}
// String implements fmt.Stringer and sanitizes sensitive fields of Config to
// prevent accidental leaking via logs.
func (c *Config) String() string {
if c == nil {
return "<nil>"
}
cc := sanitizedConfig(CopyConfig(c))
// Explicitly mark non-empty credential fields as redacted.
if cc.Password != "" {
cc.Password = "--- REDACTED ---"
}
if cc.BearerToken != "" {
cc.BearerToken = "--- REDACTED ---"
}
if cc.AuthConfigPersister != nil {
cc.AuthConfigPersister = sanitizedAuthConfigPersister{cc.AuthConfigPersister}
}
return fmt.Sprintf("%#v", cc)
}
// ImpersonationConfig has all the available impersonation options // ImpersonationConfig has all the available impersonation options
type ImpersonationConfig struct { type ImpersonationConfig struct {
// UserName is the username to impersonate on each request. // UserName is the username to impersonate on each request.
@ -168,6 +209,40 @@ type TLSClientConfig struct {
CAData []byte CAData []byte
} }
var _ fmt.Stringer = TLSClientConfig{}
var _ fmt.GoStringer = TLSClientConfig{}
type sanitizedTLSClientConfig TLSClientConfig
// GoString implements fmt.GoStringer and sanitizes sensitive fields of
// TLSClientConfig to prevent accidental leaking via logs.
func (c TLSClientConfig) GoString() string {
return c.String()
}
// String implements fmt.Stringer and sanitizes sensitive fields of
// TLSClientConfig to prevent accidental leaking via logs.
func (c TLSClientConfig) String() string {
cc := sanitizedTLSClientConfig{
Insecure: c.Insecure,
ServerName: c.ServerName,
CertFile: c.CertFile,
KeyFile: c.KeyFile,
CAFile: c.CAFile,
CertData: c.CertData,
KeyData: c.KeyData,
CAData: c.CAData,
}
// Explicitly mark non-empty credential fields as redacted.
if len(cc.CertData) != 0 {
cc.CertData = []byte("--- TRUNCATED ---")
}
if len(cc.KeyData) != 0 {
cc.KeyData = []byte("--- REDACTED ---")
}
return fmt.Sprintf("%#v", cc)
}
type ContentConfig struct { type ContentConfig struct {
// AcceptContentTypes specifies the types the client will accept and is optional. // AcceptContentTypes specifies the types the client will accept and is optional.
// If not set, ContentType will be used to define the Accept header // If not set, ContentType will be used to define the Accept header

View File

@ -19,6 +19,7 @@ package rest
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -26,6 +27,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -381,3 +383,140 @@ func TestCopyConfig(t *testing.T) {
} }
} }
} }
func TestConfigStringer(t *testing.T) {
formatBytes := func(b []byte) string {
// %#v for []byte always pre-pends "[]byte{".
// %#v for struct with []byte field always pre-pends "[]uint8{".
return strings.Replace(fmt.Sprintf("%#v", b), "byte", "uint8", 1)
}
tests := []struct {
desc string
c *Config
expectContent []string
prohibitContent []string
}{
{
desc: "nil config",
c: nil,
expectContent: []string{"<nil>"},
},
{
desc: "non-sensitive config",
c: &Config{
Host: "localhost:8080",
APIPath: "v1",
UserAgent: "gobot",
},
expectContent: []string{"localhost:8080", "v1", "gobot"},
},
{
desc: "sensitive config",
c: &Config{
Host: "localhost:8080",
Username: "gopher",
Password: "g0ph3r",
BearerToken: "1234567890",
TLSClientConfig: TLSClientConfig{
CertFile: "a.crt",
KeyFile: "a.key",
CertData: []byte("fake cert"),
KeyData: []byte("fake key"),
},
AuthProvider: &clientcmdapi.AuthProviderConfig{
Config: map[string]string{"secret": "s3cr3t"},
},
ExecProvider: &clientcmdapi.ExecConfig{
Args: []string{"secret"},
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
},
},
expectContent: []string{
"localhost:8080",
"gopher",
"a.crt",
"a.key",
"--- REDACTED ---",
formatBytes([]byte("--- REDACTED ---")),
formatBytes([]byte("--- TRUNCATED ---")),
},
prohibitContent: []string{
"g0ph3r",
"1234567890",
formatBytes([]byte("fake cert")),
formatBytes([]byte("fake key")),
"secret",
"s3cr3t",
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := tt.c.String()
t.Logf("formatted config: %q", got)
for _, expect := range tt.expectContent {
if !strings.Contains(got, expect) {
t.Errorf("missing expected string %q", expect)
}
}
for _, prohibit := range tt.prohibitContent {
if strings.Contains(got, prohibit) {
t.Errorf("found prohibited string %q", prohibit)
}
}
})
}
}
func TestConfigSprint(t *testing.T) {
c := &Config{
Host: "localhost:8080",
APIPath: "v1",
ContentConfig: ContentConfig{
AcceptContentTypes: "application/json",
ContentType: "application/json",
},
Username: "gopher",
Password: "g0ph3r",
BearerToken: "1234567890",
Impersonate: ImpersonationConfig{
UserName: "gopher2",
},
AuthProvider: &clientcmdapi.AuthProviderConfig{
Name: "gopher",
Config: map[string]string{"secret": "s3cr3t"},
},
AuthConfigPersister: fakeAuthProviderConfigPersister{},
ExecProvider: &clientcmdapi.ExecConfig{
Command: "sudo",
Args: []string{"secret"},
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
},
TLSClientConfig: TLSClientConfig{
CertFile: "a.crt",
KeyFile: "a.key",
CertData: []byte("fake cert"),
KeyData: []byte("fake key"),
},
UserAgent: "gobot",
Transport: &fakeRoundTripper{},
WrapTransport: fakeWrapperFunc,
QPS: 1,
Burst: 2,
RateLimiter: &fakeLimiter{},
Timeout: 3 * time.Second,
Dial: fakeDialFunc,
}
want := fmt.Sprintf(
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil)}, UserAgent:"gobot", Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p)}`,
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc,
)
for _, f := range []string{"%s", "%v", "%+v", "%#v"} {
if got := fmt.Sprintf(f, c); want != got {
t.Errorf("fmt.Sprintf(%q, c)\ngot: %q\nwant: %q", f, got, want)
}
}
}

View File

@ -17,6 +17,8 @@ limitations under the License.
package api package api
import ( import (
"fmt"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@ -150,6 +152,25 @@ type AuthProviderConfig struct {
Config map[string]string `json:"config,omitempty"` Config map[string]string `json:"config,omitempty"`
} }
var _ fmt.Stringer = new(AuthProviderConfig)
var _ fmt.GoStringer = new(AuthProviderConfig)
// GoString implements fmt.GoStringer and sanitizes sensitive fields of
// AuthProviderConfig to prevent accidental leaking via logs.
func (c AuthProviderConfig) GoString() string {
return c.String()
}
// String implements fmt.Stringer and sanitizes sensitive fields of
// AuthProviderConfig to prevent accidental leaking via logs.
func (c AuthProviderConfig) String() string {
cfg := "<nil>"
if c.Config != nil {
cfg = "--- REDACTED ---"
}
return fmt.Sprintf("api.AuthProviderConfig{Name: %q, Config: map[string]string{%s}}", c.Name, cfg)
}
// ExecConfig specifies a command to provide client credentials. The command is exec'd // ExecConfig specifies a command to provide client credentials. The command is exec'd
// and outputs structured stdout holding credentials. // and outputs structured stdout holding credentials.
// //
@ -172,6 +193,29 @@ type ExecConfig struct {
APIVersion string `json:"apiVersion,omitempty"` APIVersion string `json:"apiVersion,omitempty"`
} }
var _ fmt.Stringer = new(ExecConfig)
var _ fmt.GoStringer = new(ExecConfig)
// GoString implements fmt.GoStringer and sanitizes sensitive fields of
// ExecConfig to prevent accidental leaking via logs.
func (c ExecConfig) GoString() string {
return c.String()
}
// String implements fmt.Stringer and sanitizes sensitive fields of ExecConfig
// to prevent accidental leaking via logs.
func (c ExecConfig) String() string {
var args []string
if len(c.Args) > 0 {
args = []string{"--- REDACTED ---"}
}
env := "[]ExecEnvVar(nil)"
if len(c.Env) > 0 {
env = "[]ExecEnvVar{--- REDACTED ---}"
}
return fmt.Sprintf("api.AuthProviderConfig{Command: %q, Args: %#v, Env: %s, APIVersion: %q}", c.Command, args, env, c.APIVersion)
}
// ExecEnvVar is used for setting environment variables when executing an exec-based // ExecEnvVar is used for setting environment variables when executing an exec-based
// credential plugin. // credential plugin.
type ExecEnvVar struct { type ExecEnvVar struct {