Photo by David Trinks on Unsplash
Securing AWS RDS with Fine-Grained Access Control using IAM Authentication, Terraform and Serverless
Banking on security: Fine-grained RDS access control with IAM Authentication
In the high-stakes world of enterprise software development, especially for organisations like banks, safeguarding sensitive data is critical. Traditional database user management with static credentials can become a security nightmare in complex environments. Imagine a bustling bank with a multitude of microservices - card payments, loan processing, fraud detection, and customer portals - all requiring access to the central customer database. Managing individual employees and services accounts and permissions across these services can be a logical tangled web, fraught with the risk of human error and potential breaches.
Here's where IAM Authentication for AWS RDS emerges as a game-changer. By leveraging the power of IAM, you can achieve fine-grained access control for your RDS databases, ensuring only authorised services and users can access specific data within the database. This not only simplifies management but also bolsters security by minimising the attack surface.
This article delves into the world of IAM Authentication for RDS, guiding you through the process of implementing robust access control for your sensitive financial data. We will explore different approaches to mapping IAM roles and policies to your RDS users, discuss best practices for integrating with Kubernetes environments, and highlight crucial security considerations for a watertight defence.
Understanding IAM Authentication for RDS
Enabling IAM Authentication
Imagine your bank's customer database as the vault. Enabling IAM Authentication for RDS is like installing a high-security lock system on that vault. You can do this through the AWS Management Console, the AWS CLI, the AWS SDK, or Terraform. Once enabled, any attempt to connect to the database requires authentication through IAM, not traditional usernames and passwords.
resource "aws_rds_cluster" "this" {
cluster_identifier = "..."
engine = "aurora-postgresql"
engine_version = "..."
database_name = "..."
...
iam_database_authentication_enabled = true
apply_immediately = var.apply_immediately
...
vpc_security_group_ids = [
aws_security_group.rds.id,
]
}
IAM roles
Think of IAM roles as digital keys that grant access to specific resources within your AWS environment. In the context of RDS, you can create IAM roles specifically for accessing your database objects. These roles define who (or rather, what) can access the database objects - it could be a specific microservice within your bank's application or an IAM user with restricted permissions for audit purposes.
IAM policies
An IAM policy acts as the access control list (ACL) for your IAM role. It dictates precisely what actions the role can perform on the database. You can create granular policies that allow specific read/write operations on certain tables within the database, ensuring each service or user has the minimum level of access required for their function.
For instance, a loan processing microservice might have a policy allowing it to read customer information from specific tables but wouldn't have permission to modify account balances. This granular control significantly reduces the potential damage caused by unauthorised access or human error.
Advantages
In complex environments like banks, IAM Authentication shines. It eliminates the need to manage individual user accounts across multiple services, simplifying administration. Furthermore, by leveraging IAM roles and policies, you can ensure that each service or user has the most restricted access possible, minimising the risk of data breaches and ensuring regulatory compliance.
In the next section, we will explore different strategies for mapping IAM roles and policies to your RDS users, empowering you to create a robust and secure access control system for your sensitive financial data.
Implementation
Now that we understand the core concepts of IAM Authentication for RDS, let's dive into the practicalities. Here, we will explore the implementation of fine-grained access control with IAM roles and policies. Here is how it works:
Define IAM policies for database access
Imagine crafting access control rules for the database. You create IAM policies that define the specific permissions (read, write, etc.) allowed for accessing the database.
data "aws_iam_policy_document" "rds_policies" {
source_policy_documents = [<<EOF
{
"version": "2012-10-17",
"statement": [
{
"effect": "Allow",
"action": [
"rds-db:connect"
],
"resource": "arn:aws:rds-db:eu-west-2:0123456789:dbuser:loanprocessing/loan-processing-service-user"
}
]
}
EOF
]
}
resource "aws_iam_policy" "rds_policies" {
description = "..."
policy = data.aws_iam_policy_document.rds_policies.json
}
Attach the policy to an IAM role
Think of assigning these access control rules to a relevant department or team within the bank. You attach the created IAM policy to a role that represents the service or user requiring database access, e.g. "FraudDetctionTeamRole".
resource "aws_iam_role" "rds_role" {
name = "..."
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "sts:AssumeRole"
Sid = ""
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
}
resource "aws_iam_role_policy_document" "rds_role_policy_attachment" {
role = aws_iam_role.rds_role.name
policy_arn = aws_iam_policy.rds_policies.arn
}
Map the RDS user to the IAM role
Imagine assigning the keycard to the authorised employee. In this step, you configure the RDS user (the database user) to use the IAM role defined earlier to utilise the IAM role with the attached policy. This establishes the connection between the service and its access privileges.
CREATE USER 'loan-processing-service-user' WITH LOGIN;
GRANT rds_iam TO 'loan-processing-service-user';
Bridging the gap with serverless
While the AWS CLI and Terraform allow configuration for many aspects of IAM Authentication, creating the RDS user itself requires executing SQL statements, which cannot be done directly through AWS CLI or Terraform alone if your RDS instance runs within a private VPC.
Here is where AWS Lambda, a serverless compute service, comes to the rescue. We can leverage a Lambda function to execute the necessary SQL statements within a single Terraform module, seamlessly mapping the RDS user to the IAM role.
Benefits of using a serverless function
Declarative approach: Keep your Terraform configuration clean and maintainable. Terraform can trigger the Lambda function during deployment, ensuring the RDS user is created and mapped to the IAM role in a single automated flow.
Reusability: The Lambda function can be reused across different environments or deployments, prompting code efficiency.
Security: The Lambda function code can be secured using IAM roles and policies, granting it the minimum permissions required to create the RDS user.
Implementation steps
Create the IAM role and policy for the Lambda function
resource "aws_iam_role" "create_rds_users" {
name = "..."
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Sid": "",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "lambda" {
role = aws_iam_role.create_rds_users.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy_attachment" "vpc" {
role = aws_iam_role.create_rds_users.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
data "aws_iam_policy_document" "create_rds_users" {
statement {
effect = "Allow"
actions = [
"secretsmanager:GetSecretValue",
]
resources = [
aws_rds_cluster.this.master_user_secret[0].secret_arn,
]
}
statement {
effect = "Allow"
actions = [
"kms:Decrypt",
]
resources = [
aws_rds_cluster.this.master_user_secret_kms_key_id,
]
}
}
resource "aws_iam_policy" "create_rds_users" {
name = "..."
policy = data.aws_iam_policy_document.create_rds_users.json
}
resource "aws_iam_role_policy_attachment" "create_rds_users" {
role = aws_iam_role.create_rds_users.name
policy_arn = aws_iam_policy.create_rds_users.arn
}
Create the Lambda function
resource "null_resource" "install_dependencies" {
triggers = {
always = timestamp()
}
provisioner "local-exec" {
command = "npm i --omit=dev"
working_dir = "${path.module}/lambdas/create_rds_users"
}
}
data "archive_file" "create_rds_users" {
type = "zip"
source_dir = "${path.module}/lambdas/create_rds_users"
output_dir = "${path.module}/lambdas/create_rds_users.zip"
depends_on = [
null_resource.install_dependencies,
]
}
resource "aws_lambda_function" "create_rds_users" {
role = aws_iam_role.create_rds_users.arn
function_name = "..."
filename = data.archive_file.create_rds_users.output_path
source_code_hash = data.archive_file.create_rds_users.output_base64sha256
handler = "index.handler"
runtime = "nodejs20.x"
memory_size = 128
timeout = 30
vpc_config {
subnet_ids = ...
security_group_ids = [
aws_security_group.lambda.id,
]
}
environment {
variables = {
RDS_HOST = aws_rds_cluster.this.endpoint
RDS_DATABASE_NAME = aws_rds_cluster.this.database_name
RDS_SECRET_ARN = aws_rds_cluster.this.master_user_secret[0].secret_arn
}
}
}
Create a VPC Endpoint for accessing Secrets Manager
data "aws_region" "current" {}
resource "aws_vpc_endpoint" "secrets_manager" {
vpc_id = ...(the VPC of the RDS and Lambda function)...
service_name = "com.amazonaws.${data.aws_region.current.name}.secretsmanager"
vpc_endpoint_type = "Interface"
private_dns_enabled = true
subnet_ids = ...
security_group_ids = [
aws_security_group.lambda.id,
]
}
Configure the security group for accessing the RDS instance
resource "aws_security_group" "lambda" {
name = "..."
vpc_id = ...(the VPC of the RDS instance)...
}
resource "aws_vpc_security_group_ingress_rule" "allow_rds_from_lambda" {
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
security_group_id = aws_security_group.rds.id
referenced_security_group_id = aws_security_group.lambda.id
}
resource "aws_vpc_security_group_ingress_rule" "allow_vpc_endpoint_to_lambda" {
from_port = 443
to_port = 443
ip_protocol = "tcp"
security_group_id = aws_security_group.lambda.id
referenced_security_group_id = aws_security_group.lambda.id
}
resource "aws_vpc_security_group_egress_rule" "allow_rds_from_lambda" {
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
security_group_id = aws_security_group.lambda.id
referenced_security_group_id = aws_security_group.rds.id
}
resource "aws_vpc_security_group_egress_rule" "allow_vpc_endpoint_from_lambda" {
from_port = 443
to_port = 443
ip_protocol = "tcp"
security_group_id = aws_security_group.lambda.id
referenced_security_group_id = aws_security_group.lambda.id
}
Trigger the Lambda function
data "aws_lambda_invocation" "create_rds_users" {
function_name = aws_lambda_function.create_users.function_name
input = jsonencode(var.rds_users)
depends_on = [
aws_vpc_endpoint.secrets_manager,
aws_vpc_security_group_ingress_rule.allow_rds_from_lambda,
aws_vpc_security_group_ingress_rule.allow_vpc_endpoint_to_lambda,
aws_vpc_security_group_egress_rule.allow_rds_from_lambda,
aws_vpc_security_group_egress_rule.allow_vpc_endpoint_from_lambda,
]
}
Retrieve the RDS password in Lambda
const readSecret = async (secretArn) => {
const {
GetSecretValueCommand,
SecretsManagerClient,
} = require('@aws-sdk/client-secrets-manager');
const client = new SecretsManagerClient();
try {
console.info(`Reading secret: ${secretArn}`);
const response = await client.send(new GetSecretValueCommand({
SecretId: secretArn,
}));
return response.SecretString;
} catch (error) {
console.error(error);
} finally {
client.destroy();
}
};
Executing SQL statements in Lambda
const RDS_PORT = 5432;
exports.handler = async (event, context) => {
const { Client, } = require('pg');
const client = new Client({
user : '...(aws_rds_cluster.this.master_username)...',
password : JSON.parse(await readSecret(process.env.RDS_SECRET_ARN)).password,
host : process.env.RDS_HOST,
database : process.env.RDS_DATABASE_NAME,
port : RDS_PORT,
});
try {
console.info(`Connecting to database: ${process.env.RDS_HOST}:${RDS_PORT}/${process.env.RDS_DATABASE_NAME}`);
await client.connect();
await client.query('BEGIN');
for (const user of event) {
const { rowCount, } = await client.query(`SELECT 1 FROM pg_roles WHERE rolname = '${user}'`);
if (rowCount === 0) {
console.info(`Creating user: ${user}`);
await client.query(`CREATE USER '${user}' WITH LOGIN`);
await client.query(`GRANT rds_iam TO '${user}'`);
} else {
console.info(`User ${user} already exists`);
}
}
await client.query('COMMIT');
context.succeed();
} catch (error) {
console.error(error);
await client.query('ROLLBACK');
context.fail();
} finally {
await client.end();
}
};
By employing a Lambda function within your Terraform module, you can bridge the gap and automatically map the RDS user to the IAM role, completing the IAM Authentication setup for your RDS database. This approach promotes a clean, declarative, and secure way to manage access control within your environment.
The next section explores integrating IAM Authentication with Kubernetes environments, a common scenario for modern application development. We will discuss the challenges and best practices for managing token refreshes and ensuring secure access within this context.
Integrating IAM Authentication with Kubernetes
Many modern applications leverage containerised architecture like Kubernetes for scalability and agility. Integrating IAM Authentication for RDS with Kubernetes environments presents a unique set of considerations, particularly regarding the management of temporary authentication tokens. Let's delve into the challenges, implementation, and best practices for keeping your tokens fresh and access uninterrupted.
Challenges
IAM Authentication tokens expire after 15 minutes. This poses a potential challenge in Kubernetes environments where applications might be reusing a pool of database connections.
Implementation considerations
AWS SDK: The AWS SDK for various languages (Go, Python, Java, JavaScript, etc.) offers built-in functionality for automatic token refresh. The libraries handle token expiration and renewal transparently, ensuring uninterrupted access for your pods.
Credentials caching: You can configure your application to cache credentials locally within the pods. This reduces the number of API calls needed to refresh tokens, improving performance slightly. However, ensure proper cache invalidation mechanisms are in place to avoid using stale credentials.
Best practices for token refresh
Monitoring: Set up monitoring for the token refresh process within your pods. Consider implementing alerts for any issues with refreshing credentials to ensure uninterrupted database access.
Minimise caching: While caching can improve performance, over-reliance can lead to using expired tokens. Implement mechanisms to ensure credentials are refreshed periodically, even within the cache.
Security best practices
IAM Authentication for RDS offers a robust approach to access control, but security is an ongoing journey. Here, we explore some crucial best practices to fortify your defences.
Principle of least privilege
Grant IAM roles attached to your RDS users only the minimum permissions required to perform their designated functions. This minimises the potential damage caused by unauthorised access or compromised credentials. Here's an example:
- Instead of granting full access to the database, a policy might allow a "LoanProcessingServiceRole" to read specific customer information tables but restrict write access.
Password management
Avoid storing database user passwords in plain text within your code or configuration files. Leverage secret management solutions such as AWS Secrets Manager and HashiCorp Vault to securely store and manage these credentials. These secrets can then be retrieved by your applications using the AWS SDK, further enhancing security. Furthermore, with IAM Authentication, temporary credentials will be generated on the fly without the need to store any long-term credentials.
Monitoring and auditing
Monitor token refresh: Implement monitoring for the token refresh process within your pods or applications. Set up alerts for any issues with refreshing credentials to ensure uninterrupted database access and identify potential problems early on.
Enable RDS logging: Activate CloudTrail logging for your RDS instance. This logs all API calls made to your database, allowing you to track user activity and identify any suspicious access patterns.
IAM Access Advisor: Utilise the IAM Access Advisor to review your IAM policies and identify overly permissive settings. This helps you refine your policies to grant only the necessary access.
By adhering to these best practices, you build a more secure foundation for your RDS access control. The principle of least privilege minimises the potential impact of breaches, password management protects sensitive credentials, and ongoing monitoring allows you to detect and respond to suspicious activity promptly.
Conclusion
In the ever-evolving landscape of enterprise security, IAM Authentication for RDS emerges as a powerful tool for securing your sensitive financial data. By leveraging IAM roles and policies, you can achieve fine-grained access control, ensuring only authorised services and users have access to specific data within your RDS database.
Key takeaways
Enhanced security: IAM Authentication replaces traditional static credentials with a more robust authorisation system, significantly reducing the risk of breaches.
Fine-grained control: IAM roles and policies allow you to grant granular access permissions, ensuring users and services have only the minimum level of access required for their function.
Integration with Kubernetes: Leverage libraries like the AWS SDK to manage token refresh within your pods, ensuring uninterrupted database access even in dynamic environments.
Beyond this guide
This article has equipped you with the foundational knowledge to implement IAM Authentication for RDS. Here are some additional pointers to keep your security posture strong:
Stay updated: Regularly review AWS documentation and best practices for IAM to stay abreast of the latest security recommendations.
Explore advanced techniques: Consider exploring advanced features like the IAM identity federation for even more granular control over access.
Security is a shared responsibility: Remember, security is a collaborative effort. Foster a culture of security awareness within your organisation to further strengthen your defences.
The final word
IAM Authentication for RDS empowers you to achieve fine-grained access control for your sensitive database information. By leveraging IAM roles, policies, and best practices, you can ensure robust security and streamline user management within your complex enterprise environments. Remember, security is an ongoing process. Regularly review your IAM policies, monitor access logs, and stay updated on the latest security practices to maintain an impenetrable defence for your valuable data and ensure peace of mind.
References
AWS Documentation: IAM Authentication for RDS: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
AWS Blog: Best Practices for IAM Policies: https://aws.amazon.com/iam/resources/best-practices/
AWS Security: IAM Identity Federation: https://aws.amazon.com/identity/federation/