Skip to content

Commit 179d6f9

Browse files
committed
protosanitizer: log messages without secrets
When running at glog level >= 5, CSI sidecar apps log the full CreateVolumeRequest, including the secrets. Secrets should never be logged at any level to avoid accidentally exposing them. We need to filter out the secrets. With older CSI versions, that could have been done based on the field name, which is still an option should this get backported. With CSI 1.0, a custom field option marks fields as secret. Using that option has the advantage that the code will continue to work also when new secret fields get added in the future. For the sake of simplicity, JSON is now used as representation of the string instead of the former compact text format from gRPC. That makes it possible to strip values from a map with generic types, instead of having to copy and manipulate the real generated structures. Another option would have been to copy https://github.com/golang/protobuf/blob/master/proto/text.go and modify it so that it skips secret fields, but that's over 800 lines of code. This version of the code is identical to the one reviewed in kubernetes-csi/external-provisioner#171 with the backwards parameters in assert.NotContains (kubernetes-csi/external-provisioner#171 (review)) fixed.
1 parent a761933 commit 179d6f9

File tree

542 files changed

+391349
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

542 files changed

+391349
-0
lines changed

Gopkg.lock

Lines changed: 154 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Gopkg.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
2+
# for detailed Gopkg.toml documentation.
3+
4+
[prune]
5+
go-tests = true
6+
unused-packages = true

