-
Notifications
You must be signed in to change notification settings - Fork 337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
never log secrets #171
never log secrets #171
Conversation
When running at glog level >= 5, external-provisioner logged 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. Ultimately this new package should live in a "csi-common" repo and also include other utility code, like logGRPC itself. Fixes: kubernetes-csi#82, kubernetes-csi#167
/assign @saad-ali |
@pohly is it really required to go through marshal/unmarshal? Would it not be sufficient just to strip secret and convert it into a string for printing it in the log? |
Serguei Bezverkhi <[email protected]> writes:
@pohly is it really required to go through marshal/unmarshal?
Are you worried about performance? The code doesn't run often.
Would it not be sufficient just to strip secret and convert it into a
string for printing it in the log?
You mean, converting the entire message to a string, then convert the
secret and do a string replacement of the secret string in the complete
string? That would work, but I am a bit worried that this might be too
aggressive (as in: replacing a harmless substring also elsewhere).
There's also going to be a problem with finding the instance of the
secret field in the original message - that code isn't going to be nice.
Also giving package `csigrpc` name imho
is a bit misleading, I think this function belongs more to utils/tools
package.
The long-term plan was to have it under
kubernetes-csi/csi-common/pkg/csigrpc, together with logGRPC. Does the
name make more sense from that perspective?
A package called "utils" or "tools" IMHO is too generic.
|
@@ -102,6 +102,7 @@ | |||
digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did Gopkg.toml also need to be modified?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It gets updated by "dep" when the set of input files changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we now have a conflict because of that. I can rebase if this approach is how we want to continue.
ex, err := proto.GetExtension(field.Options, csi.E_CsiSecret) | ||
if err == nil && ex != nil && *ex.(*bool) { | ||
parsedFields[field.GetName()] = "***stripped***" | ||
} else if field.GetType() == protobuf.FieldDescriptorProto_TYPE_MESSAGE { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if err != nil
do we still want to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't checked in detail which errors might be returned by proto.GetExtension. My assumption here is that we won't get an error for a field that has the csi_secret option set and that therefore getting an error indicates that it isn't a secret field. Under that assumption it is okay and actually desirable to continue filtering that field recursively despite an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is indeed normal to get errors. For the "name" field:
(dlv) p err
error(*errors.errorString) *{
s: "proto: nil *descriptor.FieldOptions is not extendable",}
(dlv) p field
*github.com/kubernetes-csi/external-provisioner/vendor/github.com/golang/protobuf/protoc-gen-go/descriptor.FieldDescriptorProto {
Name: *"name",
Number: *1,
...
For "name" we don't need to recurse, but the same error also occurs for "capacity_range", and in that case we must ignore the error and filter CapacityRange.
@pohly I would like to propose an alternative solution which I think is less complex and should be more performant. I hope you do not mind. |
Serguei Bezverkhi <[email protected]> writes:
@pohly I would like to propose an alternative solution which I think
is less complex and should be more performant. I hope you do not mind.
Of course not, please go ahead.
|
Serguei Bezverkhi <[email protected]> writes:
@pohly I would like to propose an alternative solution which I think
is less complex and should be more performant.
I assume you are still working on this? Do you have an estimate how long
it might take? Perhaps we can discuss this further in the CSI WG meeting
today?
|
Serguei Bezverkhi <[email protected]> writes:
@pohly it is ready for review, it is just a matter of creating repo
csi-utils as per @saad-ali suggestion.
Can you push it somewhere else in the meantime?
|
@pohly here you go. kubernetes/kubernetes#71336 |
Serguei Bezverkhi <[email protected]> writes:
@pohly here you
go. kubernetes/kubernetes#71336
You solution is less general than mine, for example it only works for
secret fields which are of type map[string]string and it doesn't recurse
into messages. But perhaps that's good enough.
What worries me more is that it also modifies the message in place,
doesn't it? If this was done in the logGRPC messages of the sidecar
containers, they would never pass the actual secrets to the underlying
driver.
Were you planning to do a deep copy of the message before passing it to
SanitizeMsg()? How? Any package for that then also becomes a dependency
for the sidecar containers.
The other question I have is about delayed conversion. In
glog.V(5).Infof("GRPC response: %s", SanitizeMsg(reply)), the conversion
to string always happens, whether it is needed or not. Would it be
worthwhile to delay the conversion to string until really needed?
|
Serguei Bezverkhi <[email protected]> writes:
@pohly it does not alter the original message.
It does:
```
50: for _, test := range tests {
51: result := SanitizeMsg(test.msg)
=> 52: if c := strings.Compare(test.expected, result); c != 0 {
53: t.Fatalf("Test %s failed, expected: %s got: %s, c: %d", test.name, test.expected, result, c)
54: }
55: }
56: }
(dlv) p test.msg
*k8s.io/kubernetes/vendor/github.com/container-storage-interface/spec/lib/go/csi.CreateVolumeRequest {
Name: "",
CapacityRange: *k8s.io/kubernetes/vendor/github.com/container-storage-interface/spec/lib/go/csi.CapacityRange nil,
VolumeCapabilities: []*k8s.io/kubernetes/vendor/github.com/container-storage-interface/spec/lib/go/csi.VolumeCapability len: 0, cap: 0, nil,
Parameters: map[string]string nil,
Secrets: map[string]string [
"secret1": "* * * Sanitized * * *",
"secret2": "* * * Sanitized * * *",
],
VolumeContentSource: *k8s.io/kubernetes/vendor/github.com/container-storage-interface/spec/lib/go/csi.VolumeContentSource nil,
AccessibilityRequirements: *k8s.io/kubernetes/vendor/github.com/container-storage-interface/spec/lib/go/csi.TopologyRequirement nil,
XXX_NoUnkeyedLiteral: struct {} {},
XXX_unrecognized: []uint8 len: 0, cap: 0, nil,
XXX_sizecache: 0,}
```
|
@@ -119,9 +120,9 @@ var ( | |||
//TODO consolidate ane librarize | |||
func logGRPC(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { | |||
glog.V(5).Infof("GRPC call: %s", method) | |||
glog.V(5).Infof("GRPC request: %+v", req) | |||
glog.V(5).Infof("GRPC request: %s", csigrpc.StripSecrets(req)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for performance:
if glog.V(5) {
glog.Infof("GRPC request: %s", csigrpc.StripSecrets(req))
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't necessary, csigrpc.StripSecrets was explicitly designed to not run the expensive string conversion at the time when Infof gets called.
"testing" | ||
|
||
"github.com/container-storage-interface/spec/lib/go/csi" | ||
"github.com/stretchr/testify/assert" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure it's worth adding new dependency for few asserts below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, for the external-attacher repo a simpler test would be better. But the code was meant to land in its own repo and now it looks like we'll do that right away (see kubernetes/org#268) and there I think it makes sense to use good testing infrastructure. The output of assert.Equal is much more readable than just printing the strings in manually-crafted failure messages.
// The secret is hidden because StripSecrets is a struct referencing it. | ||
dump := fmt.Sprintf("%#v", StripSecrets(createVolume)) | ||
assert.NotContains(t, secretName, dump) | ||
assert.NotContains(t, secretValue, dump) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these assertions are backwards... should be assert.NotContains(t, dump, secretName)
} | ||
|
||
// Walk through all fields and replace those with ***stripped*** that | ||
// are marked as secret. This relies on protobuf adding "json:" tags |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
relying on the json/proto correlation seems particularly fragile... is there a reason not to copy the proto message and walk it instead?
Jordan Liggitt <[email protected]> writes:
liggitt commented on this pull request.
> + // manually that it recurses properly for single and
+ // repeated values. One-of might require further work.
+ }
+
+ for _, c := range cases {
+ original := c.original
+ stripped := StripSecrets(original)
+ assert.Equal(t, c.stripped, fmt.Sprintf("%s", stripped), "unexpected result for fmt s")
+ assert.Equal(t, c.stripped, fmt.Sprintf("%v", stripped), "unexpected result for fmt v")
+ assert.Equal(t, original, c.original, "original value modified")
+ }
+
+ // The secret is hidden because StripSecrets is a struct referencing it.
+ dump := fmt.Sprintf("%#v", StripSecrets(createVolume))
+ assert.NotContains(t, secretName, dump)
+ assert.NotContains(t, secretValue, dump)
these assertions are backwards... should be `assert.NotContains(t,
dump, secretName)`
Well spotted.
FWIW, I have an updated version which also handles oneof and has tests
for various cases that don't occur in the current spec. But as we want
the code to land in a new repo (kubernetes-csi/csi-lib-utils), I haven't
pushed that code yet.
|
Jordan Liggitt <[email protected]> writes:
+ // Walk through all fields and replace those with ***stripped*** that
+ // are marked as secret. This relies on protobuf adding "json:" tags
relying on the json/proto correlation seems particularly fragile...
I consider it part of the stable protobuf API because changing that
could break code which uses marshalling to or from JSON. But yes,
documentation about it is a bit sparse.
is there a reason not to copy the proto message and walk it instead?
Then one has to implement the mapping from protobuf name to Golang
name. Doable, but more code.
Ultimately that approach leads to
https://github.com/golang/protobuf/blob/master/proto/text.go. If we want
to use the same approach, then we should either fork that code and add
field filtering to it or work with upstream gRPC to get such a feature
into the official gRPC TextMarshaler.
|
secretValue := "123" | ||
createVolume := &csi.CreateVolumeRequest{ | ||
Name: "foo", | ||
VolumeCapabilities: []*csi.VolumeCapability{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add topology into your unit test? It's one of the more complicated structures in the csi proto.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, see kubernetes-csi/csi-lib-utils@0f60cbc
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.
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.
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.
/hold kubernetes-csi/csi-lib-utils#1 needs to be merged first, then we can vendor that code into this PR. |
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.
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.
The code was moved to kubernetes-csi/csi-lib-utils#1 and later merged into external-provisioner in #179. |
When running at glog level >= 5, external-provisioner logged 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.
Ultimately this new package should live in a "csi-common" repo and
also include other utility code, like logGRPC itself.
Fixes: #82, #167