Back to EC2 & Compute
Q7Question 7 of 7

Private EC2 Instance: Access and Outbound Internet via NAT

Key Concepts
  • A private instance is launched in a private subnet with no public IP, making it unreachable from the internet by design.
  • A subnet is private because its route table has no route to an Internet Gateway.
  • Access a private instance through a bastion host (SSH hop), SSM Session Manager (no inbound ports), or VPN.
  • SSM Session Manager is the preferred method for banks because it requires no open inbound ports and logs every session to CloudTrail.
  • A NAT Gateway in a public subnet gives private instances outbound-only internet for patches and external API calls.
  • The NAT Gateway performs source address translation and blocks all unsolicited inbound connections.
  • Deploy one NAT Gateway per Availability Zone for high availability and to avoid cross-zone data charges.

Definition

A private instance has no public IP, so you reach it via a bastion, SSM, or VPN, and you give it outbound-only internet through a NAT Gateway that blocks all inbound.
  • Private EC2 Instance – An instance launched in a private subnet with no public IP address, unreachable from the internet by design.
  • Private Subnet – A subnet whose route table has no route to an Internet Gateway, keeping all instances invisible to the public internet.
  • Bastion (Jump) Host – A hardened instance in a public subnet that engineers SSH into first, then hop to private instances.
  • SSM Session Manager – AWS-managed shell access through the SSM agent, requiring no inbound ports and logging every session to CloudTrail.
  • NAT Gateway – A managed service in a public subnet that performs source address translation, giving private instances outbound-only internet while blocking unsolicited inbound.

A private EC2 instance is like the back-office room at National Bank of India that has no public entrance from the street. Customers cannot walk in. Staff reach it only through an internal corridor from the front desk (a bastion), through a secure staff passage with a logbook (SSM Session Manager), or through a private tunnel from head office (VPN). The room still needs to send letters out for supplies and updates, so a mailroom (NAT Gateway) in the public part of the building posts the room's outgoing letters and receives the replies, while never letting outsiders send mail directly into the room.

Understanding the Concepts

What is a private EC2 instance?

A subnet is private when its route table has no route to an Internet Gateway. An instance in such a subnet gets only a private IP, so no packet from the internet can reach it directly. This is exactly what we want for sensitive banking workloads like the settlement database tier – it should be invisible to the public internet.

DIAGRAM
# Private subnet route table – no IGW route
10.0.0.0/16   → local                 (VPC internal traffic)
0.0.0.0/0     → nat-0gateway123        (outbound internet via NAT, no inbound)
# No route to igw-xxxx → subnet is PRIVATE

How do you access a private instance via a bastion host?

The classic approach is a bastion host: a small hardened instance in a public subnet with a public IP. You SSH into the bastion first, then from the bastion SSH into the private instance over the VPC's internal network. The bastion is the only internet-facing door and is tightly locked down.

BASH
# Two-step hop through a bastion
# Step 1: SSH to bastion in public subnet
ssh -i bastion-key.pem ec2-user@bastion-public-ip

# Step 2: From bastion, SSH to private instance
ssh -i settlement-key.pem ec2-user@10.0.2.50

# Or use SSH ProxyJump for a single command
ssh -i settlement-key.pem \
  -J ec2-user@bastion-public-ip \
  ec2-user@10.0.2.50

How do you access a private instance via SSM Session Manager?

The modern approach is SSM Session Manager. The private instance runs the SSM agent and has an IAM role allowing Systems Manager. You open a shell through the AWS console or CLI with no inbound port open at all, no bastion, and no key pair. Every session is logged to CloudTrail. For a bank this is the preferred path because it removes the internet-facing bastion entirely and gives a full audit trail.

BASH
# SSM Session Manager – no bastion, no open ports, fully audited
aws ssm start-session --target i-0privateabc

# Port forwarding through SSM (e.g., to reach a database port)
aws ssm start-session \
  --target i-0privateabc \
  --document-name AWS-StartPortForwardingSession \
  --parameters '{"portNumber":["5432"],"localPortNumber":["15432"]}'
