kubeadm: prevent bootstrap of nodes with known names

If a Node name in the cluster is already taken and this Node is Ready,
prevent TLS bootsrap on "kubeadm join" and exit early.

This change requires that a new ClusterRole is granted to the
"system:bootstrappers:kubeadm:default-node-token" group to be
able get Nodes in the cluster. The same group already has access
to obtain objects such as the KubeletConfiguration and kubeadm's
ClusterConfiguration.

The motivation of this change is to prevent undefined behavior
and the potential control-plane breakdown if such a cluster
is racing to have two nodes with the same name for long periods
of time.

The following values are validated in the following precedence
from lower to higher:
- actual hostname
- NodeRegistration.Name (or "--node-name") from JoinConfiguration
- "--hostname-override" passed via kubeletExtraArgs

If the user decides to not let kubeadm know about a custom node name
and to instead override the hostname from a kubelet systemd unit file,
kubeadm will not be able to detect the problem.
This commit is contained in:
Lubomir I. Ivanov 2019-08-07 04:16:36 +03:00
parent 5441a99db6
commit b117a928a6
6 changed files with 116 additions and 28 deletions

View File

@ -86,6 +86,10 @@ func runBootstrapToken(c workflow.RunData) error {
if err := nodebootstraptokenphase.UpdateOrCreateTokens(client, false, data.Cfg().BootstrapTokens); err != nil {
return errors.Wrap(err, "error updating or creating token")
}
// Create RBAC rules that makes the bootstrap tokens able to get nodes
if err := nodebootstraptokenphase.AllowBoostrapTokensToGetNodes(client); err != nil {
return errors.Wrap(err, "error allowing bootstrap tokens to get Nodes")
}
// Create RBAC rules that makes the bootstrap tokens able to post CSRs
if err := nodebootstraptokenphase.AllowBootstrapTokensToPostCSRs(client); err != nil {
return errors.Wrap(err, "error allowing bootstrap tokens to post CSRs")

View File

@ -30,6 +30,9 @@ go_library(
"//cmd/kubeadm/app/preflight:go_default_library",
"//cmd/kubeadm/app/util/apiclient:go_default_library",
"//cmd/kubeadm/app/util/kubeconfig:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/version:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",

View File

@ -22,6 +22,9 @@ import (
"github.com/lithammer/dedent"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/util/wait"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
@ -128,6 +131,28 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) {
return errors.Errorf("couldn't create client from kubeconfig file %q", bootstrapKubeConfigFile)
}
// Obtain the name of this Node.
nodeName, _, err := kubeletphase.GetNodeNameAndHostname(&cfg.NodeRegistration)
if err != nil {
klog.Warning(err)
}
// Make sure to exit before TLS bootstrap if a Node with the same name exist in the cluster
// and it has the "Ready" status.
// A new Node with the same name as an existing control-plane Node can cause undefined
// behavior and ultimately control-plane failure.
klog.V(1).Infof("[kubelet-start] Checking for an existing Node in the cluster with name %q and status %q", nodeName, v1.NodeReady)
node, err := bootstrapClient.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return errors.Wrapf(err, "cannot get Node %q", nodeName)
}
for _, cond := range node.Status.Conditions {
if cond.Type == v1.NodeReady {
return errors.Errorf("a Node with name %q and status %q already exists in the cluster. "+
"You must delete the existing Node or change the name of this new joining Node", nodeName, v1.NodeReady)
}
}
// Configure the kubelet. In this short timeframe, kubeadm is trying to stop/restart the kubelet
// Try to stop the kubelet service so no race conditions occur when configuring it
klog.V(1).Infoln("[kubelet-start] Stopping the kubelet")

View File

@ -32,6 +32,8 @@ const (
NodeBootstrapperClusterRoleName = "system:node-bootstrapper"
// NodeKubeletBootstrap defines the name of the ClusterRoleBinding that lets kubelets post CSRs
NodeKubeletBootstrap = "kubeadm:kubelet-bootstrap"
// GetNodesClusterRoleName defines the name of the ClusterRole and ClusterRoleBinding to get nodes
GetNodesClusterRoleName = "kubeadm:get-nodes"
// CSRAutoApprovalClusterRoleName defines the name of the auto-bootstrapped ClusterRole for making the csrapprover controller auto-approve the CSR
// TODO: This value should be defined in an other, generic authz package instead of here
@ -67,6 +69,45 @@ func AllowBootstrapTokensToPostCSRs(client clientset.Interface) error {
})
}
// AllowBoostrapTokensToGetNodes creates RBAC rules to allow Node Bootstrap Tokens to list nodes
func AllowBoostrapTokensToGetNodes(client clientset.Interface) error {
fmt.Println("[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to get nodes")
if err := apiclient.CreateOrUpdateClusterRole(client, &rbac.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: GetNodesClusterRoleName,
Namespace: metav1.NamespaceSystem,
},
Rules: []rbac.PolicyRule{
{
Verbs: []string{"get"},
APIGroups: []string{""},
Resources: []string{"nodes"},
},
},
}); err != nil {
return err
}
return apiclient.CreateOrUpdateClusterRoleBinding(client, &rbac.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: GetNodesClusterRoleName,
Namespace: metav1.NamespaceSystem,
},
RoleRef: rbac.RoleRef{
APIGroup: rbac.GroupName,
Kind: "ClusterRole",
Name: GetNodesClusterRoleName,
},
Subjects: []rbac.Subject{
{
Kind: rbac.GroupKind,
Name: constants.NodeBootstrapTokenAuthGroup,
},
},
})
}
// AutoApproveNodeBootstrapTokens creates RBAC rules in a way that makes Node Bootstrap Tokens' CSR auto-approved by the csrapprover controller
func AutoApproveNodeBootstrapTokens(client clientset.Interface) error {
fmt.Println("[bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token")

View File

@ -41,17 +41,29 @@ type kubeletFlagsOpts struct {
registerTaintsUsingFlags bool
execer utilsexec.Interface
isServiceActiveFunc func(string) (bool, error)
defaultHostname string
}
// GetNodeNameAndHostname obtains the name for this Node using the following precedence
// (from lower to higher):
// - actual hostname
// - NodeRegistrationOptions.Name (same as "--node-name" passed to "kubeadm init/join")
// - "hostname-overide" flag in NodeRegistrationOptions.KubeletExtraArgs
// It also returns the hostname or an error if getting the hostname failed.
func GetNodeNameAndHostname(cfg *kubeadmapi.NodeRegistrationOptions) (string, string, error) {
hostname, err := kubeadmutil.GetHostname("")
nodeName := hostname
if cfg.Name != "" {
nodeName = cfg.Name
}
if name, ok := cfg.KubeletExtraArgs["hostname-override"]; ok {
nodeName = name
}
return nodeName, hostname, err
}
// WriteKubeletDynamicEnvFile writes an environment file with dynamic flags to the kubelet.
// Used at "kubeadm init" and "kubeadm join" time.
func WriteKubeletDynamicEnvFile(cfg *kubeadmapi.ClusterConfiguration, nodeReg *kubeadmapi.NodeRegistrationOptions, registerTaintsUsingFlags bool, kubeletDir string) error {
hostName, err := kubeadmutil.GetHostname("")
if err != nil {
return err
}
flagOpts := kubeletFlagsOpts{
nodeRegOpts: nodeReg,
featureGates: cfg.FeatureGates,
@ -65,7 +77,6 @@ func WriteKubeletDynamicEnvFile(cfg *kubeadmapi.ClusterConfiguration, nodeReg *k
}
return initSystem.ServiceIsActive(name), nil
},
defaultHostname: hostName,
}
stringMap := buildKubeletArgMap(flagOpts)
argList := kubeadmutil.BuildArgumentListFromMap(stringMap, nodeReg.KubeletExtraArgs)
@ -113,15 +124,19 @@ func buildKubeletArgMap(opts kubeletFlagsOpts) map[string]string {
kubeletFlags["resolv-conf"] = "/run/systemd/resolve/resolv.conf"
}
// Make sure the node name we're passed will work with Kubelet
if opts.nodeRegOpts.Name != "" && opts.nodeRegOpts.Name != opts.defaultHostname {
klog.V(1).Infof("setting kubelet hostname-override to %q", opts.nodeRegOpts.Name)
kubeletFlags["hostname-override"] = opts.nodeRegOpts.Name
// Pass the "--hostname-override" flag to the kubelet only if it's different from the hostname
nodeName, hostname, err := GetNodeNameAndHostname(opts.nodeRegOpts)
if err != nil {
klog.Warning(err)
}
if nodeName != hostname {
klog.V(1).Infof("setting kubelet hostname-override to %q", nodeName)
kubeletFlags["hostname-override"] = nodeName
}
// TODO: Conditionally set `--cgroup-driver` to either `systemd` or `cgroupfs` for CRI other than Docker
// TODO: The following code should be remvoved after dual-stack is GA.
// TODO: The following code should be removed after dual-stack is GA.
// Note: The user still retains the ability to explicitly set feature-gates and that value will overwrite this base value.
if enabled, present := opts.featureGates[features.IPv6DualStack]; present {
kubeletFlags["feature-gates"] = fmt.Sprintf("%s=%t", features.IPv6DualStack, enabled)

View File

@ -109,7 +109,6 @@ func TestBuildKubeletArgMap(t *testing.T) {
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/dockershim.sock",
Name: "foo",
Taints: []v1.Taint{ // This should be ignored as registerTaintsUsingFlags is false
{
Key: "foo",
@ -120,14 +119,13 @@ func TestBuildKubeletArgMap(t *testing.T) {
},
execer: errCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
defaultHostname: "foo",
},
expected: map[string]string{
"network-plugin": "cni",
},
},
{
name: "nodeRegOpts.Name != default hostname",
name: "hostname override from NodeRegistrationOptions.Name",
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/dockershim.sock",
@ -135,7 +133,21 @@ func TestBuildKubeletArgMap(t *testing.T) {
},
execer: errCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
defaultHostname: "default",
},
expected: map[string]string{
"network-plugin": "cni",
"hostname-override": "override-name",
},
},
{
name: "hostname override from NodeRegistrationOptions.KubeletExtraArgs",
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/dockershim.sock",
KubeletExtraArgs: map[string]string{"hostname-override": "override-name"},
},
execer: errCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
},
expected: map[string]string{
"network-plugin": "cni",
@ -147,11 +159,9 @@ func TestBuildKubeletArgMap(t *testing.T) {
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/dockershim.sock",
Name: "foo",
},
execer: systemdCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
defaultHostname: "foo",
},
expected: map[string]string{
"network-plugin": "cni",
@ -163,11 +173,9 @@ func TestBuildKubeletArgMap(t *testing.T) {
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/dockershim.sock",
Name: "foo",
},
execer: cgroupfsCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
defaultHostname: "foo",
},
expected: map[string]string{
"network-plugin": "cni",
@ -179,11 +187,9 @@ func TestBuildKubeletArgMap(t *testing.T) {
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/containerd.sock",
Name: "foo",
},
execer: cgroupfsCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
defaultHostname: "foo",
},
expected: map[string]string{
"container-runtime": "remote",
@ -195,7 +201,6 @@ func TestBuildKubeletArgMap(t *testing.T) {
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/containerd.sock",
Name: "foo",
Taints: []v1.Taint{
{
Key: "foo",
@ -212,7 +217,6 @@ func TestBuildKubeletArgMap(t *testing.T) {
registerTaintsUsingFlags: true,
execer: cgroupfsCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
defaultHostname: "foo",
},
expected: map[string]string{
"container-runtime": "remote",
@ -225,11 +229,9 @@ func TestBuildKubeletArgMap(t *testing.T) {
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/containerd.sock",
Name: "foo",
},
execer: cgroupfsCgroupExecer,
isServiceActiveFunc: serviceIsActiveFunc,
defaultHostname: "foo",
},
expected: map[string]string{
"container-runtime": "remote",
@ -242,12 +244,10 @@ func TestBuildKubeletArgMap(t *testing.T) {
opts: kubeletFlagsOpts{
nodeRegOpts: &kubeadmapi.NodeRegistrationOptions{
CRISocket: "/var/run/dockershim.sock",
Name: "foo",
},
pauseImage: "gcr.io/pause:3.1",
execer: cgroupfsCgroupExecer,
isServiceActiveFunc: serviceIsNotActiveFunc,
defaultHostname: "foo",
},
expected: map[string]string{
"network-plugin": "cni",