Skip to content

Commit

Permalink
fix: Allow RDS for MySQL to connect via TLS (#122) (#124)
Browse files Browse the repository at this point in the history
Fixes #122
  • Loading branch information
doublecompile committed Feb 26, 2024
1 parent 9cd1d8d commit 1d6dd03
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 26 deletions.
72 changes: 72 additions & 0 deletions docs/api/API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/rds/mysql-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ export interface MysqlDatabaseOptions {
* @default - rely on MySQL to choose the default collation.
*/
readonly collation?: string;
/**
* The URL to the PEM-encoded Certificate Authority file.
*
* Normally, we would just assume the Lambda runtime has the certificates to
* trust already installed. Since the current Lambda runtime environments lack
* the newer RDS certificate authority certificates, this option can be used
* to specify a URL to a remote file containing the CAs.
*
* @see https://github.com/aws/aws-lambda-base-images/issues/123
*
* @default - https://truststore.pki.rds.amazonaws.com/REGION/REGION-bundle.pem
*/
readonly certificateAuthoritiesUrl?: string;
}

/**
Expand Down Expand Up @@ -288,6 +301,9 @@ export class MysqlDatabase extends BaseDatabase {
if (props.collation) {
environment.DB_COLLATION = props.collation;
}
if (props.certificateAuthoritiesUrl) {
environment.CA_CERTS_URL = props.certificateAuthoritiesUrl;
}

this.lambdaFunction = new Function(this, "Function", {
runtime: Runtime.NODEJS_20_X,
Expand Down
16 changes: 14 additions & 2 deletions src/rds/triggers/mysql.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
// eslint-disable-next-line import/no-extraneous-dependencies
import { Connection, createConnection } from "mysql2/promise";
import type { UsernamePassword, DatabaseCredentials } from "./types";
import { fetchSecret, fetchAllSecrets, parseJsonArrayFromEnv } from "./util";
import {
fetchSecret,
fetchAllSecrets,
parseJsonArrayFromEnv,
readRemote,
} from "./util";

const adminSecretArn = process.env.ADMIN_SECRET_ARN!;
const databaseName = process.env.DB_NAME!;
Expand Down Expand Up @@ -43,12 +48,19 @@ const handler = async () => {
adminSecretArn,
secretsManagerClient
);

const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
const caBundle = await readRemote(
process.env.CA_CERTS_URL ||
`https://truststore.pki.rds.amazonaws.com/${region}/${region}-bundle.pem`
);

const connection = await createConnection({
host: adminSecret.host,
user: adminSecret.username,
password: adminSecret.password,
port: adminSecret.port,
ssl: "Amazon RDS",
ssl: { ca: caBundle },
connectTimeout: 40000,
});

Expand Down
30 changes: 6 additions & 24 deletions src/rds/triggers/pgsql.handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as https from "https";
// eslint-disable-next-line import/no-extraneous-dependencies
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
// eslint-disable-next-line import/no-extraneous-dependencies
Expand All @@ -14,7 +13,12 @@ import type { Handler } from "aws-lambda";
// eslint-disable-next-line import/no-extraneous-dependencies
import { Client, ClientConfig } from "pg";
import type { DatabaseCredentials, UsernamePassword } from "./types";
import { fetchSecret, fetchAllSecrets, parseJsonArrayFromEnv } from "./util";
import {
fetchSecret,
fetchAllSecrets,
parseJsonArrayFromEnv,
readRemote,
} from "./util";

const adminSecretArn = process.env.ADMIN_SECRET_ARN!;
const ownerSecretArn = process.env.OWNER_SECRET_ARN!;
Expand All @@ -28,28 +32,6 @@ const ownerSecretArns = parseJsonArrayFromEnv("OWNER_SECRETS");
const readerSecretArns = parseJsonArrayFromEnv("READER_SECRETS");
const unprivilegedSecretArns = parseJsonArrayFromEnv("UNPRIVILEGED_SECRETS");

/**
* Reads an HTTPS resource into a string.
*
* We need this function since newer RDS CA certificates aren't in Lambda.
*
* @see https://github.com/aws/aws-lambda-base-images/issues/123
*/
function readRemote(
url: string,
options?: https.RequestOptions
): Promise<Buffer> {
return new Promise((resolve, reject) => {
https
.get(url, options || {}, (res) => {
const data: Buffer[] = [];
res.on("data", (d) => data.push(d));
res.on("end", () => resolve(Buffer.concat(data)));
})
.on("error", (e) => reject(e));
});
}

const handler: Handler = async () => {
// Here's the first network request:
// Load the admin secret, needed to create the catalog and its owner.
Expand Down
23 changes: 23 additions & 0 deletions src/rds/triggers/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as https from "https";
// eslint-disable-next-line import/no-extraneous-dependencies
import {
SecretsManagerClient,
Expand Down Expand Up @@ -37,3 +38,25 @@ export async function fetchAllSecrets<T = Record<string, any>>(
}
return Promise.all(arns.map((a) => fetchSecret<T>(a, client)));
}

/**
* Reads an HTTPS resource into a string.
*
* We need this function since newer RDS CA certificates aren't in Lambda.
*
* @see https://github.com/aws/aws-lambda-base-images/issues/123
*/
export function readRemote(
url: string,
options?: https.RequestOptions
): Promise<Buffer> {
return new Promise((resolve, reject) => {
https
.get(url, options || {}, (res) => {
const data: Buffer[] = [];
res.on("data", (d) => data.push(d));
res.on("end", () => resolve(Buffer.concat(data)));
})
.on("error", (e) => reject(e));
});
}

0 comments on commit 1d6dd03

Please sign in to comment.