Building a HIPAA-Ready Architecture on AWS: Secure App Deployment with Private EC2, VPC Endpoints & ALB

DevOps & Cloud Engineer — building scalable, automated, and intelligent systems. Developer of sorts | Automator | Innovator
Healthcare systems deal with sensitive patient data, and if you are building or hosting an application for a U.S. healthcare provider, the architecture needs to meet the requirements of HIPAA (Health Insurance Portability and Accountability Act). HIPAA compliance in the cloud is not just about encryption, it's about designing infrastructure where data is secured both technically and logically, with the right audit controls and network boundaries.
In this guide, we will walk through how we built a HIPAA-aligned AWS environment to run a Python Flask API on an EC2 instance in a private subnet, exposed securely using an Application Load Balancer (ALB) without placing the EC2 directly on the public internet.
Here, the setup is manual, we can do the same using Terraform! Perhaps, in a different article, we shall explore that!
Signing the AWS Business Associate Addendum (BAA)
HIPAA requires that AWS legally commits to protecting PHI (Protected Health Information).
To do this you must enable the Business Associate Addendum:
How to Enable BAA
Go to AWS Console → AWS Artifact
Open Agreements
Request the Business Associate Addendum
Accept digitally

Without this, using AWS to store or process patient data is not HIPAA compliant, even if your infrastructure design is perfect.
Architecture Overview (What We're Building)

Key properties:
App servers are in private subnets → not reachable from internet
Load Balancer lives in public subnet and forwards traffic internally
NAT Gateway allows private instances to download packages, pull images, etc.
S3 / ECR / STS / Logs access does not touch public internet thanks to VPC Endpoints
Step 1: Create the VPC
We start with:
VPC CIDR: 10.0.0.0/16

This gives enough room for multiple subnets.
Step 2: Create Subnets
We create one public and one private subnet per AZ (for reliability).
| Subnet Name | CIDR | Type | Use |
| public-subnet-1 | 10.0.1.0/24 | Public (us-1a) | ALB, NAT Gateway |
| public-subnet-2 | 10.0.4.0/24 | Public (us-1b) | ALB, NAT Gateway |
| private-subnet-1 | 10.0.2.0/24 | Private (us-1a) | EC2 application |
Step 3: Internet Gateway + NAT Gateway
Attach an Internet Gateway to allow outbound traffic.
Place the NAT Gateway in the public subnet.
This allows private instances to:
Pull packages
Download updates
Clone repo / get code from S3
But prevents inbound connections.
Step 4: Route Tables
Public Route Table
0.0.0.0/0 → Internet Gateway

We associate the public subnets to this.
Private Route Table
0.0.0.0/0 → NAT Gateway

We associate the private subnet to this.
Step 5: VPC Endpoints (Important for Compliance‑Style Security)
To avoid public internet paths for AWS services, create VPC Interface / Gateway Endpoints, and make sure they are at private subnets in case of interfaces and for Gateway is in the private route table:
| Type | Service Endpoint | |
| S3 | Gateway | com.amazonaws.[region].s3 |
| EC2Messages | Interface | com.amazonaws.[region].ec2messages |
| SSM Messages | Interface | com.amazonaws.[region].ssmmessages |
| SSM | Interface | com.amazonaws.[region].ssm |
| Bedrock | Interface | com.amazonaws.[region].bedrock-runtime |
VPC → Endpoints → Create Endpoint
Make sure Enable DNS name is On, in all cases.
Endpoint #1 — SSM
| Field | Value |
| Service Name | com.amazonaws.us-east-1.ssm |
| Service Type | Interface |
| VPC | hipaa-vpc |
| Subnets | private-subnet-hipaa-1 (select this) |
| Security Group | Choose or create a SG that ALLOWS HTTPS (443) from your private subnet |
Click Create.
Endpoint #2 — EC2 Messages
| Field | Value |
| Service Name | com.amazonaws.us-east-1.ec2messages |
| Service Type | Interface |
| VPC | hipaa-vpc |
| Subnets | private-subnet-hipaa-1 |
| Security Group | Same SG as above |
Click Create.
Endpoint #3 — SSM Messages
| Field | Value |
| Service Name | com.amazonaws.us-east-1.ssmmessages |
| Service Type | Interface |
| VPC | hipaa-vpc |
| Subnets | private-subnet-hipaa-1 |
| Security Group | Same SG |
Click Create.
Endpoint #4 — S3 Gateway (Important)
| Field | Value |
| Service Name | com.amazonaws.us-east-1.s3 |
| Service Type | Gateway |
| VPC | hipaa-vpc |
| Route Tables | SELECT the Private Route Table (do not choose Public RT) |
Click Create.
Endpoint #5 — Bedrock
| Field | Value |
| Service Name | com.amazonaws.us-east-1.bedrock-runtime |
| Service Type | Interface |
| VPC | hipaa-vpc |
| Subnets | private-subnet-hipaa-1 (select this) |
| Security Group | Use the SG attached to your EC2, allow outbound HTTPS (default outbound all is fine) |
Click Create.

Security Group Rule (IMPORTANT)
Go to Security Groups → Edit inbound rules of the SG used for the 3 interface endpoints:
Add Rule:
| Type | Protocol | Port | Source |
| HTTPS | TCP | 443 | 10.0.0.0/16 (your VPC CIDR) |
Step 6: Launch EC2 Instance in Private Subnet
AMI: Amazon Linux 2023 (HIPAA supported)
Subnet: Private (
10.0.2.0/24)No Public IP
Security Group Rules:
| Type | Source | Purpose |
| Allow inbound: 5212 | From Target Group SG | ALB → EC2 only |
| Outbound | All | Required for package updates |
We accessed the instance using:
AWS Systems Manager → Session Manager
(no SSH needed)
It is also important that we add appropriate IAM Role to the EC2 machine, to make sure it can do and perform actions like pulling from S3, logging, ECR pulling and so on.

Instance Role Policies Example:
AmazonS3ReadOnlyAccess
AmazonEC2ReadOnlyAccess
AmazonSSMManagedInstanceCore
AmazonBedrockFullAccess
Step 7: Deploying the Flask Application
We can upload our code as zip to a s3 bucket and pull it in our ec2, when we are using Session Manager.
aws s3 cp s3://your-bucket/simpleflask/ /home/ec2-user/simpleflask --recursive
Directory Structure
We extract the zip of our code that we pulled , and unzip it, we can see the following:
/app
├─ app.py
└─ requirements.txt
Install Dependencies
sudo yum update -y
sudo yum install python3 git -y
Step 8: Run the Flask App
Firstly, we activate a virtual env and install the requirements:
python3 -m venv env
source env/bin/activate
pip install -r requirements.txt
App listens privately, example:
import json
import boto3
from fastapi import FastAPI
app = FastAPI()
client = boto3.client("bedrock-runtime", region_name="us-east-1")
@app.get("/")
def healthcheck():
return {"status": "running (private, HIPAA-safe)"}
@app.post("/chat")
def chat(message: str):
payload = {
"messages": [
{"role": "user", "content": message}
],
"max_tokens": 200
}
response = client.invoke_model(
modelId="mistral.mistral-large-2402-v1:0",
contentType="application/json",
accept="application/json",
body=json.dumps(payload)
)
return json.loads(response["body"].read().decode())
app.run(host="0.0.0.0", port=5212)
Step 9: Running Flask with systemd
Create service file:
sudo nano /etc/systemd/system/flask.service
[Unit]
Description=Flask App Service
After=network.target
[Service]
User=ec2-user
WorkingDirectory=/app
ExecStart=/usr/bin/python3 /app/app.py
Restart=always
[Install]
WantedBy=multi-user.target
Activate:
sudo systemctl daemon-reload
sudo systemctl enable flask
sudo systemctl start flask
This will make sure that our app runs as long as the machine is alive.
Step 10: Target Group Setup
Target Type: Instances
Port: 5212
Health Check Path:
/

Step 11: Create Application Load Balancer (ALB)
| Setting | Value |
| Scheme | Internet-facing |
| Type | Application Load Balancer |
| Subnets | Public A & Public B |
| Listener | HTTP : 80 → Target Group |
Security Group for ALB
| Type | Source |
| HTTP (80) | 0.0.0.0/0 |
Security Group for EC2
| Type | Source |
| TCP 5212 | SG of ALB |

No direct EC2 exposure.
Step 12: Testing
curl http://<ALB-DNS>/
You should receive:
Hello from HIPAA-secure backend!
Final Outcome
You now have:
Private app servers with no inbound exposure
Load balancer securely fronting traffic
VPC Endpoints eliminating public bandwidth paths
NAT‑restricted outbound traffic only
This architecture is HIPAA‑friendly because:
No PHI touches public networks
All services are inside private routing boundaries
Access is strictly controlled and logged
Conclusion
We successfully deployed a HIPAA-aligned backend on AWS using:
Private compute networks
Secure load balancing
Controlled outbound-only access
VPC endpoint isolation
No public exposure of the EC2 instance
This architecture is a strong baseline for any healthcare SaaS, EHR integrations, or PHI-processing APIs.
We can also perform the same with API Gateway and VPC link setup!
PS: Make sure delete the Load Balancer, Target groups , and stop/terminate the EC2 instance when you are done with this, as it incur costs!






