From a16bb9d455379dcdb4a96974e224827d52625b88 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 19 Jan 2024 14:46:23 -0500 Subject: [PATCH 1/5] clean up AwsIamStrategy tests Make it easier to stub and check the presign mock. Signed-off-by: Jamie Klassen --- .../src/auth/AwsIamStrategy.test.ts | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts index 873c78e177..9959e1960f 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts @@ -20,12 +20,6 @@ import { } from '@backstage/plugin-kubernetes-common'; import { AwsIamStrategy } from './AwsIamStrategy'; -let presign = jest.fn(async () => ({ - hostname: 'https://example.com', - query: {}, - path: '/asdf', -})); - const credsManager = { getCredentialProvider: async () => ({ sdkCredentialProvider: { @@ -40,12 +34,16 @@ jest.mock('@backstage/integration-aws-node', () => ({ }, })); -const config = new ConfigReader({}); +const signer = { + presign: jest.fn().mockResolvedValue({ + hostname: 'https://example.com', + query: {}, + path: '/asdf', + }), +}; jest.mock('@aws-sdk/signature-v4', () => ({ - SignatureV4: jest.fn().mockImplementation(() => ({ - presign, - })), + SignatureV4: jest.fn().mockImplementation(() => signer), })); const fromTemporaryCredentials = jest.fn(); @@ -55,9 +53,10 @@ jest.mock('@aws-sdk/credential-providers', () => ({ }, })); -describe('AwsIamStrategy tests', () => { - beforeEach(() => {}); - it('returns a signed url for AWS credentials without assume role', async () => { +describe('AwsIamStrategy#getCredential', () => { + const config = new ConfigReader({}); + + it('returns a presigned url for cluster name', async () => { const strategy = new AwsIamStrategy({ config }); const credential = await strategy.getCredential({ @@ -65,10 +64,17 @@ describe('AwsIamStrategy tests', () => { url: '', authMetadata: {}, }); + expect(credential).toEqual({ type: 'bearer token', token: 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', }); + expect(signer.presign).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ 'x-k8s-aws-id': 'test-cluster' }), + }), + expect.anything(), + ); }); it('returns a signed url for AWS credentials with assume role', async () => { @@ -129,21 +135,17 @@ describe('AwsIamStrategy tests', () => { }); }); - describe('When the credentials is failing', () => { - beforeEach(() => { - presign = jest.fn(async () => { - throw new Error('no way'); - }); - }); - it('throws the right error', async () => { - const strategy = new AwsIamStrategy({ config }); - await expect( - strategy.getCredential({ - name: 'test-cluster', - url: '', - authMetadata: {}, - }), - ).rejects.toThrow('no way'); - }); + it('fails on signer error', () => { + signer.presign.mockRejectedValue(new Error('no way')); + + const strategy = new AwsIamStrategy({ config }); + + return expect( + strategy.getCredential({ + name: 'test-cluster', + url: '', + authMetadata: {}, + }), + ).rejects.toThrow('no way'); }); }); From daad57618e02750cec81a6b2681d04a1997d07fe Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 19 Jan 2024 15:10:31 -0500 Subject: [PATCH 2/5] separate authMetadata parameter for x-k8s-aws-id Signed-off-by: Jamie Klassen --- .changeset/seven-plums-return.md | 14 +++++++++ .../src/auth/AwsIamStrategy.test.ts | 30 +++++++++++++++++-- .../src/auth/AwsIamStrategy.ts | 8 +++-- plugins/kubernetes-common/api-report.md | 4 +++ .../src/catalog-entity-constants.ts | 9 ++++++ 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 .changeset/seven-plums-return.md diff --git a/.changeset/seven-plums-return.md b/.changeset/seven-plums-return.md new file mode 100644 index 0000000000..71016577b1 --- /dev/null +++ b/.changeset/seven-plums-return.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-common': patch +--- + +Clusters configured with the `aws` authentication strategy can now customize the +`x-k8s-aws-id` header value used to generate tokens. This value can be specified +specified via the `kubernetes.io/x-k8s-aws-id` parameter (in +`metadata.annotations` for clusters in the catalog, or the `authMetadata` block +on clusters in the app-config). This is particularly helpful when a Backstage +instance contains multiple AWS clusters with the same name in different regions +-- using this new parameter, the clusters can be given different logical names +to distinguish them but still use the same ID for the purposes of generating +tokens. diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts index 9959e1960f..e6e69cc05d 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts @@ -16,6 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_CLUSTER_ID, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; import { AwsIamStrategy } from './AwsIamStrategy'; @@ -56,7 +57,7 @@ jest.mock('@aws-sdk/credential-providers', () => ({ describe('AwsIamStrategy#getCredential', () => { const config = new ConfigReader({}); - it('returns a presigned url for cluster name', async () => { + it('returns a presigned url', async () => { const strategy = new AwsIamStrategy({ config }); const credential = await strategy.getCredential({ @@ -77,7 +78,30 @@ describe('AwsIamStrategy#getCredential', () => { ); }); - it('returns a signed url for AWS credentials with assume role', async () => { + it('returns a presigned url for specified cluster ID', async () => { + const strategy = new AwsIamStrategy({ config }); + + const credential = await strategy.getCredential({ + name: 'cluster-name', + url: '', + authMetadata: { + [ANNOTATION_KUBERNETES_AWS_CLUSTER_ID]: 'other-name', + }, + }); + + expect(credential).toEqual({ + type: 'bearer token', + token: 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + }); + expect(signer.presign).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ 'x-k8s-aws-id': 'other-name' }), + }), + expect.anything(), + ); + }); + + it('returns a presigned url for AWS credentials with assumed role', async () => { const strategy = new AwsIamStrategy({ config }); const credential = await strategy.getCredential({ @@ -106,7 +130,7 @@ describe('AwsIamStrategy#getCredential', () => { }); }); - it('returns a signed url for AWS credentials and passes the external id', async () => { + it('returns a presigned url for AWS credentials and passes the external id', async () => { const strategy = new AwsIamStrategy({ config }); const credential = await strategy.getCredential({ diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index 942b93615f..4c56f86a30 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -23,6 +23,7 @@ import { import { Config } from '@backstage/config'; import { ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_CLUSTER_ID, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; import { @@ -61,7 +62,8 @@ export class AwsIamStrategy implements AuthenticationStrategy { return { type: 'bearer token', token: await this.getBearerToken( - clusterDetails.name, + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_CLUSTER_ID] ?? + clusterDetails.name, clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], ), @@ -73,7 +75,7 @@ export class AwsIamStrategy implements AuthenticationStrategy { } private async getBearerToken( - clusterName: string, + clusterId: string, assumeRole?: string, externalId?: string, ): Promise { @@ -105,7 +107,7 @@ export class AwsIamStrategy implements AuthenticationStrategy { { headers: { host: `sts.${region}.amazonaws.com`, - 'x-k8s-aws-id': clusterName, + 'x-k8s-aws-id': clusterId, }, hostname: `sts.${region}.amazonaws.com`, method: 'GET', diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index abd340f368..e4dbf79f4a 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -39,6 +39,10 @@ export const ANNOTATION_KUBERNETES_AUTH_PROVIDER = export const ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE = 'kubernetes.io/aws-assume-role'; +// @public +export const ANNOTATION_KUBERNETES_AWS_CLUSTER_ID = + 'kubernetes.io/x-k8s-aws-id'; + // @public export const ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID = 'kubernetes.io/aws-external-id'; diff --git a/plugins/kubernetes-common/src/catalog-entity-constants.ts b/plugins/kubernetes-common/src/catalog-entity-constants.ts index e7e5cddc25..996faf670d 100644 --- a/plugins/kubernetes-common/src/catalog-entity-constants.ts +++ b/plugins/kubernetes-common/src/catalog-entity-constants.ts @@ -84,6 +84,7 @@ export const ANNOTATION_KUBERNETES_DASHBOARD_APP = */ export const ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS = 'kubernetes.io/dashboard-parameters'; + /** * Annotation for specifying the assume role use to authenticate with AWS. * @@ -92,6 +93,14 @@ export const ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS = export const ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE = 'kubernetes.io/aws-assume-role'; +/** + * Annotation for specifying the AWS ID of a cluster when signing STS tokens + * + * @public + */ +export const ANNOTATION_KUBERNETES_AWS_CLUSTER_ID = + 'kubernetes.io/x-k8s-aws-id'; + /** * Annotation for specifying an external id when communicating with AWS * From a81b1ba7dda2871a07d8ea3ca296c533107f4edc Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 19 Jan 2024 15:25:20 -0500 Subject: [PATCH 3/5] update default EKS cluster entity transformer This change has no functional effect, but it seems like a helpful illustration for adopters embarking on writing the first EKS cluster entity transformer of their own. Signed-off-by: Jamie Klassen --- .changeset/dull-dolphins-explain.md | 6 ++++++ .../src/lib/defaultTransformers.ts | 5 ++++- .../src/processors/AwsEKSClusterProcessor.test.ts | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/dull-dolphins-explain.md diff --git a/.changeset/dull-dolphins-explain.md b/.changeset/dull-dolphins-explain.md new file mode 100644 index 0000000000..d8864e72e8 --- /dev/null +++ b/.changeset/dull-dolphins-explain.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +The default EKS cluster entity transformer now sets the new +`kubernetes.io/x-k8s-aws-id` annotation. diff --git a/plugins/catalog-backend-module-aws/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-aws/src/lib/defaultTransformers.ts index 9de79e9811..07862bb298 100644 --- a/plugins/catalog-backend-module-aws/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-aws/src/lib/defaultTransformers.ts @@ -18,6 +18,7 @@ import { ANNOTATION_KUBERNETES_API_SERVER, ANNOTATION_KUBERNETES_API_SERVER_CA, ANNOTATION_KUBERNETES_AUTH_PROVIDER, + ANNOTATION_KUBERNETES_AWS_CLUSTER_ID, } from '@backstage/plugin-kubernetes-common'; import type { EksClusterEntityTransformer } from '../processors/types'; import { ANNOTATION_AWS_ACCOUNT_ID, ANNOTATION_AWS_ARN } from '../constants'; @@ -29,6 +30,7 @@ import { ANNOTATION_AWS_ACCOUNT_ID, ANNOTATION_AWS_ARN } from '../constants'; export const defaultEksClusterEntityTransformer: EksClusterEntityTransformer = async (cluster: Cluster, accountId: string) => { const { arn, endpoint, certificateAuthority, name } = cluster; + const normalizedName = normalizeName(name as string); return { apiVersion: 'backstage.io/v1alpha1', kind: 'Resource', @@ -40,8 +42,9 @@ export const defaultEksClusterEntityTransformer: EksClusterEntityTransformer = [ANNOTATION_KUBERNETES_API_SERVER_CA]: certificateAuthority?.data || '', [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'aws', + [ANNOTATION_KUBERNETES_AWS_CLUSTER_ID]: normalizedName, }, - name: normalizeName(name as string), + name: normalizedName, namespace: 'default', }, spec: { diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts index 6cec6ed66d..a07f775fb9 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts @@ -67,6 +67,7 @@ describe('AwsEKSClusterProcessor', () => { 'kubernetes.io/api-server-certificate-authority': cluster.cluster?.certificateAuthority?.data, 'kubernetes.io/auth-provider': 'aws', + 'kubernetes.io/x-k8s-aws-id': 'backstage-test', }, name: 'backstage-test', namespace: 'default', From 7ab6713ce12906ba6da64e7875b8ef83ba3299ba Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 19 Jan 2024 17:54:13 -0500 Subject: [PATCH 4/5] refactor kubernetes auth docs some grammar/wordsmithing and formatting Signed-off-by: Jamie Klassen --- docs/features/kubernetes/authentication.md | 79 +++++++++++++--------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/docs/features/kubernetes/authentication.md b/docs/features/kubernetes/authentication.md index e0cddefbcf..beec5d2950 100644 --- a/docs/features/kubernetes/authentication.md +++ b/docs/features/kubernetes/authentication.md @@ -4,23 +4,26 @@ title: Kubernetes Authentication description: Authentication in Kubernetes plugin --- -The authentication process in Kubernetes relies on `KubernetesAuthProviders`, which are -not the same as the application's auth providers, the default providers are defined in -`plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts`, you can -add custom providers there if needed. +The authentication process in Kubernetes relies on `KubernetesAuthProviders` -- +which are not the same as the application's auth providers. the default +providers are defined in +`plugins/kubernetes-react/src/kubernetes-auth-provider/KubernetesAuthProviders.ts`; +you can add custom providers there if needed. -These providers are configured so your Kubernetes plugin can locate and access the -clusters you have access to, some of them have special requirements in the third party in -question, like Microsoft Entra ID (formerly Azure Active Directory) subscription or Azure RBAC support active on the cluster. +These providers are configured so your Kubernetes plugin can locate and access +the clusters you have access to, some of them have special requirements in the +third party in question, like Microsoft Entra ID (formerly Azure Active +Directory) subscription or Azure RBAC support active on the cluster. -The providers currently available are divided into server side and client side. +The providers currently available are summarized below: ## Server Side Providers -These providers authenticate your _application_ with the cluster, meaning anyone that is -logged in into your backstage app will be granted the same access to Kubernetes objects, including guest users. +These providers authenticate your _application_ with the cluster, meaning anyone +that is logged in into your Backstage app will be granted the same access to +Kubernetes objects, including guest users. -The providers available as server side are: +The server side providers are: - `aws` - `azure` @@ -30,11 +33,19 @@ The providers available as server side are: ### AWS -For AWS, in addition to the "kubernetes" configuration, you will have to set up AWS authentication. The AWS server-side authentication provider uses [AWS Identity and Access Management (IAM)][3] to authenticate to the target Account(s), you can read more about it on the page for the [Integration AWS node][4]. +For AWS, in addition to Kubernetes configuration, you will have to set up +AWS authentication. The AWS server-side authentication provider uses [AWS +Identity and Access Management (IAM)][3] to authenticate to the target +Account(s); you can read more about it on the page for the [Integration AWS +node][4]. -Using the plugin, you can authenticate to several AWS accounts using either [static AWS Access keys][5] or short-lived Access keys generated by [assuming a role][6], for either case you will need to install the [AWS CLI utility][7] and set it up following the steps in the linked documentation. +Using the plugin, you can authenticate to several AWS accounts using either +[static AWS Access keys][5] or short-lived Access keys generated by [assuming a +role][6], for either case you will need to install the [AWS CLI utility][7] and +set it up following the steps in the linked documentation. -If you have generated static AWS security credentials, the configuration block for AWS will look like this: +If you have generated static AWS security credentials, the configuration block +for AWS will look like this: ```yaml aws: @@ -47,7 +58,8 @@ aws: accountDefaults: ``` -If your environment is set up to assume a role, the configuration would instead look like this: +If your environment is set up to assume a role, the configuration would instead +look like this: ```yaml aws: @@ -58,7 +70,8 @@ aws: accountDefaults: ``` -Either of these sections needs to be present for the Kubernetes configuration to use the `aws` `authProvider`. The Kubernetes configuration looks like this: +Either of these sections needs to be present for the Kubernetes configuration to +use the `aws` `authProvider`. The Kubernetes configuration looks like this: ```yaml kubernetes: @@ -77,14 +90,17 @@ You get both, the cluster `url` and `caData` directly from the AWS console by go ### Azure -The Azure server side authentication provider works by authenticating on the server with -the Azure CLI, please note that [Microsoft Entra authentication][1] is a requirement and has to +The Azure provider works by authenticating on the server with the Azure CLI, +please note that [Microsoft Entra authentication][1] is a requirement and has to be enabled in your AKS cluster, then follow these steps: -- [Install the Azure CLI][2] in the environment where the backstage application will run. -- Login with your Azure/Microsoft account with `az login` in the server's terminal. -- Go to your AKS cluster's resource page in Azure Console and follow the steps in the - `Connect` tab to set the subscription and get your credentials for `kubectl` integration. +- [Install the Azure CLI][2] in the environment where the backstage application + will run. +- Login with your Azure/Microsoft account with `az login` in the server's + terminal. +- Go to your AKS cluster's resource page in Azure Console and follow the steps + in the `Connect` tab to set the subscription and get your credentials for + `kubectl` integration. - Configure your cluster to use the `azure` auth provider like this: ```yaml @@ -98,18 +114,19 @@ kubernetes: skipTLSVerify: true ``` -To get the API server address for your Azure cluster, go to the Azure console page for the -cluster resource, go to `Overview` > `Properties` tab > `Networking` section and copy paste -the API server address directly in that `url` field. +To get the API server address for your Azure cluster, go to the Azure console +page for the cluster resource, go to `Overview` > `Properties` tab > +`Networking` section and copy paste the API server address directly in that +`url` field. ## Client Side Providers -These providers authenticate your _user_ with the cluster. Each Backstage user will be -prompted for credentials and will have access to the clusters as long as the user has been -authorized to access said cluster. If the cluster is listed in the `clusterLocatorMethods`, -but the user hasn't been authorized to access, the user will see the cluster listed but -will not see any resources in the plugin page for that cluster, and the error will show -as `401` or similar. +These providers authenticate a _user_ with the cluster. Each Backstage user will +be prompted for credentials and will have access to the clusters as long as the +user has been authorized to access said cluster. If Backstage is configured to +communicate with a cluster but the user isn't authorized to access it, they will +see the cluster listed but will not see any resources in the plugin page for +that cluster. The error will show as `401` or similar. The providers available as client side are: From 8754168225431b39db48a9805e447f31c7526221 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 19 Jan 2024 17:55:51 -0500 Subject: [PATCH 5/5] document extra AWS auth parameters including the new x-k8s-aws-id one Signed-off-by: Jamie Klassen --- docs/features/kubernetes/authentication.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/features/kubernetes/authentication.md b/docs/features/kubernetes/authentication.md index beec5d2950..5057218f17 100644 --- a/docs/features/kubernetes/authentication.md +++ b/docs/features/kubernetes/authentication.md @@ -81,12 +81,23 @@ kubernetes: - type: 'config' clusters: - url: https://..eks.amazonaws.com - name: + name: ${CLUSTER_NAME_TO_DISPLAY} authProvider: 'aws' caData: ${EKS_CA_DATA} + authMetadata: + kubernetes.io/aws-assume-role: ${ROLE_ARN_TO_ASSUME} + kubernetes.io/aws-external-id: ${ID_FROM_AWS_ADMIN} + kubernetes.io/x-k8s-aws-id: ${CLUSTER_NAME_IN_AWS_CONSOLE} ``` -You get both, the cluster `url` and `caData` directly from the AWS console by going to `EKS` > `Your cluster` > `Overview` > `Details`. You will find them under 'API server endpoint' and 'Certificate authority' respectively. +You get both the cluster URL and CA directly from the AWS console by going to +`EKS` > `Your cluster` > `Overview` > `Details`. You will find them under 'API +server endpoint' and 'Certificate authority' respectively. + +If Backstage needs to assume a role when authenticating with EKS clusters, the +`kubernetes.io/aws-assume-role` parameter can be set to the ARN of the desired +role. the `kubernetes.io/aws-external-id` parameter in the config corresponds to +the `ExternalId` parameter of the [`AssumeRole` API in STS][8]. ### Azure @@ -141,3 +152,4 @@ The providers available as client side are: [5]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html [6]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html [7]: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html +[8]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_RequestParameters