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:
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user