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.

AspectStatic passwordIAM token
Credential lifetimeUntil manually rotated15 minutes maximum
Stored in environmentYes (or Secrets Manager)No — generated at runtime
Rotation impact on running connectionsNone (existing connections keep using old password)None (token is checked only at connection open)
Rotation impact on new connectionsBreaks new connections until env var updatedNo impact — each new connection generates a fresh token
Requires SSLNo (configurable)Yes (mandatory)
Works with RDS ProxyYes (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:

// 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

SymptomCauseFix
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 SSLAdd 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 minutesCached token has expired; stale token passed to new connection attemptsImplement 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:connectLambda execution role is missing the rds-db:connect IAM policy, or the resource ARN uses the wrong DbiResourceIdVerify 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 existThe PostgreSQL user was not created, or was created with a different username than the one in the IAM policy and tokenConnect as the master user and run CREATE USER app_user WITH LOGIN; GRANT rds_iam TO app_user;
Connections succeed locally but fail in LambdaLambda environment lacks the RDS CA certificate bundle needed for SSL verificationDownload rds-combined-ca-bundle.pem from the RDS documentation and bundle it with the Lambda deployment package; set ssl.ca to the certificate contents