DEV Community

Cover image for Stop Hardcoding Your Database Credentials in Lambda — Use IAM Authentication with RDS Proxy Instead
Jeancy Joachim Mukaka
Jeancy Joachim Mukaka

Posted on

Stop Hardcoding Your Database Credentials in Lambda — Use IAM Authentication with RDS Proxy Instead

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
Enter fullscreen mode Exit fullscreen mode

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'@'%';
Enter fullscreen mode Exit fullscreen mode

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) │                           │
│  └──────────────┘                           │
└─────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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

  1. Go to RDS → Databases
  2. Select your RDS MySQL instance
  3. Click Modify
  4. Under Database authentication, select Password and IAM database authentication
  5. Click ContinueApply immediately

Step 2 — Enable IAM Authentication on RDS Proxy

  1. Go to RDS → Proxies
  2. Select your existing Proxy
  3. Click Modify
  4. Under IAM authentication, select Required
  5. 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;
Enter fullscreen mode Exit fullscreen mode

Result:

Capture of 'lambda_user'creation

This user has no password — AWS IAM handles authentication entirely.

Step 4 — Attach the IAM policy to your Lambda role

  1. Go to IAM → Roles
  2. Find the execution role attached to your Lambda function
  3. Click Add permissions → Create inline policy
  4. 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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Go to Lambda → your function → Configuration → VPC
  2. Select the same VPC, subnets and security group as your RDS Proxy
  3. 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)
Enter fullscreen mode Exit fullscreen mode

Result:
The problematic pattern test

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)
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Result:
The result of correct pattern

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'])
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Then update your Lambda environment variable:

DB_NAME = appdb
Enter fullscreen mode Exit fullscreen mode

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)