Documented the way to add a new KubernetesAuthStrategy

Signed-off-by: Andres Mauricio Gomez P <andmagom@outlook.com>
This commit is contained in:
Andres Mauricio Gomez P
2024-02-13 15:19:15 -05:00
committed by blam
parent 02a79cbe4c
commit 5f435833f1
+314
View File
@@ -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<KubernetesCredential>;
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<KubernetesCredential> {
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<KubernetesCredential> {
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<string> {
// ... 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<KubernetesCredential> {
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<Router> {
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