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-20 11:15:08 -07:00
parent 281fb0ce0f
commit b141a44de0
46 changed files with 451 additions and 893 deletions

View File

@ -13,7 +13,6 @@ import (
"google.golang.org/grpc"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/digest"
"github.com/docker/docker/daemon/cluster/convert"
executorpkg "github.com/docker/docker/daemon/cluster/executor"
"github.com/docker/docker/daemon/cluster/executor/container"
@ -42,16 +41,16 @@ const (
)
// ErrNoSwarm is returned on leaving a cluster that was never initialized
var ErrNoSwarm = fmt.Errorf("This node is not part of Swarm")
var ErrNoSwarm = fmt.Errorf("This node is not part of swarm")
// ErrSwarmExists is returned on initialize or join request for a cluster that has already been activated
var ErrSwarmExists = fmt.Errorf("This node is already part of a Swarm cluster. Use \"docker swarm leave\" to leave this cluster and join another one.")
var ErrSwarmExists = fmt.Errorf("This node is already part of a swarm cluster. Use \"docker swarm leave\" to leave this cluster and join another one.")
// ErrPendingSwarmExists is returned on initialize or join request for a cluster that is already processing a similar request but has not succeeded yet.
var ErrPendingSwarmExists = fmt.Errorf("This node is processing an existing join request that has not succeeded yet. Use \"docker swarm leave\" to cancel the current request.")
// ErrSwarmJoinTimeoutReached is returned when cluster join could not complete before timeout was reached.
var ErrSwarmJoinTimeoutReached = fmt.Errorf("Timeout was reached before node was joined. Attempt to join the cluster will continue in the background. Use \"docker info\" command to see the current Swarm status of your node.")
var ErrSwarmJoinTimeoutReached = fmt.Errorf("Timeout was reached before node was joined. Attempt to join the cluster will continue in the background. Use \"docker info\" command to see the current swarm status of your node.")
// defaultSpec contains some sane defaults if cluster options are missing on init
var defaultSpec = types.Spec{
@ -127,7 +126,7 @@ func New(config Config) (*Cluster, error) {
return nil, err
}
n, err := c.startNewNode(false, st.ListenAddr, "", "", "", false)
n, err := c.startNewNode(false, st.ListenAddr, "", "")
if err != nil {
return nil, err
}
@ -196,7 +195,7 @@ func (c *Cluster) reconnectOnFailure(n *node) {
return
}
var err error
n, err = c.startNewNode(false, c.listenAddr, c.getRemoteAddress(), "", "", false)
n, err = c.startNewNode(false, c.listenAddr, c.getRemoteAddress(), "")
if err != nil {
c.err = err
close(n.done)
@ -205,7 +204,7 @@ func (c *Cluster) reconnectOnFailure(n *node) {
}
}
func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, secret, cahash string, ismanager bool) (*node, error) {
func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, joinToken string) (*node, error) {
if err := c.config.Backend.IsSwarmCompatible(); err != nil {
return nil, err
}
@ -219,12 +218,10 @@ func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, secre
ListenRemoteAPI: listenAddr,
JoinAddr: joinAddr,
StateDir: c.root,
CAHash: cahash,
Secret: secret,
JoinToken: joinToken,
Executor: container.NewExecutor(c.config.Backend),
HeartbeatTick: 1,
ElectionTick: 3,
IsManager: ismanager,
})
if err != nil {
return nil, err
@ -291,7 +288,7 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
if node := c.node; node != nil {
if !req.ForceNewCluster {
c.Unlock()
return "", errSwarmExists(node)
return "", ErrSwarmExists
}
if err := c.stopNode(); err != nil {
c.Unlock()
@ -305,7 +302,7 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
}
// todo: check current state existing
n, err := c.startNewNode(req.ForceNewCluster, req.ListenAddr, "", "", "", false)
n, err := c.startNewNode(req.ForceNewCluster, req.ListenAddr, "", "")
if err != nil {
c.Unlock()
return "", err
@ -336,40 +333,32 @@ func (c *Cluster) Join(req types.JoinRequest) error {
c.Lock()
if node := c.node; node != nil {
c.Unlock()
return errSwarmExists(node)
return ErrSwarmExists
}
if err := validateAndSanitizeJoinRequest(&req); err != nil {
c.Unlock()
return err
}
// todo: check current state existing
n, err := c.startNewNode(false, req.ListenAddr, req.RemoteAddrs[0], req.Secret, req.CACertHash, req.Manager)
n, err := c.startNewNode(false, req.ListenAddr, req.RemoteAddrs[0], req.JoinToken)
if err != nil {
c.Unlock()
return err
}
c.Unlock()
certificateRequested := n.CertificateRequested()
for {
select {
case <-certificateRequested:
if n.NodeMembership() == swarmapi.NodeMembershipPending {
return fmt.Errorf("Your node is in the process of joining the cluster but needs to be accepted by existing cluster member.\nTo accept this node into cluster run \"docker node accept %v\" in an existing cluster manager. Use \"docker info\" command to see the current Swarm status of your node.", n.NodeID())
}
certificateRequested = nil
case <-time.After(swarmConnectTimeout):
// attempt to connect will continue in background, also reconnecting
go c.reconnectOnFailure(n)
return ErrSwarmJoinTimeoutReached
case <-n.Ready():
go c.reconnectOnFailure(n)
return nil
case <-n.done:
c.RLock()
defer c.RUnlock()
return c.err
}
select {
case <-time.After(swarmConnectTimeout):
// attempt to connect will continue in background, also reconnecting
go c.reconnectOnFailure(n)
return ErrSwarmJoinTimeoutReached
case <-n.Ready():
go c.reconnectOnFailure(n)
return nil
case <-n.done:
c.RLock()
defer c.RUnlock()
return c.err
}
}
@ -489,7 +478,7 @@ func (c *Cluster) Inspect() (types.Swarm, error) {
}
// Update updates configuration of a managed swarm cluster.
func (c *Cluster) Update(version uint64, spec types.Spec) error {
func (c *Cluster) Update(version uint64, spec types.Spec, flags types.UpdateFlags) error {
c.RLock()
defer c.RUnlock()
@ -505,7 +494,7 @@ func (c *Cluster) Update(version uint64, spec types.Spec) error {
return err
}
swarmSpec, err := convert.SwarmSpecToGRPCandMerge(spec, &swarm.Spec)
swarmSpec, err := convert.SwarmSpecToGRPC(spec)
if err != nil {
return err
}
@ -518,6 +507,10 @@ func (c *Cluster) Update(version uint64, spec types.Spec) error {
ClusterVersion: &swarmapi.Version{
Index: version,
},
Rotation: swarmapi.JoinTokenRotation{
RotateWorkerToken: flags.RotateWorkerToken,
RotateManagerToken: flags.RotateManagerToken,
},
},
)
return err
@ -611,10 +604,6 @@ func (c *Cluster) Info() types.Info {
}
}
}
if swarm, err := getSwarm(ctx, c.client); err == nil && swarm != nil {
info.CACertHash = swarm.RootCA.CACertHash
}
}
if c.node != nil {
@ -636,12 +625,12 @@ func (c *Cluster) isActiveManager() bool {
// Call with read lock.
func (c *Cluster) errNoManager() error {
if c.node == nil {
return fmt.Errorf("This node is not a Swarm manager. Use \"docker swarm init\" or \"docker swarm join --manager\" to connect this node to Swarm and try again.")
return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join --manager\" to connect this node to swarm and try again.")
}
if c.node.Manager() != nil {
return fmt.Errorf("This node is not a Swarm manager. Manager is being prepared or has trouble connecting to the cluster.")
return fmt.Errorf("This node is not a swarm manager. Manager is being prepared or has trouble connecting to the cluster.")
}
return fmt.Errorf("This node is not a Swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager.")
return fmt.Errorf("This node is not a swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager.")
}
// GetServices returns all services of a managed swarm cluster.
@ -1219,11 +1208,6 @@ func validateAndSanitizeJoinRequest(req *types.JoinRequest) error {
return fmt.Errorf("invalid remoteAddr %q: %v", req.RemoteAddrs[i], err)
}
}
if req.CACertHash != "" {
if _, err := digest.ParseDigest(req.CACertHash); err != nil {
return fmt.Errorf("invalid CACertHash %q, %v", req.CACertHash, err)
}
}
return nil
}
@ -1238,13 +1222,6 @@ func validateAddr(addr string) (string, error) {
return strings.TrimPrefix(newaddr, "tcp://"), nil
}
func errSwarmExists(node *node) error {
if node.NodeMembership() != swarmapi.NodeMembershipAccepted {
return ErrPendingSwarmExists
}
return ErrSwarmExists
}
func initClusterSpec(node *node, spec types.Spec) error {
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
for conn := range node.ListenControlSocket(ctx) {
@ -1269,7 +1246,7 @@ func initClusterSpec(node *node, spec types.Spec) error {
cluster = lcr.Clusters[0]
break
}
newspec, err := convert.SwarmSpecToGRPCandMerge(spec, &cluster.Spec)
newspec, err := convert.SwarmSpecToGRPC(spec)
if err != nil {
return fmt.Errorf("error updating cluster settings: %v", err)
}

View File

@ -15,7 +15,6 @@ func NodeFromGRPC(n swarmapi.Node) types.Node {
ID: n.ID,
Spec: types.NodeSpec{
Role: types.NodeRole(strings.ToLower(n.Spec.Role.String())),
Membership: types.NodeMembership(strings.ToLower(n.Spec.Membership.String())),
Availability: types.NodeAvailability(strings.ToLower(n.Spec.Availability.String())),
},
Status: types.NodeStatus{
@ -79,12 +78,6 @@ func NodeSpecToGRPC(s types.NodeSpec) (swarmapi.NodeSpec, error) {
return swarmapi.NodeSpec{}, fmt.Errorf("invalid Role: %q", s.Role)
}
if membership, ok := swarmapi.NodeSpec_Membership_value[strings.ToUpper(string(s.Membership))]; ok {
spec.Membership = swarmapi.NodeSpec_Membership(membership)
} else {
return swarmapi.NodeSpec{}, fmt.Errorf("invalid Membership: %q", s.Membership)
}
if availability, ok := swarmapi.NodeSpec_Availability_value[strings.ToUpper(string(s.Availability))]; ok {
spec.Availability = swarmapi.NodeSpec_Availability(availability)
} else {

View File

@ -5,8 +5,6 @@ import (
"strings"
"time"
"golang.org/x/crypto/bcrypt"
types "github.com/docker/engine-api/types/swarm"
swarmapi "github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/protobuf/ptypes"
@ -28,6 +26,10 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm {
ElectionTick: c.Spec.Raft.ElectionTick,
},
},
JoinTokens: types.JoinTokens{
Worker: c.RootCA.JoinTokens.Worker,
Manager: c.RootCA.JoinTokens.Manager,
},
}
heartbeatPeriod, _ := ptypes.Duration(c.Spec.Dispatcher.HeartbeatPeriod)
@ -52,23 +54,11 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm {
swarm.Spec.Name = c.Spec.Annotations.Name
swarm.Spec.Labels = c.Spec.Annotations.Labels
for _, policy := range c.Spec.AcceptancePolicy.Policies {
p := types.Policy{
Role: types.NodeRole(strings.ToLower(policy.Role.String())),
Autoaccept: policy.Autoaccept,
}
if policy.Secret != nil {
secret := string(policy.Secret.Data)
p.Secret = &secret
}
swarm.Spec.AcceptancePolicy.Policies = append(swarm.Spec.AcceptancePolicy.Policies, p)
}
return swarm
}
// SwarmSpecToGRPCandMerge converts a Spec to a grpc ClusterSpec and merge AcceptancePolicy from an existing grpc ClusterSpec if provided.
func SwarmSpecToGRPCandMerge(s types.Spec, existingSpec *swarmapi.ClusterSpec) (swarmapi.ClusterSpec, error) {
// SwarmSpecToGRPC converts a Spec to a grpc ClusterSpec.
func SwarmSpecToGRPC(s types.Spec) (swarmapi.ClusterSpec, error) {
spec := swarmapi.ClusterSpec{
Annotations: swarmapi.Annotations{
Name: s.Name,
@ -104,63 +94,5 @@ func SwarmSpecToGRPCandMerge(s types.Spec, existingSpec *swarmapi.ClusterSpec) (
})
}
if err := SwarmSpecUpdateAcceptancePolicy(&spec, s.AcceptancePolicy, existingSpec); err != nil {
return swarmapi.ClusterSpec{}, err
}
return spec, nil
}
// SwarmSpecUpdateAcceptancePolicy updates a grpc ClusterSpec using AcceptancePolicy.
func SwarmSpecUpdateAcceptancePolicy(spec *swarmapi.ClusterSpec, acceptancePolicy types.AcceptancePolicy, oldSpec *swarmapi.ClusterSpec) error {
spec.AcceptancePolicy.Policies = nil
hashs := make(map[string][]byte)
for _, p := range acceptancePolicy.Policies {
role, ok := swarmapi.NodeRole_value[strings.ToUpper(string(p.Role))]
if !ok {
return fmt.Errorf("invalid Role: %q", p.Role)
}
policy := &swarmapi.AcceptancePolicy_RoleAdmissionPolicy{
Role: swarmapi.NodeRole(role),
Autoaccept: p.Autoaccept,
}
if p.Secret != nil {
if *p.Secret == "" { // if provided secret is empty, it means erase previous secret.
policy.Secret = nil
} else { // if provided secret is not empty, we generate a new one.
hashPwd, ok := hashs[*p.Secret]
if !ok {
hashPwd, _ = bcrypt.GenerateFromPassword([]byte(*p.Secret), 0)
hashs[*p.Secret] = hashPwd
}
policy.Secret = &swarmapi.AcceptancePolicy_RoleAdmissionPolicy_Secret{
Data: hashPwd,
Alg: "bcrypt",
}
}
} else if oldSecret := getOldSecret(oldSpec, policy.Role); oldSecret != nil { // else use the old one.
policy.Secret = &swarmapi.AcceptancePolicy_RoleAdmissionPolicy_Secret{
Data: oldSecret.Data,
Alg: oldSecret.Alg,
}
}
spec.AcceptancePolicy.Policies = append(spec.AcceptancePolicy.Policies, policy)
}
return nil
}
func getOldSecret(oldSpec *swarmapi.ClusterSpec, role swarmapi.NodeRole) *swarmapi.AcceptancePolicy_RoleAdmissionPolicy_Secret {
if oldSpec == nil {
return nil
}
for _, p := range oldSpec.AcceptancePolicy.Policies {
if p.Role == role {
return p.Secret
}
}
return nil
}