fix: Discover specs from namespaces user is allowed (#1098)

* fix: Discover specs from namespaces user is allowed

If a user has limited access to read secrets and config maps
from certain namespaces in a cluster, we'd need to gracefully
fail when forbidden errors are caught. We'll log them and continue
searching for specs in other namespaces.
This commit is contained in:
Evans Mungai
2023-04-05 07:50:46 +01:00
committed by GitHub
parent 0f3f827974
commit dc1687a76a
3 changed files with 217 additions and 130 deletions

View File

@@ -1,6 +1,7 @@
package cli
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
@@ -27,6 +28,8 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
"github.com/spf13/viper"
spin "github.com/tj/go-spin"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
@@ -34,7 +37,7 @@ import (
)
func runTroubleshoot(v *viper.Viper, arg []string) error {
if v.GetBool("load-cluster-specs") == false && len(arg) < 1 {
if !v.GetBool("load-cluster-specs") && len(arg) < 1 {
return errors.New("flag load-cluster-specs must be set if no specs are provided on the command line")
}
@@ -107,105 +110,15 @@ func runTroubleshoot(v *viper.Viper, arg []string) error {
}
if v.GetBool("load-cluster-specs") {
labelSelector := strings.Join(v.GetStringSlice("selector"), ",")
parsedSelector, err := labels.Parse(labelSelector)
sbFromCluster, redactorsFromCluster, err := loadClusterSpecs()
if err != nil {
return errors.Wrap(err, "unable to parse selector")
return err
}
config, err := k8sutil.GetRESTConfig()
if err != nil {
return errors.Wrap(err, "failed to convert kube flags to rest config")
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return errors.Wrap(err, "failed to convert create k8s client")
}
namespace := ""
if v.GetString("namespace") != "" {
namespace = v.GetString("namespace")
} else {
IsNamespacedScopeRBAC, err := k8sutil.IsNamespacedScopeRBAC(client)
if err != nil {
return errors.Wrap(err, "failed to check if cluster is namespaced")
}
if !IsNamespacedScopeRBAC {
kubeconfig := k8sutil.GetKubeconfig()
namespace, _, _ = kubeconfig.Namespace()
}
}
var bundlesFromCluster []string
// Search cluster for Troubleshoot objects in cluster
bundlesFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), namespace, specs.SupportBundleKey)
if err != nil {
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
}
bundlesFromCluster = append(bundlesFromCluster, bundlesFromSecrets...)
bundlesFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), namespace, specs.SupportBundleKey)
if err != nil {
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
}
bundlesFromCluster = append(bundlesFromCluster, bundlesFromConfigMaps...)
for _, bundle := range bundlesFromCluster {
multidocs := strings.Split(string(bundle), "\n---\n")
parsedBundleFromSecret, err := supportbundle.ParseSupportBundleFromDoc([]byte(multidocs[0]))
if err != nil {
klog.Errorf("failed to parse support bundle spec: %s", err)
continue
}
if mainBundle == nil {
mainBundle = parsedBundleFromSecret
} else {
mainBundle = supportbundle.ConcatSpec(mainBundle, parsedBundleFromSecret)
}
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
if err != nil {
klog.Errorf("failed to parse redactors from doc: %s", err)
continue
}
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, parsedRedactors...)
}
var redactorsFromCluster []string
// Search cluster for Troubleshoot objects in ConfigMaps
redactorsFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), namespace, specs.RedactorKey)
if err != nil {
klog.Errorf("failed to load redactor specs from config maps: %s", err)
}
redactorsFromCluster = append(redactorsFromCluster, redactorsFromSecrets...)
redactorsFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), namespace, specs.RedactorKey)
if err != nil {
klog.Errorf("failed to load redactor specs from config maps: %s", err)
}
redactorsFromCluster = append(redactorsFromCluster, redactorsFromConfigMaps...)
for _, redactor := range redactorsFromCluster {
multidocs := strings.Split(string(redactor), "\n---\n")
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
if err != nil {
klog.Errorf("failed to parse redactors from doc: %s", err)
}
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, parsedRedactors...)
}
if mainBundle == nil {
if sbFromCluster == nil {
return errors.New("no specs found in cluster")
}
mainBundle = supportbundle.ConcatSpec(mainBundle, sbFromCluster)
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, redactorsFromCluster.Spec.Redactors...)
}
if mainBundle == nil {
@@ -346,6 +259,162 @@ the %s Admin Console to begin analysis.`
return nil
}
// loadClusterSpecs loads the support bundle and redactor specs from the cluster
// based on troubleshoot.io/kind=support-bundle label selector. We search for secrets
// and configmaps with the label selector and parse the data as a support bundle. If the
// user does not have sufficient permissions to list & read secrets and configmaps from
// all namespaces, we will fallback to trying each namespace individually, and eventually
// default to the configured kubeconfig namespace.
func loadClusterSpecs() (*troubleshootv1beta2.SupportBundle, *troubleshootv1beta2.Redactor, error) {
var parsedBundle *troubleshootv1beta2.SupportBundle
redactors := &troubleshootv1beta2.Redactor{}
v := viper.GetViper() // It's singleton, so we can use it anywhere
klog.Info("Discover troubleshoot specs from cluster")
labelSelector := strings.Join(v.GetStringSlice("selector"), ",")
parsedSelector, err := labels.Parse(labelSelector)
if err != nil {
return nil, nil, errors.Wrap(err, "unable to parse selector")
}
config, err := k8sutil.GetRESTConfig()
if err != nil {
return nil, nil, errors.Wrap(err, "failed to convert kube flags to rest config")
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to convert create k8s client")
}
// List of namespaces we want to search for secrets and configmaps with support bundle specs
namespaces := []string{}
ctx := context.Background()
if v.GetString("namespace") != "" {
// Just progress with the namespace provided
namespaces = []string{v.GetString("namespace")}
} else {
// Check if I can read secrets and configmaps in all namespaces
ican, err := k8sutil.CanIListAndGetAllSecretsAndConfigMaps(ctx, client)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to check if I can read secrets and configmaps")
}
klog.V(1).Infof("Can I read any secrets and configmaps: %v", ican)
if ican {
// I can read secrets and configmaps in all namespaces
// No need to iterate over all namespaces
namespaces = []string{""}
} else {
// Get list of all namespaces and try to find specs from each namespace
nsList, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
if k8serrors.IsForbidden(err) {
kubeconfig := k8sutil.GetKubeconfig()
ns, _, err := kubeconfig.Namespace()
if err != nil {
return nil, nil, errors.Wrap(err, "failed to get namespace from kubeconfig")
}
// If we are not allowed to list namespaces, just use the default namespace
// configured in the kubeconfig
namespaces = []string{ns}
} else {
return nil, nil, errors.Wrap(err, "failed to list namespaces")
}
}
for _, ns := range nsList.Items {
namespaces = append(namespaces, ns.Name)
}
}
}
var bundlesFromCluster []string
// Search cluster for support bundle specs
klog.V(1).Infof("Search support bundle specs from [%q] namespaces using %q selector", strings.Join(namespaces, ", "), parsedSelector.String())
for _, ns := range namespaces {
bundlesFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), ns, specs.SupportBundleKey)
if err != nil {
if !k8serrors.IsForbidden(err) {
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
} else {
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
}
}
bundlesFromCluster = append(bundlesFromCluster, bundlesFromSecrets...)
bundlesFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), ns, specs.SupportBundleKey)
if err != nil {
if !k8serrors.IsForbidden(err) {
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
} else {
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
}
}
bundlesFromCluster = append(bundlesFromCluster, bundlesFromConfigMaps...)
}
for _, bundle := range bundlesFromCluster {
multidocs := strings.Split(string(bundle), "\n---\n")
parsedBundle, err = supportbundle.ParseSupportBundleFromDoc([]byte(multidocs[0]))
if err != nil {
klog.Errorf("failed to parse support bundle spec: %s", err)
continue
}
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
if err != nil {
klog.Errorf("failed to parse redactors from doc: %s", err)
continue
}
redactors.Spec.Redactors = append(redactors.Spec.Redactors, parsedRedactors...)
}
var redactorsFromCluster []string
// Search cluster for redactor specs
klog.V(1).Infof("Search redactor specs from [%q] namespaces using %q selector", strings.Join(namespaces, ", "), parsedSelector.String())
for _, ns := range namespaces {
redactorsFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), ns, specs.RedactorKey)
if err != nil {
if !k8serrors.IsForbidden(err) {
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
} else {
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
}
}
redactorsFromCluster = append(redactorsFromCluster, redactorsFromSecrets...)
redactorsFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), ns, specs.RedactorKey)
if err != nil {
if !k8serrors.IsForbidden(err) {
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
} else {
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
}
}
redactorsFromCluster = append(redactorsFromCluster, redactorsFromConfigMaps...)
}
for _, redactor := range redactorsFromCluster {
multidocs := strings.Split(string(redactor), "\n---\n")
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
if err != nil {
klog.Errorf("failed to parse redactors from doc: %s", err)
}
redactors.Spec.Redactors = append(redactors.Spec.Redactors, parsedRedactors...)
}
return parsedBundle, redactors, nil
}
func parseTimeFlags(v *viper.Viper) (*time.Time, error) {
var (
sinceTime time.Time

52
pkg/k8sutil/auth.go Normal file
View File

@@ -0,0 +1,52 @@
package k8sutil
import (
"context"
authorizationv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// CanIListAndGetAllSecretsAndConfigMaps checks if the current user can list and get secrets and configmaps
// from all namespaces
func CanIListAndGetAllSecretsAndConfigMaps(ctx context.Context, client kubernetes.Interface) (bool, error) {
canis := []struct{ ns, verb, resource string }{
{"", "get", "secrets"},
{"", "get", "configmaps"},
{"", "list", "secrets"},
{"", "list", "configmaps"},
}
for _, cani := range canis {
ican, err := authCanI(ctx, client, cani.ns, cani.verb, cani.resource)
if err != nil {
return false, err
}
if !ican {
return false, nil
}
}
return true, nil
}
func authCanI(ctx context.Context, client kubernetes.Interface, ns, verb, resource string) (bool, error) {
sar := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: ns,
Verb: verb,
Resource: resource,
},
},
}
resp, err := client.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{})
if err != nil {
return false, err
}
return resp.Status.Allowed, nil
}

View File

@@ -1,34 +0,0 @@
package k8sutil
import (
"context"
authorizationv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
func IsNamespacedScopeRBAC(client kubernetes.Interface) (bool, error) {
ctx := context.Background()
sar := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: "",
Verb: "list",
Resource: "secrets,configmaps",
},
},
}
resp, err := client.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{})
if err != nil {
return false, err
}
if resp.Status.Allowed {
return true, nil
} else {
return false, nil
}
}