Replace secrets with join tokens

Implement the proposal from
https://github.com/docker/docker/issues/24430#issuecomment-233100121

Removes acceptance policy and secret in favor of an automatically
generated join token that combines the secret, CA hash, and
manager/worker role into a single opaque string.

Adds a docker swarm join-token subcommand to inspect and rotate the
tokens.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Upstream-commit: 2cc5bd33eef038bf5721582e2410ba459bb656e9
Component: engine
This commit is contained in:
Aaron Lehmann
2016-07-21 15:23:03 -07:00
parent 281fb0ce0f
commit b141a44de0
46 changed files with 451 additions and 893 deletions
@@ -216,15 +216,17 @@ func (s *DockerSwarmSuite) AddDaemon(c *check.C, joinSwarm, manager bool) *Swarm
if joinSwarm == true {
if len(s.daemons) > 0 {
tokens := s.daemons[0].joinTokens(c)
token := tokens.Worker
if manager {
token = tokens.Manager
}
c.Assert(d.Join(swarm.JoinRequest{
RemoteAddrs: []string{s.daemons[0].listenAddr},
Manager: manager}), check.IsNil)
} else {
c.Assert(d.Init(swarm.InitRequest{
Spec: swarm.Spec{
AcceptancePolicy: autoAcceptPolicy,
},
JoinToken: token,
}), check.IsNil)
} else {
c.Assert(d.Init(swarm.InitRequest{}), check.IsNil)
}
}
@@ -22,14 +22,6 @@ type SwarmDaemon struct {
listenAddr string
}
// default policy in tests is allow-all
var autoAcceptPolicy = swarm.AcceptancePolicy{
Policies: []swarm.Policy{
{Role: swarm.NodeRoleWorker, Autoaccept: true},
{Role: swarm.NodeRoleManager, Autoaccept: true},
},
}
// Init initializes a new swarm cluster.
func (d *SwarmDaemon) Init(req swarm.InitRequest) error {
if req.ListenAddr == "" {
@@ -271,6 +263,28 @@ func (d *SwarmDaemon) updateSwarm(c *check.C, f ...specConstructor) {
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
}
func (d *SwarmDaemon) rotateTokens(c *check.C) {
var sw swarm.Swarm
status, out, err := d.SockRequest("GET", "/swarm", nil)
c.Assert(err, checker.IsNil)
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
c.Assert(json.Unmarshal(out, &sw), checker.IsNil)
url := fmt.Sprintf("/swarm/update?version=%d&rotate_worker_token=true&rotate_manager_token=true", sw.Version.Index)
status, out, err = d.SockRequest("POST", url, sw.Spec)
c.Assert(err, checker.IsNil)
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
}
func (d *SwarmDaemon) joinTokens(c *check.C) swarm.JoinTokens {
var sw swarm.Swarm
status, out, err := d.SockRequest("GET", "/swarm", nil)
c.Assert(err, checker.IsNil)
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
c.Assert(json.Unmarshal(out, &sw), checker.IsNil)
return sw.JoinTokens
}
func (d *SwarmDaemon) checkLocalNodeState(c *check.C) (interface{}, check.CommentInterface) {
info, err := d.info()
c.Assert(err, checker.IsNil)
@@ -43,7 +43,7 @@ func (s *DockerSwarmSuite) TestApiSwarmInit(c *check.C) {
c.Assert(info.ControlAvailable, checker.False)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
c.Assert(d2.Join(swarm.JoinRequest{RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
c.Assert(d2.Join(swarm.JoinRequest{JoinToken: d1.joinTokens(c).Worker, RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
info, err = d2.info()
c.Assert(err, checker.IsNil)
@@ -72,89 +72,29 @@ func (s *DockerSwarmSuite) TestApiSwarmInit(c *check.C) {
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateActive)
}
func (s *DockerSwarmSuite) TestApiSwarmManualAcceptance(c *check.C) {
func (s *DockerSwarmSuite) TestApiSwarmJoinToken(c *check.C) {
testRequires(c, Network)
s.testAPISwarmManualAcceptance(c, "")
}
func (s *DockerSwarmSuite) TestApiSwarmManualAcceptanceSecret(c *check.C) {
testRequires(c, Network)
s.testAPISwarmManualAcceptance(c, "foobaz")
}
func (s *DockerSwarmSuite) testAPISwarmManualAcceptance(c *check.C, secret string) {
d1 := s.AddDaemon(c, false, false)
c.Assert(d1.Init(swarm.InitRequest{
Spec: swarm.Spec{
AcceptancePolicy: swarm.AcceptancePolicy{
Policies: []swarm.Policy{
{Role: swarm.NodeRoleWorker, Secret: &secret},
{Role: swarm.NodeRoleManager, Secret: &secret},
},
},
},
}), checker.IsNil)
c.Assert(d1.Init(swarm.InitRequest{}), checker.IsNil)
d2 := s.AddDaemon(c, false, false)
err := d2.Join(swarm.JoinRequest{RemoteAddrs: []string{d1.listenAddr}})
c.Assert(err, checker.NotNil)
if secret == "" {
c.Assert(err.Error(), checker.Contains, "needs to be accepted")
info, err := d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStatePending)
c.Assert(d2.Leave(false), checker.IsNil)
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
} else {
c.Assert(err.Error(), checker.Contains, "valid secret token is necessary")
info, err := d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
}
d3 := s.AddDaemon(c, false, false)
c.Assert(d3.Join(swarm.JoinRequest{Secret: secret, RemoteAddrs: []string{d1.listenAddr}}), checker.NotNil)
info, err := d3.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStatePending)
c.Assert(len(info.NodeID), checker.GreaterThan, 5)
d1.updateNode(c, info.NodeID, func(n *swarm.Node) {
n.Spec.Membership = swarm.NodeMembershipAccepted
})
waitAndAssert(c, defaultReconciliationTimeout, d3.checkLocalNodeState, checker.Equals, swarm.LocalNodeStateActive)
}
func (s *DockerSwarmSuite) TestApiSwarmSecretAcceptance(c *check.C) {
testRequires(c, Network)
d1 := s.AddDaemon(c, false, false)
secret := "foobar"
c.Assert(d1.Init(swarm.InitRequest{
Spec: swarm.Spec{
AcceptancePolicy: swarm.AcceptancePolicy{
Policies: []swarm.Policy{
{Role: swarm.NodeRoleWorker, Autoaccept: true, Secret: &secret},
{Role: swarm.NodeRoleManager, Secret: &secret},
},
},
},
}), checker.IsNil)
d2 := s.AddDaemon(c, false, false)
err := d2.Join(swarm.JoinRequest{RemoteAddrs: []string{d1.listenAddr}})
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, "secret token is necessary")
c.Assert(err.Error(), checker.Contains, "join token is necessary")
info, err := d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
err = d2.Join(swarm.JoinRequest{Secret: "foobaz", RemoteAddrs: []string{d1.listenAddr}})
err = d2.Join(swarm.JoinRequest{JoinToken: "foobaz", RemoteAddrs: []string{d1.listenAddr}})
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, "secret token is necessary")
c.Assert(err.Error(), checker.Contains, "join token is necessary")
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
c.Assert(d2.Join(swarm.JoinRequest{Secret: "foobar", RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
workerToken := d1.joinTokens(c).Worker
c.Assert(d2.Join(swarm.JoinRequest{JoinToken: workerToken, RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateActive)
@@ -163,22 +103,19 @@ func (s *DockerSwarmSuite) TestApiSwarmSecretAcceptance(c *check.C) {
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
// change secret
d1.updateSwarm(c, func(s *swarm.Spec) {
for i := range s.AcceptancePolicy.Policies {
p := "foobaz"
s.AcceptancePolicy.Policies[i].Secret = &p
}
})
// change tokens
d1.rotateTokens(c)
err = d2.Join(swarm.JoinRequest{Secret: "foobar", RemoteAddrs: []string{d1.listenAddr}})
err = d2.Join(swarm.JoinRequest{JoinToken: workerToken, RemoteAddrs: []string{d1.listenAddr}})
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, "secret token is necessary")
c.Assert(err.Error(), checker.Contains, "join token is necessary")
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
c.Assert(d2.Join(swarm.JoinRequest{Secret: "foobaz", RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
workerToken = d1.joinTokens(c).Worker
c.Assert(d2.Join(swarm.JoinRequest{JoinToken: workerToken, RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateActive)
@@ -187,24 +124,17 @@ func (s *DockerSwarmSuite) TestApiSwarmSecretAcceptance(c *check.C) {
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
// change policy, don't change secret
d1.updateSwarm(c, func(s *swarm.Spec) {
for i, p := range s.AcceptancePolicy.Policies {
if p.Role == swarm.NodeRoleManager {
s.AcceptancePolicy.Policies[i].Autoaccept = false
}
s.AcceptancePolicy.Policies[i].Secret = nil
}
})
// change spec, don't change tokens
d1.updateSwarm(c, func(s *swarm.Spec) {})
err = d2.Join(swarm.JoinRequest{RemoteAddrs: []string{d1.listenAddr}})
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, "secret token is necessary")
c.Assert(err.Error(), checker.Contains, "join token is necessary")
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
c.Assert(d2.Join(swarm.JoinRequest{Secret: "foobaz", RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
c.Assert(d2.Join(swarm.JoinRequest{JoinToken: workerToken, RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateActive)
@@ -212,51 +142,24 @@ func (s *DockerSwarmSuite) TestApiSwarmSecretAcceptance(c *check.C) {
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
// clear secret
d1.updateSwarm(c, func(s *swarm.Spec) {
for i := range s.AcceptancePolicy.Policies {
p := ""
s.AcceptancePolicy.Policies[i].Secret = &p
}
})
c.Assert(d2.Join(swarm.JoinRequest{RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateActive)
c.Assert(d2.Leave(false), checker.IsNil)
info, err = d2.info()
c.Assert(err, checker.IsNil)
c.Assert(info.LocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
}
func (s *DockerSwarmSuite) TestApiSwarmCAHash(c *check.C) {
testRequires(c, Network)
d1 := s.AddDaemon(c, true, true)
d2 := s.AddDaemon(c, false, false)
err := d2.Join(swarm.JoinRequest{CACertHash: "foobar", RemoteAddrs: []string{d1.listenAddr}})
splitToken := strings.Split(d1.joinTokens(c).Worker, "-")
splitToken[2] = "1kxftv4ofnc6mt30lmgipg6ngf9luhwqopfk1tz6bdmnkubg0e"
replacementToken := strings.Join(splitToken, "-")
err := d2.Join(swarm.JoinRequest{JoinToken: replacementToken, RemoteAddrs: []string{d1.listenAddr}})
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, "invalid checksum digest format")
c.Assert(len(d1.CACertHash), checker.GreaterThan, 0)
c.Assert(d2.Join(swarm.JoinRequest{CACertHash: d1.CACertHash, RemoteAddrs: []string{d1.listenAddr}}), checker.IsNil)
c.Assert(err.Error(), checker.Contains, "remote CA does not match fingerprint")
}
func (s *DockerSwarmSuite) TestApiSwarmPromoteDemote(c *check.C) {
testRequires(c, Network)
d1 := s.AddDaemon(c, false, false)
c.Assert(d1.Init(swarm.InitRequest{
Spec: swarm.Spec{
AcceptancePolicy: swarm.AcceptancePolicy{
Policies: []swarm.Policy{
{Role: swarm.NodeRoleWorker, Autoaccept: true},
{Role: swarm.NodeRoleManager},
},
},
},
}), checker.IsNil)
c.Assert(d1.Init(swarm.InitRequest{}), checker.IsNil)
d2 := s.AddDaemon(c, true, false)
info, err := d2.info()
@@ -838,9 +741,7 @@ func (s *DockerSwarmSuite) TestApiSwarmForceNewCluster(c *check.C) {
c.Assert(d1.Init(swarm.InitRequest{
ForceNewCluster: true,
Spec: swarm.Spec{
AcceptancePolicy: autoAcceptPolicy,
},
Spec: swarm.Spec{},
}), checker.IsNil)
waitAndAssert(c, defaultReconciliationTimeout, d1.checkActiveContainerCount, checker.Equals, instances)
@@ -937,7 +838,6 @@ func checkClusterHealth(c *check.C, cl []*SwarmDaemon, managerCount, workerCount
for _, n := range d.listNodes(c) {
c.Assert(n.Status.State, checker.Equals, swarm.NodeStateReady, check.Commentf("state of node %s, reported by %s", n.ID, d.Info.NodeID))
c.Assert(n.Spec.Availability, checker.Equals, swarm.NodeAvailabilityActive, check.Commentf("availability of node %s, reported by %s", n.ID, d.Info.NodeID))
c.Assert(n.Spec.Membership, checker.Equals, swarm.NodeMembershipAccepted, check.Commentf("membership of node %s, reported by %s", n.ID, d.Info.NodeID))
if n.Spec.Role == swarm.NodeRoleManager {
c.Assert(n.ManagerStatus, checker.NotNil, check.Commentf("manager status of node %s (manager), reported by %s", n.ID, d.Info.NodeID))
if n.ManagerStatus.Leader {
@@ -25,50 +25,13 @@ func (s *DockerSwarmSuite) TestSwarmUpdate(c *check.C) {
return sw[0].Spec
}
out, err := d.Cmd("swarm", "update", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s", "--auto-accept", "manager", "--auto-accept", "worker", "--secret", "foo")
out, err := d.Cmd("swarm", "update", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s")
c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
spec := getSpec()
c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour)
c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, uint64(11*time.Second))
c.Assert(spec.AcceptancePolicy.Policies, checker.HasLen, 2)
for _, p := range spec.AcceptancePolicy.Policies {
c.Assert(p.Autoaccept, checker.Equals, true)
c.Assert(p.Secret, checker.NotNil)
c.Assert(*p.Secret, checker.Not(checker.Equals), "")
}
out, err = d.Cmd("swarm", "update", "--auto-accept", "none")
c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
spec = getSpec()
c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour)
c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, uint64(11*time.Second))
c.Assert(spec.AcceptancePolicy.Policies, checker.HasLen, 2)
for _, p := range spec.AcceptancePolicy.Policies {
c.Assert(p.Autoaccept, checker.Equals, false)
// secret is still set
c.Assert(p.Secret, checker.NotNil)
c.Assert(*p.Secret, checker.Not(checker.Equals), "")
}
out, err = d.Cmd("swarm", "update", "--auto-accept", "manager", "--secret", "")
c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
spec = getSpec()
c.Assert(spec.AcceptancePolicy.Policies, checker.HasLen, 2)
for _, p := range spec.AcceptancePolicy.Policies {
c.Assert(p.Autoaccept, checker.Equals, p.Role == swarm.NodeRoleManager)
// secret has been removed
c.Assert(p.Secret, checker.IsNil)
}
// setting anything under 30m for cert-expiry is not allowed
out, err = d.Cmd("swarm", "update", "--cert-expiry", "15m")
c.Assert(err, checker.NotNil)
@@ -89,37 +52,21 @@ func (s *DockerSwarmSuite) TestSwarmInit(c *check.C) {
return sw[0].Spec
}
out, err := d.Cmd("swarm", "init", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s", "--auto-accept", "manager", "--auto-accept", "worker", "--secret", "foo")
out, err := d.Cmd("swarm", "init", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s")
c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
spec := getSpec()
c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour)
c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, uint64(11*time.Second))
c.Assert(spec.AcceptancePolicy.Policies, checker.HasLen, 2)
for _, p := range spec.AcceptancePolicy.Policies {
c.Assert(p.Autoaccept, checker.Equals, true)
c.Assert(p.Secret, checker.NotNil)
c.Assert(*p.Secret, checker.Not(checker.Equals), "")
}
c.Assert(d.Leave(true), checker.IsNil)
out, err = d.Cmd("swarm", "init", "--auto-accept", "none", "--secret", "")
out, err = d.Cmd("swarm", "init")
c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
spec = getSpec()
c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 90*24*time.Hour)
c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, uint64(5*time.Second))
c.Assert(spec.AcceptancePolicy.Policies, checker.HasLen, 2)
for _, p := range spec.AcceptancePolicy.Policies {
c.Assert(p.Autoaccept, checker.Equals, false)
c.Assert(p.Secret, checker.IsNil)
}
}
func (s *DockerSwarmSuite) TestSwarmInitIPv6(c *check.C) {