Skip to content

Commit d5d8547

Browse files
committed
fix(runtime-secrets): fetch the boot secret through the configured proxy
The Secrets Manager client used the SDK's Node handler, which ignores HTTPS_PROXY, so a container whose only egress is a proxy could not load its env and never booted. Under Bun with a proxy set, send through Bun's fetch, which honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY. apps/sim's awsRequestHandler is out of reach for packages and the realtime bootstrap needs the same path.
1 parent 88a8a85 commit d5d8547

4 files changed

Lines changed: 83 additions & 4 deletions

File tree

‎bun.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/runtime-secrets/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"dependencies": {
3232
"@aws-sdk/client-secrets-manager": "3.1150.0",
3333
"@sim/logger": "workspace:*",
34-
"@sim/utils": "workspace:*"
34+
"@sim/utils": "workspace:*",
35+
"@smithy/fetch-http-handler": "5.8.0"
3536
},
3637
"devDependencies": {
3738
"@sim/tsconfig": "workspace:*",

‎packages/runtime-secrets/src/index.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { getErrorMessage } from '@sim/utils/errors'
55
import { sleep } from '@sim/utils/helpers'
66
import { backoffWithJitter } from '@sim/utils/retry'
7+
import { FetchHttpHandler } from '@smithy/fetch-http-handler'
78

89
const logger = createLogger('RuntimeSecrets')
910

@@ -56,9 +57,11 @@ export async function loadRuntimeSecrets(): Promise<void> {
5657
* times, then throws, so a caller never proceeds on a partial or missing secret.
5758
*/
5859
export async function fetchRuntimeSecrets(secretId: string): Promise<Record<string, string>> {
59-
const client = new SecretsManagerClient(
60-
process.env.AWS_REGION ? { region: process.env.AWS_REGION } : {}
61-
)
60+
const requestHandler = proxyAwareRequestHandler()
61+
const client = new SecretsManagerClient({
62+
...(process.env.AWS_REGION ? { region: process.env.AWS_REGION } : {}),
63+
...(requestHandler ? { requestHandler } : {}),
64+
})
6265
const entries = parseSecretJson(await fetchSecretString(client, secretId))
6366
return Object.fromEntries(
6467
Object.entries(entries).map(([key, value]) => [
@@ -68,6 +71,22 @@ export async function fetchRuntimeSecrets(secretId: string): Promise<Record<stri
6871
)
6972
}
7073

74+
const PROXY_ENV = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy'] as const
75+
76+
/**
77+
* The SDK's default Node handler ignores `HTTPS_PROXY`, so a container whose only egress is a
78+
* proxy could not fetch its config and would never boot. While a proxy is set under Bun (both
79+
* container bootstraps run on Bun), requests go through Bun's `fetch`, which honors
80+
* `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY`. The app's `awsRequestHandler` lives in `apps/sim`,
81+
* which packages cannot import, and the realtime bootstrap needs this path too. Undefined
82+
* otherwise, leaving the SDK default in place.
83+
*/
84+
function proxyAwareRequestHandler(): FetchHttpHandler | undefined {
85+
if (!process.versions.bun) return undefined
86+
if (!PROXY_ENV.some((name) => process.env[name]?.trim())) return undefined
87+
return new FetchHttpHandler()
88+
}
89+
7190
async function fetchSecretString(client: SecretsManagerClient, secretId: string): Promise<string> {
7291
const response = await sendWithRetry(client, secretId)
7392
if (!response.SecretString) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { execFile } from 'node:child_process'
2+
import { createServer, type Server } from 'node:http'
3+
import type { AddressInfo } from 'node:net'
4+
import { fileURLToPath } from 'node:url'
5+
import { promisify } from 'node:util'
6+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
7+
8+
const tunnels: string[] = []
9+
let proxy: Server
10+
let proxyUrl = ''
11+
12+
beforeAll(async () => {
13+
proxy = createServer((_req, res) => res.end())
14+
proxy.on('connect', (req, socket) => {
15+
socket.on('error', () => {})
16+
tunnels.push(String(req.url))
17+
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n')
18+
})
19+
await new Promise<void>((resolve) => proxy.listen(0, '127.0.0.1', resolve))
20+
proxyUrl = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}`
21+
})
22+
23+
afterAll(async () => {
24+
proxy.closeAllConnections()
25+
await new Promise<void>((resolve) => proxy.close(() => resolve()))
26+
})
27+
28+
describe('loadRuntimeSecrets under a proxy-only egress', () => {
29+
it('reaches Secrets Manager through HTTPS_PROXY under the Bun runtime', async () => {
30+
const entry = fileURLToPath(new URL('./index.ts', import.meta.url))
31+
const run = promisify(execFile)(
32+
'bun',
33+
[
34+
'--no-env-file',
35+
'-e',
36+
`await (await import(${JSON.stringify(entry)})).loadRuntimeSecrets()`,
37+
],
38+
{
39+
timeout: 20_000,
40+
env: {
41+
PATH: process.env.PATH,
42+
HOME: process.env.HOME,
43+
SIM_ENV_SECRET_ID: '/test/sim/env-vars',
44+
AWS_REGION: 'us-east-1',
45+
AWS_ACCESS_KEY_ID: 'AKIDEXAMPLE',
46+
AWS_SECRET_ACCESS_KEY: 'secret',
47+
AWS_CONFIG_FILE: '/dev/null',
48+
AWS_SHARED_CREDENTIALS_FILE: '/dev/null',
49+
AWS_ENDPOINT_URL_SECRETS_MANAGER: 'https://secrets.invalid',
50+
HTTPS_PROXY: proxyUrl,
51+
NO_PROXY: '',
52+
},
53+
}
54+
)
55+
await expect(run).rejects.toThrow(/Failed to fetch runtime secrets/)
56+
expect(tunnels).toContain('secrets.invalid:443')
57+
}, 30_000)
58+
})

0 commit comments

Comments
 (0)