protosanitizer/protosanitizer.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package protosanitizer supports logging of gRPC messages without
18+
// accidentally revealing sensitive fields.
19+
package protosanitizer
20+
21+
import (
22+
"encoding/json"
23+
"fmt"
24+
"reflect"
25+
"strings"
26+
27+
"github.com/container-storage-interface/spec/lib/go/csi"
28+
"github.com/golang/protobuf/descriptor"
29+
"github.com/golang/protobuf/proto"
30+
protobuf "github.com/golang/protobuf/protoc-gen-go/descriptor"
31+
)
32+
33+
// StripSecrets returns a wrapper around the original CSI gRPC message
34+
// which has a Stringer implementation that serializes the message
35+
// as one-line JSON, but without including secret information.
36+
// Instead of the secret value(s), the string "***stripped***" is
37+
// included in the result.
38+
//
39+
// StripSecrets itself is fast and therefore it is cheap to pass the
40+
// result to logging functions which may or may not end up serializing
41+
// the parameter depending on the current log level.
42+
func StripSecrets(msg interface{}) fmt.Stringer {
43+
return &stripSecrets{msg}
44+
}
45+
46+
type stripSecrets struct {
47+
msg interface{}
48+
}
49+
50+
func (s *stripSecrets) String() string {
51+
// First convert to a generic representation. That's less efficient
52+
// than using reflect directly, but easier to work with.
53+
var parsed interface{}
54+
b, err := json.Marshal(s.msg)
55+
if err != nil {
56+
return fmt.Sprintf("<<json.Marshal %T: %s>>", s.msg, err)
57+
}
58+
if err := json.Unmarshal(b, &parsed); err != nil {
59+
return fmt.Sprintf("<<json.Unmarshal %T: %s>>", s.msg, err)
60+
}
61+
62+
// Now remove secrets from the generic representation of the message.
63+
strip(parsed, s.msg)
64+
65+
// Re-encoded the stripped representation and return that.
66+
b, err = json.Marshal(parsed)
67+
if err != nil {
68+
return fmt.Sprintf("<<json.Marshal %T: %s>>", s.msg, err)
69+
}
70+
return string(b)
71+
}
72+
73+
func strip(parsed interface{}, msg interface{}) {
74+
protobufMsg, ok := msg.(descriptor.Message)
75+
if !ok {
76+
// Not a protobuf message, so we are done.
77+
return
78+
}
79+
80+
// The corresponding map in the parsed JSON representation.
81+
parsedFields, ok := parsed.(map[string]interface{})
82+
if !ok {
83+
// Probably nil.
84+
return
85+
}
86+
87+
// Walk through all fields and replace those with ***stripped*** that
88+
// are marked as secret. This relies on protobuf adding "json:" tags
89+
// on each field where the name matches the field name in the protobuf
90+
// spec (like volume_capabilities). The field.GetJsonName() method returns
91+
// a different name (volumeCapabilities) which we don't use.
92+
_, md := descriptor.ForMessage(protobufMsg)
93+
fields := md.GetField()
94+
if fields != nil {
95+
for _, field := range fields {
96+
ex, err := proto.GetExtension(field.Options, csi.E_CsiSecret)
97+
if err == nil && ex != nil && *ex.(*bool) {
98+
parsedFields[field.GetName()] = "***stripped***"
99+
} else if field.GetType() == protobuf.FieldDescriptorProto_TYPE_MESSAGE {
100+
// When we get here,
101+
// the type name is something like ".csi.v1.CapacityRange" (leading dot!)
102+
// and looking up "csi.v1.CapacityRange"
103+
// returns the type of a pointer to a pointer
104+
// to CapacityRange. We need a pointer to such
105+
// a value for recursive stripping.
106+
typeName := field.GetTypeName()
107+
if strings.HasPrefix(typeName, ".") {
108+
typeName = typeName[1:]
109+
}
110+
t := proto.MessageType(typeName)
111+
if t == nil || t.Kind() != reflect.Ptr {
112+
// Shouldn't happen, but
113+
// better check anyway instead
114+
// of panicking.
115+
continue
116+
}
117+
v := reflect.New(t.Elem())
118+
119+
// Recursively strip the message(s) that
120+
// the field contains.
121+
i := v.Interface()
122+
entry := parsedFields[field.GetName()]
123+
if slice, ok := entry.([]interface{}); ok {
124+
// Array of values, like VolumeCapabilities in CreateVolumeRequest.
125+
for _, entry := range slice {
126+
strip(entry, i)
127+
}
128+
} else {
129+
// Single value.
130+
strip(entry, i)
131+
}
132+
}
133+
}
134+
}
135+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package protosanitizer
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"github.com/container-storage-interface/spec/lib/go/csi"
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestStripSecrets(t *testing.T) {
28+
secretName := "secret-abc"
29+
secretValue := "123"
30+
createVolume := &csi.CreateVolumeRequest{
31+
Name: "foo",
32+
VolumeCapabilities: []*csi.VolumeCapability{
33+
&csi.VolumeCapability{
34+
AccessType: &csi.VolumeCapability_Mount{
35+
Mount: &csi.VolumeCapability_MountVolume{
36+
FsType: "ext4",
37+
},
38+
},
39+
},
40+
},
41+
CapacityRange: &csi.CapacityRange{
42+
RequiredBytes: 1024,
43+
},
44+
Secrets: map[string]string{
45+
secretName: secretValue,
46+
"secret-xyz": "987",
47+
},
48+
}
49+
50+
cases := []struct {
51+
original, stripped interface{}
52+
}{
53+
{nil, "null"},
54+
{1, "1"},
55+
{"hello world", `"hello world"`},
56+
{true, "true"},
57+
{false, "false"},
58+
{createVolume, `{"capacity_range":{"required_bytes":1024},"name":"foo","secrets":"***stripped***","volume_capabilities":[{"AccessType":{"Mount":{"fs_type":"ext4"}}}]}`},
59+
60+
// There is currently no test case that can verify
61+
// that recursive stripping works, because there is no
62+
// message where that is necessary. The code
63+
// nevertheless implements it and it has been verified
64+
// manually that it recurses properly for single and
65+
// repeated values. One-of might require further work.
66+
}
67+
68+
for _, c := range cases {
69+
before := fmt.Sprint(c.original)
70+
stripped := StripSecrets(c.original)
71+
if assert.Equal(t, c.stripped, fmt.Sprintf("%s", stripped), "unexpected result for fmt s of %s", c.original) {
72+
assert.Equal(t, c.stripped, fmt.Sprintf("%v", stripped), "unexpected result for fmt v of %s", c.original)
73+
}
74+
assert.Equal(t, before, fmt.Sprint(c.original), "original value modified")
75+
}
76+
77+
// The secret is hidden because StripSecrets is a struct referencing it.
78+
dump := fmt.Sprintf("%#v", StripSecrets(createVolume))
79+
assert.NotContains(t, dump, secretName)
80+
assert.NotContains(t, dump, secretValue)
81+
}

0 commit comments

Comments
 (0)