2026-02-23Sandro Volpicella

How to Access RDS in a Private Subnet: A Secure Guide Using SSM

AWSRDSSecuritySSMDevOpsBastion HostNetwork
H

Accessing an Amazon RDS instance located in a private subnet is a common challenge for developers. While isolating your database is a security best practice, it often leads to connection hurdles during development or debugging.

In this guide, we'll explore why traditional SSH bastion hosts fall short and how AWS Systems Manager (SSM) provides a superior, more secure alternative.

Table of Contents

The Connectivity Challenge

When you follow AWS best practices, you place your RDS instances in a private subnet. This means they have no public IP address and cannot be reached directly from the internet. While this is great for security, it creates a friction point: How do you connect to your database from your local machine (e.g., using DBeaver, pgAdmin, or terminal)?

Developers often resort to creating a "Jump Host" or "Bastion Host" in a public subnet to proxy the connection. Traditionally, this meant using SSH.

Why Traditional SSH Bastion Hosts are Risky

The classic approach involves launching an EC2 instance in a public subnet, opening port 22 (SSH), and managing SSH keys. However, this introduces several security and operational overheads:

  1. Key Management: SSH keys can be lost, stolen, or shared insecurely among team members. Rotating them is a manual burden.
  2. Attack Surface: Opening port 22 to the internet (even if restricted to specific IPs) increases the attack surface. Scanners constantly probe for open SSH ports.
  3. Audit Logs: Tracking who accessed the database and when is difficult with standard SSH logs.
  4. Network Configuration: You need to manage Security Groups, NACLs, and potentially an Internet Gateway for the bastion.

The Modern Solution: AWS Systems Manager (SSM)

AWS Systems Manager Session Manager offers a more secure way to manage your instances without opening inbound ports or managing SSH keys.

Key Benefits of SSM Session Manager

  • No Open Ports: You don't need to open port 22 or any inbound port in your Security Group.
  • No SSH Keys: Authentication is handled via IAM policies. No more PEM files to manage!
  • Centralized Access Control: Grant or revoke access using standard IAM roles and policies.
  • Auditability: Every session and command can be logged to CloudTrail and S3/CloudWatch Logs.
  • Port Forwarding: SSM supports tunneling remote ports (like 5432 for Postgres) to your local machine effortlessly.

Architecture Overview

Here is the target architecture:

  1. RDS Instance: Located in a private subnet (e.g., 10.0.1.0/24).
  2. EC2 Bastion Host: Located in the same private subnet (or another private subnet with route to RDS). Yes, the bastion itself can be private!
  3. VPC Endpoints: The EC2 instance connects to SSM via VPC Endpoints (or a NAT Gateway), ensuring traffic stays within the AWS network.
  4. IAM Role: The EC2 instance has an IAM role with the AmazonSSMManagedInstanceCore policy attached.

This setup eliminates the need for any public subnets or public IPs for your management infrastructure.

Step-by-Step Implementation with AWS CDK

Below is a TypeScript CDK snippet to deploy this architecture. We creates a VPC, an RDS instance, and a private EC2 instance configured for SSM.

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export class RdsSsmStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. Create a VPC with private subnets only (isolated)
    // Note: In a real scenario, you might need NAT Gateways or VPC Endpoints for SSM
    const vpc = new ec2.Vpc(this, 'MyVpc', {
      maxAzs: 2,
      natGateways: 0, // Cost saving for demo; use VPC Endpoints for SSM in prod
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
    });

    // Add VPC Endpoints for SSM (Critical for private-only subnets)
    vpc.addInterfaceEndpoint('SsmEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.SSM,
    });
    vpc.addInterfaceEndpoint('Ec2MessagesEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
    });
    vpc.addInterfaceEndpoint('SsmMessagesEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
    });

    // 2. Security Group for RDS
    const rdsSg = new ec2.SecurityGroup(this, 'RdsSg', { vpc });
    
    // 3. Security Group for Bastion
    const bastionSg = new ec2.SecurityGroup(this, 'BastionSg', { vpc });

    // Allow Bastion to connect to RDS on port 5432
    rdsSg.addIngressRule(bastionSg, ec2.Port.tcp(5432), 'Allow connection from Bastion');

    // 4. Create RDS Instance
    const database = new rds.DatabaseInstance(this, 'MyRds', {
      engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_15 }),
      vpc,
      securityGroups: [rdsSg],
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
    });

    // 5. Create EC2 Bastion Host for SSM
    const bastion = new ec2.Instance(this, 'BastionHost', {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.NANO),
      machineImage: ec2.MachineImage.latestAmazonLinux2(),
      securityGroup: bastionSg,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
      role: new iam.Role(this, 'BastionRole', {
        assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
        managedPolicies: [
          iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
        ],
      }),
    });
  }
}

Connecting to Your Database

Once your infrastructure is deployed, you can start a session from your local terminal using the AWS CLI.

Prerequisites

  • AWS CLI installed and configured.
  • Session Manager plugin for AWS CLI installed.

Start the Tunnel

Run the following command to open a tunnel from your local port (e.g., 54320) to the remote RDS endpoint via the bastion host.

# Replace variables with your actual values
BASTION_INSTANCE_ID="i-0123456789abcdef0"
RDS_ENDPOINT="myrds.cluster-xyz.us-east-1.rds.amazonaws.com"
LOCAL_PORT="54320"
REMOTE_PORT="5432"

aws ssm start-session     --target $BASTION_INSTANCE_ID     --document-name AWS-StartPortForwardingSessionToRemoteHost     --parameters '{"host":["'$RDS_ENDPOINT'"],"portNumber":["'$REMOTE_PORT'"],"localPortNumber":["'$LOCAL_PORT'"]}'

Now, you can connect to your database using localhost:54320!

psql -h localhost -p 54320 -U myuser -d mydb

FAQ

1. Is AWS Systems Manager Session Manager free?

Yes, the standard Session Manager functionality (including port forwarding) is available at no additional charge for EC2 instances. You only pay for the underlying resources (EC2, VPC Endpoints, etc.).

2. Can I use this with Windows instances?

Yes, SSM Session Manager supports both Linux and Windows instances.

3. Do I need a public IP for the Bastion Host?

No! With VPC Endpoints for SSM, your Bastion Host can reside in a purely private subnet, further enhancing security.

Conclusion

Switching from SSH to SSM Session Manager for database access is a significant security upgrade. It simplifies access management, removes the need for public IPs, and provides comprehensive auditing. By following the architecture and steps outlined above, you can ensure your RDS instances remain secure while still being accessible for development and maintenance tasks.

For more on cloud security and infrastructure, check out our AWS Consultancy and Kubernetes Services.


References:

JSON-LD Schema (for SEO)

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "How to Access RDS in a Private Subnet: A Secure Guide Using SSM",
  "image": "https://www.devopsn.cloud/blog/116/hero.jpg",
  "author": "Sandro Volpicella",
  "publisher": {
    "@type": "Organization",
    "name": "DevOpsN",
    "logo": {
      "@type": "ImageObject",
      "url": "https://www.devopsn.cloud/logo.png"
    }
  },
  "datePublished": "2026-02-23",
  "description": "Learn how to securely access your private RDS database without public IPs or SSH keys using AWS Systems Manager (SSM) and an EC2 Bastion host."
}