Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,23 @@ const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
...
```

## Configure the minimum TLS version

Set the minimum TLS version before making API requests:

```javascript
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
kc.setTLSMinVersion('TLSv1.3');
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
```

`setTLSMinVersion` accepts Node.js TLS version names (`TLSv1`, `TLSv1.1`, `TLSv1.2`, or `TLSv1.3`).
The setting applies to API-server TLS connections, including direct connections and connections
through HTTP(S) or SOCKS proxies. It does not configure TLS to the proxy itself.
When unset, or reset with `kc.setTLSMinVersion(undefined)`, the Node.js runtime default is preserved
(including command-line TLS settings); the client does not impose its own minimum.

## Call APIs with protocol buffers

```javascript
Expand Down
20 changes: 20 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface ApiType {}

export class KubeConfig implements SecurityAuthentication {
private execAuth: ExecAuth = new ExecAuth();
private tlsMinVersion?: tls.SecureVersion;

private authenticators: Authenticator[] = [
new AzureAuth(),
Expand All @@ -165,6 +166,15 @@ export class KubeConfig implements SecurityAuthentication {
this.custom_authenticators.push(authenticator);
}

/**
* Sets the minimum TLS version for API connections. Configure before making requests.
* @param minVersion undefined preserves the Node.js runtime default.
*/
public setTLSMinVersion(minVersion?: tls.SecureVersion): void {
this.tlsMinVersion = minVersion;
this.agentCache.clear();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to destroy existing agents here as well first or is it fine to assume people will call it before?

}

/**
* Sets the application policy for kubeconfig `exec` credential plugins.
* The semantics match the `credentialPluginPolicy` and `credentialPluginAllowlist`
Expand Down Expand Up @@ -322,6 +332,9 @@ export class KubeConfig implements SecurityAuthentication {
agentOptions.key = opts.key;
agentOptions.pfx = opts.pfx;
agentOptions.passphrase = opts.passphrase;
if (opts.minVersion !== undefined) {
agentOptions.minVersion = opts.minVersion;
}
// Only set rejectUnauthorized if explicitly configured. When not set, fetch will use NODE_TLS_REJECT_UNAUTHORIZED env var
if (opts.rejectUnauthorized !== undefined) {
agentOptions.rejectUnauthorized = opts.rejectUnauthorized;
Expand Down Expand Up @@ -374,6 +387,9 @@ export class KubeConfig implements SecurityAuthentication {
agentOptions.key = httpsOptions.key;
agentOptions.pfx = httpsOptions.pfx;
agentOptions.passphrase = httpsOptions.passphrase;
if (httpsOptions.minVersion !== undefined) {
agentOptions.minVersion = httpsOptions.minVersion;
}
// Only set rejectUnauthorized if explicitly configured. When not set, fetch will use NODE_TLS_REJECT_UNAUTHORIZED env var
if (httpsOptions.rejectUnauthorized !== undefined) {
agentOptions.rejectUnauthorized = httpsOptions.rejectUnauthorized;
Expand Down Expand Up @@ -718,6 +734,7 @@ export class KubeConfig implements SecurityAuthentication {
if (agentOptions.key !== undefined) tlsOptions.key = agentOptions.key;
if (agentOptions.pfx !== undefined) tlsOptions.pfx = agentOptions.pfx;
if (agentOptions.passphrase !== undefined) tlsOptions.passphrase = agentOptions.passphrase;
if (agentOptions.minVersion !== undefined) tlsOptions.minVersion = agentOptions.minVersion;
if (agentOptions.rejectUnauthorized !== undefined)
tlsOptions.rejectUnauthorized = agentOptions.rejectUnauthorized;
if ((agentOptions as any).servername !== undefined)
Expand Down Expand Up @@ -770,6 +787,9 @@ export class KubeConfig implements SecurityAuthentication {
}

private applyHTTPSOptions(opts: https.RequestOptions | WebSocket.ClientOptions): void {
if (this.tlsMinVersion !== undefined) {
opts.minVersion = this.tlsMinVersion;
}
const cluster = this.getCurrentCluster();
const user = this.getCurrentUser();
if (!user) {
Expand Down
68 changes: 68 additions & 0 deletions src/config_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,74 @@ describe('KubeConfig', () => {
expectedCA.toString(),
);
});
for (const proxyUrl of [undefined, 'http://proxy:8080', 'socks5://proxy:1080']) {
it(`should pass the minimum TLS version to dispatcher options (${proxyUrl ?? 'direct'})`, () => {
const kc = new KubeConfig();
const cluster = {
name: 'cluster',
server: 'https://example.com',
skipTLSVerify: false,
proxyUrl,
};
const options = kc.createDispatcherOptions(cluster, { minVersion: 'TLSv1.3' });
assert(options.type !== 'none');
const tlsOptions = options.type === 'agent' ? options.connect : options.requestTls;
strictEqual(tlsOptions.minVersion, 'TLSv1.3');
});

it(`should configure the minimum TLS version through authentication (${proxyUrl ?? 'direct'})`, async () => {
const kc = new KubeConfig();
kc.loadFromClusterAndUser(
{ name: 'cluster', server: 'https://example.com', skipTLSVerify: false, proxyUrl },
{ name: 'user' },
);
kc.setTLSMinVersion('TLSv1.3');
const createOptions = kc.createDispatcherOptions.bind(kc);
let observedMinVersion: tls.SecureVersion | undefined;
mock.method(kc, 'createDispatcherOptions', (cluster, agentOptions) => {
const options = createOptions(cluster, agentOptions);
assert(options.type !== 'none');
const tlsOptions = options.type === 'agent' ? options.connect : options.requestTls;
observedMinVersion = tlsOptions.minVersion;
return options;
});
const rc = new RequestContext('https://example.com', HttpMethod.GET);
await kc.applySecurityAuthentication(rc);
try {
strictEqual(observedMinVersion, 'TLSv1.3');
} finally {
await rc.getDispatcher()?.close();
}

const opts: https.RequestOptions = {};
await kc.applyToHTTPSOptions(opts);
strictEqual(opts.minVersion, 'TLSv1.3');
strictEqual((opts.agent as https.Agent).options.minVersion, 'TLSv1.3');

kc.setTLSMinVersion(undefined);
const defaultOpts: https.RequestOptions = {};
await kc.applyToHTTPSOptions(defaultOpts);
strictEqual(defaultOpts.minVersion, undefined);
strictEqual((defaultOpts.agent as https.Agent).options.minVersion, undefined);
notStrictEqual(defaultOpts.agent, opts.agent);
(opts.agent as https.Agent).destroy();
(defaultOpts.agent as https.Agent).destroy();
});

it(`should preserve the runtime TLS default when unset (${proxyUrl ?? 'direct'})`, () => {
const kc = new KubeConfig();
const options = kc.createDispatcherOptions(
{ name: 'cluster', server: 'https://example.com', skipTLSVerify: false, proxyUrl },
{},
);
if (options.type !== 'none') {
const tlsOptions = options.type === 'agent' ? options.connect : options.requestTls;
strictEqual(tlsOptions.minVersion, undefined);
} else {
strictEqual(proxyUrl, undefined);
}
});
}
it('should override default 60s keep-alive with 30s for cloud LB compatibility', () => {
const kc = new KubeConfig();
kc.loadFromClusterAndUser(
Expand Down
Loading