Guide · AWS RDS
MCP Server RDS IAM Authentication — token-based DB auth, no static passwords
RDS IAM database authentication eliminates static database passwords from your MCP server's environment variables — but three details catch teams off guard. First, the IAM token is used as the password in the connection string, not as an HTTP header: you pass the 15-minute token to pg.Pool as the password field, and PostgreSQL validates it against AWS IAM via the rds_iam role — it looks like a password authentication flow from the client's perspective, but uses a pre-signed URL under the hood. Second, existing connections authenticated with an old token keep working past the 15-minute expiry — the token is only checked at connection open time, not on each query — but any attempt to open a new connection with a stale token will fail immediately with an auth error; you must generate a fresh token before creating new connections. Third, SSL is mandatory: if ssl is not configured in your connection options, IAM auth will be rejected even if the instance has IAM auth enabled, because RDS requires encrypted connections to transmit the token.
TL;DR
Enable IAM database authentication on the RDS instance, create a PostgreSQL user with the rds_iam role, attach an IAM policy allowing rds-db:connect on the instance ARN to your Lambda role, then use AWS SDK's RDSSigner to generate a fresh token in Lambda initialization code. Cache the token and refresh it before the 15-minute expiry. Always enable SSL in the PostgreSQL client config or connections will be rejected.
How IAM database authentication works
IAM authentication for RDS is a pre-signed URL mechanism. The RDSSigner creates a SigV4-signed request URL using your Lambda's IAM role credentials — the URL encodes the region, hostname, port, DB username, and a 15-minute expiry timestamp. This URL is passed as the PostgreSQL password. RDS validates the signature against the role's current credentials via the IAM service.
The mechanism requires SSL because the token travels over the PostgreSQL wire protocol as a cleartext password field — TLS prevents interception. RDS enforces this: without SSL, the connection is rejected before authentication begins.
| Aspect | Static password | IAM token |
|---|---|---|
| Credential lifetime | Until manually rotated | 15 minutes maximum |
| Stored in environment | Yes (or Secrets Manager) | No — generated at runtime |
| Rotation impact on running connections | None (existing connections keep using old password) | None (token is checked only at connection open) |
| Rotation impact on new connections | Breaks new connections until env var updated | No impact — each new connection generates a fresh token |
| Requires SSL | No (configurable) | Yes (mandatory) |
| Works with RDS Proxy | Yes (proxy fetches from Secrets Manager) | Yes — Lambda authenticates to proxy with IAM token; proxy connects to DB with stored secret |
Step 1: Enable IAM auth on the RDS instance
# Enable IAM authentication on an existing RDS PostgreSQL instance
aws rds modify-db-instance \
--db-instance-identifier mcp-prod \
--enable-iam-database-authentication \
--apply-immediately
# For Aurora clusters, modify the cluster (not the instance)
aws rds modify-db-cluster \
--db-cluster-identifier mcp-prod-cluster \
--enable-iam-database-authentication \
--apply-immediately
# Verify the setting is enabled
aws rds describe-db-instances \
--db-instance-identifier mcp-prod \
--query 'DBInstances[0].IAMDatabaseAuthenticationEnabled'
# Should return: true
Step 2: Create the PostgreSQL user with rds_iam
-- Connect to the RDS instance as the master user (e.g., postgres)
-- Create an application user that will be authenticated via IAM
-- No password needed — the rds_iam role replaces the password check
CREATE USER app_user WITH LOGIN;
GRANT rds_iam TO app_user;
-- Grant the user access to the database and schema
GRANT CONNECT ON DATABASE mcpdb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
-- Grant on future tables too
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_user;
The GRANT rds_iam TO app_user statement is the key line. This tells RDS that this PostgreSQL user should be authenticated via IAM rather than a password. Attempting to connect with a regular password as app_user will now fail — only IAM token authentication works for this user.
Step 3: Attach IAM policy to the Lambda role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "rds-db:connect",
"Resource": [
"arn:aws:rds-db:us-east-1:123456789012:dbuser:db-ABCDEFGHIJKLMNOP/app_user"
]
}
]
}
The resource ARN format is: arn:aws:rds-db:{region}:{account-id}:dbuser:{db-resource-id}/{db-user-name}. The db-resource-id is the DbiResourceId for an RDS instance (visible in the console under "Configuration") or the cluster resource ID for Aurora. This is distinct from the instance identifier — it starts with db- followed by an uppercase alphanumeric string.
# Get the DbiResourceId for an RDS instance
aws rds describe-db-instances \
--db-instance-identifier mcp-prod \
--query 'DBInstances[0].DbiResourceId'
# Returns: "db-ABCDEFGHIJKLMNOP"
# For Aurora, get the cluster resource ID
aws rds describe-db-clusters \
--db-cluster-identifier mcp-prod-cluster \
--query 'DBClusters[0].DbClusterResourceId'
# Returns: "cluster-ABCDEFGHIJKLMNOP"
Step 4: Generate and refresh tokens in Lambda
import { RDSSigner } from "@aws-sdk/rds-signer";
import { Pool } from "pg";
const signer = new RDSSigner({
region: process.env.AWS_REGION!,
hostname: process.env.RDS_ENDPOINT!, // RDS instance or cluster endpoint
port: 5432,
username: "app_user",
});
const TOKEN_TTL_MS = 900_000; // 15 minutes
const REFRESH_BUFFER_MS = 60_000; // Refresh 60s before expiry
let pool: Pool | null = null;
let tokenExpiresAt = 0;
async function getPool(): Promise {
const now = Date.now();
if (!pool || now > tokenExpiresAt - REFRESH_BUFFER_MS) {
// Generate new token before the old one expires
// getAuthToken() call takes ~100-200ms (SigV4 signing, no network call)
const token = await signer.getAuthToken();
tokenExpiresAt = now + TOKEN_TTL_MS;
const oldPool = pool;
pool = new Pool({
host: process.env.RDS_ENDPOINT!,
port: 5432,
database: process.env.DB_NAME!,
user: "app_user",
password: token,
ssl: {
// RDS uses a CA certificate; rejectUnauthorized ensures the cert is valid
rejectUnauthorized: true,
// Download the RDS CA bundle and bundle it with your Lambda
ca: process.env.RDS_CA_CERT,
},
max: 1, // One connection per Lambda invocation
idleTimeoutMillis: 10_000,
connectionTimeoutMillis: 5_000,
});
// Drain old pool after creating new one (not before — avoid gap)
if (oldPool) {
oldPool.end().catch(() => {}); // Fire and forget; old connections close gracefully
}
}
return pool!;
}
export const handler = async (event: any) => {
const db = await getPool();
const client = await db.connect();
try {
// Your MCP tool logic here
const result = await client.query(
"SELECT * FROM sessions WHERE id = $1",
[event.sessionId]
);
return result.rows;
} finally {
client.release();
}
};
Token generation is local and fast. signer.getAuthToken() computes the SigV4 signature using local credentials — it does not make a network call to IAM. Latency is typically 50–200ms (local crypto). The token itself is a URL-encoded pre-signed string around 1 KB in size.
Using RDS Proxy with IAM authentication (recommended)
When you use RDS Proxy with IAM authentication, the trust relationship changes. The Lambda authenticates to the proxy using an IAM token (same mechanism as above, but the hostname in the token is the proxy endpoint). The proxy connects to the RDS instance using the DB credentials stored in a Secrets Manager secret — the Lambda never sees those credentials.
This is the recommended architecture because:
- The proxy handles DB credential rotation without Lambda restarts
- The proxy provides connection pooling and failover transparency
- The Lambda's IAM role only needs
rds-db:connectpermission on the proxy ARN, not on the underlying DB instance
// When using RDS Proxy, point the signer at the proxy endpoint
const signer = new RDSSigner({
region: process.env.AWS_REGION!,
hostname: process.env.RDS_PROXY_ENDPOINT!, // proxy-name.proxy-xxxx.region.rds.amazonaws.com
port: 5432,
username: "app_user",
});
// The rest of the connection code is identical —
// the pool connects to the proxy endpoint with the IAM token as the password.
// The proxy validates the token against IAM, then routes to the DB
// using its internally configured Secrets Manager secret.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
FATAL: PAM authentication failed or password authentication failed for user "app_user" | SSL is not enabled in the client connection config; RDS rejects IAM auth without SSL | Add ssl: { rejectUnauthorized: true } to the pg.Pool options; verify with openssl s_client -connect endpoint:5432 -starttls postgres |
| Token accepted for existing connections, but new connections fail after 15 minutes | Cached token has expired; stale token passed to new connection attempts | Implement token refresh logic that generates a new token at least 60 seconds before the 15-minute expiry; track tokenExpiresAt relative to pool creation time |
AccessDenied: User: arn:aws:sts::... is not authorized to perform: rds-db:connect | Lambda execution role is missing the rds-db:connect IAM policy, or the resource ARN uses the wrong DbiResourceId | Verify the policy is attached to the Lambda role; double-check the DbiResourceId (not the instance identifier) with aws rds describe-db-instances --query 'DBInstances[0].DbiResourceId' |
FATAL: role "app_user" does not exist | The PostgreSQL user was not created, or was created with a different username than the one in the IAM policy and token | Connect as the master user and run CREATE USER app_user WITH LOGIN; GRANT rds_iam TO app_user; |
| Connections succeed locally but fail in Lambda | Lambda environment lacks the RDS CA certificate bundle needed for SSL verification | Download rds-combined-ca-bundle.pem from the RDS documentation and bundle it with the Lambda deployment package; set ssl.ca to the certificate contents |