Merge pull request #22399 from jamieklassen/aws-id-authmetadata-field
authMetadata field for AWS cluster ID
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
@@ -68,23 +81,37 @@ kubernetes:
|
||||
- type: 'config'
|
||||
clusters:
|
||||
- url: https://<unique-identifier>.<region>.eks.amazonaws.com
|
||||
name: <cluster-name-to-use>
|
||||
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
|
||||
|
||||
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 +125,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:
|
||||
|
||||
@@ -124,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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -16,16 +16,11 @@
|
||||
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';
|
||||
|
||||
let presign = jest.fn(async () => ({
|
||||
hostname: 'https://example.com',
|
||||
query: {},
|
||||
path: '/asdf',
|
||||
}));
|
||||
|
||||
const credsManager = {
|
||||
getCredentialProvider: async () => ({
|
||||
sdkCredentialProvider: {
|
||||
@@ -40,12 +35,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 +54,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', async () => {
|
||||
const strategy = new AwsIamStrategy({ config });
|
||||
|
||||
const credential = await strategy.getCredential({
|
||||
@@ -65,13 +65,43 @@ 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 () => {
|
||||
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({
|
||||
@@ -100,7 +130,7 @@ describe('AwsIamStrategy tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
@@ -129,21 +159,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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string> {
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user