Secure RDS Access Without Bastion Hosts: Using ECS Containers and SSM

The Problem
Accessing RDS databases in production environments presents a common security challenge. Direct internet access to databases is a significant security risk, so most organizations place their RDS instances in private subnets without public endpoints. But how do you access these databases for debugging, data analysis, or administrative tasks?
Traditional approaches include:
- Bastion hosts: Requires maintaining and securing additional EC2 instances
- VPN connections: Complex setup and ongoing maintenance overhead
- SSH tunneling: Still requires a jump server with SSH access
There's a more elegant solution: leveraging your existing ECS containers as secure tunnels to your RDS databases using AWS Systems Manager (SSM) Session Manager.
The Solution
This script creates a secure tunnel from your local machine to an RDS database through a running ECS container, using SSM Session Manager for the connection. No SSH keys, no bastion hosts, no exposed ports—just IAM-based authentication and encrypted connections.
Prerequisites
Before using this script, ensure you have:
- AWS CLI installed and configured with appropriate credentials
- Session Manager Plugin for AWS CLI
- ECS Task Role with permissions to use SSM:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel" ], "Resource": "*" } ]
- ECS Exec enabled on your service (can be enabled with
aws ecs update-service --cluster <cluster> --service <service> --enable-execute-command --force-new-deployment)
How It Works
The script performs the following operations:
1. Task Discovery
TASK_ID=$(aws ecs list-tasks \ --cluster "$CLUSTER" \ --service-name "$SERVICE_NAME" \ --desired-status RUNNING \ --query 'taskArns[0]')
Finds the first running task in the specified ECS service.
2. Container Runtime ID Retrieval
CONTAINER_RUNTIME_ID=$(aws ecs describe-tasks \ --cluster "$CLUSTER" \ --tasks "$TASK_ID" \ --query 'tasks[0].containers[?name==`'"$SERVICE_NAME"'`].runtimeId | [0]')
3. Port Forwarding Session
aws ssm start-session \ --target "ecs:${CLUSTER}_${TASK_ID}_${CONTAINER_RUNTIME_ID}" \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters '{"host":["$DB_HOST"],"portNumber":["5432"], "localPortNumber":["$LOCAL_PORT"]}'
Establishes a secure tunnel through the ECS container to the database.
Usage
#!/usr/bin/env bash set -eu usage() { echo "Usage: $0 <CLUSTER> <SERVICE_NAME> <DB_HOST> <LOCAL_PORT> <REGION>" echo " cluster: ECS cluster name" echo " service: ECS service name" echo " db_host: Database host" echo " local_port: Local port to forward" echo " region: AWS region (e.g., us-east-2)" exit 1 } if [ $# -ne 5 ]; then echo "Error: Expected 5 arguments, got $#" >&2 usage fi CLUSTER="$1" SERVICE_NAME="$2" DB_HOST="$3" LOCAL_PORT="$4" REGION="$5" get_first_task_id() { local FIRST_TASK_ID FIRST_TASK_ID=$(aws ecs list-tasks \ --cluster "$CLUSTER" \ --service-name "$SERVICE_NAME" \ --desired-status RUNNING \ --output text \ --query 'taskArns[0]' \ --region "$REGION" | sed 's/.*\///') if [ "$FIRST_TASK_ID" = "None" ] || [ -z "$FIRST_TASK_ID" ]; then echo "Error: No running tasks found for service '$SERVICE_NAME' in cluster '$CLUSTER'" >&2 return 1 fi echo "$FIRST_TASK_ID" } TASK_ID=$(get_first_task_id) CONTAINER_RUNTIME_ID=$(aws ecs describe-tasks \ --output text \ --cluster "$CLUSTER" \ --tasks "$TASK_ID" \ --region "$REGION" \ --query 'tasks[0].containers[?name==`'"$SERVICE_NAME"'`].runtimeId | [0]') TARGET="ecs:${CLUSTER}_${TASK_ID}_${CONTAINER_RUNTIME_ID}" aws ssm start-session \ --target "$TARGET" \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters "{\"host\":[\"$DB_HOST\"],\"portNumber\":[\"5432\"], \"localPortNumber\":[\"$LOCAL_PORT\"]}" \ --region "$REGION"
Save the script as ecs-db-tunnel.sh and make it executable:
chmod +x ecs-db-tunnel.sh
Run the script with the required parameters:
./ecs-db-tunnel.sh <CLUSTER> <SERVICE_NAME> <DB_HOST> <LOCAL_PORT> <REGION>
Example:
./ecs-db-tunnel.sh \ production-cluster \ api-service \ mydb.c9akciq32.us-east-2.rds.amazonaws.com \ 5432 \ us-east-2
Once connected, you can access the database from your local machine:
psql -h localhost -p 5432 -U dbuser -d mydb
or using other tools such as DBeaver.
Key Benefits
1. No Infrastructure Overhead
No need to maintain bastion hosts or VPN servers. You leverage existing ECS containers.
2. Enhanced Security
- No SSH keys to manage or rotate
- No exposed ports or public endpoints
- IAM-based authentication and authorization
- All traffic encrypted through SSM
- Audit trail through CloudTrail
3. Zero Configuration
If your ECS service is already running with ECS Exec enabled, you're ready to go.
4. Cost Effective
No additional EC2 instances to run. Session Manager has no additional cost.
5. Temporary Access
Connection exists only while the script is running—perfect for adhoc administrative tasks.
Security Considerations
Network Security
This approach works because:
- The ECS container is in the same VPC as the RDS instance
- The container's security group allows outbound connections to the RDS security group
- The RDS security group permits inbound connections from the ECS container's security group
Access Control
Control who can establish tunnels using IAM policies:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["ecs:ListTasks", "ecs:DescribeTasks", "ssm:StartSession"], "Resource": "*", "Condition": { "StringEquals": { "aws:RequestedRegion": "us-east-2" } } } ] }
Troubleshooting
No running tasks found
Ensure the ECS service has at least one running task:
aws ecs list-tasks --cluster <cluster> --service-name <service> --desired-status RUNNING
Session Manager plugin not found
Install the Session Manager plugin following AWS documentation.
Connection refused
Verify:
- RDS security group allows inbound from ECS container security group
- Database endpoint is correct
- ECS task has network connectivity to RDS
ECS Exec not enabled
Enable it on your service:
aws ecs update-service \ --cluster <cluster> \ --service <service> \ --enable-execute-command \ --force-new-deployment
Conclusion
Using ECS containers as secure tunnels to RDS databases is an elegant solution that leverages existing infrastructure while maintaining strong security posture. This approach eliminates the need for bastion hosts, reduces attack surface, and provides auditable, temporary access to private databases.
The script demonstrates that sometimes the best security solutions are those that work with your existing architecture rather than adding more complexity on top of it.






