Skip to content
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

Pluggable secret backend #2239

Merged
merged 1 commit into from
Jul 14, 2017
Merged

Conversation

liron-l
Copy link
Contributor

@liron-l liron-l commented Jun 11, 2017

This commit extends SwarmKit secret management with pluggable secret
backends support. The solution uses the existing docker plugin
framework for loading plugins and the existing SwarmKit data backend for
storing them.

The approach is to add a new driver parameter to existing secrets,
which defines whether the values are taken as is or fetched from one of
the secret plugins. The loading of secrets is done using the standard
docker plugin infrastructure, which is already accessible in SwarmKit
and used in other flows (e.g., networking).
The fetched values are are stored as regular SwarmKit secrets.

Remarks:

  • I've added support for mocking the plugin subsystem when settings up
    the controlapi server.
    I preferred this approach over loading the full plugin subsystem in UT.

Work still needed in this CR:

  • More unit tests (pending initial iteration)
  • Customized error handling (e.g., customize error string for Not
    Found)

Work still needed to complete this feature:

  • Inject secrets as part of plugin initialization
  • CLI support in docker
  • Docs
  • Support scheduling plugins in swarm
    Plugins on swarm moby#33575

Signed-off-by: liron liron@twistlock.com

@liron-l liron-l force-pushed the pluggable_secret_backend branch 3 times, most recently from 23a2ee6 to f966946 Compare June 11, 2017 11:38
@codecov
Copy link

codecov bot commented Jun 11, 2017

Codecov Report

Merging #2239 into master will increase coverage by 0.03%.
The diff coverage is 81.25%.

@@            Coverage Diff            @@
##           master   #2239      +/-   ##
=========================================
+ Coverage   61.07%   61.1%   +0.03%     
=========================================
  Files         128     128              
  Lines       20556   20579      +23     
=========================================
+ Hits        12554   12575      +21     
+ Misses       6627    6619       -8     
- Partials     1375    1385      +10