# Now connect to localhost:15432 to reach the private DB

How does a VPN provide access?

A VPN (site-to-site to a corporate data center, or AWS Client VPN for individuals) extends the private network to trusted users so they reach the private IP as if on the local network. Site-to-site VPN connects the corporate data center to the VPC over an encrypted tunnel, and Client VPN gives individual engineers VPN access from their laptops.

DIAGRAM
# Site-to-Site VPN topology
┌──────────────┐     Encrypted tunnel     ┌──────────────┐
│ Corporate DC  │ ←──────────────────────→ │ AWS VPC       │
│ 172.16.0.0/12 │     IPsec / BGP          │ 10.0.0.0/16   │
│               │                          │               │
│ Engineer PC   │ → reaches 10.0.2.50     │ Private EC2   │
└──────────────┘   as if local             └──────────────┘

How does a NAT Gateway give outbound internet to private instances?

A private instance needs to download OS patches or call an external payment API. You cannot give it an Internet Gateway route without making it reachable inbound. Instead you create a NAT Gateway in a public subnet and add a route in the private subnet's route table sending 0.0.0.0/0 to the NAT Gateway.

The NAT Gateway performs network address translation: it forwards the instance's outbound requests to the internet using its own public IP, and returns the responses, but it never allows an unsolicited inbound connection.

BASH
# Create a NAT Gateway in a public subnet
aws ec2 create-nat-gateway \
  --subnet-id subnet-public-a \
  --allocation-id eipalloc-0abc  # Elastic IP for the NAT

# Add route in private subnet's route table
aws ec2 create-route \
  --route-table-id rtb-private-a \
  --destination-cidr-block 0.0.0.0/0 \
  --nat-gateway-id nat-0abc

How is a bastion different from SSM Session Manager?

| Aspect | Bastion Host | SSM Session Manager |
|---|---|---|
| Inbound ports | Port 22 open to allowed IPs | No inbound ports needed |
| Key management | Requires .pem key file | No key pair needed |
| Audit trail | OS-level auth.log on bastion | Full CloudTrail integration |
| Attack surface | Internet-facing instance | No internet-facing component |
| Cost | Runs 24/7 as an EC2 instance | No extra instance (agent runs on target) |
| Maintenance | Must patch and harden bastion | AWS-managed service |

JAVA
public class AccessMethodComparison {
    // Bastion: simple but adds attack surface
    // SSM: zero attack surface, full audit, preferred for banks
    // VPN: extends corporate network, good for many users
}

What are common mistakes?

The first mistake is giving a private instance an Internet Gateway route to solve SSH access. This makes the instance public, defeating the purpose of the private subnet. Use a bastion or SSM instead.

The second mistake is deploying only one NAT Gateway across all Availability Zones. If that AZ goes down, all private instances in other AZs lose outbound internet.

The third mistake is routing all traffic through the NAT Gateway, including traffic to AWS services like S3. A VPC Gateway Endpoint is free and keeps that traffic private.

DIAGRAM
# WRONG: one NAT Gateway for all AZs
All private subnets → nat-0abc (in AZ-a only)
# If AZ-a fails, AZ-b and AZ-c lose outbound internet

# CORRECT: one NAT per AZ
Private subnet AZ-a → nat-0abc-a
Private subnet AZ-b → nat-0abc-b
Private subnet AZ-c → nat-0abc-c
# Each AZ is independent

What should a senior engineer know?

A NAT Gateway lives in one Availability Zone, so for high availability deploy one per AZ and route each private subnet to its zone-local NAT Gateway, which also avoids cross-zone data charges. NAT Gateways bill per hour and per gigabyte processed, so for reaching AWS services specifically, a VPC endpoint is even better because the traffic never leaves the AWS network.

