From 5f435833f161f042f3779bcb71d952e894aa3fc8 Mon Sep 17 00:00:00 2001 From: Andres Mauricio Gomez P Date: Tue, 13 Feb 2024 15:19:15 -0500 Subject: [PATCH] Documented the way to add a new KubernetesAuthStrategy Signed-off-by: Andres Mauricio Gomez P --- docs/features/kubernetes/authstrategies.md | 314 +++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 docs/features/kubernetes/authstrategies.md diff --git a/docs/features/kubernetes/authstrategies.md b/docs/features/kubernetes/authstrategies.md new file mode 100644 index 0000000000..d151bac986 --- /dev/null +++ b/docs/features/kubernetes/authstrategies.md @@ -0,0 +1,314 @@ +--- +id: authenticationstrategy +title: Kubernetes Authentication Strategies +description: Authentication Strategies in Kubernetes plugin +--- + +# Kubernetes Auth Strategies + +A Kubernetes Auth Strategy specifies the authentication steps executed on the **server side** to authenticate against a Kubernetes Cluster, +it also defines what authentication metadata info of a kubernetes cluster configuration could be returned to the front-end in case a +**client side auth provider** requires it. + +## Context + +Backstage includes by default some [Kubernetes Auth Providers](./authentication.md) to ease the authentication proccess to +kubernetes clusters, it includes: + +- `Server Side Providers` like `localKubectlProxy` or `serviceAccount` where the same set + of kubernetes permissions are shared and granted among the Backstage users and plugins. +- `Client Side Providers` like `aks` or `oidc` where the user is authenticated with the cluster, getting only the + kubernetes permissions granted to that specific user. + +Although there are `Server Side Providers` and `Client Side Providers`, an auth provider requires to have code on both sides, perhaps one of them doing +most of the authentication job, but notice that not all steps to authenticate against a Kubernetes Cluster are always executed exclusively on the server side or client side. +A Kubernetes authentication flow could require to split the authentication process among steps on the client side **and** steps on the server side. + +## AuthenticationStrategy interface + +This is how the [`AuthenticationStrategy`][2] interface has been defined to state the signature that the Kubernetes AuthStrategy +instances should implement, it defines the authentication steps executed on the **server side** to authenticate against a Kubernetes Cluster. It is similar to [KubernetesAuthProvider](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-react/src/kubernetes-auth-provider/types.ts#L21) interface who defines the authentication steps on the **client side**. + +```ts title="plugins/kubernetes-node/src/types/types.ts" +export interface AuthenticationStrategy { + getCredential( + clusterDetails: ClusterDetails, + authConfig: KubernetesRequestAuth, + ): Promise; + + presentAuthMetadata(authMetadata: AuthMetadata): AuthMetadata; + + validateCluster(authMetadata: AuthMetadata): Error[]; +} +``` + +The `AuthenticationStrategy` interface defines the following signature: + +- `getCredential`: Executes the steps require on the server side to authenticate against a Kubernetes Cluster. It receives the cluster info on the `clusterDetails` parameter and the authentication data provided from the Client Side on the `authConfig` parameter. +- `presentAuthMetadata`: A Kubernetes Cluster configuration could define authMetadata info (like the [aws provider](authentication.md#aws) does). The `presentAuthMetadata` receives that authMetadata and filters/adds information that could be required by the Front-end in a Client Side authentication process. The Front-end gets this info each time the clusters endpoint is [invoked](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts#L379). +- `validateCluster`: Applies custom validations on the cluster authMetadata info from the AuthenticationStrategy perspective when it is [being reading](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts#L96) from the Backstage config yaml. + +### KubernetesCredential type + +Something to highlight is that `AuthenticationStrategies` will return a [`KubernetesCredential`](https://github.com/backstage/backstage/blob/0226d424f5a3104239eb9e1eaa9f0cbf29cc1f1c/plugins/kubernetes-node/src/types/types.ts#L140) object from the `getCredential` method with the authentication data to consume a kubernetes cluster, it could be: + +- A bearer token +- A x509 client certificate and key +- It could be an anonymous authentication + +```ts title="plugins/kubernetes-node/src/types/types.ts" +export type KubernetesCredential = + | { type: 'bearer token'; token: string } + | { type: 'x509 client certificate'; cert: string; key: string } + | { type: 'anonymous' }; +``` + +## AuthenticationStrategies examples + +### AksStrategy + +Some kubernetes Authentication Strategies are pretty simple, since the Authentication process was executed on the client side by the `KubernetesAuthProvider`, +So Authentication Strategies like [AksStrategy](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/auth/AksStrategy.ts#L28) or [googleStrategy](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts#L29C14-L29C28) are only mapping the info that the respective `KubernetesAuthProvider` was able to get in the client side authentication flow. + +```ts title="plugins/kubernetes-backend/src/auth/AksStrategy.ts" +export class AksStrategy implements AuthenticationStrategy { + public async getCredential( + _: ClusterDetails, + requestAuth: KubernetesRequestAuth, + ): Promise { + const token = requestAuth.aks; + return token + ? { type: 'bearer token', token: token as string } + : { type: 'anonymous' }; + } + + public validateCluster(): Error[] { + return []; + } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } +} +``` + +The `AksStrategy` is pretty simple, it is only mapping the token that [`AksKubernetesAuthProvider.ts`](https://github.com/backstage/backstage/blob/f0ffd38136163edd75ae340e5653cf6b349dcbc1/plugins/kubernetes-react/src/kubernetes-auth-provider/AksKubernetesAuthProvider.ts#L21) was able to get in the Client Side authentication flow. + +### AwsIamStrategy + +Another AuthenticationStrategy is [`AwsIamStrategy`][3], it is more complex than `AksStrategy`, since it consumes some AWS APIs to get a kubernetes token. + +```ts title="plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts" +export class AwsIamStrategy implements AuthenticationStrategy { + // ... code ... + + public async getCredential( + clusterDetails: ClusterDetails, + ): Promise { + return { + type: 'bearer token', + token: await this.getBearerToken( + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_CLUSTER_ID] ?? + clusterDetails.name, + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], + ), + }; + } + + private async getBearerToken( + clusterId: string, + assumeRole?: string, + externalId?: string, + ): Promise { + // ... code ... + + const request = await signer.presign( + { + headers: { + host: `sts.${region}.amazonaws.com`, + 'x-k8s-aws-id': clusterId, + }, + hostname: `sts.${region}.amazonaws.com`, + method: 'GET', + path: '/', + protocol: 'https:', + query: { + Action: 'GetCallerIdentity', + Version: '2011-06-15', + }, + }, + { expiresIn: 0 }, + ); + + // ... code ... + } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } + public validateCluster(): Error[] { + return []; + } +} +``` + +## Custom AuthStrategy + +Sometimes you need to add a new way to authenticate against a kubernetes cluster not support by default by Backstage. This is how integrators can bring their own kubernetes auth strategies through the use of the [`addAuthStrategy`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts#L211) method on `KubernetesBuilder` or through the [AuthStrategyExtensionPoint](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/plugin.ts#L112). So, on the following sections, we are going to introduce a new AuthStrategy for [Pinniped](1), an authentication service for Kubernetes clusters. + +### Custom Pinniped auth strategy in the new backend system + +To add a new AuthStrategy, we need to create a new Pinniped [backend module](../../../backend-system/building-plugins-and-modules/01-index.md#modules) to extend the Kubernetes-Backend plugin. The Pinniped module will interact with the Kubernetes-Backend plugin through the [extension points](../../../backend-system/architecture/05-extension-points.md) registered by the plugin. The Kubernetes-Backend plugin [registers](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-backend/src/plugin.ts#L155) multiple extension points like `kubernetesObjectsProvider`, `kubernetesClusterSupplier`, `kubernetesFetcher`, `kubernetesServiceLocator` and the `kubernetesAuthStrategy`. + +Notice that this guide assumes that you already installed the [Kubernetes Plugin](../installation.md). + +To create the Backend module, run `yarn new`, select `backend-module`. Then fill out: + +``` +? What do you want to create? backend-module - A new backend module +? Enter the ID of the plugin [required] kubernetes +? Enter the ID of the module [required] pinniped +``` + +This will create a new package at `plugins/kubernetes-backend-module-pinniped`. We are going to need also the `@backstage/plugin-kubernetes-node` and `@backstage/plugin-kubernetes-common` dependencies, the `@backstage/plugin-kubernetes-node` houses the [kubernetesAuthStrategyExtensionPoint](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-node/src/extensions.ts#L77) and a [Pinniped Helper](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-node/src/auth/PinnipedHelper.ts#L53) class. + +```bash +# From your Backstage root directory +yarn --cwd plugins/kubernetes-backend-module-pinniped add @backstage/plugin-kubernetes-node +yarn --cwd plugins/kubernetes-backend-module-pinniped add @backstage/plugin-kubernetes-common +``` + +Let's create a new file to house the Pinniped authentication strategy which will implement the `AuthenticationStrategy` interface. + +```ts title="plugins/kubernetes-backend-module-pinniped/src/PinnipedStrategy.ts" +import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +import { Logger } from 'winston'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, + PinnipedClientCerts, + PinnipedHelper, + PinnipedParameters, +} from '@backstage/plugin-kubernetes-node'; + +export class PinnipedStrategy implements AuthenticationStrategy { + private pinnipedHelper: PinnipedHelper; + + constructor(private readonly logger: Logger) { + this.pinnipedHelper = new PinnipedHelper(logger); + } + + public async getCredential( + clusterDetails: ClusterDetails, + requestAuth: KubernetesRequestAuth, + ): Promise { + const params: PinnipedParameters = { + token: requestAuth.token as string, + authenticator: { + apiGroup: 'authentication.concierge.pinniped.dev', + kind: 'JWTAuthenticator', + name: 'supervisor', + }, + tokenCredentialRequest: { + apiGroup: 'login.concierge.pinniped.dev/v1alpha1', + }, + }; + + const x509Data: PinnipedClientCerts = + await this.pinnipedHelper.tokenCredentialRequest(clusterDetails, params); + return { + type: 'x509 client certificate', + cert: x509Data.cert, + key: x509Data.key, + }; + } + + public validateCluster(): Error[] { + return []; + } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } +} +``` + +The `PinnipedStrategy` implements the `AuthenticationStrategy` interface, it uses the PinnipedHelper class to exchange the clusterIdToken ( created by a custom Pinniped client-side `KubernetesAuthProvider` ) for a x509 certificate, certificate that will allow us to consume the kubernetes cluster. + +> Notice that the PinnipedHelper class will help you only to exchange the token, It doesn't introduce a cache layer, something that your strategy could introduce. + +Finally we could use the `kubernetesAuthStrategyExtensionPoint` to register our new PinnipedStrategy. + +```ts title="plugins/kubernetes-backend-module-pinniped/src/module.ts" +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { + AuthenticationStrategy, + kubernetesAuthStrategyExtensionPoint, +} from '@backstage/plugin-kubernetes-node'; +import { PinnipedStrategy } from './PinnipedStrategy'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; + +export const kubernetesModulePinniped = createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'pinniped', + register(reg) { + reg.registerInit({ + deps: { + logger: coreServices.logger, + authStrategy: kubernetesAuthStrategyExtensionPoint, + }, + async init({ logger, authStrategy }) { + const winstonLogger = loggerToWinstonLogger(logger); + const pinnipedStrategy: AuthenticationStrategy = new PinnipedStrategy( + winstonLogger, + ); + authStrategy.addAuthStrategy('pinniped', pinnipedStrategy); + }, + }); + }, +}); +``` + +### Custom Pinniped auth strategy in the old backend system + +To add a new AuthStrategy, You could use [`addAuthStrategy`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts#L211) method on `KubernetesBuilder`. +We are going to reuse the `PinnipedStrategy` created on the previous section. So when setting up the [Kubernetes Backend plugin](../installation.md#adding-kubernetes-backend-plugin), you could add a new Strategy: + +```ts title="packages/backend/src/plugins/kubernetes.ts" +import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { AuthenticationStrategy } from '@backstage/plugin-kubernetes-node'; +import { PinnipedStrategy } from '@internal/plugin-kubernetes-backend-module-pinniped'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const catalogApi = new CatalogClient({ discoveryApi: env.discovery }); + const winstonLogger = loggerToWinstonLogger(env.logger); + const pinnipedStrategy: AuthenticationStrategy = new PinnipedStrategy( + winstonLogger, + ); + const { router } = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, + catalogApi, + permissions: env.permissions, + }) + .addAuthStrategy('pinniped', pinnipedStrategy) + .build(); + return router; +} +``` + +[1]: https://pinniped.dev/ +[2]: https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-node/src/types/types.ts#L149 +[3]: https://github.com/backstage/backstage/blob/f0ffd38136163edd75ae340e5653cf6b349dcbc1/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts#L52C40-L52C62