diff --git a/.changeset/tame-numbers-smile.md b/.changeset/tame-numbers-smile.md new file mode 100644 index 0000000000..1d675f8c9c --- /dev/null +++ b/.changeset/tame-numbers-smile.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-node': patch +--- + +Enabling authentication to kubernetes clusters with mTLS x509 client certs diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index e388589e74..0d9f067707 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -895,7 +895,7 @@ describe('KubernetesFetcher', () => { customResources: [], }); return expect(result).rejects.toThrow( - "no bearer token for cluster 'unauthenticated-cluster' and not running in Kubernetes", + "no bearer token or client cert for cluster 'unauthenticated-cluster' and not running in Kubernetes", ); }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 99501c48c6..4daf3e80f6 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -224,13 +224,14 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { [url, requestInit] = this.fetchArgsInCluster(credential); } else if ( credential.type === 'bearer token' || + credential.type === 'x509 client certificate' || authProvider === 'localKubectlProxy' ) { [url, requestInit] = this.fetchArgs(clusterDetails, credential); } else { return Promise.reject( new Error( - `no bearer token for cluster '${clusterDetails.name}' and not running in Kubernetes`, + `no bearer token or client cert for cluster '${clusterDetails.name}' and not running in Kubernetes`, ), ); } @@ -272,6 +273,10 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { clusterDetails.caData, ) ?? undefined, rejectUnauthorized: !clusterDetails.skipTLSVerify, + ...(credential.type === 'x509 client certificate' && { + cert: credential.cert, + key: credential.key, + }), }); } return [url, requestInit]; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 8a07267cde..209d01b703 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -113,24 +113,6 @@ export class KubernetesProxy { return; } - const authHeader = req.header(HEADER_KUBERNETES_AUTH); - if (authHeader) { - req.headers.authorization = authHeader; - } else { - // Map Backstage-Kubernetes-Authorization-X-X headers to a KubernetesRequestAuth object - const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth( - req.headers, - ); - - const credential = await this.getClusterForRequest(req).then(cd => { - return this.authStrategy.getCredential(cd, authObj); - }); - - if (credential.type === 'bearer token') { - req.headers.authorization = `Bearer ${credential.token}`; - } - } - const middleware = await this.getMiddleware(req); // If req is an upgrade handshake, use middleware upgrade instead of http request handler https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade @@ -173,7 +155,7 @@ export class KubernetesProxy { const cluster = await this.getClusterForRequest(req); const url = new URL(cluster.url); - return { + const target: any = { protocol: url.protocol, host: url.hostname, port: url.port, @@ -182,6 +164,29 @@ export class KubernetesProxy { cluster.caData, )?.toString(), }; + + const authHeader = req.header(HEADER_KUBERNETES_AUTH); + if (authHeader) { + req.headers.authorization = authHeader; + } else { + // Map Backstage-Kubernetes-Authorization-X-X headers to a KubernetesRequestAuth object + const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth( + req.headers, + ); + + const credential = await this.getClusterForRequest(req).then(cd => { + return this.authStrategy.getCredential(cd, authObj); + }); + + if (credential.type === 'bearer token') { + req.headers.authorization = `Bearer ${credential.token}`; + } else if (credential.type === 'x509 client certificate') { + target.key = credential.key; + target.cert = credential.cert; + } + } + + return target; }, onError: (error, req, res) => { const wrappedError = new ForwardedError( diff --git a/plugins/kubernetes-node/api-report.md b/plugins/kubernetes-node/api-report.md index 2272035742..4c858ec969 100644 --- a/plugins/kubernetes-node/api-report.md +++ b/plugins/kubernetes-node/api-report.md @@ -100,6 +100,11 @@ export type KubernetesCredential = type: 'bearer token'; token: string; } + | { + type: 'x509 client certificate'; + cert: string; + key: string; + } | { type: 'anonymous'; }; diff --git a/plugins/kubernetes-node/src/types/types.ts b/plugins/kubernetes-node/src/types/types.ts index a0f48d0c26..036b54ed98 100644 --- a/plugins/kubernetes-node/src/types/types.ts +++ b/plugins/kubernetes-node/src/types/types.ts @@ -139,6 +139,7 @@ export interface KubernetesClustersSupplier { */ export type KubernetesCredential = | { type: 'bearer token'; token: string } + | { type: 'x509 client certificate'; cert: string; key: string } | { type: 'anonymous' }; /**