For SSM to work on a private instance without a NAT Gateway, you need VPC Interface Endpoints for SSM, SSM Messages, and EC2 Messages services. This keeps even the management traffic fully private.

BASH
# Create VPC endpoints for SSM to work without NAT
for svc in ssm ssmmessages ec2messages; do
  aws ec2 create-vpc-endpoint \
    --vpc-id vpc-0abc \
    --service-name com.amazonaws.ap-south-1.$svc \
    --vpc-endpoint-type Interface \
    --subnet-ids subnet-private-a subnet-private-b \
    --security-group-ids sg-ssm-endpoint
done

Visual Overview

DIAGRAM
┌──────── Private Instance Access and Outbound ────────┐
│                                                       │
│  Internet                                             │
│     │                                                 │
│     ▼                                                 │
│  ┌──────────────── Public Subnet ───────────────┐    │
│  │                                               │    │
│  │  Bastion Host ←── SSH from engineer          │    │
│  │  (port 22 open)                               │    │
│  │                                               │    │
│  │  NAT Gateway ←── outbound from private        │    │
│  │  (Elastic IP)     instances goes through here  │    │
│  │       │                                        │    │
│  └───────┼────────────────────────────────────────┘    │
│          │                                             │
│          ▼                                             │
│  ┌──────────────── Private Subnet ───────────────┐    │
│  │                                               │    │
│  │  Settlement EC2 (10.0.2.50)                   │    │
│  │  ├── No public IP                             │    │
│  │  ├── Access: Bastion hop, SSM, or VPN         │    │
│  │  └── Outbound: via NAT Gateway (patches, API) │    │
│  │                                               │    │
│  │  Inbound from internet: BLOCKED (no IGW route)│    │
│  └───────────────────────────────────────────────┘    │
│                                                       │
│  SSM Session Manager Path (preferred):                │
│  Engineer → AWS SSM API → SSM Agent on instance       │
│  (no inbound ports, no bastion, CloudTrail logged)    │
└───────────────────────────────────────────────────────┘

Key Takeaway

A private EC2 instance is one launched in a private subnet with no public IP, which means nothing on the internet can reach it directly. A subnet is private precisely because its route table has no route to an Internet Gateway, so an instance there gets only a private IP. In banking this is exactly what we want for sensitive tiers like the settlement database – it should be invisible to the public internet.

Because it has no public IP, I reach it through a controlled path. The classic way is a bastion or jump host – a small hardened instance in a public subnet that I SSH into first, then hop from there to the private instance over the VPC's internal network. The modern and preferred way is SSM Session Manager: the private instance runs the SSM agent and has an IAM role for Systems Manager, and I open a shell with no inbound port open, no bastion, and no key pair, while every session is logged. For a bank that removes the internet-facing bastion entirely and gives a full audit trail, so it is my default. A VPN is the third option when I want trusted users to reach the private IP as if they were on the local network.

The outbound side is the other half. A private instance still needs to download operating-system patches or call an external payment API, but I cannot give it an Internet Gateway route without making it reachable inbound. So I place a NAT Gateway in a public subnet and add a default route in the private subnet sending 0.0.0.0/0 to that NAT Gateway. The NAT Gateway translates the instance's outbound requests to the internet using its own public IP and returns the responses, but it never allows an unsolicited inbound connection.

At National Bank of India our settlement instance runs with no public IP, engineers reach it only through SSM Session Manager with full logging, and it patches itself and calls partner APIs outbound through a NAT Gateway. One senior detail is that a NAT Gateway lives in a single Availability Zone, so for resilience I deploy one per zone and route each private subnet to its zone-local gateway, which also avoids cross-zone charges. And when the destination is an AWS service like S3, I prefer a VPC endpoint over the NAT Gateway, because then the traffic never leaves the AWS network.

Remember one sentence: A private instance has no public IP by design; reach it through SSM or a bastion, and give it outbound-only internet through a NAT Gateway that blocks all inbound.

---

Share
Use ← → arrow keys to navigate