Skip to content
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

[v14] Prevent double registration of Kubernetes GVK for older Kube clusters #33402

Merged
merged 2 commits into from Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 53 additions & 0 deletions lib/kube/proxy/resource_deletecollection.go
Expand Up @@ -29,6 +29,7 @@ import (
batchv1 "k8s.io/api/batch/v1"
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
authv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -415,6 +416,58 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe
return internalErrStatus, trace.Wrap(err)
}
o.Items = pointerArrayToArray(items)
case *extensionsv1beta1.IngressList:
items, err := deleteResources(
params,
types.KindKubeIngress,
arrayToPointerArray(o.Items),
func(ctx context.Context, client kubernetes.Interface, name, namespace string) error {
return trace.Wrap(client.ExtensionsV1beta1().Ingresses(namespace).Delete(ctx, name, deleteOptions))
},
)
if err != nil {
return internalErrStatus, trace.Wrap(err)
}
o.Items = pointerArrayToArray(items)
case *extensionsv1beta1.DaemonSetList:
items, err := deleteResources(
params,
types.KindKubeDaemonSet,
arrayToPointerArray(o.Items),
func(ctx context.Context, client kubernetes.Interface, name, namespace string) error {
return trace.Wrap(client.ExtensionsV1beta1().DaemonSets(namespace).Delete(ctx, name, deleteOptions))
},
)
if err != nil {
return internalErrStatus, trace.Wrap(err)
}
o.Items = pointerArrayToArray(items)
case *extensionsv1beta1.DeploymentList:
items, err := deleteResources(
params,
types.KindKubeDeployment,
arrayToPointerArray(o.Items),
func(ctx context.Context, client kubernetes.Interface, name, namespace string) error {
return trace.Wrap(client.ExtensionsV1beta1().Deployments(namespace).Delete(ctx, name, deleteOptions))
},
)
if err != nil {
return internalErrStatus, trace.Wrap(err)
}
o.Items = pointerArrayToArray(items)
case *extensionsv1beta1.ReplicaSetList:
items, err := deleteResources(
params,
types.KindKubeReplicaSet,
arrayToPointerArray(o.Items),
func(ctx context.Context, client kubernetes.Interface, name, namespace string) error {
return trace.Wrap(client.ExtensionsV1beta1().ReplicaSets(namespace).Delete(ctx, name, deleteOptions))
},
)
if err != nil {
return internalErrStatus, trace.Wrap(err)
}
o.Items = pointerArrayToArray(items)
default:
return internalErrStatus, trace.BadParameter("unexpected type %T", obj)
}
Expand Down
62 changes: 61 additions & 1 deletion lib/kube/proxy/resource_filters.go
Expand Up @@ -26,6 +26,7 @@ import (
batchv1 "k8s.io/api/batch/v1"
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
authv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -491,6 +492,66 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *extensionsv1beta1.Ingress:
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil {
d.log.WithError(err).Warn("Unable to compile regex expressions within kubernetes_resources.")
}
// if err is not nil or result is false, we should not include it.
return result, false, nil
case *extensionsv1beta1.IngressList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil

case *extensionsv1beta1.DaemonSet:
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil {
d.log.WithError(err).Warn("Unable to compile regex expressions within kubernetes_resources.")
}
// if err is not nil or result is false, we should not include it.
return result, false, nil
case *extensionsv1beta1.DaemonSetList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil

case *extensionsv1beta1.Deployment:
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil {
d.log.WithError(err).Warn("Unable to compile regex expressions within kubernetes_resources.")
}
// if err is not nil or result is false, we should not include it.
return result, false, nil
case *extensionsv1beta1.DeploymentList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil

case *extensionsv1beta1.ReplicaSet:
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil {
d.log.WithError(err).Warn("Unable to compile regex expressions within kubernetes_resources.")
}
// if err is not nil or result is false, we should not include it.
return result, false, nil
case *extensionsv1beta1.ReplicaSetList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil

case *unstructured.Unstructured:
if o.IsList() {
hasElemts := filterUnstructuredList(d.verb, o, d.allowedResources, d.deniedResources, d.log)
Expand All @@ -514,7 +575,6 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
return false, false, trace.Wrap(err)
}
return len(o.Rows) > 0, true, nil

default:
// It's important default types are never blindly forwarded or protocol
// extensions could result in information disclosures.
Expand Down
9 changes: 9 additions & 0 deletions lib/kube/proxy/resource_filters_test.go
Expand Up @@ -31,6 +31,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
authv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -192,6 +193,14 @@ func Test_filterBuffer(t *testing.T) {
resources = collectResourcesFromResponse(arrayToPointerArray(o.Items))
case *networkingv1.IngressList:
resources = collectResourcesFromResponse(arrayToPointerArray(o.Items))
case *extensionsv1beta1.IngressList:
resources = collectResourcesFromResponse(arrayToPointerArray(o.Items))
case *extensionsv1beta1.DaemonSetList:
resources = collectResourcesFromResponse(arrayToPointerArray(o.Items))
case *extensionsv1beta1.ReplicaSetList:
resources = collectResourcesFromResponse(arrayToPointerArray(o.Items))
case *extensionsv1beta1.DeploymentList:
resources = collectResourcesFromResponse(arrayToPointerArray(o.Items))
case *metav1.Table:
for i := range o.Rows {
row := &(o.Rows[i])
Expand Down
14 changes: 10 additions & 4 deletions lib/kube/proxy/scheme.go
Expand Up @@ -99,7 +99,7 @@ func newClientNegotiator(codecFactory *serializer.CodecFactory) runtime.ClientNe
// This schema includes all well-known Kubernetes types and all namespaced
// custom resources.
// It also returns a map of resources that we support RBAC restrictions for.
func newClusterSchemaBuilder(client *kubernetes.Clientset) (serializer.CodecFactory, rbacSupportedResources, error) {
func newClusterSchemaBuilder(client kubernetes.Interface) (serializer.CodecFactory, rbacSupportedResources, error) {
kubeScheme := runtime.NewScheme()
kubeCodecs := serializer.NewCodecFactory(kubeScheme)
supportedResources := maps.Clone(defaultRBACResources)
Expand All @@ -110,7 +110,7 @@ func newClusterSchemaBuilder(client *kubernetes.Clientset) (serializer.CodecFact
// discoveryErr is returned when the discovery of one or more API groups fails.
var discoveryErr *discovery.ErrGroupDiscoveryFailed
// register all namespaced custom resources
_, apiGroups, err := client.DiscoveryClient.ServerGroupsAndResources()
_, apiGroups, err := client.Discovery().ServerGroupsAndResources()
switch {
case errors.As(err, &discoveryErr) && len(discoveryErr.Groups) == 1:
// If the discovery error is of type `ErrGroupDiscoveryFailed` and it
Expand Down Expand Up @@ -154,11 +154,17 @@ func newClusterSchemaBuilder(client *kubernetes.Clientset) (serializer.CodecFact
// the namespace where the resource is located.
// This means that we need to map the resource to the namespace kind.
supportedResources[resourceKey] = utils.KubeCustomResource

// create the group version kind for the resource
gvk := groupVersion.WithKind(apiResource.Kind)
// check if the resource is already registered in the scheme
// if it is, we don't need to register it again.
if _, err := kubeScheme.New(gvk); err == nil {
continue
}
// register the resource with the scheme to be able to decode it
// into an unstructured object
kubeScheme.AddKnownTypeWithName(
groupVersion.WithKind(apiResource.Kind),
gvk,
&unstructured.Unstructured{},
)
// register the resource list with the scheme to be able to decode it
Expand Down
57 changes: 57 additions & 0 deletions lib/kube/proxy/scheme_test.go
@@ -0,0 +1,57 @@
/*
Copyright 2022 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package proxy

import (
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
)

// TestNewClusterSchemaBuilder tests that newClusterSchemaBuilder doesn't panic
// when it's given types already registered in the global scheme.
func Test_newClusterSchemaBuilder(t *testing.T) {
_, _, err := newClusterSchemaBuilder(&clientSet{})
require.NoError(t, err)
}

type clientSet struct {
kubernetes.Interface
discovery.DiscoveryInterface
}

func (c *clientSet) Discovery() discovery.DiscoveryInterface {
return c
}

func (c *clientSet) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return nil, []*metav1.APIResourceList{
{
GroupVersion: "extensions/v1beta1",
APIResources: []metav1.APIResource{
{
Name: "ingresses",
Kind: "Ingress",
Namespaced: true,
},
},
},
}, nil
}
4 changes: 4 additions & 0 deletions lib/kube/proxy/url.go
Expand Up @@ -203,6 +203,10 @@ var defaultRBACResources = rbacSupportedResources{
{apiGroup: "batch", resourceKind: "jobs"}: types.KindKubeJob,
{apiGroup: "certificates.k8s.io", resourceKind: "certificatesigningrequests"}: types.KindKubeCertificateSigningRequest,
{apiGroup: "networking.k8s.io", resourceKind: "ingresses"}: types.KindKubeIngress,
{apiGroup: "extensions", resourceKind: "deployments"}: types.KindKubeDeployment,
{apiGroup: "extensions", resourceKind: "replicasets"}: types.KindKubeReplicaSet,
{apiGroup: "extensions", resourceKind: "daemonsets"}: types.KindKubeDaemonSet,
{apiGroup: "extensions", resourceKind: "ingresses"}: types.KindKubeIngress,
}

// getResourceFromRequest returns a KubernetesResource if the user tried to access
Expand Down