Configuring IAM Authentication for RDS Proxy Pools
This guide is part of AWS RDS Proxy Connection Pooling. IAM authentication removes long-lived database passwords from the application: the client generates a signed token from its role’s credentials and presents it as the password. RDS Proxy makes the pattern practical, because it holds the real credentials in Secrets Manager and authenticates clients itself. The two mistakes that follow are both about the token’s short life — fifteen minutes — and both show up as connection failures rather than as authentication errors:
FATAL: PAM authentication failed for user "orders_app"
org.postgresql.util.PSQLException: FATAL: password authentication failed for user "orders_app"
com.zaxxer.hikari.pool.HikariPool - Exception during pool initialization
The first is a token that expired before it was used; the second is often a pool whose connection string embeds a token, which both fragments the pool and stops working when the token rotates. The objective is a configuration where the token is generated per physical connection, the pool key never changes, and TLS and permissions are correct.
Rapid incident diagnosis
Check where the token enters the connection. A token in the connection string is the fragmenting pattern:
grep -rn "generateAuthToken\|rds.signer\|RdsIamAuthTokenGenerator\|generateDbAuthToken" --include=* . | head
grep -rn "jdbc:.*password=" --include=*.yml --include=*.properties . | head
The correct place is a credential or password provider that the pool calls when it opens a connection, not a string assembled once at startup.
Confirm the proxy’s authentication configuration. IAM must be required or allowed on the proxy, and the proxy needs a secret for the real credentials:
aws rds describe-db-proxies --db-proxy-name orders-proxy \
--query 'DBProxies[0].{RequireTLS:RequireTLS, IdleTimeout:IdleClientTimeout, Auth:Auth}' --output json
IAMAuth of REQUIRED means clients must use tokens; DISABLED means they must not.
Check the database grant. The database user must be allowed to authenticate through IAM:
-- PostgreSQL
SELECT r.rolname, m.rolname AS member_of
FROM pg_roles r
JOIN pg_auth_members am ON am.member = r.oid
JOIN pg_roles m ON m.oid = am.roleid
WHERE r.rolname = 'orders_app'; -- expect rds_iam
-- MySQL
SELECT user, host, plugin FROM mysql.user WHERE user = 'orders_app'; -- expect AWSAuthenticationPlugin
Verify TLS. RDS Proxy commonly requires TLS, and IAM authentication to RDS requires it in general. A client without the right SSL mode fails at connect time with a message that mentions SSL rather than IAM.
Look at the timing of failures. Failures clustered around a multiple of fifteen minutes after startup point at a token being reused past its lifetime.
Mathematical sizing or parameter formula
Token generation rate follows connection creation, not request rate:
tokens_per_second = connections_created_per_second
≈ pool_connections ÷ maxLifetime_seconds × replicas
A fleet of 12 replicas with 16 connections each and a 25-minute lifetime generates 12 × 16 ÷ 1500 = 0.13 tokens per second. Token generation is a local signing operation with no network call in the AWS SDK, so even a much higher rate is inexpensive — but a pool that opens and closes connections constantly multiplies it.
Token lifetime versus connection lifetime. A token authenticates a connection at creation; the connection remains valid afterwards even when the token expires:
maxLifetime can exceed token_lifetime (15 min) safely
connectionTimeout must be well under token_lifetime
The constraint that matters is that a generated token must be used within fifteen minutes. A pool that generates a token and then queues for a slow connect can exceed that, which is why the generation belongs immediately before the connect attempt.
Connection churn is the real cost. Each new connection with IAM performs a TLS handshake plus token verification:
connect_cost ≈ tls_handshake + iam_verification + backend_handshake
Typically tens of milliseconds. With minimumIdle above zero and a sensible lifetime, that cost is paid rarely; with a pool that shrinks to zero between requests, it is paid on the request path.
Failover multiplies it. After a failover, every connection is recreated at once, each requiring a fresh token and a handshake:
failover_burst = replicas × pool_size
Keeping the pool sized from in-flight work rather than generously bounds that burst.
Exact remediation & configuration
Java with HikariCP: use a credential provider, not a password string. The AWS Advanced JDBC Wrapper’s IAM plugin generates a token per connection:
spring:
datasource:
driver-class-name: software.amazon.jdbc.Driver
url: jdbc:aws-wrapper:postgresql://orders-proxy.proxy-abc123.eu-west-1.rds.amazonaws.com:5432/orders?sslmode=require
username: orders_app
hikari:
maximum-pool-size: 16
minimum-idle: 4
connection-timeout: 3000
max-lifetime: 1500000
data-source-properties:
wrapperPlugins: iam,failover2,efm2
iamRegion: eu-west-1
iamExpiration: 870 # seconds; below the 15-minute maximum
Without the wrapper, a HikariConfig with a custom DataSource that overrides getConnection() to mint a token achieves the same; the essential property is that the token is created inside the connection factory.
.NET with Npgsql: use the periodic password provider:
var builder = new NpgsqlDataSourceBuilder(
"Host=orders-proxy.proxy-abc123.eu-west-1.rds.amazonaws.com;Database=orders;Username=orders_app;SSL Mode=Require;Maximum Pool Size=16");
builder.UsePeriodicPasswordProvider(
async (settings, ct) =>
{
var token = RDSAuthTokenGenerator.GenerateAuthToken(
RegionEndpoint.EUWest1, settings.Host!, settings.Port, settings.Username!);
return token;
},
successRefreshInterval: TimeSpan.FromMinutes(10),
failureRefreshInterval: TimeSpan.FromSeconds(10));
await using var dataSource = builder.Build();
Node.js with pg: a function password, evaluated per connection:
import { Signer } from "@aws-sdk/rds-signer";
import pg from "pg";
const signer = new Signer({
region: "eu-west-1",
hostname: process.env.PROXY_HOST,
port: 5432,
username: "orders_app",
});
export const pool = new pg.Pool({
host: process.env.PROXY_HOST,
port: 5432,
database: "orders",
user: "orders_app",
password: () => signer.getAuthToken(), // called for each new connection
ssl: { rejectUnauthorized: true, ca: fs.readFileSync("/etc/ssl/certs/rds-combined-ca-bundle.pem") },
max: 16,
idleTimeoutMillis: 120_000,
});
Python with SQLAlchemy: generate the token in the do_connect event:
import boto3
from sqlalchemy import create_engine, event
rds = boto3.client("rds", region_name="eu-west-1")
engine = create_engine("postgresql+psycopg://orders_app@orders-proxy...:5432/orders?sslmode=require",
pool_size=8, max_overflow=4, pool_pre_ping=True)
@event.listens_for(engine, "do_connect")
def provide_token(dialect, conn_rec, cargs, cparams):
cparams["password"] = rds.generate_db_auth_token(
DBHostname=cparams["host"], Port=5432, DBUsername=cparams["user"], Region="eu-west-1")
Grant the IAM permission on the proxy, not the instance. The resource is the proxy’s resource id:
{
"Effect": "Allow",
"Action": "rds-db:connect",
"Resource": "arn:aws:rds-db:eu-west-1:123456789012:dbuser:prx-0123456789abcdef/orders_app"
}
Grant the database role so the user can authenticate through IAM:
CREATE USER orders_app; -- no password
GRANT rds_iam TO orders_app; -- PostgreSQL
CREATE USER 'orders_app'@'%' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'; -- MySQL
Keep TLS verification on. Download the regional certificate bundle and verify it; disabling verification defeats a large part of the reason for using IAM in the first place.
Operating IAM authentication over time
Watch the proxy’s own metrics. RDS Proxy publishes authentication and connection metrics to CloudWatch; failures appear as client connection errors rather than as a named IAM metric, so correlate with the application’s logs:
aws cloudwatch get-metric-statistics --namespace AWS/RDS \
--metric-name ClientConnectionsSetupFailedAuth --dimensions Name=ProxyName,Value=orders-proxy \
--start-time $(date -u -d '-1 hour' +%FT%TZ) --end-time $(date -u +%FT%TZ) --period 300 --statistics Sum
A non-zero sum during a deploy usually means the new task’s role is missing rds-db:connect.
Rotate the underlying secret independently. The proxy authenticates to the database with a secret from Secrets Manager; rotating it does not affect clients, which is one of the main operational benefits. Confirm the proxy picks up a rotation without client impact as part of the rotation runbook.
Keep the token out of logs. Tokens are signed URLs and appear as passwords; a connection string logged at debug level leaks a credential valid for fifteen minutes. Ensure the connection string in configuration has no password field at all, which is automatic with the provider patterns above.
Plan for the burst. Any event that recreates connections — a failover, a deploy, a proxy restart — generates a token per connection. The arithmetic is the same as for any reconnect burst, and the mitigation is the same: a pool sized from demand, and a lifetime with jitter where the pool supports it.
| Operational aspect | With IAM through the proxy | With a static password |
|---|---|---|
| Credential in configuration | none | a secret to distribute and rotate |
| Rotation impact on clients | none | reconnect, or a restart |
| Cost per new connection | token signing plus TLS | TLS |
| Failure mode | rds-db:connect denied, or expired token |
wrong password |
| Auditability | IAM identity per connection | database user only |
Validation & verification
A fresh connection authenticates. From the application host, generate a token and connect manually:
TOKEN=$(aws rds generate-db-auth-token --hostname "$PROXY_HOST" --port 5432 --region eu-west-1 --username orders_app)
PGPASSWORD="$TOKEN" psql "host=$PROXY_HOST port=5432 dbname=orders user=orders_app sslmode=verify-full sslrootcert=/etc/ssl/certs/rds-combined-ca-bundle.pem" -c "SELECT 1"
The pool survives token expiry. Run the service for longer than fifteen minutes with steady traffic and confirm there are no authentication errors — proving tokens are minted per connection rather than reused.
Exactly one pool exists. Count distinct pools in the client’s metrics, or sessions by application_name on the database; a rising count over time means the token reached the pool key.
Connection churn is low. New connections per minute should match pool_size ÷ maxLifetime, not the request rate.
Failure is attributable. Remove rds-db:connect from a staging role and confirm the resulting error is distinguishable in logs from a network failure, so the next occurrence is diagnosed quickly.
Frequently Asked Questions
Does an expired token drop an existing connection?
Can IAM and password authentication coexist?
IAMAuth is set to allow rather than require. It is useful during a migration, though the goal is usually to disable password authentication entirely.Is the token generation call rate-limited?
How does this interact with session pinning?
SET statements, temporary tables and prepared statements; authentication method has no bearing on it. The pinning causes are covered in Resolving RDS Proxy Session Pinning.Related
- AWS RDS Proxy Connection Pooling — the parent guide to the proxy’s pooling model.
- Sizing RDS Proxy MaxConnectionsPercent — sizing the backend pool behind the proxy.
- Diagnosing Npgsql Connection Pool Exhaustion — the pool-splitting failure mode in .NET.
- Using the AWS Advanced JDBC Wrapper for Fast Failover — the wrapper whose IAM plugin is used above.