1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

TLS cert per cluster for lens-k8s-proxy (#5081)

Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Jari Kolehmainen 2022-03-28 15:39:57 +03:00 committed by GitHub
parent 477814f1e0
commit b3574e1a21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 82 additions and 137 deletions

View File

@ -44,7 +44,7 @@
"postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version" "postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version"
}, },
"config": { "config": {
"k8sProxyVersion": "0.1.5", "k8sProxyVersion": "0.2.1",
"bundledKubectlVersion": "1.23.3", "bundledKubectlVersion": "1.23.3",
"bundledHelmVersion": "3.7.2", "bundledHelmVersion": "3.7.2",
"sentryDsn": "" "sentryDsn": ""

View File

@ -24,8 +24,6 @@ import { createClusterInjectionToken } from "../cluster/create-cluster-injection
import directoryForUserDataInjectable import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import kubeAuthProxyCaInjectable from "../../main/kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyCertFilesInjectable from "../../main/kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -89,8 +87,6 @@ describe("cluster-store", () => {
mainDi = dis.mainDi; mainDi = dis.mainDi;
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
mainDi.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
mainDi.override(kubeAuthProxyCaInjectable, () => ({} as any));
await dis.runSetups(); await dis.runSetups();

View File

@ -10,7 +10,6 @@ import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
@ -83,7 +82,6 @@ describe("ContextHandler", () => {
}); });
di.override(createKubeAuthProxyInjectable, () => ({} as any)); di.override(createKubeAuthProxyInjectable, () => ({} as any));
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
await di.runSetups(); await di.runSetups();

View File

@ -52,8 +52,6 @@ import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-p
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import path from "path"; import path from "path";
import spawnInjectable from "../child-process/spawn.injectable"; import spawnInjectable from "../child-process/spawn.injectable";
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyCertFilesInjectable from "../kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -97,8 +95,6 @@ describe("kube auth proxy tests", () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(spawnInjectable, () => mockSpawn); di.override(spawnInjectable, () => mockSpawn);
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
mockFs(mockMinikubeConfig); mockFs(mockMinikubeConfig);

View File

@ -14,11 +14,8 @@ import { ClusterManager } from "../../cluster-manager";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import directoryForKubeConfigsInjectable import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import kubeAuthProxyCaInjectable from "../../kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyCertFilesInjectable from "../../kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
@ -42,9 +39,6 @@ describe("kubeconfig-sync.source tests", () => {
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(kubeAuthProxyCaInjectable, () => Promise.resolve(Buffer.from("ca")));
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
mockFs(); mockFs();
await di.runSetups(); await di.runSetups();
@ -62,6 +56,7 @@ describe("kubeconfig-sync.source tests", () => {
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
ClusterManager.resetInstance(); ClusterManager.resetInstance();
ClusterStore.resetInstance();
}); });
describe("configsToModels", () => { describe("configsToModels", () => {

View File

@ -28,7 +28,7 @@ interface PrometheusServicePreferences {
interface Dependencies { interface Dependencies {
createKubeAuthProxy: CreateKubeAuthProxy; createKubeAuthProxy: CreateKubeAuthProxy;
authProxyCa: Promise<Buffer>; authProxyCa: string;
} }
export class ContextHandler { export class ContextHandler {
@ -123,7 +123,7 @@ export class ContextHandler {
return `https://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`; return `https://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`;
} }
async resolveAuthProxyCa() { resolveAuthProxyCa() {
return this.dependencies.authProxyCa; return this.dependencies.authProxyCa;
} }
@ -140,7 +140,7 @@ export class ContextHandler {
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> { protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
await this.ensureServer(); await this.ensureServer();
const caFileContents = await this.resolveAuthProxyCa(); const ca = this.dependencies.authProxyCa;
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : ""; const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`; const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
@ -150,7 +150,7 @@ export class ContextHandler {
host: "127.0.0.1", host: "127.0.0.1",
port: this.kubeAuthProxy.port, port: this.kubeAuthProxy.port,
path: apiPrefix, path: apiPrefix,
ca: caFileContents.toString(), ca,
}, },
changeOrigin: true, changeOrigin: true,
timeout, timeout,

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import selfsigned from "selfsigned";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable"; import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate";
import URLParse from "url-parse";
const createContextHandlerInjectable = getInjectable({ const createContextHandlerInjectable = getInjectable({
id: "create-context-handler", id: "create-context-handler",
instantiate: (di) => { instantiate: (di) => {
const authProxyCa = di.inject(kubeAuthProxyCaInjectable); return (cluster: Cluster) => {
const clusterUrl = new URLParse(cluster.apiUrl);
const dependencies = { const dependencies = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable), createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
authProxyCa, authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
}; };
return (cluster: Cluster) => new ContextHandler(dependencies, cluster); return new ContextHandler(dependencies, cluster);
};
}, },
}); });

View File

@ -1,26 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import * as selfsigned from "selfsigned";
import { createKubeAuthProxyCertFiles } from "./create-kube-auth-proxy-cert-files";
import writeFileInjectable from "../../common/fs/write-file.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import path from "path";
const createKubeAuthProxyCertFilesInjectable = getInjectable({
id: "create-kube-auth-proxy-cert-files",
instantiate: async (di) => {
const userData = di.inject(directoryForUserDataInjectable);
const certPath = path.join(userData, "kube-auth-proxy");
return createKubeAuthProxyCertFiles(certPath, {
generate: selfsigned.generate,
writeFile: di.inject(writeFileInjectable),
});
},
});
export default createKubeAuthProxyCertFilesInjectable;

View File

@ -1,43 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import type * as selfsigned from "selfsigned";
type SelfSignedGenerate = typeof selfsigned.generate;
interface CreateKubeAuthProxyCertificateFilesDependencies {
generate: SelfSignedGenerate;
writeFile: (path: string, content: string | Buffer) => Promise<void>;
}
function getKubeAuthProxyCertificate(generate: SelfSignedGenerate): selfsigned.SelfSignedCert {
const opts = [
{ name: "commonName", value: "Lens Certificate Authority" },
{ name: "organizationName", value: "Lens" },
];
return generate(opts, {
keySize: 2048,
algorithm: "sha256",
days: 365,
extensions: [
{ name: "basicConstraints", cA: true },
{ name: "subjectAltName", altNames: [
{ type: 2, value: "localhost" },
{ type: 7, ip: "127.0.0.1" },
] },
],
});
}
export async function createKubeAuthProxyCertFiles(dir: string, dependencies: CreateKubeAuthProxyCertificateFilesDependencies): Promise<string> {
const cert = getKubeAuthProxyCertificate(dependencies.generate);
await dependencies.writeFile(path.join(dir, "proxy.key"), cert.private);
await dependencies.writeFile(path.join(dir, "proxy.crt"), cert.cert);
return dir;
}

View File

@ -6,10 +6,11 @@ import { getInjectable } from "@ogre-tools/injectable";
import { KubeAuthProxy, KubeAuthProxyDependencies } from "./kube-auth-proxy"; import { KubeAuthProxy, KubeAuthProxyDependencies } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import path from "path"; import path from "path";
import selfsigned from "selfsigned";
import { getBinaryName } from "../../common/vars"; import { getBinaryName } from "../../common/vars";
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
import spawnInjectable from "../child-process/spawn.injectable"; import spawnInjectable from "../child-process/spawn.injectable";
import createKubeAuthProxyCertFilesInjectable from "./create-kube-auth-proxy-cert-files.injectable"; import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
@ -18,15 +19,17 @@ const createKubeAuthProxyInjectable = getInjectable({
instantiate: (di): CreateKubeAuthProxy => { instantiate: (di): CreateKubeAuthProxy => {
const binaryName = getBinaryName("lens-k8s-proxy"); const binaryName = getBinaryName("lens-k8s-proxy");
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
const clusterUrl = new URL(cluster.apiUrl);
const dependencies: KubeAuthProxyDependencies = { const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName), proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
proxyCertPath: di.inject(createKubeAuthProxyCertFilesInjectable), proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
spawn: di.inject(spawnInjectable), spawn: di.inject(spawnInjectable),
}; };
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => ( return new KubeAuthProxy(dependencies, cluster, environmentVariables);
new KubeAuthProxy(dependencies, cluster, environmentVariables) };
);
}, },
}); });

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type * as selfsigned from "selfsigned";
type SelfSignedGenerate = typeof selfsigned.generate;
const certCache: Map<string, selfsigned.SelfSignedCert> = new Map();
export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSignedGenerate, useCache = true): selfsigned.SelfSignedCert {
if (useCache && certCache.has(hostname)) {
return certCache.get(hostname);
}
const opts = [
{ name: "commonName", value: "Lens Certificate Authority" },
{ name: "organizationName", value: "Lens" },
];
const cert = generate(opts, {
keySize: 2048,
algorithm: "sha256",
days: 365,
extensions: [
{ name: "basicConstraints", cA: true },
{ name: "subjectAltName", altNames: [
{ type: 2, value: hostname },
{ type: 2, value: "localhost" },
{ type: 7, ip: "127.0.0.1" },
] },
],
});
certCache.set(hostname, cert);
return cert;
}

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import readFileInjectable from "../../common/fs/read-file.injectable";
import createKubeAuthProxyCertFilesInjectable from "./create-kube-auth-proxy-cert-files.injectable";
const kubeAuthProxyCaInjectable = getInjectable({
id: "kube-auth-proxy-ca",
instantiate: async (di) => {
const certPath = await di.inject(createKubeAuthProxyCertFilesInjectable);
const readFile = di.inject(readFileInjectable);
return readFile(path.join(certPath, "proxy.crt"));
},
});
export default kubeAuthProxyCaInjectable;

View File

@ -10,12 +10,13 @@ import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger"; import logger from "../logger";
import { getPortFrom } from "../utils/get-port"; import { getPortFrom } from "../utils/get-port";
import { makeObservable, observable, when } from "mobx"; import { makeObservable, observable, when } from "mobx";
import type { SelfSignedCert } from "selfsigned";
const startingServeRegex = /starting to serve on (?<address>.+)/i; const startingServeRegex = /starting to serve on (?<address>.+)/i;
export interface KubeAuthProxyDependencies { export interface KubeAuthProxyDependencies {
proxyBinPath: string; proxyBinPath: string;
proxyCertPath: Promise<string>; proxyCert: SelfSignedCert;
spawn: typeof spawn; spawn: typeof spawn;
} }
@ -44,7 +45,7 @@ export class KubeAuthProxy {
} }
const proxyBin = this.dependencies.proxyBinPath; const proxyBin = this.dependencies.proxyBinPath;
const certPath = await this.dependencies.proxyCertPath; const cert = this.dependencies.proxyCert;
this.proxyProcess = this.dependencies.spawn(proxyBin, [], { this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
env: { env: {
@ -52,7 +53,8 @@ export class KubeAuthProxy {
KUBECONFIG: this.cluster.kubeConfigPath, KUBECONFIG: this.cluster.kubeConfigPath,
KUBECONFIG_CONTEXT: this.cluster.contextName, KUBECONFIG_CONTEXT: this.cluster.contextName,
API_PREFIX: this.apiPrefix, API_PREFIX: this.apiPrefix,
CERT_PATH: certPath, PROXY_KEY: cert.private,
PROXY_CERT: cert.cert,
}, },
}); });
this.proxyProcess.on("error", (error) => { this.proxyProcess.on("error", (error) => {

View File

@ -57,8 +57,9 @@ const NonInjectedLogsDockTab = observer(({ className, tab, model, subscribeStore
setTimeout(() => { setTimeout(() => {
const overlay = document.querySelector(".PodLogs .list span.active"); const overlay = document.querySelector(".PodLogs .list span.active");
if (!overlay) return; if (typeof overlay?.scrollIntoViewIfNeeded === "function") {
overlay.scrollIntoViewIfNeeded(); overlay.scrollIntoViewIfNeeded();
}
}, 100); }, 100);
}; };

View File

@ -81,7 +81,9 @@ export class Tab extends React.PureComponent<TabProps> {
} }
scrollIntoView() { scrollIntoView() {
this.ref.current?.scrollIntoViewIfNeeded(); if (typeof this.ref.current?.scrollIntoViewIfNeeded === "function") {
this.ref.current.scrollIntoViewIfNeeded();
}
} }
@boundMethod @boundMethod

2
types/dom.d.ts vendored
View File

@ -6,7 +6,7 @@ export {};
declare global { declare global {
interface Element { interface Element {
scrollIntoViewIfNeeded(opt_center?: boolean): void; scrollIntoViewIfNeeded?(opt_center?: boolean): void;
} }
interface Window { interface Window {