diff --git a/src/config.ts b/src/config.ts index 6c0dd335c4..b625642c41 100644 --- a/src/config.ts +++ b/src/config.ts @@ -202,7 +202,10 @@ export class KubeConfig implements SecurityAuthentication { agentOptions.key = opts.key; agentOptions.pfx = opts.pfx; agentOptions.passphrase = opts.passphrase; - agentOptions.rejectUnauthorized = opts.rejectUnauthorized; + // 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; + } // The ws docs say that it accepts anything that https.RequestOptions accepts, // but Typescript doesn't understand that idea (yet) probably could be fixed in // the typings, but for now just cast to any @@ -259,7 +262,10 @@ export class KubeConfig implements SecurityAuthentication { agentOptions.key = httpsOptions.key; agentOptions.pfx = httpsOptions.pfx; agentOptions.passphrase = httpsOptions.passphrase; - agentOptions.rejectUnauthorized = httpsOptions.rejectUnauthorized; + // 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; + } context.setAgent(this.createAgent(cluster, agentOptions)); } diff --git a/src/config_test.ts b/src/config_test.ts index 96e126a09b..aab45ebbfe 100644 --- a/src/config_test.ts +++ b/src/config_test.ts @@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url'; import mockfs from 'mock-fs'; import { Authenticator } from './auth.js'; -import { Headers } from 'node-fetch'; +import fetch, { Headers } from 'node-fetch'; import { HttpMethod } from './index.js'; import { assertRequestAgentsEqual, assertRequestOptionsEqual } from './test/match-buffer.js'; import { CoreV1Api, RequestContext } from './api.js'; @@ -27,6 +27,7 @@ import { ActionOnInvalid, Cluster, newClusters, newContexts, newUsers, User } fr import { ExecAuth } from './exec_auth.js'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { SocksProxyAgent } from 'socks-proxy-agent'; +import { AddressInfo } from 'node:net'; const kcFileName = 'testdata/kubeconfig.yaml'; const kc2FileName = 'testdata/kubeconfig-2.yaml'; @@ -40,6 +41,9 @@ const kcInvalidContextFileName = 'testdata/empty-context-kubeconfig.yaml'; const kcInvalidClusterFileName = 'testdata/empty-cluster-kubeconfig.yaml'; const kcTlsServerNameFileName = 'testdata/tls-server-name-kubeconfig.yaml'; +const testCertFileName = 'testdata/certs/test-cert.pem'; +const testKeyFileName = 'testdata/certs/test-key.pem'; + const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Config', () => {}); @@ -491,6 +495,61 @@ describe('KubeConfig', () => { strictEqual(rc.getAgent() instanceof https.Agent, true); }); + + it('should apply NODE_TLS_REJECT_UNAUTHORIZED from environment to agent', async () => { + const { server, host, port } = await createTestHttpsServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + if (req.url?.includes('/api/v1/namespaces')) { + res.writeHead(200); + res.end( + JSON.stringify({ + apiVersion: 'v1', + kind: 'NamespaceList', + items: [ + { + apiVersion: 'v1', + kind: 'Namespace', + metadata: { name: 'default' }, + }, + ], + }), + ); + } else { + res.writeHead(200); + res.end('ok'); + } + }); + + const originalValue = process.env.NODE_TLS_REJECT_UNAUTHORIZED; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] + after(() => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalValue; + server.close(); + }); + + const kc = new KubeConfig(); + kc.loadFromClusterAndUser( + { + name: 'test-cluster', + server: `https://${host}:${port}`, + // ignore skipTLSVerify specified from environment variables + } as Cluster, + { + name: 'test-user', + token: 'test-token', + }, + ); + const coreV1Api = kc.makeApiClient(CoreV1Api); + const namespaceList = await coreV1Api.listNamespace(); + + strictEqual(namespaceList.kind, 'NamespaceList'); + strictEqual(namespaceList.items.length, 1); + strictEqual(namespaceList.items[0].metadata?.name, 'default'); + + const res2 = await fetch(`https://${host}:${port}`, await kc.applyToFetchOptions({})); + strictEqual(res2.status, 200); + strictEqual(await res2.text(), 'ok'); + }); }); describe('loadClusterConfigObjects', () => { @@ -1827,3 +1886,38 @@ describe('KubeConfig', () => { }); }); }); + +// create a self-signed HTTPS test server +async function createTestHttpsServer( + requestHandler?: (req: http.IncomingMessage, res: http.ServerResponse) => void, +): Promise<{ + server: https.Server; + host: string; + port: number; + ca: string; +}> { + const host = 'localhost'; + + const cert = readFileSync(testCertFileName, 'utf8'); + const key = readFileSync(testKeyFileName, 'utf8'); + + const defaultHandler = (req: http.IncomingMessage, res: http.ServerResponse) => { + res.writeHead(200); + res.end('ok'); + }; + + const server = https.createServer({ key, cert }, requestHandler ?? defaultHandler); + + const port = await new Promise((resolve) => { + server.listen(0, () => { + resolve((server.address() as AddressInfo).port); + }); + }); + + return { + server, + host, + port, + ca: cert, // ca is the same as cert here + }; +} diff --git a/testdata/certs/test-cert.pem b/testdata/certs/test-cert.pem new file mode 100644 index 0000000000..89591a0a25 --- /dev/null +++ b/testdata/certs/test-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCzCCAfOgAwIBAgIUPTyeIJ44dN2PZYW0a3WGYfcB6iwwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI1MDkwNjEwNDAzN1oYDzQ3NjMw +ODAzMTA0MDM3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCdDiYdfXRhzDLum5pqa6BICCPfQ+vqTfF3aYrqAwV1 +C63hYs/yU+IK83ohiPScmShmAP2ofHsP/8R9HK7LEWkvO5ZlGxebE9ARkXa51Gs9 +g8IBjH+10EL5BcTHnb+T187rTlSaSpM59LVXhlsI/zzDB6VnvApPyLFpYJ0YoYau +4gA4rMrkZGkziCx85ONdWxYyjh4RemwNxOIzmEHg5R7v7g5yPxmNcmK4BQ0XLFAf +4KgMAlhIpGz03vOz8mP/JTKO8PoB9rmKmsEANB3MQW9C/n4yosVjqN9lyaJXLII3 +6QPRi7bxqH5sq2rRfNNA9KbiszySWda7jupB8JgiBcnDAgMBAAGjUzBRMB0GA1Ud +DgQWBBTuleDZd59erSUzMOu46Yz1q9iDoDAfBgNVHSMEGDAWgBTuleDZd59erSUz +MOu46Yz1q9iDoDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBB +dfUNSeJj5oZi9QCFkjqIW0Zr3x1ODjaPVtlvp0lfcRF2qUBbaA8qvvDTbhWrS4Xl +NLzgK+aFhgJOcTj13BYNy1yag7ZnkwunInzsEGYJgC/JgZ93De/gWs88icOUHTo0 +Eg/eco6usqykz/1ZDbUwNf5rOItdXt+cp6kpWkrapz4RISddgN0kIdwEOjCKh0+b +EvJ5lH/UUwVrfZ2KI4kz1A1gQzgA1flqwLm7CNxZtRywfZR4F2mpX9dafBFqzm4w +Y9jCrrhS7Y9p3Q0muHLjOOXOAYO+w/Z0av3JqvbQC1bxz3ybjSPjL8bhP3ptJarW +yd4YH2zt3+0omzYwHfRs +-----END CERTIFICATE----- diff --git a/testdata/certs/test-key.pem b/testdata/certs/test-key.pem new file mode 100644 index 0000000000..520a2a96fb --- /dev/null +++ b/testdata/certs/test-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCdDiYdfXRhzDLu +m5pqa6BICCPfQ+vqTfF3aYrqAwV1C63hYs/yU+IK83ohiPScmShmAP2ofHsP/8R9 +HK7LEWkvO5ZlGxebE9ARkXa51Gs9g8IBjH+10EL5BcTHnb+T187rTlSaSpM59LVX +hlsI/zzDB6VnvApPyLFpYJ0YoYau4gA4rMrkZGkziCx85ONdWxYyjh4RemwNxOIz +mEHg5R7v7g5yPxmNcmK4BQ0XLFAf4KgMAlhIpGz03vOz8mP/JTKO8PoB9rmKmsEA +NB3MQW9C/n4yosVjqN9lyaJXLII36QPRi7bxqH5sq2rRfNNA9KbiszySWda7jupB +8JgiBcnDAgMBAAECggEAD0Uk55EfE7Mq8JAof1hfiSFhe3+7HFjftWCJpR8OFMdB +7LwSw9jsDWyG32PVhLRPfTtzbkJMJM4VaKS1SgEzXOhKQyJTNTzD6jFefcrtclmx +Lz1d3WuWV2f8LfxkeBdvgulmyGmfzu7AAvaJO2K1obDIoEFkL0WwGjLOk2qBEde4 +V3hXEoBiHkoEE5mEgfUarCL8tLmyiIc0gpE231vKrbuSjyi1V/nV2elYujeVmJ/F +23c5/SodcQnI/tUrN+rIvhBoP6V0ddrieTBtzf/jKrAvvYge3o+X/Z0idIQowyQs +boUD2XHieImMEXwfuGyKj2dtCd8rbhOI5Mfroqa04QKBgQDZWOO1iv2vrz8VevCn +se4n3mBaxfScdbVLNKnwe/7FW+4UKuxB5F5lMMAWPwgN85+NSH7YbcvUkFcLf/Ge +zBPXtDrvkTeQxyzAfvmjrD+1dMgP6wM5PDJ1e7Cz2yo4Hsql1VJ1H+nd1JFfJysL +YwkcDcrIx6aEAdw8qxUZLDdOVwKBgQC4/Fz9IS1UKpuYoU60YPSKABB7JqAJCUlm +trS6eI8qwJW9vpg+9w1T/y+lOYPiYq16u+rF59vdh2883mwJnYiR8QCsv5VfRPuR +dLzZAMMqWtqSXnLbHMHXdyZEZOxh6Qfix0tSRd0A6y876kWE1OkDCi6ARkXVAWnC +oPLxHeNkdQKBgG2v0GskE9b/yARdIOpgf2IbdeEZmdMEDFRB5al5yh9rv4DqEIVI +bOMAcVBIyxXPZyvz9B/heUZy+ZrSHOwY7cKkMEIKtVIZUlprOi0Blr1KjFSMM/pE +iOqFW63I40ujLn32ZEC7tFjBGAQ/ThfXCRfhVf9x0nU4Qx9S77jeeaLNAoGBALVL +N0MpkcgsHeQfKwhjASaCW6SmPS+99z8ADu21m/I1XkvgkEsdSuWociSG0rc7KHPh +2Xxt+LAKvL0160IdLyyAur2S4azF6Zsrgq1WLu/CrPXINN6DN4KYlltvYa+vd3gN +A8e1CpyM4fTha5J8K4U8JEi5FlVklicWICKovSPFAoGAYSgPvteAo2RAN6su9d8O +s7oLXnFLqaF+Fo9vdc9uEKzzdROf7GCpz/6uOb9NCiRFpu8bNyDKK4UpNqMbO97E +Km+QQuBOms16ic/lOUT6sWVe3V6FIs18xBxNNE7LrfPfa8Vory7YoVsXji6SWikT +oLTjd9Tt7SOW/v7q9GHW798= +-----END PRIVATE KEY-----