From b9a08881c3eea395383cdc77746c6ec8f05d1e29 Mon Sep 17 00:00:00 2001 From: Andres Mauricio Gomez P Date: Thu, 22 Feb 2024 10:50:28 -0500 Subject: [PATCH 1/2] when ServiceAccount Strategy is used and no serviceAccountToken has been provided, the Kubernetes proxy is able to get the token and CA from the pod instance Signed-off-by: Andres Mauricio Gomez P --- .changeset/silver-bears-wave.md | 5 ++ .../src/service/KubernetesProxy.test.ts | 85 ++++++++++++++++++- .../src/service/KubernetesProxy.ts | 28 +++++- 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 .changeset/silver-bears-wave.md diff --git a/.changeset/silver-bears-wave.md b/.changeset/silver-bears-wave.md new file mode 100644 index 0000000000..9836949e11 --- /dev/null +++ b/.changeset/silver-bears-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Fixed bug at Kubernetes proxy, now when ServiceAccount Strategy is used and no serviceAccountToken has been provided, the Kubernetes Proxy assumes backstage is running on k8s, so it is able to get the token and CA from the Pod instance diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 527cf94a6a..241df1c6f0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -17,13 +17,19 @@ import 'buffer'; import { resolve as resolvePath } from 'path'; import { errorHandler, getVoidLogger } from '@backstage/backend-common'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { NotFoundError } from '@backstage/errors'; import { AuthorizeResult, PermissionEvaluator, } from '@backstage/plugin-permission-common'; -import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, + KubernetesRequestAuth, +} from '@backstage/plugin-kubernetes-common'; import { getMockReq, getMockRes } from '@jest-mock/express'; import express from 'express'; import Router from 'express-promise-router'; @@ -32,6 +38,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import request from 'supertest'; import { AddressInfo, WebSocket, WebSocketServer } from 'ws'; +import { Config } from '@kubernetes/client-node'; import { LocalKubectlProxyClusterLocator } from '../cluster-locator/LocalKubectlProxyLocator'; import { @@ -49,6 +56,12 @@ import { import type { Request } from 'express'; +const mockCertDir = createMockDirectory({ + content: { + 'ca.crt': 'MOCKCA', + }, +}); + describe('KubernetesProxy', () => { let proxy: KubernetesProxy; let authStrategy: jest.Mocked; @@ -977,4 +990,72 @@ describe('KubernetesProxy', () => { await closePromise; }); }); + + describe('Backstage running on k8s', () => { + const initialHost = process.env.KUBERNETES_SERVICE_HOST; + const initialPort = process.env.KUBERNETES_SERVICE_PORT; + const initialCaPath = Config.SERVICEACCOUNT_CA_PATH; + + afterEach(() => { + process.env.KUBERNETES_SERVICE_HOST = initialHost; + process.env.KUBERNETES_SERVICE_PORT = initialPort; + Config.SERVICEACCOUNT_CA_PATH = initialCaPath; + }); + + it('makes in-cluster requests when cluster details has no token', async () => { + process.env.KUBERNETES_SERVICE_HOST = '10.10.10.10'; + process.env.KUBERNETES_SERVICE_PORT = '443'; + Config.SERVICEACCOUNT_CA_PATH = mockCertDir.resolve('ca.crt'); + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'http://ignored', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, + }, + ] as ClusterDetails[]); + + authStrategy.getCredential.mockResolvedValue({ + type: 'bearer token', + token: 'SA_token', + }); + + worker.use( + rest.get( + 'https://10.10.10.10/api/v1/namespaces', + (req: any, res: any, ctx: any) => { + if (req.headers.get('Authorization') === 'Bearer SA_token') { + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + } + return res(ctx.status(403)); + }, + ), + ); + + const requestPromise = setupProxyPromise({ + proxyPath: '/mountpath', + requestPath: '/api/v1/namespaces', + headers: { + [HEADER_KUBERNETES_CLUSTER]: 'cluster1', + }, + }); + + const response = await requestPromise; + + expect(response.body).toStrictEqual({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }); + }); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 209d01b703..b4536c5880 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -22,6 +22,7 @@ import { } from '@backstage/errors'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, KubernetesRequestAuth, kubernetesProxyPermission, } from '@backstage/plugin-kubernetes-common'; @@ -29,9 +30,15 @@ import { AuthorizeResult, PermissionEvaluator, } from '@backstage/plugin-permission-common'; -import { bufferFromFileOrString } from '@kubernetes/client-node'; +import { + Cluster, + KubeConfig, + bufferFromFileOrString, +} from '@kubernetes/client-node'; import { createProxyMiddleware, RequestHandler } from 'http-proxy-middleware'; import { Logger } from 'winston'; +import fs from 'fs-extra'; +import { Config } from '@kubernetes/client-node'; import { AuthenticationStrategy } from '../auth'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; @@ -234,6 +241,25 @@ export class KubernetesProxy { throw new NotFoundError(`Cluster '${clusterName}' not found`); } + const authProvider = + cluster.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; + + if ( + authProvider === 'serviceAccount' && + fs.pathExistsSync(Config.SERVICEACCOUNT_CA_PATH) && + !cluster.authMetadata.serviceAccountToken + ) { + const kc = new KubeConfig(); + kc.loadFromCluster(); + const clusterFromKubeConfig = kc.getCurrentCluster() as Cluster; + + const url = new URL(clusterFromKubeConfig.server); + cluster.url = clusterFromKubeConfig.server; + if (url.protocol === 'https:') { + cluster.caFile = clusterFromKubeConfig.caFile; + } + } + return cluster; } From 05ccd2ea8bac96cfe1cef13ce6f8c94682005b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Mauricio=20G=C3=B3mez=20P?= Date: Thu, 22 Feb 2024 13:26:11 -0500 Subject: [PATCH 2/2] Update .changeset/silver-bears-wave.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jamie Klassen Signed-off-by: Andrés Mauricio Gómez P --- .changeset/silver-bears-wave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silver-bears-wave.md b/.changeset/silver-bears-wave.md index 9836949e11..9c98220fdc 100644 --- a/.changeset/silver-bears-wave.md +++ b/.changeset/silver-bears-wave.md @@ -2,4 +2,4 @@ '@backstage/plugin-kubernetes-backend': patch --- -Fixed bug at Kubernetes proxy, now when ServiceAccount Strategy is used and no serviceAccountToken has been provided, the Kubernetes Proxy assumes backstage is running on k8s, so it is able to get the token and CA from the Pod instance +Fixed a bug in the proxy endpoint. Now when the `serviceAccount` strategy is used and no `serviceAccountToken` has been provided, the proxy endpoint assumes backstage is running on Kubernetes and gets the URL and CA from the Pod instance.