Skip to content

Commit 94b49b9

Browse files
committed
fix(db/postgres): surface RDS IAM transport pitfalls instead of failing quietly
Review feedback on IAM auth: - When awsIamAuth is enabled a configured connectionString is ignored by design; that now logs a warning instead of happening silently. - ssl: true verifies against Node's default trust store, which does not contain Amazon's RDS root CA, so the documented fallback would fail certificate verification against a real RDS endpoint. The default stays verify-on, but startup now warns (both when ssl is defaulted and when supplied ssl options lack a ca) that the RDS CA bundle must be provided via ssl.ca, and the architecture doc leads with the bundle requirement including the download location.
1 parent 4c7003d commit 94b49b9

3 files changed

Lines changed: 75 additions & 1 deletion

File tree

src/db/postgres/helper.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ const buildPoolConfig = (db: DatabaseConfig): PoolConfig => {
106106
if (iamAuthEnabled) {
107107
// IAM auth supplies the password as a generated token, so the connection is
108108
// driven by the discrete fields (or PG* env), never a connection string.
109+
if (db.connectionString) {
110+
console.warn(
111+
'[postgres] awsIamAuth is enabled; ignoring connectionString (IAM mode uses the discrete host/port/user/database fields)',
112+
);
113+
}
109114
if (db.host !== undefined) config.host = db.host;
110115
if (db.port !== undefined) config.port = db.port;
111116
if (db.user !== undefined) config.user = db.user;
@@ -136,8 +141,26 @@ const buildPoolConfig = (db: DatabaseConfig): PoolConfig => {
136141
// auth mandates TLS, so default it on when IAM is enabled and `ssl` is unset.
137142
if (db.ssl !== undefined) {
138143
config.ssl = db.ssl as PoolConfig['ssl'];
144+
if (iamAuthEnabled && typeof db.ssl === 'object' && db.ssl !== null && !('ca' in db.ssl)) {
145+
console.warn(
146+
'[postgres] awsIamAuth: the ssl options carry no `ca`; RDS server certificates chain to ' +
147+
"Amazon's RDS root CA, which is not in Node's default trust store, so verification " +
148+
'will fail unless the RDS CA bundle is supplied via ssl.ca',
149+
);
150+
}
139151
} else if (iamAuthEnabled) {
152+
// RDS requires TLS for IAM auth, so it defaults on. `ssl: true` verifies
153+
// against Node's default trust store, which does NOT contain Amazon's RDS
154+
// root CA; connecting to a real RDS endpoint therefore needs the RDS CA
155+
// bundle supplied via `ssl.ca`. The default stays verify-on rather than
156+
// silently downgrading transport security.
140157
config.ssl = true;
158+
console.warn(
159+
'[postgres] awsIamAuth: ssl defaulted to true, which verifies against ' +
160+
"Node's default trust store; connections to RDS will fail certificate " +
161+
'verification unless the RDS CA bundle is supplied via ssl.ca ' +
162+
'(see the PostgreSQL section of the architecture doc)',
163+
);
141164
}
142165

143166
// Optional pool tuning.

test/db/postgres/helper.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,57 @@ describe('PostgreSQL - helper', async () => {
354354
expect(getOpts().ssl).toEqual(ssl);
355355
});
356356

357+
it('warns that the connection string is ignored when IAM auth is enabled', async () => {
358+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
359+
getDatabaseMock.mockReturnValue({
360+
type: 'postgres',
361+
enabled: true,
362+
connectionString: 'postgresql://ignored/x',
363+
host: 'rds.example.com',
364+
user: 'gp',
365+
awsIamAuth: { enabled: true, region: 'eu-west-2' },
366+
});
367+
368+
await connect();
369+
370+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ignoring connectionString'));
371+
warnSpy.mockRestore();
372+
});
373+
374+
it('warns when ssl defaults to true in IAM mode (RDS CA is not in the default trust store)', async () => {
375+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
376+
getDatabaseMock.mockReturnValue({
377+
type: 'postgres',
378+
enabled: true,
379+
host: 'rds.example.com',
380+
user: 'gp',
381+
awsIamAuth: { enabled: true, region: 'eu-west-2' },
382+
});
383+
384+
await connect();
385+
386+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ssl.ca'));
387+
warnSpy.mockRestore();
388+
});
389+
390+
it('does not warn about ssl when a CA bundle is supplied', async () => {
391+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
392+
getDatabaseMock.mockReturnValue({
393+
type: 'postgres',
394+
enabled: true,
395+
host: 'rds.example.com',
396+
user: 'gp',
397+
ssl: { rejectUnauthorized: true, ca: 'RDS_CA' },
398+
awsIamAuth: { enabled: true, region: 'eu-west-2' },
399+
});
400+
401+
await connect();
402+
403+
const sslWarnings = warnSpy.mock.calls.filter(([m]) => String(m).includes('ssl.ca'));
404+
expect(sslWarnings).toEqual([]);
405+
warnSpy.mockRestore();
406+
});
407+
357408
it('ignores a connection string when IAM auth is enabled', async () => {
358409
getDatabaseMock.mockReturnValue({
359410
type: 'postgres',

website/docs/architecture/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ For Amazon RDS or Aurora, GitProxy can authenticate with a short-lived IAM auth
552552

553553
- A fresh token is generated for every new pool connection from the AWS SDK default credential chain (via `@aws-sdk/rds-signer`), so no password is stored and token refresh is automatic.
554554
- `region` falls back to the `AWS_REGION` / `AWS_DEFAULT_REGION` environment variables, then the SDK's default region resolution.
555-
- TLS is required by RDS for IAM auth, so `ssl` defaults to `true` when omitted. Supply an `ssl` object (for example `{ "rejectUnauthorized": true, "ca": "<RDS CA bundle>" }`) to verify against the RDS certificate authority.
555+
- TLS is required by RDS for IAM auth. Supply the RDS certificate authority bundle via `ssl` (for example `{ "rejectUnauthorized": true, "ca": "<contents of the RDS global-bundle.pem>" }`, downloadable from https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem): RDS server certificates chain to Amazon's own root CA, which is not in Node's default trust store, so the `ssl: true` fallback (used when `ssl` is omitted) fails certificate verification against a real RDS endpoint and logs a startup warning saying so. Do not work around a verification failure with `rejectUnauthorized: false`; that discards the transport security IAM auth depends on.
556556
- The database user must be granted the `rds_iam` role (`GRANT rds_iam TO gitproxy_iam;`).
557557
- IAM auth needs the optional `@aws-sdk/rds-signer` dependency, which installs by default. On a slim install (`npm install --omit=optional`) add it explicitly with `npm install @aws-sdk/rds-signer`.
558558

0 commit comments

Comments
 (0)