Skip to content

Commit 184a88a

Browse files
author
liron
committed
Pluggable secret backend
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>
1 parent fd73175 commit 184a88a

14 files changed

Lines changed: 524 additions & 141 deletions

File tree

api/specs.pb.go

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

api/specs.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,9 @@ message SecretSpec {
393393
// The currently recognized values are:
394394
// - golang: Go templating
395395
Driver templating = 3;
396+
397+
// Driver is the the secret driver that is used to store the specified secret
398+
Driver driver = 4;
396399
}
397400

398401
// ConfigSpec specifies user-provided configuration files.

api/validation/secrets.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package validation
2+
3+
import "fmt"
4+
5+
// MaxSecretSize is the maximum byte length of the `Secret.Spec.Data` field.
6+
const MaxSecretSize = 500 * 1024 // 500KB
7+
8+
// ValidateSecretPayload validates the secret payload size
9+
func ValidateSecretPayload(data []byte) error {
10+
if len(data) >= MaxSecretSize || len(data) < 1 {
11+
return fmt.Errorf("secret data must be larger than 0 and less than %d bytes", MaxSecretSize)
12+
}
13+
return nil
14+
}

cmd/swarmctl/secret/create.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ var createCmd = &cobra.Command{
2424
var (
2525
secretData []byte
2626
err error
27+
driver string
2728
)
2829

30+
driver, err = flags.GetString("driver")
31+
if err != nil {
32+
return fmt.Errorf("Error reading secret driver %s", err.Error())
33+
}
2934
if flags.Changed("file") {
3035
filename, err := flags.GetString("file")
3136
if err != nil {
@@ -35,7 +40,7 @@ var createCmd = &cobra.Command{
3540
if err != nil {
3641
return fmt.Errorf("Error reading from file '%s': %s", filename, err.Error())
3742
}
38-
} else {
43+
} else if driver == "" {
3944
secretData, err = ioutil.ReadAll(os.Stdin)
4045
if err != nil {
4146
return fmt.Errorf("Error reading content from STDIN: %s", err.Error())
@@ -51,6 +56,9 @@ var createCmd = &cobra.Command{
5156
Annotations: api.Annotations{Name: args[0]},
5257
Data: secretData,
5358
}
59+
if driver != "" {
60+
spec.Driver = &api.Driver{Name: driver}
61+
}
5462

5563
resp, err := client.CreateSecret(common.Context(cmd), &api.CreateSecretRequest{Spec: spec})
5664
if err != nil {
@@ -63,4 +71,5 @@ var createCmd = &cobra.Command{
6371

6472
func init() {
6573
createCmd.Flags().StringP("file", "f", "", "Rather than read the secret from STDIN, read from the given file")
74+
createCmd.Flags().StringP("driver", "d", "", "Rather than read the secret from STDIN, read the value from an external secret driver")
6675
}

cmd/swarmctl/secret/list.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,21 @@ var (
6363
// Ignore flushing errors - there's nothing we can do.
6464
_ = w.Flush()
6565
}()
66-
common.PrintHeader(w, "ID", "Name", "Created")
66+
common.PrintHeader(w, "ID", "Name", "Driver", "Created")
6767
output = func(s *api.Secret) {
6868
created, err := gogotypes.TimestampFromProto(s.Meta.CreatedAt)
6969
if err != nil {
7070
panic(err)
7171
}
72-
fmt.Fprintf(w, "%s\t%s\t%s\n",
72+
var driver string
73+
if s.Spec.Driver != nil {
74+
driver = s.Spec.Driver.Name
75+
}
76+
77+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
7378
s.ID,
7479
s.Spec.Annotations.Name,
80+
driver,
7581
humanize.Time(created),
7682
)
7783
}

manager/controlapi/secret.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/Sirupsen/logrus"
88
"github.com/docker/swarmkit/api"
9+
"github.com/docker/swarmkit/api/validation"
910
"github.com/docker/swarmkit/identity"
1011
"github.com/docker/swarmkit/log"
1112
"github.com/docker/swarmkit/manager/state/store"
@@ -14,9 +15,6 @@ import (
1415
"google.golang.org/grpc/codes"
1516
)
1617

17-
// MaxSecretSize is the maximum byte length of the `Secret.Spec.Data` field.
18-
const MaxSecretSize = 500 * 1024 // 500KB
19-
2018
// assumes spec is not nil
2119
func secretFromSecretSpec(spec *api.SecretSpec) *api.Secret {
2220
return &api.Secret{
@@ -56,7 +54,6 @@ func (s *Server) UpdateSecret(ctx context.Context, request *api.UpdateSecretRequ
5654
if request.SecretID == "" || request.SecretVersion == nil {
5755
return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
5856
}
59-
6057
var secret *api.Secret
6158
err := s.store.Update(func(tx store.Tx) error {
6259
secret = store.GetSecret(tx, request.SecretID)
@@ -245,9 +242,16 @@ func validateSecretSpec(spec *api.SecretSpec) error {
245242
if err := validateConfigOrSecretAnnotations(spec.Annotations); err != nil {
246243
return err
247244
}
248-
249-
if len(spec.Data) >= MaxSecretSize || len(spec.Data) < 1 {
250-
return grpc.Errorf(codes.InvalidArgument, "secret data must be larger than 0 and less than %d bytes", MaxSecretSize)
245+
// Check if secret driver is defined
246+
if spec.Driver != nil {
247+
// Ensure secret driver has a name
248+
if spec.Driver.Name == "" {
249+
return grpc.Errorf(codes.InvalidArgument, "secret driver must have a name")
250+
}
251+
return nil
252+
}
253+
if err := validation.ValidateSecretPayload(spec.Data); err != nil {
254+
return grpc.Errorf(codes.InvalidArgument, "%s", err.Error())
251255
}
252256
return nil
253257
}

manager/controlapi/secret_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ func TestValidateSecretSpec(t *testing.T) {
8989
err := validateSecretSpec(good)
9090
assert.NoError(t, err)
9191
}
92+
93+
// Ensure secret driver has a name
94+
spec := createSecretSpec("secret-driver", make([]byte, 1), nil)
95+
spec.Driver = &api.Driver{}
96+
err := validateSecretSpec(spec)
97+
assert.Error(t, err)
98+
assert.Equal(t, codes.InvalidArgument, grpc.Code(err), grpc.ErrorDesc(err))
99+
spec.Driver.Name = "secret-driver"
100+
err = validateSecretSpec(spec)
101+
assert.NoError(t, err)
92102
}
93103

94104
func TestCreateSecret(t *testing.T) {

manager/controlapi/server.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
)
1111

1212
var (
13-
errNotImplemented = errors.New("not implemented")
1413
errInvalidArgument = errors.New("invalid argument")
1514
)
1615

manager/dispatcher/assignments.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package dispatcher
22

33
import (
4+
"fmt"
45
"github.com/Sirupsen/logrus"
56
"github.com/docker/swarmkit/api"
67
"github.com/docker/swarmkit/api/equality"
8+
"github.com/docker/swarmkit/api/validation"
9+
"github.com/docker/swarmkit/manager/drivers"
710
"github.com/docker/swarmkit/manager/state/store"
811
)
912

@@ -24,15 +27,16 @@ type typeAndID struct {
2427
}
2528

2629
type assignmentSet struct {
30+
dp *drivers.DriverProvider
2731
tasksMap map[string]*api.Task
2832
tasksUsingDependency map[typeAndID]map[string]struct{}
2933
changes map[typeAndID]*api.AssignmentChange
30-
31-
log *logrus.Entry
34+
log *logrus.Entry
3235
}
3336

34-
func newAssignmentSet(log *logrus.Entry) *assignmentSet {
37+
func newAssignmentSet(log *logrus.Entry, dp *drivers.DriverProvider) *assignmentSet {
3538
return &assignmentSet{
39+
dp: dp,
3640
changes: make(map[typeAndID]*api.AssignmentChange),
3741
tasksMap: make(map[string]*api.Task),
3842
tasksUsingDependency: make(map[typeAndID]map[string]struct{}),
@@ -53,12 +57,13 @@ func (a *assignmentSet) addTaskDependencies(readTx store.ReadTx, t *api.Task) {
5357
if len(a.tasksUsingDependency[mapKey]) == 0 {
5458
a.tasksUsingDependency[mapKey] = make(map[string]struct{})
5559

56-
secret := store.GetSecret(readTx, secretID)
57-
if secret == nil {
60+
secret, err := a.secret(readTx, secretID)
61+
if err != nil {
5862
a.log.WithFields(logrus.Fields{
5963
"secret.id": secretID,
6064
"secret.name": secretRef.SecretName,
61-
}).Debug("secret not found")
65+
"error": err,
66+
}).Error("failed to fetch secret")
6267
continue
6368
}
6469

@@ -245,3 +250,29 @@ func (a *assignmentSet) message() api.AssignmentsMessage {
245250

246251
return message
247252
}
253+
254+
// secret populates the secret value from raft store. For external secrets, the value is populated
255+
// from the secret driver.
256+
func (a *assignmentSet) secret(readTx store.ReadTx, secretID string) (*api.Secret, error) {
257+
secret := store.GetSecret(readTx, secretID)
258+
if secret == nil {
259+
return nil, fmt.Errorf("secret not found")
260+
}
261+
if secret.Spec.Driver == nil {
262+
return secret, nil
263+
}
264+
d, err := a.dp.NewSecretDriver(secret.Spec.Driver)
265+
if err != nil {
266+
return nil, err
267+
}
268+
value, err := d.Get(&secret.Spec)
269+
if err != nil {
270+
return nil, err
271+
}
272+
if err := validation.ValidateSecretPayload(value); err != nil {
273+
return nil, err
274+
}
275+
// Assign the secret
276+
secret.Spec.Data = value
277+
return secret, nil
278+
}

manager/dispatcher/dispatcher.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/docker/swarmkit/api/equality"
1818
"github.com/docker/swarmkit/ca"
1919
"github.com/docker/swarmkit/log"
20+
"github.com/docker/swarmkit/manager/drivers"
2021
"github.com/docker/swarmkit/manager/state/store"
2122
"github.com/docker/swarmkit/remotes"
2223
"github.com/docker/swarmkit/watch"
@@ -125,6 +126,7 @@ type Dispatcher struct {
125126
ctx context.Context
126127
cancel context.CancelFunc
127128
clusterUpdateQueue *watch.Queue
129+
dp *drivers.DriverProvider
128130

129131
taskUpdates map[string]*api.TaskStatus // indexed by task ID
130132
taskUpdatesLock sync.Mutex
@@ -142,8 +144,9 @@ type Dispatcher struct {
142144
}
143145

144146
// New returns Dispatcher with cluster interface(usually raft.Node).
145-
func New(cluster Cluster, c *Config) *Dispatcher {
147+
func New(cluster Cluster, c *Config, dp *drivers.DriverProvider) *Dispatcher {
146148
d := &Dispatcher{
149+
dp: dp,
147150
nodes: newNodeStore(c.HeartbeatPeriod, c.HeartbeatEpsilon, c.GracePeriodMultiplier, c.RateLimitPeriod),
148151
downNodes: newNodeStore(defaultNodeDownPeriod, 0, 1, 0),
149152
store: cluster.MemoryStore(),
@@ -836,7 +839,7 @@ func (d *Dispatcher) Assignments(r *api.AssignmentsRequest, stream api.Dispatche
836839
var (
837840
sequence int64
838841
appliesTo string
839-
assignments = newAssignmentSet(log)
842+
assignments = newAssignmentSet(log, d.dp)
840843
)
841844

842845
sendMessage := func(msg api.AssignmentsMessage, assignmentType api.AssignmentsMessage_Type) error {

0 commit comments

Comments
 (0)