Your Lambda function works. It connects to RDS, queries the database, returns results. Somewhere in your environment variables sits a username and a password — plain text, sitting there quietly.
You know it's not ideal. But it works, so it stays.
Here's the thing: one misconfigured IAM policy, one accidental log exposure, one leaked deployment artifact — and those credentials are compromised.
AWS gives you a way to eliminate database passwords entirely from your Lambda code. No rotation headaches. No secrets to leak. Just IAM — the same authentication mechanism you already use for everything else on AWS.
This is what we're building today.
How IAM Authentication Works with RDS Proxy
Instead of a static username and password, Lambda generates a temporary IAM token valid for 15 minutes. This token is used as the database password for the connection. AWS validates it internally — no credentials ever leave your environment.
The flow looks like this:
Lambda
→ generates temporary IAM token via boto3 (valid 15 min)
→ connects to RDS Proxy using token as password
→ RDS Proxy validates token against IAM
→ connection authorized
→ RDS MySQL
Three components make this work together:
1. IAM Role attached to Lambda
Must have the permission rds-db:connect — this is what allows Lambda to generate the auth token in the first place.
2. Database user created with AWSAuthenticationPlugin
Instead of a password-based user, you create a MySQL user that delegates authentication entirely to AWS IAM:
CREATE USER 'lambda_user' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'lambda_user'@'%';
3. RDS Proxy with IAM Authentication enabled
The Proxy must be configured with IAM Authentication = Required — it will reject any connection attempt that doesn't carry a valid IAM token.
Architecture Overview
Before diving into the setup, here is the full picture of what we are building:
┌─────────────────────────────────────────────┐
│ VPC │
│ │
│ ┌──────────┐ ┌─────────────┐ ┌─────┐ │
│ │ Lambda │───▶│ RDS Proxy │───▶│ RDS │ │
│ │(IAM Role)│ │(IAM Auth ON)│ │MySQL│ │
│ └──────────┘ └─────────────┘ └─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │Secrets Manager│ │
│ │(Proxy creds) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────┘
Prerequisites before starting:
- An existing RDS MySQL instance inside a VPC
- An existing RDS Proxy pointing to that RDS instance (if you haven't set one up yet, refer to the previous article in this series)
- A Lambda function in the same VPC as RDS and the Proxy
- AWS Secrets Manager already storing your DB credentials for the Proxy
Note: This article builds directly on top of the RDS Proxy setup covered in Why Your Lambda Functions Are Silently Killing Your RDS Database. If you are starting from scratch, read that one first.
Setting Up IAM Authentication: Step by Step
Step 1 — Enable IAM Authentication on RDS
- Go to RDS → Databases
- Select your RDS MySQL instance
- Click Modify
- Under Database authentication, select Password and IAM database authentication
- Click Continue → Apply immediately
Step 2 — Enable IAM Authentication on RDS Proxy
- Go to RDS → Proxies
- Select your existing Proxy
- Click Modify
- Under IAM authentication, select Required
- Save changes
Step 3 — Create the IAM database user in MySQL
Connect to your RDS instance via Cloud9 or EC2 and run:
CREATE USER 'lambda_user' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'lambda_user'@'%';
FLUSH PRIVILEGES;
Result:
This user has no password — AWS IAM handles authentication entirely.
Step 4 — Attach the IAM policy to your Lambda role
- Go to IAM → Roles
- Find the execution role attached to your Lambda function
- Click Add permissions → Create inline policy
- Use this policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "rds-db:connect",
"Resource": "arn:aws:rds-db:us-east-1:YOUR_ACCOUNT_ID:dbuser:YOUR_PROXY_RESOURCE_ID/lambda_user"
}
]
}
Name it LambdaRDSProxyIAMAuth and save
Step 5 — Configure Lambda VPC settings
Your Lambda must be in the same VPC and subnets as your RDS Proxy:
- Go to Lambda → your function → Configuration → VPC
- Select the same VPC, subnets and security group as your RDS Proxy
- Save
The Code: Generating and Using the IAM Token
Here is the problematic pattern — static credentials hardcoded in environment variables:
import pymysql
import os
def lambda_handler(event, context):
# Static credentials — a security risk ❌
connection = pymysql.connect(
host=os.environ['RDS_PROXY_ENDPOINT'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'], # Plain text password
database=os.environ['DB_NAME'],
ssl={'ssl': True}
)
cursor = connection.cursor()
cursor.execute("SELECT NOW()")
result = cursor.fetchone()
connection.close()
return str(result)
Here is the correct pattern — IAM token replacing the password entirely:
import pymysql
import boto3
import os
# RDS client to generate the IAM auth token
rds_client = boto3.client('rds', region_name=os.environ['AWS_REGION'])
# Connection initialized outside the handler for reuse ✅
connection = None
def get_connection():
global connection
if connection is None or not connection.open:
# Generate a temporary IAM token (valid 15 minutes)
token = rds_client.generate_db_auth_token(
DBHostname=os.environ['RDS_PROXY_ENDPOINT'],
Port=3306,
DBUsername='lambda_user'
)
connection = pymysql.connect(
host=os.environ['RDS_PROXY_ENDPOINT'],
user='lambda_user',
password=token, # IAM token used as password ✅
database=os.environ['DB_NAME'],
ssl={'ssl': True}, # SSL required for IAM auth
connect_timeout=5
)
return connection
def lambda_handler(event, context):
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT NOW()")
result = cursor.fetchone()
return str(result)
Three critical points:
1. generate_db_auth_token — boto3 generates a signed token using your Lambda's IAM role. No password needed, no secrets to manage.
2. SSL is mandatory — IAM authentication requires an encrypted connection. Without ssl={'ssl': True}, the connection will be refused.
3. Token reuse — the token is valid 15 minutes. By initializing the connection outside the handler, you avoid regenerating a token on every invocation.
Provisioning Everything with Terraform
Here is the complete Terraform configuration to provision IAM authentication for Lambda + RDS Proxy:
# IAM policy allowing Lambda to connect via IAM auth
resource "aws_iam_policy" "lambda_rds_iam_auth" {
name = "LambdaRDSProxyIAMAuth"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "rds-db:connect"
Resource = "arn:aws:rds-db:${var.region}:${var.account_id}:dbuser:${aws_db_proxy.myapp.id}/lambda_user"
}]
})
}
# Attach policy to Lambda execution role
resource "aws_iam_role_policy_attachment" "lambda_rds_iam_auth" {
role = aws_iam_role.lambda_exec.name
policy_arn = aws_iam_policy.lambda_rds_iam_auth.arn
}
# Lambda execution role
resource "aws_iam_role" "lambda_exec" {
name = "lambda-rds-proxy-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
# Basic Lambda execution policy (CloudWatch logs)
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_exec.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
# Lambda function
resource "aws_lambda_function" "myapp" {
function_name = "myapp-lambda"
role = aws_iam_role.lambda_exec.arn
runtime = "python3.12"
handler = "lambda_function.lambda_handler"
filename = "lambda_function.zip"
vpc_config {
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.lambda_sg.id]
}
environment {
variables = {
RDS_PROXY_ENDPOINT = aws_db_proxy.myapp.endpoint
DB_NAME = var.db_name
AWS_REGION = var.region
}
}
tags = {
Name = "myapp-lambda"
Environment = var.environment
}
}
# RDS Proxy with IAM authentication required
resource "aws_db_proxy" "myapp" {
name = "myapp-rds-proxy"
engine_family = "MYSQL"
role_arn = aws_iam_role.rds_proxy_role.arn
vpc_subnet_ids = var.private_subnet_ids
vpc_security_group_ids = [aws_security_group.rds_proxy_sg.id]
require_tls = true
auth {
auth_scheme = "SECRETS"
iam_auth = "REQUIRED" # IAM authentication enforced ✅
secret_arn = aws_secretsmanager_secret.rds_credentials.arn
}
tags = {
Name = "myapp-rds-proxy"
Environment = var.environment
}
}
Two key points in this Terraform configuration:
1. iam_auth = "REQUIRED" — this enforces IAM authentication at the Proxy level. Any connection attempt without a valid IAM token is rejected, including direct MySQL client connections with a password.
2. No DB password in Lambda environment variables — compare this to the previous article's Terraform. The Lambda environment block contains only the Proxy endpoint, the database name, and the region. No credentials anywhere.
The Broader Lesson: Credentials Are a Liability, IAM Is a Feature
Every static credential is a liability. It can be leaked, stolen, forgotten in a .env file, committed to a Git repository, or exposed in CloudWatch logs. The longer it lives, the greater the risk.
IAM authentication flips this model entirely. There are no credentials to rotate, no secrets to manage, no passwords to expire. Your Lambda function proves its identity through the same IAM mechanism that governs everything else on AWS — and RDS Proxy enforces it at the network level.
This is not just a security best practice. It is the architecture AWS designed RDS Proxy for.
When to use IAM Authentication with RDS Proxy:
- Any Lambda + RDS workload in production — always
- When your security policy requires credential-free database access
- When you want to centralize access control in IAM rather than managing DB users and passwords separately
- When you need auditability — every connection attempt is logged via CloudTrail
When it adds complexity without proportional benefit:
- Short-lived development environments where security posture is not a priority
- Applications connecting from EC2 with a proper secrets rotation already in place via Secrets Manager
Common Pitfalls (and How to Fix Them)
Setting up IAM authentication with RDS Proxy is straightforward in theory. In practice, several subtle misconfigurations can cost you hours of debugging. Here is what I ran into during the real lab setup.
1. AWS_REGION is a reserved Lambda environment variable
If you try to add AWS_REGION as an environment variable in Lambda, AWS will block it with:
"The environment variables you have provided contains reserved keys"
Use a custom name instead — REGION works perfectly:
rds_client = boto3.client('rds', region_name=os.environ['REGION'])
2. pymysql is not included in the Lambda runtime
Lambda does not ship with pymysql — you will hit this immediately on your first test:
ModuleNotFoundError: No module named 'pymysql'
The fix is to create a custom Lambda Layer. From an EC2 instance in the same region:
sudo dnf install -y python3-pip
pip3 install pymysql -t /tmp/python/
cd /tmp
zip -r pymysql_layer.zip python/
aws s3 cp pymysql_layer.zip s3://your-bucket/
Then in the console: Lambda → Layers → Create layer → upload from S3 → attach to your function.
Note: Public layers from other AWS accounts may not be accessible depending on your account permissions.
3. The RDS Proxy Trust Policy must include rds.amazonaws.com
This one is invisible until you check CloudWatch logs. If the IAM role attached to your Proxy has a Trust Policy that only allows lambda.amazonaws.com, the Proxy cannot assume the role to access Secrets Manager or generate IAM tokens. The Target Group stays Unavailable indefinitely.
CloudWatch will show:
[WARN] The new database connection using IAM authentication failed.
RDS Proxy failed to generate the IAM authentication token.
The fix — your Trust Policy must include both services:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"rds.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
4. The admin user on RDS cannot grant privileges on system databases
When you try to grant permissions to lambda_user on the mysql system database, RDS blocks it:
ERROR 1044 (42000): Access denied for user 'admin'@'%' to database 'mysql'
This is an AWS RDS limitation — the admin user is not a true superuser. It cannot access or modify system databases.
The fix is simple — create a dedicated application database instead:
CREATE DATABASE appdb;
GRANT ALL PRIVILEGES ON appdb.* TO 'lambda_user'@'%';
FLUSH PRIVILEGES;
Then update your Lambda environment variable:
DB_NAME = appdb
Never use mysql or information_schema as your application database — always create a dedicated one.
Key Takeaways
- Static database credentials in Lambda environment variables are a security risk — eliminate them
- IAM authentication generates a temporary token (15 min) that replaces the password entirely
- SSL is mandatory — IAM auth will not work without an encrypted connection
- Initialize the connection outside the handler to reuse the token across warm invocations
- Set
iam_auth = "REQUIRED"in Terraform to enforce IAM auth at the Proxy level — no exceptions - This pattern integrates seamlessly with the RDS Proxy connection pooling covered in the previous article
This article is the follow-up to Why Your Lambda Functions Are Silently Killing Your RDS Database — part of my AWS architecture series as an AWS Community Builder.
Follow along for more practical AWS and Terraform content.



Top comments (0)