Skip to content

Commit 9de035e

Browse files
authored
Merge pull request #1591 from dcoric/feat/postgres-rds-iam-auth
feat: AWS RDS/Aurora IAM authentication for the PostgreSQL sink
2 parents f04ccba + 772d4ea commit 9de035e

7 files changed

Lines changed: 496 additions & 5 deletions

File tree

config.schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,21 @@
621621
"description": "Milliseconds to wait for a connection before timing out."
622622
}
623623
}
624+
},
625+
"awsIamAuth": {
626+
"type": "object",
627+
"description": "Authenticate to Amazon RDS/Aurora with an IAM auth token instead of a static password. When enabled, a short-lived token is generated for each new connection from the AWS SDK default credential chain, so no password is stored. Requires the discrete `host`/`port`/`user` fields (or the `PGHOST`/`PGPORT`/`PGUSER` environment variables) rather than a `connectionString`, requires TLS (`ssl` defaults to `true` when omitted), and needs the optional `@aws-sdk/rds-signer` dependency to be installed.",
628+
"properties": {
629+
"enabled": {
630+
"type": "boolean",
631+
"description": "Enable IAM token authentication for the PostgreSQL connection."
632+
},
633+
"region": {
634+
"type": "string",
635+
"description": "AWS region of the RDS/Aurora instance. Falls back to the `AWS_REGION` / `AWS_DEFAULT_REGION` environment variables, then the AWS SDK's default region resolution."
636+
}
637+
},
638+
"required": ["enabled"]
624639
}
625640
},
626641
"required": ["type", "enabled"]

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@
218218
"underscore": "^1.13.8"
219219
},
220220
"optionalDependencies": {
221+
"@aws-sdk/rds-signer": "^3.980.0",
221222
"@esbuild/darwin-arm64": "^0.27.2",
222223
"@esbuild/darwin-x64": "^0.27.2",
223224
"@esbuild/linux-x64": "0.27.2",

src/config/generated/config.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,15 @@ export interface Database {
566566
*/
567567
options?: Options;
568568
type: DatabaseType;
569+
/**
570+
* Authenticate to Amazon RDS/Aurora with an IAM auth token instead of a static password.
571+
* When enabled, a short-lived token is generated for each new connection from the AWS SDK
572+
* default credential chain, so no password is stored. Requires the discrete
573+
* `host`/`port`/`user` fields (or the `PGHOST`/`PGPORT`/`PGUSER` environment variables)
574+
* rather than a `connectionString`, requires TLS (`ssl` defaults to `true` when omitted),
575+
* and needs the optional `@aws-sdk/rds-signer` dependency to be installed.
576+
*/
577+
awsIamAuth?: AwsIamAuth;
569578
/**
570579
* Database name. Used when `connectionString` is not set. Falls back to the `PGDATABASE`
571580
* environment variable.
@@ -604,6 +613,27 @@ export interface Database {
604613
[property: string]: any;
605614
}
606615

616+
/**
617+
* Authenticate to Amazon RDS/Aurora with an IAM auth token instead of a static password.
618+
* When enabled, a short-lived token is generated for each new connection from the AWS SDK
619+
* default credential chain, so no password is stored. Requires the discrete
620+
* `host`/`port`/`user` fields (or the `PGHOST`/`PGPORT`/`PGUSER` environment variables)
621+
* rather than a `connectionString`, requires TLS (`ssl` defaults to `true` when omitted),
622+
* and needs the optional `@aws-sdk/rds-signer` dependency to be installed.
623+
*/
624+
export interface AwsIamAuth {
625+
/**
626+
* Enable IAM token authentication for the PostgreSQL connection.
627+
*/
628+
enabled: boolean;
629+
/**
630+
* AWS region of the RDS/Aurora instance. Falls back to the `AWS_REGION` /
631+
* `AWS_DEFAULT_REGION` environment variables, then the AWS SDK's default region resolution.
632+
*/
633+
region?: string;
634+
[property: string]: any;
635+
}
636+
607637
/**
608638
* mongoDB Client connection options. Please note that only custom options are described
609639
* here, see
@@ -1178,6 +1208,7 @@ const typeMap: any = {
11781208
{ json: 'enabled', js: 'enabled', typ: true },
11791209
{ json: 'options', js: 'options', typ: u(undefined, r('Options')) },
11801210
{ json: 'type', js: 'type', typ: r('DatabaseType') },
1211+
{ json: 'awsIamAuth', js: 'awsIamAuth', typ: u(undefined, r('AwsIamAuth')) },
11811212
{ json: 'database', js: 'database', typ: u(undefined, '') },
11821213
{ json: 'host', js: 'host', typ: u(undefined, '') },
11831214
{ json: 'password', js: 'password', typ: u(undefined, '') },
@@ -1188,6 +1219,13 @@ const typeMap: any = {
11881219
],
11891220
'any',
11901221
),
1222+
AwsIamAuth: o(
1223+
[
1224+
{ json: 'enabled', js: 'enabled', typ: true },
1225+
{ json: 'region', js: 'region', typ: u(undefined, '') },
1226+
],
1227+
'any',
1228+
),
11911229
Options: o(
11921230
[
11931231
{

src/db/postgres/helper.ts

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,80 @@ const hasConnectionConfig = (db: DatabaseConfig): boolean =>
4343
process.env.PGDATABASE,
4444
);
4545

46+
/**
47+
* Minimal shape of the optional `@aws-sdk/rds-signer` module, declared locally
48+
* so the project type-checks whether or not the optional dependency is present.
49+
*/
50+
interface RdsSignerModule {
51+
Signer: new (config: { hostname: string; port: number; username: string; region?: string }) => {
52+
getAuthToken: () => Promise<string>;
53+
};
54+
}
55+
56+
/**
57+
* Load the optional RDS signer, raising a clear error if it is not installed.
58+
* Kept optional so installs that do not use IAM auth stay lean.
59+
*/
60+
const loadRdsSigner = async (): Promise<RdsSignerModule> => {
61+
try {
62+
return (await import('@aws-sdk/rds-signer')) as unknown as RdsSignerModule;
63+
} catch {
64+
throw new Error(
65+
'AWS RDS IAM authentication requires the optional `@aws-sdk/rds-signer` dependency. Install it with `npm install @aws-sdk/rds-signer`.',
66+
);
67+
}
68+
};
69+
70+
/**
71+
* Build the per-connection password provider for RDS/Aurora IAM auth. `pg`
72+
* invokes it for every new connection, so each one receives a fresh (~15 min)
73+
* token and refresh is automatic — no static password is ever stored.
74+
*/
75+
const buildIamTokenProvider = (db: DatabaseConfig): (() => Promise<string>) => {
76+
const host = db.host ?? process.env.PGHOST;
77+
const port = db.port ?? (process.env.PGPORT ? Number(process.env.PGPORT) : 5432);
78+
const user = db.user ?? process.env.PGUSER;
79+
const region = db.awsIamAuth?.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
80+
81+
if (!host || !user) {
82+
throw new Error(
83+
'AWS RDS IAM authentication requires `host` and `user` (or the PGHOST/PGUSER environment variables) to generate an auth token.',
84+
);
85+
}
86+
87+
return async () => {
88+
const { Signer } = await loadRdsSigner();
89+
const signer = new Signer({ hostname: host, port, username: user, region });
90+
return signer.getAuthToken();
91+
};
92+
};
93+
4694
/**
4795
* Build a `pg` PoolConfig from the resolved database config. A connection
4896
* string (already env-resolved by `getDatabase`) takes precedence; otherwise
4997
* the discrete fields are used. When neither is set, `pg` reads the `PG*`
50-
* environment variables itself.
98+
* environment variables itself. When `awsIamAuth` is enabled, the static
99+
* password is replaced by a generated IAM token and the discrete fields drive
100+
* the connection.
51101
*/
52102
const buildPoolConfig = (db: DatabaseConfig): PoolConfig => {
53103
const config: PoolConfig = {};
54-
if (db.connectionString) {
104+
const iamAuthEnabled = Boolean(db.awsIamAuth?.enabled);
105+
106+
if (iamAuthEnabled) {
107+
// IAM auth supplies the password as a generated token, so the connection is
108+
// 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+
}
114+
if (db.host !== undefined) config.host = db.host;
115+
if (db.port !== undefined) config.port = db.port;
116+
if (db.user !== undefined) config.user = db.user;
117+
if (db.database !== undefined) config.database = db.database;
118+
config.password = buildIamTokenProvider(db);
119+
} else if (db.connectionString) {
55120
if (
56121
db.host !== undefined ||
57122
db.port !== undefined ||
@@ -71,8 +136,32 @@ const buildPoolConfig = (db: DatabaseConfig): PoolConfig => {
71136
if (db.password !== undefined) config.password = db.password;
72137
if (db.database !== undefined) config.database = db.database;
73138
}
74-
// TLS applies regardless of how the connection itself was configured.
75-
if (db.ssl !== undefined) config.ssl = db.ssl as PoolConfig['ssl'];
139+
140+
// TLS applies regardless of how the connection itself was configured. RDS IAM
141+
// auth mandates TLS, so default it on when IAM is enabled and `ssl` is unset.
142+
if (db.ssl !== undefined) {
143+
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+
}
151+
} 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.
157+
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+
);
164+
}
76165

77166
// Optional pool tuning.
78167
if (db.pool) {

0 commit comments

Comments
 (0)