Skip to content

Commit

Permalink
Handle extension ServiceAccounts in Seed{Authorizer,Restriction} (g…
Browse files Browse the repository at this point in the history
…ardener#8264)

* Update docs for feature and `Seed{Authorizer,Restriction}`

* Accept extension clients in `Seed{Authorizer,Restriction}`

Co-Authored-By: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com>

* Prevent privilege escalation by extension clients

Extension clients are only allowed read access to `ServiceAccounts`, `ClusterRoleBindings`, and `CertificateSigningRequests`.

Co-Authored-By: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com>

* Restrict extension operations for `leases` to seed namespace

* Document how we want to handle future use cases for exceptions

* Verify garden access in e2e tests

* Address review suggestions

* Address review suggestions (2)

---------

Co-authored-by: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com>
  • Loading branch information
timebertt and dergeberl authored Jul 27, 2023
1 parent 81b9671 commit 44e8950
Show file tree
Hide file tree
Showing 11 changed files with 4,155 additions and 3,345 deletions.
15 changes: 11 additions & 4 deletions cmd/gardener-extension-provider-local/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,17 +322,24 @@ func NewControllerManagerCommand(ctx context.Context) *cobra.Command {
// verifyGardenAccess uses the extension's access to the garden cluster to request objects related to the seed it is
// running on, but doesn't do anything useful with the objects. We do this for verifying the extension's garden access
// in e2e tests. If something fails in this runnable, the extension will crash loop.
func verifyGardenAccess(ctx context.Context, log logr.Logger, c client.Reader, seedName string) error {
log = log.WithName("garden-reader").WithValues("seedName", seedName)
func verifyGardenAccess(ctx context.Context, log logr.Logger, c client.Client, seedName string) error {
log = log.WithName("garden-access").WithValues("seedName", seedName)

log.Info("Getting Seed")
log.Info("Reading Seed")
// NB: reading seeds is allowed by gardener.cloud:system:read-global-resources (bound to all authenticated users)
seed := &gardencorev1beta1.Seed{}
if err := c.Get(ctx, client.ObjectKey{Name: seedName}, seed); err != nil {
return fmt.Errorf("failed reading seed %s: %w", seedName, err)
}

log.Info("Garden access successfully verified")
log.Info("Annotating Seed")
patch := client.MergeFrom(seed.DeepCopy())
metav1.SetMetaDataAnnotation(&seed.ObjectMeta, "provider-local-e2e-test-garden-access", time.Now().UTC().Format(time.RFC3339))
if err := c.Patch(ctx, seed, patch); err != nil {
return fmt.Errorf("failed annotating seed %s: %w", seedName, err)
}

log.Info("Garden access successfully verified")
return nil
}

Expand Down
53 changes: 41 additions & 12 deletions docs/deployment/gardenlet_api_access.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: gardenlet API Access
title: Scoped API Access for gardenlets and Extensions
---

# Scoped API Access for gardenlets
# Scoped API Access for gardenlets and Extensions

By default, `gardenlet`s have administrative access in the garden cluster.
They are able to execute any API request on any object independent of whether the object is related to the seed cluster the `gardenlet` is responsible for.
Expand All @@ -17,6 +17,14 @@ It is a special-purpose admission plugin that specifically limits the Kubernetes
📚 You might be interested to look into the [design proposal for scoped Kubelet API access](https://github.com/kubernetes/design-proposals-archive/blob/main/node/kubelet-authorizer.md) from the Kubernetes community.
It can be translated to Gardener and Gardenlets with their `Seed` and `Shoot` resources.

Historically, `gardenlet` has been the only component running in the seed cluster that has access to both the seed cluster and the garden cluster.
Starting from Gardener [`v1.74.0`](https://github.com/gardener/gardener/releases/v1.74.0), extensions running on seed clusters can also get [access to the garden cluster](../extensions/garden-api-access.md) using a token for a dedicated ServiceAccount.
Extensions using this mechanism only get permission to read global resources like `CloudProfiles` (this is granted to all authenticated users) unless the plugins described in this document are enabled.

Generally, the plugins handle extension clients exactly like gardenlet clients with some minor [exceptions](#rule-exceptions-for-extension-clients).
Extension clients in the sense of the plugins are clients authenticated as a `ServiceAccount` with the `extension-` name prefix in a `seed-` namespace of the garden cluster.
Other `ServiceAccounts` are not considered as seed clients, not handled by the plugins, and only get the described read access to global resources.

## Flow Diagram

The following diagram shows how the two plugins are included in the request flow of a `gardenlet`.
Expand All @@ -30,6 +38,8 @@ Please note that the example shows a request to an object (`Shoot`) residing in
However, the `gardenlet` is also interacting with objects in API groups served by the `kube-apiserver` (e.g., `Secret`,`ConfigMap`).
In this case, the consultation of the `SeedRestriction` admission plugin is performed by the `kube-apiserver` itself before it forwards the request to the `gardener-apiserver`.

## Implemented Rules

Today, the following rules are implemented:

| Resource | Verbs | Path(s) | Description |
Expand Down Expand Up @@ -61,6 +71,17 @@ Today, the following rules are implemented:
ℹ️ It is allowed to delete the `Seed` resources if the corresponding `ManagedSeed` objects already have a `deletionTimestamp` (this is secure as `gardenlet`s themselves don't have permissions for deleting `ManagedSeed`s).

### Rule Exceptions for Extension Clients

Extension clients are allowed to perform the same operations as gardenlet clients with the following exceptions:

- Extension clients are granted the read-only subset of verbs for `CertificateSigningRequests`, `ClusterRoleBindings`, and `ServiceAccounts` (to prevent privilege escalation).
- Extension clients are granted full access to `Lease` objects but only in the seed-specific namespace.

When the need arises, more exceptions might be added to the access rules for resources that are already handled by the plugins.
E.g., if an extension needs to populate additional shoot-specific `InternalSecrets`, according handling can be introduced.
Permissions for resources that are not handled by the plugins can be granted using additional RBAC rules (independent of the plugins).

## `SeedAuthorizer` Authorization Webhook Enablement

The `SeedAuthorizer` is implemented as a [Kubernetes authorization webhook](https://kubernetes.io/docs/reference/access-authn-authz/webhook/) and part of the [`gardener-admission-controller`](../concepts/admission-controller.md) component running in the garden cluster.
Expand Down Expand Up @@ -118,15 +139,23 @@ As mentioned earlier, it's the authorizer's job to evaluate API requests and ret
For backwards compatibility, no requests are denied at the moment, so that they are still deferred to a subsequent authorizer like RBAC.
Though, this might change in the future.

First, the `SeedAuthorizer` extracts the `Seed` name from the API request. This requires a proper TLS certificate that the `gardenlet` uses to contact the API server and is automatically given if [TLS bootstrapping](../concepts/gardenlet.md#TLS-Bootstrapping) is used.
Concretely, the authorizer checks the certificate for name `gardener.cloud:system:seed:<seed-name>` and group `gardener.cloud:system:seeds`.
In cases where this information is missing e.g., when a custom Kubeconfig is used, the authorizer cannot make any decision. Thus, RBAC is still a considerable option to restrict the `gardenlet`'s access permission if the above explained preconditions are not given.
First, the `SeedAuthorizer` extracts the `Seed` name from the API request.
This step considers the following two cases:

1. If the authenticated user belongs to the `gardener.cloud:system:seeds` group, it is considered a gardenlet client.
- This requires a proper TLS certificate that the `gardenlet` uses to contact the API server and is automatically given if [TLS bootstrapping](../concepts/gardenlet.md#TLS-Bootstrapping) is used.
- The authorizer extracts the seed name from the username by stripping the `gardener.cloud:system:seed:` prefix.
- In cases where this information is missing e.g., when a custom Kubeconfig is used, the authorizer cannot make any decision. Thus, RBAC is still a considerable option to restrict the `gardenlet`'s access permission if the above explained preconditions are not given.
2. If the authenticated user belongs to the `system:serviceaccounts` group, it is considered an extension client under the following conditions:
- The `ServiceAccount` must be located in a `seed-` namespace. I.e., the user has to belong to a group with the `system:serviceaccounts:seed-` prefix. The seed name is extracted from this group by stripping the prefix.
- The `ServiceAccount` must have the `extension-` prefix. I.e., the username must have the `system:serviceaccount:seed-<seed-name>:extension-` prefix.

With the `Seed` name at hand, the authorizer checks for an **existing path** from the resource that a request is being made for to the `Seed` belonging to the `gardenlet`. Take a look at the [Implementation Details](#implementation-details) section for more information.
With the `Seed` name at hand, the authorizer checks for an **existing path** from the resource that a request is being made for to the `Seed` belonging to the `gardenlet`/extension.
Take a look at the [Implementation Details](#implementation-details) section for more information.

### Implementation Details

Internally, the `SeedAuthorizer` uses a directed, acyclic graph data structure in order to efficiently respond to authorization requests for `gardenlet`s:
Internally, the `SeedAuthorizer` uses a directed, acyclic graph data structure in order to efficiently respond to authorization requests for `gardenlet`s/extensions:

* A vertex in this graph represents a Kubernetes resource with its kind, namespace, and name (e.g., `Shoot:garden-my-project/my-shoot`).
* An edge from vertex `u` to vertex `v` in this graph exists when
Expand All @@ -143,10 +172,10 @@ In the above picture, the resources that are actively watched are shaded.
Gardener resources are green, while Kubernetes resources are blue.
It shows the dependencies between the resources and how the graph is built based on the above rules.

ℹ️ The above picture shows all resources that may be accessed by `gardenlet`s, except for the `Quota` resource which is only included for completeness.
ℹ️ The above picture shows all resources that may be accessed by `gardenlet`s/extensions, except for the `Quota` resource which is only included for completeness.

Now, when a `gardenlet` wants to access certain resources, then the `SeedAuthorizer` uses a Depth-First traversal starting from the vertex representing the resource in question, e.g., from a `Project` vertex.
If there is a path from the `Project` vertex to the vertex representing the `Seed` the `gardenlet` is responsible for. then it allows the request.
Now, when a `gardenlet`/extension wants to access certain resources, then the `SeedAuthorizer` uses a Depth-First traversal starting from the vertex representing the resource in question, e.g., from a `Project` vertex.
If there is a path from the `Project` vertex to the vertex representing the `Seed` the `gardenlet`/extension is responsible for. then it allows the request.

#### Metrics

Expand Down Expand Up @@ -230,6 +259,6 @@ Please note that it should only be activated when the `SeedAuthorizer` is active
### Admission Decisions

The admission's purpose is to perform extended validation on requests which require the body of the object in question.
Additionally, it handles `CREATE` requests of `gardenlet`s (the above discussed resource dependency graph cannot be used in such cases because there won't be any vertex/edge for non-existing resources).
Additionally, it handles `CREATE` requests of `gardenlet`s/extensions (the above discussed resource dependency graph cannot be used in such cases because there won't be any vertex/edge for non-existing resources).

Gardenlets are restricted to only create new resources which are somehow related to the seed clusters they are responsible for.
Gardenlets/extensions are restricted to only create new resources which are somehow related to the seed clusters they are responsible for.
13 changes: 11 additions & 2 deletions docs/extensions/garden-api-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ title: Access to the Garden Cluster for Extensions

Extensions that are installed on seed clusters via a `ControllerInstallation` can simply read the kubeconfig file specified by the `GARDEN_KUBECONFIG` environment variable to create a garden cluster client.
With this, they use a short-lived token (valid for `12h`) associated with a dedicated `ServiceAccount` in the `seed-<seed-name>` namespace to securely access the garden cluster.

> ⚠️ This feature is under development. The managed `ServiceAccounts` in the garden cluster don't have any API permissions as of now. They will be handled by the `SeedAuthorizer` in the future and equipped with permissions similar to the gardenlets' credentials. See [gardener/gardener#8001](https://github.com/gardener/gardener/issues/8001) for more information.
The used `ServiceAccounts` are granted permissions in the garden cluster similar to gardenlet clients.

## Background

Expand Down Expand Up @@ -121,6 +120,16 @@ users:
tokenFile: /var/run/secrets/gardener.cloud/garden/generic-kubeconfig/token
```

## Permissions in the Garden Cluster

Both the [`SeedAuthorizer` and the `SeedRestriction` plugin](../deployment/gardenlet_api_access.md) handle extensions clients and generally grant the same permissions in the garden cluster to them as to gardenlet clients.
With this, extensions are restricted to work with objects in the garden cluster that are related to seed they are running one just like gardenlet.
Note that if the plugins are not enabled, extension clients are only granted read access to global resources like `CloudProfiles` (this is granted to all authenticated users).
There are a few exceptions to the granted permissions as documented [here](../deployment/gardenlet_api_access.md#rule-exceptions-for-extension-clients).

If an extension needs access to additional resources in the garden cluster (e.g., extension-specific custom resources), permissions need to be granted via the usual RBAC means.
Note that this is done outside of Gardener and might require an additional controller that manages RBAC for extension clients in the garden cluster.

## Renewing All Garden Access Secrets

Operators can trigger an automatic renewal of all garden access secrets in a given `Seed` and their requested `ServiceAccount` tokens, e.g., when rotating the garden cluster's `ServiceAccount` signing key.
Expand Down
80 changes: 63 additions & 17 deletions pkg/admissioncontroller/seedidentity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,46 @@ import (
"strings"

authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"

v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
"github.com/gardener/gardener/pkg/utils"
gardenerutils "github.com/gardener/gardener/pkg/utils/gardener"
)

// FromUserInfoInterface returns the seed name and a boolean indicating whether the provided user has the
// gardener.cloud:system:seeds group.
func FromUserInfoInterface(u user.Info) (string, bool) {
// UserType is used for distinguishing between clients running on a seed cluster when authenticating against the garden
// cluster.
type UserType string

const (
// UserTypeGardenlet is the UserType of a gardenlet client.
UserTypeGardenlet UserType = "gardenlet"
// UserTypeExtension is the UserType of a extension client.
UserTypeExtension UserType = "extension"
)

// FromUserInfoInterface returns the seed name, a boolean indicating whether the provided user is a seed client,
// and the client's UserType.
func FromUserInfoInterface(u user.Info) (string, bool, UserType) {
if u == nil {
return "", false
return "", false, ""
}

userName := u.GetName()
if !strings.HasPrefix(userName, v1beta1constants.SeedUserNamePrefix) {
return "", false
if utils.ValueExists(v1beta1constants.SeedsGroup, u.GetGroups()) {
return getIdentityForSeedsGroup(u)
}

if !utils.ValueExists(v1beta1constants.SeedsGroup, u.GetGroups()) {
return "", false
if utils.ValueExists(serviceaccount.AllServiceAccountsGroup, u.GetGroups()) {
return getIdentityForServiceAccountsGroup(u)
}

seedName := strings.TrimPrefix(userName, v1beta1constants.SeedUserNamePrefix)
if seedName == "" {
return "", false
}

return seedName, true
return "", false, ""
}

// FromAuthenticationV1UserInfo converts an authenticationv1.UserInfo structure to the user.Info interface and calls
// FromUserInfoInterface to return the seed name.
func FromAuthenticationV1UserInfo(userInfo authenticationv1.UserInfo) (string, bool) {
func FromAuthenticationV1UserInfo(userInfo authenticationv1.UserInfo) (string, bool, UserType) {
return FromUserInfoInterface(&user.DefaultInfo{
Name: userInfo.Username,
UID: userInfo.UID,
Expand All @@ -62,7 +69,7 @@ func FromAuthenticationV1UserInfo(userInfo authenticationv1.UserInfo) (string, b

// FromCertificateSigningRequest converts a *x509.CertificateRequest structure to the user.Info interface and calls
// FromUserInfoInterface to return the seed name.
func FromCertificateSigningRequest(csr *x509.CertificateRequest) (string, bool) {
func FromCertificateSigningRequest(csr *x509.CertificateRequest) (string, bool, UserType) {
return FromUserInfoInterface(&user.DefaultInfo{
Name: csr.Subject.CommonName,
Groups: csr.Subject.Organization,
Expand All @@ -80,3 +87,42 @@ func convertAuthenticationV1ExtraValueToUserInfoExtra(extra map[string]authentic

return ret
}

func getIdentityForSeedsGroup(u user.Info) (string, bool, UserType) {
userName := u.GetName()

if !strings.HasPrefix(userName, v1beta1constants.SeedUserNamePrefix) {
return "", false, ""
}

seedName := strings.TrimPrefix(userName, v1beta1constants.SeedUserNamePrefix)
if seedName == "" {
return "", false, ""
}

return seedName, true, UserTypeGardenlet
}

func getIdentityForServiceAccountsGroup(u user.Info) (string, bool, UserType) {
var serviceAccountNamespaceGroup string
for _, g := range u.GetGroups() {
if strings.HasPrefix(g, serviceaccount.ServiceAccountGroupPrefix) {
serviceAccountNamespaceGroup = g
break
}
}

seedNamespace := strings.TrimPrefix(serviceAccountNamespaceGroup, serviceaccount.ServiceAccountGroupPrefix)
if !strings.HasPrefix(seedNamespace, gardenerutils.SeedNamespaceNamePrefix) {
return "", false, ""
}

seedName := strings.TrimPrefix(seedNamespace, gardenerutils.SeedNamespaceNamePrefix)
name := strings.TrimPrefix(u.GetName(), serviceaccount.ServiceAccountUsernamePrefix+seedNamespace+serviceaccount.ServiceAccountUsernameSeparator)

if seedName != "" && strings.HasPrefix(name, v1beta1constants.ExtensionGardenServiceAccountPrefix) {
return seedName, true, UserTypeExtension
}

return "", false, ""
}
Loading

0 comments on commit 44e8950

Please sign in to comment.