api/specs.proto Outdated
@@ -386,6 +386,9 @@ message SecretSpec {

// Data is the secret payload - the maximum size is 500KB (that is, 500*1024 bytes)
bytes data = 2;

// Driver is the name of the secret driver that is used to store the specified secret
string driver = 3;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it makes sense to use the Driver type here, to allow the future possibility of passing secret-specific options to the driver.

const MaxSecretSize = 500 * 1024 // 500KB
const (
// SecretsPluginAPI is the endpoint for fetching secrets from plugins
SecretsPluginAPI = "/SecretsDriver.GetSecret"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecretProvider? Same with the capability name.

@@ -157,12 +169,16 @@ func (s *Server) ListSecrets(ctx context.Context, request *api.ListSecretsReques
// or if the secret data is too long or contains invalid characters.
// - Returns an error if the creation fails.
func (s *Server) CreateSecret(ctx context.Context, request *api.CreateSecretRequest) (*api.CreateSecretResponse, error) {
err := s.populateSecretFromPlugin(ctx, request.Spec)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative would be to put this resolution in the dispatcher, so the secret is fetched at the time the task is sent to the node where it will run. This is what I suggested earlier. The advantages would be:

  • swarmkit would not be modifying the secret Spec. Specs are meant to be under the control of the user, and so far swarmkit never changes them. We're considering signing specs with a user-controlled key in the future.
  • Calling UpdateSecret (for example, to change the secret's labels) would not have side effects on the payload, if it happened to change in the backend.
  • We would avoid storing a copy of the secret payload inside the Raft datastore, which people may not want to do for security reasons.

The disadvantages would be:

  • Inability to access the secrets backend could block deploying tasks, not just creating/updating secrets.
  • To avoid redundant queries to the backend for each of N tasks that reference a secret, it would make sense to have an in-memory cache of secret payloads with a time-to-live, but this would add complexity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@diogomonica @cpuguy83 what do you think?
@aaronlehmann i think that caching should be the plugin responsibility. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGMT @aaronlehmann

One way around issues is to have a separate call that the the manager runs here to ensure that the secret is accessible before sending it for dispatch. Doesn't need to store anything.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think we should create a driver interface that have implementations for the built-in store and plugins.
Then it's a simple d := GetDriver(spec.Driver); d.<method>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aaronlehmann, the assignment solution still means that we need to modify the secret spec payload inside Assignment_Secret before we send the assignment (that is, after the secret query in addTaskDependencies). Is this a valid approach?
I think your first concern might cause debugging issues with secret plugins, @diogomonica WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpuguy83 I've abstracted the plugin/driver initialization and setup with a new drivers package. This package can be used to load any type of plugin in Swarmkit. Let me know what you think.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aaronlehmann, the assignment solution still means that we need to modify the secret spec payload inside Assignment_Secret before we send the assignment (that is, after the secret query in addTaskDependencies). Is this a valid approach?

That's a fair point, however I think it's preferable to modify the spec for last-mile delivery versus storing a modified version in the data store. One possibility to avoid modifying the spec at all would be to introduce another Data field outside of Spec that could be freely modified by the manager. When the worker receives a secret, it would check both fields to see which one is populated. @diogomonica WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaronlehmann I like that approach. Would still allow us to sign the spec itself, and if the secret is external, that component would be unsigned.

@liron-l liron-l force-pushed the pluggable_secret_backend branch 13 times, most recently from 6d827fc to 1ac46c1 Compare June 18, 2017 13:03
@liron-l
Copy link
Contributor Author

liron-l commented Jun 18, 2017

@aaronlehmann @cpuguy83, @diogomonica I moved the secret resolution to the assignmentSet flow. Let me know if this makes sense and I will add dedicated UT.

}

// Get gets a secret from the secret provider
func (d *SecretDriver) Get(spec *api.SecretSpec) ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to provide an api.SecretSpec here? Would be great if the driver got information of which service is requesting this secret.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@diogomonica @cpuguy83 @aaronlehmann, I can think about two options to pass service parameters to plugin in a deterministic backward compatible way:

  1. Send the Task as binary blob inside the request
  2. Use the ServiceAnnotations
    Do you have other ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Riyaz and I came up with a list. For now let's not worry about passing more stuff and agree on the general API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks!


// populateSecretFromPlugin populates the secret value for the given specification using the secret plugin subsystem.
func (a *assignmentSet) populateSecretFromDriver(spec *api.SecretSpec) error {
if spec == nil || spec.Driver.Name == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use validateSecretSpec

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've added shared code to validate the secret payload, after the value is populated.

}

// SecretsProviderRequest is the request specification for retrieving secrets from plugins.
type SecretsProviderRequest struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if we have extra metadata on the caller of the request, so that external plugin can issue customized secrets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I passed the ServiceAnnotations, I hope this makes sense.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from discussion in slack: it might be more useful to pass the full ServiceSpec, though that would require converting into JSON

@diogomonica
Copy link
Contributor

Overall I think this is a good start. We need to figure out the best way of providing external metadata on the ultimate service this secret is being requested for, such that an external plugin can issue secrets for a specific service.

An example would be a TLS certificate that gets issued on-the-fly by the secrets plugin, and needs to include the service name/other metadata on the x509 certificate that depends on the service itself.

@liron-l
Copy link
Contributor Author

liron-l commented Jun 19, 2017

@diogomonica @cpuguy83 @aaronlehmann
I published another iteration with the following changes:

  1. Moved the secret resolution to the dispatcher next to fetching the secret values from raft store.
  2. Add the ServiceAnnotations metadata to the driver request
  3. Added a simple dispatcher UT for resolving the secret values

assert.NoError(t, err)
defer stream.CloseSend()

time.Sleep(500 * time.Millisecond)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the sleep necessary? Recv is a blocking function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed

@@ -238,16 +238,26 @@ func (s *Server) RemoveSecret(ctx context.Context, request *api.RemoveSecretRequ
}
}

// ValidateSecretPayload validates the secret payload size
func ValidateSecretPayload(data []byte) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to move this to a subpackage like api/secret or api/validation, so that dispatcher doesn't import controlapi.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to api/validation/secrets.go, I kept the prefix validation in the function name, let me know if you prefer validation.SecretPayload

@@ -15,16 +15,23 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"

"encoding/json"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Standard library imports such as this are typically put in the top section of the import statement, sorted alphabetically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks fixed

@liron-l
Copy link
Contributor Author

liron-l commented Jun 22, 2017

Thanks @aaronlehmann I've updated the review according to your comments.

@liron-l
Copy link
Contributor Author

liron-l commented Jun 27, 2017

Thanks @aaronlehmann! I've fixed the test and added validation that the ServiceSpec propagated correctly to the secrets driver.

@liron-l
Copy link
Contributor Author

liron-l commented Jul 2, 2017

Thanks for all comments, @aaronlehmann, @diogomonica, @cpuguy83.
I've update the review according to the comments, please take a look.

@liron-l
Copy link
Contributor Author

liron-l commented Jul 5, 2017

Per discussion with @diogomonica, I removed ServerSpec from plugin request. Additional service properties will be added if required.


if len(spec.Data) >= MaxSecretSize || len(spec.Data) < 1 {
return grpc.Errorf(codes.InvalidArgument, "secret data must be larger than 0 and less than %d bytes", MaxSecretSize)
if spec.Driver.Name != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the built-in backend must have a name, or is likely to have a name at some point just to be explicit... maybe this check is not sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cpuguy83, I made this type nullable, and if defined, validation will throw an error if no name is specified.

api/specs.proto Outdated
@@ -393,6 +393,9 @@ message SecretSpec {
// The currently recognized values are:
// - golang: Go templating
Driver templating = 3;

// Driver is the the secret driver that is used to store the specified secret
Driver driver = 4 [(gogoproto.nullable) = false];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure this shouldn't be nullable?
I see a lot of checks for if spec.Driver.Name != "" {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cpuguy83, I made this nullable. Now, I separate between nullable driver (OK) to initialized value with name (added validation)

@@ -245,3 +260,24 @@ func (a *assignmentSet) message() api.AssignmentsMessage {

return message
}

// populateSecretFromPlugin populates the secret value for the given specification using the secret plugin subsystem.
func (a *assignmentSet) populateSecretFromDriver(spec *api.SecretSpec, readTx store.ReadTx) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function still confuses me a bit. How is the secret populated for the built-in raft store?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cpuguy83, the secret spec was populated by the calling method. I melded everything to a single function (secret, which fetch the value from raft store and populate the secret value if needed). To keep flow consistent, I've change the Debug message in case the secret is not found in raft-store to Error, hope this makes more sense now.

@liron-l
Copy link
Contributor Author

liron-l commented Jul 5, 2017

Thanks for the comments @cpuguy83, I've updated the review based on your feedback.

@@ -63,4 +69,5 @@ var createCmd = &cobra.Command{

func init() {
createCmd.Flags().StringP("file", "f", "", "Rather than read the secret from STDIN, read from the given file")
createCmd.Flags().StringP("driver", "d", "", "The secret driver")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we should note that it is STDIN if not specified

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @riyazdf, I changed it according to the file flag format.

// Check if secret driver is defined
if spec.Driver != nil {
// Ensure secret driver has a name
if spec.Driver.Name == "" {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be tracing the code incorrectly but it seems that we could have a non-nil driver with an empty name? See: https://github.com/docker/swarmkit/pull/2239/files#diff-b7cdf7ddfbe8b31d75bc99e8d2d0fa78R58

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @riyazdf, you are right, I modified the create flow accordingly.

@liron-l
Copy link
Contributor Author

liron-l commented Jul 5, 2017

Thanks @riyazdf, let me know if you think we should make the Driver non-nullable type.


// secret populates the secret value from raft store. For external secrets, the value is populated
// from the secret driver.
func (a *assignmentSet) secret(secretID string, readTx store.ReadTx) (*api.Secret, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit, I like to pass the transaction first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, makes sense, I didn't notice the order.

// secret populates the secret value from raft store. For external secrets, the value is populated
// from the secret driver.
func (a *assignmentSet) secret(secretID string, readTx store.ReadTx) (*api.Secret, error) {
secret := store.GetSecret(readTx, secretID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only do this if the driver is nil?

Copy link
Contributor Author

@liron-l liron-l Jul 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cpuguy83, as far as I understand no, for two reasons:

  1. We need to fetch the actual Driver object in case additional Driver metadata is needed for initiating the plugin
  2. We need the api.Secret to initiate the assignment. Since I have the secret ID, name and value I can probably generate this object. However, I feel that using the raft store is more robust.
    WDYT?

@riyazdf
Copy link

riyazdf commented Jul 5, 2017

@liron-l: thanks! I think nullable makes sense, though I'll let you know if I think of a reason we should make it non-nullable 👍

@liron-l
Copy link
Contributor Author

liron-l commented Jul 8, 2017

Thanks @aaronlehmann, @riyazdf and @cpuguy83, I hope the last iteration satisfies all requirements.

// from the secret driver.
func (a *assignmentSet) secret(readTx store.ReadTx, secretID string) (*api.Secret, error) {
secret := store.GetSecret(readTx, secretID)
if secret == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is returning any time the local store doesn't have the secret.

I still think it's better to check the local store only if driver was not specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpuguy83 but if a driver was specified, I still need to query the api.Secret object to fetch the api.Driver (to correctly materialize the secret).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see now.
Thanks.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow: doesn't this function return if the store.GetSecret returns nil?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secret metadata is still stored in the raft store and must exist there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@riyazdf, just to clarify, following @cpuguy83 comments, I consolidate all secret fetching functionality to a single function.
The flow:

  1. Fetch the secret (since the secret is required by the task, return error if not found)
  2. If secrets driver is defined, fetch the secret value from the driver (otherwise return the secret value)
    Let me know if additional refactoring is needed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. Thanks @cpuguy83 and @liron-l for clarifying. :)


// SecretsProviderRequest is the request specification for retrieving secrets from plugins.
type SecretsProviderRequest struct {
Name string `json:"name"` // Name is the name of the secret plugin
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/plugin//

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks fixed.

Copy link
Member

@cpuguy83 cpuguy83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all that familiar with the swarmkit side of things, but this code LGTM.

Not a fan of plugingetter, but it's what's available from docker right now.

Copy link

@riyazdf riyazdf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you @liron-l for the hard work!

@liron-l
Copy link
Contributor Author

liron-l commented Jul 11, 2017

Thanks so much @riyazdf @cpuguy83 @aaronlehmann, @diogomonica who else needs to review this commit before merging?

@@ -1,9 +1,12 @@
package dispatcher

import (
"fmt"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor nit: normally there would be a blank line here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aaronlehmann fixed.

This commit extends SwarmKit secret management with pluggable secret
backends support. The solution uses the existing docker plugin
framework for loading plugins and the existing SwarmKit data backend for
storing them.

The approach is to add a new `driver` parameter to existing secrets,
which defines whether the values are taken as is or fetched from one of
the secret plugins. The loading of secrets is done using the standard
docker plugin infrastructure, which is already accessible in SwarmKit
and used in other flows (e.g., networking).
The fetched values are evaluated before assigning them to worker nodes,
so the payload is not stored in the raft store.

Remarks:
* I've added support for mocking the plugin subsystem when settings up
the controlapi server.
I preferred this approach over loading the full plugin subsystem in UT.

Work still needed in this CR:
- [ ] More unit tests (pending initial iteration)
- [ ] Customized error handling (e.g., customize error string for Not
Found)

Work still needed to complete this feature:
- [ ] Inject secrets as part of plugin initialization
- [ ] CLI support in docker
- [ ] Docs
- [ ] Support scheduling plugins in swarm
moby/moby#33575

Signed-off-by: liron <liron@twistlock.com>
@liron-l
Copy link
Contributor Author

liron-l commented Jul 14, 2017

Thanks @aaronlehmann, @cpuguy83, @riyazdf, @diogomonica.
What is the process of merging this change?

@diogomonica diogomonica merged commit eebac27 into moby:master Jul 14, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants