Skip to content

Commit

Permalink
Default enableStaticTokenKubeconfig to false for Shoots with K8s ve…
Browse files Browse the repository at this point in the history
…rsion >= 1.26

This commit also adapts the testmachinery integration tests to use the `shoots/adminkubeconfig` subresource instead of the static kubeconfig.
  • Loading branch information
ialidzhikov committed Dec 31, 2022
1 parent 365f9ec commit 8110869
Show file tree
Hide file tree
Showing 19 changed files with 166 additions and 86 deletions.
5 changes: 3 additions & 2 deletions docs/api-reference/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -5657,8 +5657,9 @@ bool
</td>
<td>
<em>(Optional)</em>
<p>EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret should be present in garden cluster
(default: true).</p>
<p>EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret will be created for the Shoot cluster.
Defaults to true for Shoots with Kubernetes versions &lt; 1.26. Defaults to false for Shoots with Kubernetes versions &gt;= 1.26.
Starting Kubernetes 1.27 the field will be locked to false.</p>
</td>
</tr>
</tbody>
Expand Down
8 changes: 7 additions & 1 deletion docs/usage/shoot_access.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ It is **not** the recommended method to access the shoot cluster as the static t
- The static token in the `kubeconfig` doesn't have any expiration date. Read [this document](shoot_credentials_rotation.md#kubeconfig) to learn how to rotate the static token.
- The static token doesn't have any user identity associated with it. The user in that token will always be `system:cluster-admin` irrespective of the person accessing the cluster. Hence, it is impossible to audit the events in cluster.

When `enableStaticTokenKubeconfig` field is not explicitly set in the Shoot spec:
- for Shoot clusters using Kubernetes version < 1.26 the field is defaulted to `true`.
- for Shoot clusters using Kubernetes version >= 1.26 the field is defaulted to `false`.

