-
Notifications
You must be signed in to change notification settings - Fork 257
Forbid kubeconfig exec #2356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Forbid kubeconfig exec #2356
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -25,8 +25,6 @@ import ( | |||||
| "k8s.io/apimachinery/pkg/types" | ||||||
| "k8s.io/apimachinery/pkg/util/sets" | ||||||
| "k8s.io/apimachinery/pkg/util/wait" | ||||||
| "k8s.io/client-go/rest" | ||||||
| "k8s.io/client-go/tools/clientcmd" | ||||||
| "k8s.io/client-go/util/flowcontrol" | ||||||
| "k8s.io/client-go/util/retry" | ||||||
| "k8s.io/client-go/util/workqueue" | ||||||
|
|
@@ -301,7 +299,13 @@ func (r *ReconcileAWSPrivateLink) Reconcile(ctx context.Context, request reconci | |||||
| return reconcile.Result{}, nil | ||||||
| } | ||||||
|
|
||||||
| return r.reconcilePrivateLink(cd, &hivev1.ClusterMetadata{InfraID: *cp.Spec.InfraID, AdminKubeconfigSecretRef: *cp.Spec.AdminKubeconfigSecretRef}, logger) | ||||||
| return r.reconcilePrivateLink( | ||||||
| cd, | ||||||
| &hivev1.ClusterMetadata{ | ||||||
| InfraID: *cp.Spec.InfraID, | ||||||
| AdminKubeconfigSecretRef: *cp.Spec.AdminKubeconfigSecretRef, // HIVE-2585: via ClusterMetadata | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have perms to view this issue but just wanted to ensure this comment is intentional
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You do now. But that does put rather an ick on the posterity argument. Then again, hopefully someone whacking moles in this area later would be given access 🤷
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. D'oh, the comment has a typo here.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't catch it earlier but yeah, this is a typo - 2485 not 2585 |
||||||
| }, | ||||||
| logger) | ||||||
| } | ||||||
|
|
||||||
| // shouldSync returns if we should sync the desired ClusterDeployment. If it returns false, it also returns | ||||||
|
|
@@ -533,7 +537,7 @@ func (r *ReconcileAWSPrivateLink) reconcilePrivateLink(cd *hivev1.ClusterDeploym | |||||
|
|
||||||
| // Figure out the API address for cluster. | ||||||
| apiDomain, err := initialURL(r.Client, | ||||||
| client.ObjectKey{Namespace: cd.Namespace, Name: clusterMetadata.AdminKubeconfigSecretRef.Name}) | ||||||
| client.ObjectKey{Namespace: cd.Namespace, Name: clusterMetadata.AdminKubeconfigSecretRef.Name}) // HIVE-2485 ✓ | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This too |
||||||
| if err != nil { | ||||||
| logger.WithError(err).Error("could not get API URL from kubeconfig") | ||||||
|
|
||||||
|
|
@@ -1303,7 +1307,7 @@ func initialURL(c client.Client, key client.ObjectKey) (string, error) { | |||||
| ); err != nil { | ||||||
| return "", err | ||||||
| } | ||||||
| cfg, err := restConfigFromSecret(kubeconfigSecret) | ||||||
| cfg, err := controllerutils.RestConfigFromSecret(kubeconfigSecret, true) | ||||||
| if err != nil { | ||||||
| return "", errors.Wrap(err, "failed to load the kubeconfig") | ||||||
| } | ||||||
|
|
@@ -1315,22 +1319,6 @@ func initialURL(c client.Client, key client.ObjectKey) (string, error) { | |||||
| return strings.TrimSuffix(u.Hostname(), "."), nil | ||||||
| } | ||||||
|
|
||||||
| func restConfigFromSecret(kubeconfigSecret *corev1.Secret) (*rest.Config, error) { | ||||||
| kubeconfigData := kubeconfigSecret.Data[constants.RawKubeconfigSecretKey] | ||||||
| if len(kubeconfigData) == 0 { | ||||||
| kubeconfigData = kubeconfigSecret.Data[constants.KubeconfigSecretKey] | ||||||
| } | ||||||
| if len(kubeconfigData) == 0 { | ||||||
| return nil, errors.New("kubeconfig secret does not contain necessary data") | ||||||
| } | ||||||
| config, err := clientcmd.Load(kubeconfigData) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| kubeConfig := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}) | ||||||
| return kubeConfig.ClientConfig() | ||||||
| } | ||||||
|
|
||||||
| // awsErrCodeEquals returns true if the error matches all these conditions: | ||||||
| // - err is of type awserr.Error | ||||||
| // - Error.Code() equals code | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,23 +2,76 @@ package utils | |||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "errors" | ||||||
| "fmt" | ||||||
|
|
||||||
| "github.com/openshift/hive/pkg/constants" | ||||||
| corev1 "k8s.io/api/core/v1" | ||||||
| "k8s.io/apimachinery/pkg/types" | ||||||
| "k8s.io/client-go/rest" | ||||||
| "k8s.io/client-go/tools/clientcmd" | ||||||
| "k8s.io/client-go/tools/clientcmd/api" | ||||||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| ) | ||||||
|
|
||||||
| // LoadSecretData loads a given secret key and returns it's data as a string. | ||||||
| // LoadSecretData loads a given secret key and returns its data as a string. | ||||||
| func LoadSecretData(c client.Client, secretName, namespace, dataKey string) (string, error) { | ||||||
| s := &corev1.Secret{} | ||||||
| err := c.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: namespace}, s) | ||||||
| if err != nil { | ||||||
| return "", err | ||||||
| } | ||||||
| retStr, ok := s.Data[dataKey] | ||||||
| retBytes, ok := s.Data[dataKey] | ||||||
| if !ok { | ||||||
| return "", fmt.Errorf("secret %s did not contain key %s", secretName, dataKey) | ||||||
| } | ||||||
| return string(retStr), nil | ||||||
| switch dataKey { | ||||||
| case constants.KubeconfigSecretKey, constants.RawKubeconfigSecretKey: | ||||||
| if _, err := validateKubeconfig(retBytes); err != nil { | ||||||
| return "", err | ||||||
| } | ||||||
| } | ||||||
| return string(retBytes), nil | ||||||
| } | ||||||
|
|
||||||
| // RestConfigFromSecret accepts a Secret containing `kubeconfig` and/or `raw-kubeconfig` keys | ||||||
| // and returns a rest.Config loaded therefrom. | ||||||
| // If tryRaw is true, we will look for `raw-kubeconfig` first and use it if present, falling | ||||||
| // back to `kubeconfig` otherwise. | ||||||
| // The error return is non-nil if: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was intentionally a noun ("the error return variable"?) but I can change it in a fup if it bugs ya :)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh - either makes sense to me so ignore |
||||||
| // - The Secret's Data does not contain the [raw-]kubeconfig key(s) | ||||||
| // - The kubeconfig data cannot be Load()ed | ||||||
| // - The kubeconfig is insecure | ||||||
| func RestConfigFromSecret(kubeconfigSecret *corev1.Secret, tryRaw bool) (*rest.Config, error) { | ||||||
| var kubeconfigData []byte | ||||||
| if tryRaw { | ||||||
| kubeconfigData = kubeconfigSecret.Data[constants.RawKubeconfigSecretKey] | ||||||
| } | ||||||
| if len(kubeconfigData) == 0 { | ||||||
| kubeconfigData = kubeconfigSecret.Data[constants.KubeconfigSecretKey] | ||||||
| } | ||||||
| if len(kubeconfigData) == 0 { | ||||||
| return nil, errors.New("kubeconfig secret does not contain necessary data") | ||||||
| } | ||||||
| config, err := validateKubeconfig(kubeconfigData) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| kubeConfig := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}) | ||||||
| return kubeConfig.ClientConfig() | ||||||
| } | ||||||
|
|
||||||
| // validateKubeconfig ensures the kubeconfig represented by kc does not use insecure paths. | ||||||
| // HIVE-2485 | ||||||
| func validateKubeconfig(kc []byte) (*api.Config, error) { | ||||||
| config, err := clientcmd.Load(kc) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| for k, ai := range config.AuthInfos { | ||||||
| if ai.Exec != nil { | ||||||
| return nil, fmt.Errorf("insecure exec in AuthInfos[%s]", k) | ||||||
| } | ||||||
| } | ||||||
| return config, nil | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this comment intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I wanted to leave a paper trail for posterity since this is a whack-a-mole effort by nature. At worst it's harmless cruft, but ideally if someone comes along later and needs to whack more moles, they will have a useful way to search for these breadcrumbs.
If you think they're more crufty than potentially useful, I can scrub them out in a fup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't have to scrub them per-se, but maybe label them something other than a ticket that's inaccessible to many? Or - how about we include a note where AdminKubeconfigSecretRef is defined, and all someone has to do is
Find usageson that constant to see where we need to change/debug? And if that constant is just fetched and not used, we can include a note there as well. Essentially decouple the ticket and its specific use-case from here's-where-we-actually-use-kubeconfig-to-connect-to-the-cluster.