From 56bb5f593dd2ae23324afd41f3d69d1aaffb4032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ir=C3=A9n=C3=A9e?= Date: Tue, 18 Aug 2026 15:46:43 +0000 Subject: [PATCH] fix(core): Restore AWS Secrets Manager connection reuse (#36426) Co-authored-by: Claude Opus 4.8 (1M context) --- .../outbound-http.node-agent.test.ts | 34 +++++++++++++++++++ .../__tests__/aws-secrets-manager.test.ts | 5 +++ .../providers/aws-secrets-manager.ts | 6 +++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/@n8n/backend-network/src/http/__tests__/outbound-http.node-agent.test.ts b/packages/@n8n/backend-network/src/http/__tests__/outbound-http.node-agent.test.ts index fc735f214b6..28b65428431 100644 --- a/packages/@n8n/backend-network/src/http/__tests__/outbound-http.node-agent.test.ts +++ b/packages/@n8n/backend-network/src/http/__tests__/outbound-http.node-agent.test.ts @@ -74,6 +74,40 @@ describe('getNodeAgent', () => { }); }); +// --------------------------------------------------------------------------- +// getNodeAgent — connection-reuse options propagate across all routing paths +// +// Callers may pass connection-management options (e.g. `keepAlive`, +// `maxSockets`) to enable connection reuse. Regardless of how the request is +// routed, those options must reach the constructed agent — otherwise a proxied +// deployment would silently drop them. +// --------------------------------------------------------------------------- + +describe('getNodeAgent connection-reuse options', () => { + // keep-alive/maxSockets are connection-management options, so Node's Agent (and + // the proxy agents that extend it) expose them as instance properties. + function getReuseOptions(agent: http.Agent | https.Agent): { + keepAlive: unknown; + maxSockets: unknown; + } { + const a = agent as { keepAlive?: unknown; maxSockets?: unknown }; + return { keepAlive: a.keepAlive, maxSockets: a.maxSockets }; + } + + it.each([ + ['direct (proxy: false)', { proxy: false } as const], + ['env proxy', { proxy: 'env' } as const], + ['explicit proxy URL', { proxy: 'http://proxy.internal:3128' } as const], + ])('forwards keepAlive/maxSockets on the %s path', (_label, transportOptions) => { + const { httpAgent, httpsAgent } = makeFacade() + .transport(transportOptions) + .getNodeAgent({ keepAlive: true, maxSockets: 50 }); + + expect(getReuseOptions(httpAgent)).toEqual({ keepAlive: true, maxSockets: 50 }); + expect(getReuseOptions(httpsAgent)).toEqual({ keepAlive: true, maxSockets: 50 }); + }); +}); + // --------------------------------------------------------------------------- // getNodeAgent — SSRF lookup injection // --------------------------------------------------------------------------- diff --git a/packages/cli/src/modules/external-secrets.ee/providers/__tests__/aws-secrets-manager.test.ts b/packages/cli/src/modules/external-secrets.ee/providers/__tests__/aws-secrets-manager.test.ts index d399a0aff01..028272e06b1 100644 --- a/packages/cli/src/modules/external-secrets.ee/providers/__tests__/aws-secrets-manager.test.ts +++ b/packages/cli/src/modules/external-secrets.ee/providers/__tests__/aws-secrets-manager.test.ts @@ -105,6 +105,11 @@ describe('AwsSecretsManager', () => { expect(outboundHttp.transport).toHaveBeenCalledWith({ ssrf: 'disabled' }); + // Request connection reuse from our agents. Smithy treats a supplied agent as + // external and skips its own keep-alive defaults, so we pass them explicitly to + // restore the AWS SDK-equivalent behavior (Smithy NodeHttpHandler defaults). + expect(transport.getNodeAgent).toHaveBeenCalledWith({ keepAlive: true, maxSockets: 50 }); + // The SDK client is built with our agents as its requestHandler, while the // region and credentials it was already given are left untouched. const SecretsManagerMock = SecretsManager as unknown as Mock; diff --git a/packages/cli/src/modules/external-secrets.ee/providers/aws-secrets-manager.ts b/packages/cli/src/modules/external-secrets.ee/providers/aws-secrets-manager.ts index 48b2d5b35d4..43476c5f678 100644 --- a/packages/cli/src/modules/external-secrets.ee/providers/aws-secrets-manager.ts +++ b/packages/cli/src/modules/external-secrets.ee/providers/aws-secrets-manager.ts @@ -136,11 +136,15 @@ export class AwsSecretsManager extends SecretsProvider { // Drive the AWS SDK's HTTP transport through n8n's outbound client, // so its calls reuse our agents (proxy + TLS) like every other outbound request. // SigV4 signing and the credential chain stay with the SDK. + // + // Keep connections pooled so the sequential ListSecrets/BatchGetSecretValue refresh + // reuses sockets instead of paying a fresh TLS handshake per request. `maxSockets` + // caps concurrent sockets, matching the AWS SDK's own defaults. clientConfig.requestHandler = this.outboundHttp .transport({ ssrf: 'disabled', // fixed AWS-resolved Secrets Manager host, not user-controlled }) - .getNodeAgent(); + .getNodeAgent({ keepAlive: true, maxSockets: 50 }); const { SecretsManager } = await import('@aws-sdk/client-secrets-manager'); this.client = new SecretsManager(clientConfig);