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

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**

1. Go to **AWS Console → AWS Artifact**
    
2. Open **Agreements**
    
3. Request the **Business Associate Addendum**
    
4. Accept digitally
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761599232365/fb7b86f7-32b3-4d88-9107-0216a5d1f785.png align="center")

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)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761599090219/f3d482d8-9939-46e7-825d-8b63093390d7.png align="center")

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:

```json
VPC CIDR: 10.0.0.0/16
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761599927594/bc1dc22b-319d-4564-b2e1-6896f0d08553.png align="center")

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

```json
0.0.0.0/0 → Internet Gateway
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761599889115/929d1bd2-8854-4c67-898b-f6ef5eddbd57.png align="center")

We associate the public subnets to this.

### Private Route Table

```json
0.0.0.0/0 → NAT Gateway
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761599893748/d454e0da-cda1-498a-b065-26e315c7f66a.png align="center")

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.](http://com.amazonaws.us)\[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`](http://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.ec`](http://com.amazonaws.us-east-1.ec)`2messages` |
| **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`](http://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`](http://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`](http://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**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761599198710/bb27e67d-168d-45e4-acbe-b7b064ed3d17.png align="center")

## **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:

```json
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761600087457/dd6306b3-73df-4c23-9d9c-cf6868866b04.png align="center")

Instance Role Policies Example:

```json
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.

```json
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:

```json
/app
  ├─ app.py
  └─ requirements.txt
```

### **Install Dependencies**

```json
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:

```json
python3 -m venv env
source env/bin/activate
pip install -r requirements.txt
```

App listens **privately**, example:

```json
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:

```json
sudo nano /etc/systemd/system/flask.service
```

```json
[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:

```json
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:** `/`
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761600215335/f88591dc-c5ec-4daa-bbc4-e63bdf629185.png align="center")

## **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 |

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761600651226/d7e8f1e6-100c-4d5a-80c1-4877062abf08.png align="center")

No direct EC2 exposure.

---

## **Step 12: Testing**

```json
curl http://<ALB-DNS>/
```

You should receive:

```python
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!