> Note: Starting Kubernetes 1.27 the `enableStaticTokenKubeconfig` field will be locked to `false`. The [`shoots/adminkubeconfig` subresource](#shootsadminkubeconfig-subresource) should be used instead.
## `shoots/adminkubeconfig` subresource

The [`shoots/adminkubeconfig`](../proposals/16-adminkubeconfig-subresource.md) subresource allows users to dynamically generate temporary `kubeconfig`s that can be used to access shoot cluster with `cluster-admin` privileges. The credentials associated with this `kubeconfig` are client certificates which have a very short validity and must be renewed before they expire (by calling the subresource endpoint again).
Expand Down Expand Up @@ -53,7 +59,7 @@ Here, the `kubeconfig-request.json` has the following content:
## OpenID Connect

The `kube-apiserver` of shoot clusters can be provided with [OpenID Connect configuration](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) via the `ShootSpec`:
The `kube-apiserver` of shoot clusters can be provided with [OpenID Connect configuration](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) via the Shoot spec:

```yaml
apiVersion: core.gardener.cloud/v1beta1
Expand Down
2 changes: 1 addition & 1 deletion hack/local-development/start-apiserver
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ apiserver_flags="
--tls-private-key-file $TLS_KEY_FILE \
--feature-gates HAControlPlanes=true \
--feature-gates SeedChange=true \
--shoot-admin-kubeconfig-max-expiration=1h \
--shoot-admin-kubeconfig-max-expiration=24h \
--enable-admission-plugins=ShootVPAEnabledByDefault \
--v 2"

Expand Down
4 changes: 3 additions & 1 deletion pkg/apis/core/types_shoot.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,9 @@ type Kubernetes struct {
Version string
// VerticalPodAutoscaler contains the configuration flags for the Kubernetes vertical pod autoscaler.
VerticalPodAutoscaler *VerticalPodAutoscaler
// EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret will be created for shoot (default: true).
// EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret will be created for the Shoot cluster.
// Defaults to true for Shoots with Kubernetes versions < 1.26. Defaults to false for Shoots with Kubernetes versions >= 1.26.
// Starting Kubernetes 1.27 the field will be locked to false.
EnableStaticTokenKubeconfig *bool
}

Expand Down
7 changes: 6 additions & 1 deletion pkg/apis/core/v1alpha1/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,12 @@ func SetDefaults_Shoot(obj *Shoot) {
}

if obj.Spec.Kubernetes.EnableStaticTokenKubeconfig == nil {
obj.Spec.Kubernetes.EnableStaticTokenKubeconfig = pointer.Bool(true)
// Error is ignored here because we cannot do anything meaningful with it - variable will default to "false".
if k8sLessThan126, _ := versionutils.CheckVersionMeetsConstraint(obj.Spec.Kubernetes.Version, "< 1.26"); k8sLessThan126 {
obj.Spec.Kubernetes.EnableStaticTokenKubeconfig = pointer.Bool(true)
} else {
obj.Spec.Kubernetes.EnableStaticTokenKubeconfig = pointer.Bool(false)
}
}

if obj.Spec.Addons == nil {
Expand Down
30 changes: 27 additions & 3 deletions pkg/apis/core/v1alpha1/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,11 +734,35 @@ var _ = Describe("Defaults", func() {
Expect(obj.Spec.SystemComponents).To(Equal(&SystemComponents{CoreDNS: &CoreDNS{Autoscaling: &CoreDNSAutoscaling{Mode: CoreDNSAutoscalingModeHorizontal}}}))
})

It("should default the enableStaticTokenKubeconfig field", func() {
SetDefaults_Shoot(obj)
Context("static token kubeconfig", func() {
It("should not default the enableStaticTokenKubeconfig field when it is set", func() {
obj.Spec.Kubernetes = Kubernetes{
Version: "1.24.0",
EnableStaticTokenKubeconfig: pointer.Bool(false),
}

SetDefaults_Shoot(obj)

Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeTrue()))
Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeFalse()))
})

It("should default the enableStaticTokenKubeconfig field to true for k8s version < 1.26", func() {
obj.Spec.Kubernetes = Kubernetes{Version: "1.25.0"}

SetDefaults_Shoot(obj)

Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeTrue()))
})

It("should default the enableStaticTokenKubeconfig field to false for k8s version >= 1.26", func() {
obj.Spec.Kubernetes = Kubernetes{Version: "1.26.0"}

SetDefaults_Shoot(obj)

Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeFalse()))
})
})

Context("k8s version < 1.25", func() {
BeforeEach(func() {
obj.Spec.Kubernetes = Kubernetes{
Expand Down
5 changes: 3 additions & 2 deletions pkg/apis/core/v1alpha1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pkg/apis/core/v1alpha1/types_shoot.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,9 @@ type Kubernetes struct {
// VerticalPodAutoscaler contains the configuration flags for the Kubernetes vertical pod autoscaler.
// +optional
VerticalPodAutoscaler *VerticalPodAutoscaler `json:"verticalPodAutoscaler,omitempty" protobuf:"bytes,9,opt,name=verticalPodAutoscaler"`
// EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret should be present in garden cluster
// (default: true).
// EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret will be created for the Shoot cluster.
// Defaults to true for Shoots with Kubernetes versions < 1.26. Defaults to false for Shoots with Kubernetes versions >= 1.26.
// Starting Kubernetes 1.27 the field will be locked to false.
// +optional
EnableStaticTokenKubeconfig *bool `json:"enableStaticTokenKubeconfig,omitempty" protobuf:"varint,10,opt,name=enableStaticTokenKubeconfig"`
}
Expand Down
9 changes: 7 additions & 2 deletions pkg/apis/core/v1beta1/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,12 @@ func SetDefaults_Shoot(obj *Shoot) {
}

if obj.Spec.Kubernetes.EnableStaticTokenKubeconfig == nil {
obj.Spec.Kubernetes.EnableStaticTokenKubeconfig = pointer.Bool(true)
// Error is ignored here because we cannot do anything meaningful with it - variable will default to "false".
if k8sLessThan126, _ := versionutils.CheckVersionMeetsConstraint(obj.Spec.Kubernetes.Version, "< 1.26"); k8sLessThan126 {
obj.Spec.Kubernetes.EnableStaticTokenKubeconfig = pointer.Bool(true)
} else {
obj.Spec.Kubernetes.EnableStaticTokenKubeconfig = pointer.Bool(false)
}
}

if obj.Spec.Addons == nil {
Expand Down Expand Up @@ -296,7 +301,7 @@ func SetDefaults_Shoot(obj *Shoot) {

if k8sVersionGreaterOrEqualThan122, _ := versionutils.CompareVersions(kubernetesVersion, ">=", "1.22"); !k8sVersionGreaterOrEqualThan122 {
// Error is ignored here because we cannot do anything meaningful with it.
// k8sVersionLessThan116 and k8sVersionGreaterOrEqualThan122 will default to `false`.
// k8sVersionGreaterOrEqualThan122 will default to `false`.
continue
}

Expand Down
43 changes: 26 additions & 17 deletions pkg/apis/core/v1beta1/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,33 @@ var _ = Describe("Defaults", func() {
Expect(obj.Spec.SystemComponents).To(Equal(&SystemComponents{CoreDNS: &CoreDNS{Autoscaling: &CoreDNSAutoscaling{Mode: CoreDNSAutoscalingModeHorizontal}}}))
})

It("should default the enableStaticTokenKubeconfig field", func() {
SetDefaults_Shoot(obj)
Context("static token kubeconfig", func() {
It("should not default the enableStaticTokenKubeconfig field when it is set", func() {
obj.Spec.Kubernetes = Kubernetes{
Version: "1.24.0",
EnableStaticTokenKubeconfig: pointer.Bool(false),
}

SetDefaults_Shoot(obj)

Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeFalse()))
})

It("should default the enableStaticTokenKubeconfig field to true for k8s version < 1.26", func() {
obj.Spec.Kubernetes = Kubernetes{Version: "1.25.0"}

SetDefaults_Shoot(obj)

Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeTrue()))
})

It("should default the enableStaticTokenKubeconfig field to false for k8s version >= 1.26", func() {
obj.Spec.Kubernetes = Kubernetes{Version: "1.26.0"}

SetDefaults_Shoot(obj)

Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeTrue()))
Expect(obj.Spec.Kubernetes.EnableStaticTokenKubeconfig).To(PointTo(BeFalse()))
})
})

Context("k8s version < 1.25", func() {
Expand Down Expand Up @@ -782,20 +805,6 @@ var _ = Describe("Defaults", func() {
})
})
})

Context("k8s version >= 1.25", func() {
BeforeEach(func() {
obj.Spec.Kubernetes.Version = "1.25.0"
})

Context("allowPrivilegedContainers field is not set", func() {
It("should not set the field", func() {
SetDefaults_Shoot(obj)

Expect(obj.Spec.Kubernetes.AllowPrivilegedContainers).To(BeNil())
})
})
})
})

Describe("#SetDefaults_Maintenance", func() {
Expand Down
5 changes: 3 additions & 2 deletions pkg/apis/core/v1beta1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pkg/apis/core/v1beta1/types_shoot.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,9 @@ type Kubernetes struct {
// VerticalPodAutoscaler contains the configuration flags for the Kubernetes vertical pod autoscaler.
// +optional
VerticalPodAutoscaler *VerticalPodAutoscaler `json:"verticalPodAutoscaler,omitempty" protobuf:"bytes,9,opt,name=verticalPodAutoscaler"`
// EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret should be present in garden cluster
// (default: true).
// EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret will be created for the Shoot cluster.
// Defaults to true for Shoots with Kubernetes versions < 1.26. Defaults to false for Shoots with Kubernetes versions >= 1.26.
// Starting Kubernetes 1.27 the field will be locked to false.
// +optional
EnableStaticTokenKubeconfig *bool `json:"enableStaticTokenKubeconfig,omitempty" protobuf:"varint,10,opt,name=enableStaticTokenKubeconfig"`
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/openapi/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 15 additions & 5 deletions test/framework/k8s_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,15 +396,25 @@ func GetObjectFromSecret(ctx context.Context, k8sClient kubernetes.Interface, na
return "", fmt.Errorf("secret %s/%s did not contain object key %q", namespace, secretName, objectKey)
}

// NewClientFromServiceAccount returns a kubernetes client for a service account.
func NewClientFromServiceAccount(ctx context.Context, k8sClient kubernetes.Interface, account *corev1.ServiceAccount) (kubernetes.Interface, error) {
// CreateTokenForServiceAccount requests a service account token.
func CreateTokenForServiceAccount(ctx context.Context, k8sClient kubernetes.Interface, serviceAccount *corev1.ServiceAccount, expirationSeconds *int64) (string, error) {
tokenRequest := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
ExpirationSeconds: pointer.Int64(3600),
ExpirationSeconds: expirationSeconds,
},
}

token, err := k8sClient.Kubernetes().CoreV1().ServiceAccounts(account.Namespace).CreateToken(ctx, account.Name, tokenRequest, metav1.CreateOptions{})
result, err := k8sClient.Kubernetes().CoreV1().ServiceAccounts(serviceAccount.Namespace).CreateToken(ctx, serviceAccount.Name, tokenRequest, metav1.CreateOptions{})
if err != nil {
return "", err
}

return result.Status.Token, nil
}

// NewClientFromServiceAccount returns a kubernetes client for a service account.
func NewClientFromServiceAccount(ctx context.Context, k8sClient kubernetes.Interface, serviceAccount *corev1.ServiceAccount) (kubernetes.Interface, error) {
token, err := CreateTokenForServiceAccount(ctx, k8sClient, serviceAccount, pointer.Int64(3600))
if err != nil {
return nil, err
}
Expand All @@ -415,7 +425,7 @@ func NewClientFromServiceAccount(ctx context.Context, k8sClient kubernetes.Inter
Insecure: false,
CAData: k8sClient.RESTConfig().CAData,
},
BearerToken: token.Status.Token,
BearerToken: token,
}

return kubernetes.NewWithConfig(
Expand Down
13 changes: 5 additions & 8 deletions test/framework/shootframework.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import (
v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
"github.com/gardener/gardener/pkg/apis/core/v1beta1/helper"
"github.com/gardener/gardener/pkg/client/kubernetes"
gutil "github.com/gardener/gardener/pkg/utils/gardener"
kutil "github.com/gardener/gardener/pkg/utils/kubernetes"
"github.com/gardener/gardener/pkg/utils/retry"
"github.com/gardener/gardener/test/utils/shoots/access"

"github.com/onsi/ginkgo/v2"
appsv1 "k8s.io/api/apps/v1"
Expand Down Expand Up @@ -156,9 +156,8 @@ func (f *ShootFramework) AddShoot(ctx context.Context, shootName, shootNamespace
}

var (
shootClient kubernetes.Interface
shoot = &gardencorev1beta1.Shoot{}
err error
shoot = &gardencorev1beta1.Shoot{}
err error
)

if err := f.GardenClient.Client().Get(ctx, client.ObjectKey{Namespace: shootNamespace, Name: shootName}, shoot); err != nil {
Expand Down Expand Up @@ -190,11 +189,9 @@ func (f *ShootFramework) AddShoot(ctx context.Context, shootName, shootNamespace
}

if !f.GardenerFramework.Config.SkipAccessingShoot {
var shootClient kubernetes.Interface
if err := retry.UntilTimeout(ctx, k8sClientInitPollInterval, k8sClientInitTimeout, func(ctx context.Context) (bool, error) {
shootClient, err = kubernetes.NewClientFromSecret(ctx, f.GardenClient.Client(), shoot.Namespace, shoot.Name+"."+gutil.ShootProjectSecretSuffixKubeconfig,
kubernetes.WithClientOptions(client.Options{Scheme: kubernetes.ShootScheme}),
kubernetes.WithDisabledCachedClient(),
)
shootClient, err = access.CreateShootClientFromAdminKubeconfig(ctx, f.GardenClient, f.Shoot)
if err != nil {
return retry.MinorError(fmt.Errorf("could not construct Shoot client: %w", err))
}
Expand Down
29 changes: 14 additions & 15 deletions test/testmachinery/shoots/applications/shoot_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,14 @@ import (
"fmt"
"time"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
gutil "github.com/gardener/gardener/pkg/utils/gardener"
v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
"github.com/gardener/gardener/test/framework"
"github.com/gardener/gardener/test/framework/applications"
clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest"
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"

"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
)

const (
Expand All @@ -58,7 +57,7 @@ var _ = ginkgo.Describe("Shoot application testing", func() {
f := framework.NewShootFramework(nil)

f.Default().Release().CIt("should download shoot kubeconfig successfully", func(ctx context.Context) {
err := framework.DownloadKubeconfig(ctx, f.SeedClient, f.ShootSeedNamespace(), gardencorev1beta1.GardenerName, "")
err := framework.DownloadKubeconfig(ctx, f.SeedClient, f.ShootSeedNamespace(), v1beta1constants.SecretNameGardener, "")
framework.ExpectNoError(err)

ginkgo.By("Shoot Kubeconfig downloaded successfully from seed")
Expand All @@ -85,20 +84,20 @@ var _ = ginkgo.Describe("Shoot application testing", func() {
f.Default().Release().CIt("Dashboard should be available", func(ctx context.Context) {
shoot := f.Shoot
if !shoot.Spec.Addons.KubernetesDashboard.Enabled {
ginkgo.Fail("The test requires .spec.addons.kubernetesDashboard.enabled to be be true")
ginkgo.Fail("The test requires .spec.addons.kubernetesDashboard.enabled to be true")
}

url := fmt.Sprintf("https://api.%s/api/v1/namespaces/%s/services/https:kubernetes-dashboard:/proxy", *f.Shoot.Spec.DNS.Domain, "kubernetes-dashboard")
kubeconfigData, err := framework.GetObjectFromSecret(ctx, f.GardenClient, f.Shoot.Namespace, f.Shoot.Name+"."+gutil.ShootProjectSecretSuffixKubeconfig, framework.KubeconfigSecretKeyName)
framework.ExpectNoError(err)
kubeconfig := &clientcmdv1.Config{}
_, _, err = clientcmdlatest.Codec.Decode([]byte(kubeconfigData), nil, kubeconfig)
serviceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: v1beta1constants.SecretNameGardener,
Namespace: metav1.NamespaceSystem,
},
}
token, err := framework.CreateTokenForServiceAccount(ctx, f.ShootClient, serviceAccount, pointer.Int64(3600))
framework.ExpectNoError(err)
Expect(kubeconfig.AuthInfos).To(HaveLen(1))
Expect(kubeconfig.AuthInfos[0].AuthInfo.Token).NotTo(BeEmpty())
dashboardToken := kubeconfig.AuthInfos[0].AuthInfo.Token

err = framework.TestHTTPEndpointWithToken(ctx, url, dashboardToken)
err = framework.TestHTTPEndpointWithToken(ctx, url, token)
framework.ExpectNoError(err)
}, dashboardAvailableTimeout)

Expand Down

0 comments on commit 8110869

Please sign in to comment.