Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable TLS on lens-k8s-proxy #4941

Merged
merged 14 commits into from
Mar 17, 2022
Merged

Enable TLS on lens-k8s-proxy #4941

merged 14 commits into from
Mar 17, 2022

Conversation

jakolehm
Copy link
Contributor

@jakolehm jakolehm commented Mar 1, 2022

Enables secure TLS connections from clients (mainly LensProxy) to lens-k8s-proxy. Follow-up PR will enable TLS also from clients (renderer etc.) to LensProxy.

Signed-off-by: Jari Kolehmainen <[email protected]>
@jakolehm jakolehm added enhancement New feature or request area/security labels Mar 1, 2022
@jakolehm jakolehm added this to the 5.5.0 milestone Mar 1, 2022
export async function createKubeAuthProxyCertificateFiles(dir: string): Promise<string> {
const cert = getKubeAuthProxyCertificate();

try {
Copy link
Contributor

@Iku-turso Iku-turso Mar 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoidable try/catch as part of business-logic, which is a code-smell. Try/catch is best reserved for fatal errors, and using it for something else damages credibility of eg. centralized error handling.


try {
await access(dir);
} catch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"catch all" could be "catch specific" if decision is to use catch here.

import path from "path";
import * as selfsigned from "selfsigned";

export function getKubeAuthProxyCertificate(): selfsigned.SelfSignedCert {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not require to be a function?

{ name: "organizationName", value: "Lens" },
];

return selfsigned.generate(opts, {
Copy link
Contributor

@Iku-turso Iku-turso Mar 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is selfSigned.generate deterministic? If not, it is an explicit side-effect, and should be a dependency instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% deterministic because cert creation always creates an unique certificate. That said, it has deterministic fields... not sure where we should draw the line here.

});
}

export function getKubeAuthProxyCertificatePath(baseDir: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought: a convention is to use function to advertise that this has special significance. Here it does not, and lambda could be used to advertise that instead.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would disagree, this codebase only uses class-es for this significant code and lambdas can sometimes make the code harder to read.

import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { createKubeAuthProxyCertificateFiles, getKubeAuthProxyCertificatePath } from "./kube-auth-proxy-cert";

const getKubeAuthProxyCertDirInjectable = getInjectable({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name of injectable does not document unrelated stuff done in setup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be better naming convention for injectables that have setup?

import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { createKubeAuthProxyCertificateFiles, getKubeAuthProxyCertificatePath } from "./kube-auth-proxy-cert";

const getKubeAuthProxyCertDirInjectable = getInjectable({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be wonderfully unit-testable either locally, or preferably as part of a larger system ;)

@@ -27,6 +29,7 @@ interface PrometheusServicePreferences {

interface Dependencies {
createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
certPath: string;
}

export class ContextHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: some unrelated janky code around these parts needs a lovin' champion at some.

Signed-off-by: Jari Kolehmainen <[email protected]>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2022

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions
Copy link
Contributor

Conflicts have been resolved. A maintainer will review the pull request shortly.

Signed-off-by: Jari Kolehmainen <[email protected]>
Signed-off-by: Jari Kolehmainen <[email protected]>
Signed-off-by: Jari Kolehmainen <[email protected]>
@@ -62,9 +62,11 @@ const di = getDi();

app.setName(appName);

di.runSetups().then(() => {
injectSystemCAs();
app.on("ready", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

App start order was previously wrong... any async setups caused app.on("ready") registration happen too late -> we missed it completely.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we can catch these async errors I would suggest a slightly different shape:

async function main() {
  const di = getDi();

  await app.whenReady();
  await di.runSetups();

  ...
}

main().catch(error => {
  console.error(error);
  dialog.showErrorBox("Lens Startup Failed", String(error));
  app.exit(1);
});

Signed-off-by: Jari Kolehmainen <[email protected]>
Signed-off-by: Jari Kolehmainen <[email protected]>
import url from "url";
import { apiKubePrefix } from "../../common/vars";
import type { ProxyApiRequestArgs } from "./types";

const skipRawHeaders = new Set(["Host", "Authorization"]);

export async function kubeApiRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed because name was misleading, only upgrade requests are handled here.

Signed-off-by: Jari Kolehmainen <[email protected]>
@jakolehm jakolehm marked this pull request as ready for review March 11, 2022 13:47
@jakolehm jakolehm requested a review from a team as a code owner March 11, 2022 13:47
@jakolehm jakolehm requested review from nevalla and Nokel81 and removed request for a team March 11, 2022 13:47
src/main/context-handler/context-handler.ts Outdated Show resolved Hide resolved
Comment on lines 128 to 129
async resolveAuthProxyCa() {
return readFile(path.join(this.dependencies.certPath, "proxy.crt"));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this file never changes I think it should be a setup-able dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored. Now authProxyCa is a dependency (but still a promise because of createContextHandlerInjectable instantiate has to be non-async for now). This way ContextHandler does not need to know readFile at all.

@@ -62,9 +62,11 @@ const di = getDi();

app.setName(appName);

di.runSetups().then(() => {
injectSystemCAs();
app.on("ready", async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we can catch these async errors I would suggest a slightly different shape:

async function main() {
  const di = getDi();

  await app.whenReady();
  await di.runSetups();

  ...
}

main().catch(error => {
  console.error(error);
  dialog.showErrorBox("Lens Startup Failed", String(error));
  app.exit(1);
});


instantiate: async (di) => {
const userData = di.inject(directoryForUserDataInjectable);
const certPath = getKubeAuthProxyCertificatePath(userData);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds more like a dependency which should depend on directoryForUserDataInjectable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, there was already an injectable for this. Refactored.

const userData = di.inject(directoryForUserDataInjectable);
const certPath = getKubeAuthProxyCertificatePath(userData);

return createKubeAuthProxyCertFiles(certPath, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this creates the files and then returns certPath (which coincendentally should probably be called something like directoryForProxyCertificates. I think that this injectable should actually be mostly just setup and then instantiate should return undefined or maybe a new SetupOnly marker type.

Then whatever depends on createKubeAuthProxyCertFilesInjectable could instead just depend on directoryForProxyCertificatesInjectable and know that the setup and creation of the files will already have been done.

const binaryName = getBinaryName("lens-k8s-proxy");
const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
proxyCertPath: di.inject(getKubeAuthProxyCertDirInjectable),
spawn,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be its own injectable, maybe within a folder src/main/child-process since we probably shouldn't have any of those calls within renderer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* Licensed under MIT License. See LICENSE in root directory for more information.
*/

declare module "selfsigned" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if these were hosted into @types. The PR process is actually really simple, I have done it a couple of times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better yes but I think it can be separately from this PR. Note: this declaration is probably not complete, it only touches parts this PR needs.

Signed-off-by: Jari Kolehmainen <[email protected]>
@github-actions
Copy link
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions
Copy link
Contributor

Conflicts have been resolved. A maintainer will review the pull request shortly.

Signed-off-by: Jari Kolehmainen <[email protected]>
@jakolehm
Copy link
Contributor Author

@Nokel81 PTAL again.

@@ -61,7 +61,7 @@ export class LensProxy extends Singleton {

public port: number;

constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiRequest, getClusterForRequest }: LensProxyFunctions) {
constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiUpgradeRequest, getClusterForRequest }: LensProxyFunctions) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: all of these feel like dependencies. Should we consolidate them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably yes but sounds like out of scope for this PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

@@ -91,6 +92,8 @@ describe("<DeleteClusterDialog />", () => {
beforeEach(async () => {
const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });

mainDi.override(createContextHandlerInjectable, () => () => undefined);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be done in getDiForUnitTesting so that it is always run by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It cannot be there because some tests do require the real implementation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, that is why I would put it behind some sort of flag (like doGeneralOverrides is). I suggested moving it because I think that the vast majority of tests would want it overridden.

Copy link
Collaborator

@Nokel81 Nokel81 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can change how certain injectables are overridden by default in a future PR

@jakolehm jakolehm merged commit 0fa89ec into master Mar 17, 2022
@jakolehm jakolehm deleted the lens-k8s-proxy-tls branch March 17, 2022 13:07
@Nokel81 Nokel81 mentioned this pull request May 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/security enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants