All Articles
HealthcareHIPAAHealthcare Engineering

HIPAA-Compliant AWS Infrastructure: The Reference Architecture for Healthcare Applications

AWS provides the building blocks for HIPAA-compliant infrastructure — VPCs, encrypted RDS, CloudTrail, GuardDuty. Knowing which services to use and how to configure them is the difference between infrastructure that passes a HIPAA audit and infrastructure that looks like it should but does not.

Gaurang Ghinaiya
Gaurang Ghinaiya

Founder & CEO

April 8, 2026
7 min read
HIPAA-Compliant AWS Infrastructure: The Reference Architecture for Healthcare Applications

AWS signs Business Associate Agreements (BAAs) for a specific set of services. That list covers most of what you need for a healthcare application. But signing a BAA and building compliant infrastructure are different things — a BAA means AWS is contractually obligated to protect ePHI in their environment; it does not mean your configuration of their services is compliant. That is your responsibility.

This post is the reference architecture we use for HIPAA-compliant healthcare applications on AWS, covering the services, configurations, and monitoring setup that together constitute a defensible infrastructure design.

Network architecture

VPC design

The foundational HIPAA-relevant network decision: no ePHI systems should be in a public subnet. The VPC architecture:

VPC (10.0.0.0/16)
├── Public subnets (10.0.0.0/24, 10.0.1.0/24)
│   └── Application Load Balancer (receives HTTPS from internet)
│   └── NAT Gateways (outbound internet for private subnet resources)
├── Private application subnets (10.0.10.0/24, 10.0.11.0/24)
│   └── ECS tasks / EC2 instances running the application
│   └── Lambda functions handling ePHI
└── Private data subnets (10.0.20.0/24, 10.0.21.0/24)
    └── RDS instances (encrypted)
    └── ElastiCache (if used for session state — no ePHI in cache)

Application servers in private subnets can initiate outbound connections (for API calls, dependency updates, etc.) through the NAT Gateway, but they cannot be reached from the internet directly. Only the ALB, in the public subnet, accepts inbound traffic, and it only forwards HTTPS traffic to the application layer.

Security groups

Security groups enforce the network-level access control. The principle: only allow the minimum necessary traffic.

# ALB security group — accepts HTTPS from anywhere, health check from Route 53
resource "aws_security_group" "alb" {
  name        = "healthcare-alb"
  description = "ALB accepts HTTPS from internet"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
}

# App security group — accepts from ALB only, talks to RDS
resource "aws_security_group" "app" {
  name   = "healthcare-app"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.rds.id]
  }

  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]   # for calls to external APIs (AWS services, etc.)
  }
}

# RDS security group — accepts only from app layer
resource "aws_security_group" "rds" {
  name   = "healthcare-rds"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
}

Database: RDS with encryption

resource "aws_db_instance" "main" {
  identifier        = "healthcare-db"
  engine            = "postgres"
  engine_version    = "15.4"
  instance_class    = "db.t3.medium"
  allocated_storage = 100
  storage_type      = "gp3"

  # HIPAA-required: encryption at rest
  storage_encrypted = true
  kms_key_id        = aws_kms_key.rds.arn

  # HIPAA-required: automated backups for recovery
  backup_retention_period = 7
  backup_window           = "03:00-04:00"

  # Multi-AZ for high availability
  multi_az = true

  # No public access — private subnet only
  publicly_accessible    = false
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  # Enable Performance Insights for operational visibility
  performance_insights_enabled = true

  # Enable deletion protection
  deletion_protection = true

  # Enable enhanced monitoring
  monitoring_interval = 60
  monitoring_role_arn = aws_iam_role.rds_monitoring.arn
}

S3 for ePHI storage

resource "aws_s3_bucket" "phi_documents" {
  bucket = "healthcare-phi-documents-\${var.environment}"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "phi_documents" {
  bucket = aws_s3_bucket.phi_documents.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_versioning" "phi_documents" {
  bucket = aws_s3_bucket.phi_documents.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_public_access_block" "phi_documents" {
  bucket = aws_s3_bucket.phi_documents.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Lifecycle policy — transition to Glacier after 90 days, retain 7 years
resource "aws_s3_bucket_lifecycle_configuration" "phi_documents" {
  bucket = aws_s3_bucket.phi_documents.id

  rule {
    id     = "phi-retention"
    status = "Enabled"

    transition {
      days          = 90
      storage_class = "GLACIER"
    }

    expiration {
      days = 2555  # 7 years
    }
  }
}

KMS key management

Separate KMS keys for each service that handles ePHI. Separate keys mean that a compromised application key does not expose the database encryption key, and you can rotate keys independently.

resource "aws_kms_key" "rds" {
  description             = "RDS encryption key for ePHI database"
  deletion_window_in_days = 30

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { AWS = "arn:aws:iam::\${var.account_id}:root" }
        Action    = "kms:*"
        Resource  = "*"
      },
      {
        Effect    = "Allow"
        Principal = { AWS = aws_iam_role.app.arn }
        Action    = ["kms:Decrypt", "kms:GenerateDataKey"]
        Resource  = "*"
        Condition = {
          StringEquals = { "kms:ViaService" = "rds.us-east-1.amazonaws.com" }
        }
      }
    ]
  })
}

resource "aws_kms_key_rotation" "rds" {
  key_id              = aws_kms_key.rds.id
  rotation_period_in_days = 365
}

CloudTrail: the HIPAA audit log for AWS API activity

CloudTrail records every AWS API call — who did what to which resource and when. For HIPAA, this covers the infrastructure-level audit trail: who modified security groups, who changed bucket policies, who accessed the KMS keys.

resource "aws_cloudtrail" "main" {
  name                          = "healthcare-audit-trail"
  s3_bucket_name                = aws_s3_bucket.cloudtrail_logs.id
  include_global_service_events = true
  is_multi_region_trail         = true
  enable_log_file_validation    = true   # detects log tampering

  event_selector {
    read_write_type           = "All"
    include_management_events = true

    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3:::healthcare-phi-documents-\${var.environment}/"]
    }

    data_resource {
      type   = "AWS::Lambda::Function"
      values = ["arn:aws:lambda"]
    }
  }

  # Encrypt CloudTrail logs at rest
  kms_key_id = aws_kms_key.cloudtrail.arn

  cloud_watch_logs_group_arn = "\${aws_cloudwatch_log_group.cloudtrail.arn}:*"
  cloud_watch_logs_role_arn  = aws_iam_role.cloudtrail_cloudwatch.arn
}

GuardDuty and Security Hub

GuardDuty provides threat detection — it analyzes VPC Flow Logs, DNS logs, and CloudTrail events to identify suspicious activity like unusual API calls, cryptocurrency mining, or exfiltration attempts. Security Hub aggregates findings from GuardDuty, Inspector, and Config into a centralized security posture view.

resource "aws_guardduty_detector" "main" {
  enable = true

  datasources {
    s3_logs { enable = true }
    kubernetes { audit_logs { enable = false } }
    malware_protection { scan_ec2_instance_with_findings { ebs_volumes { enable = true } } }
  }
}

resource "aws_securityhub_account" "main" {}

resource "aws_securityhub_standards_subscription" "hipaa" {
  standards_arn = "arn:aws:securityhub:::ruleset/hipaa-security-standard/v/1.0.0"
}

CloudWatch alarms for HIPAA-relevant events

HIPAA's audit controls require examining activity, not just recording it. CloudWatch alarms notify the security team of potentially significant events:

locals {
  security_alarms = {
    root_account_usage = {
      filter_pattern  = "{ $.userIdentity.type = \"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != \"AwsServiceEvent\" }"
      alarm_name      = "RootAccountUsage"
      alarm_desc      = "Root account was used — investigate immediately"
    }
    console_signin_failures = {
      filter_pattern  = "{ ($.eventName = ConsoleLogin) && ($.errorMessage = \"Failed authentication\") }"
      alarm_name      = "ConsoleLoginFailures"
      alarm_desc      = "Multiple console login failures — potential brute force"
    }
    security_group_changes = {
      filter_pattern  = "{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) }"
      alarm_name      = "SecurityGroupChanges"
      alarm_desc      = "Security group rule changed — verify this was authorized"
    }
    s3_bucket_policy_changes = {
      filter_pattern  = "{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }"
      alarm_name      = "S3BucketPolicyChanges"
      alarm_desc      = "S3 bucket policy modified — verify this was authorized"
    }
  }
}

IAM least-privilege

IAM role permissions for the application service — the minimum required to run:

resource "aws_iam_role_policy" "app" {
  name = "healthcare-app-policy"
  role = aws_iam_role.app.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
        Resource = "\${aws_s3_bucket.phi_documents.arn}/*"
      },
      {
        Effect   = "Allow"
        Action   = ["kms:Decrypt", "kms:GenerateDataKey"]
        Resource = [aws_kms_key.rds.arn, aws_kms_key.s3.arn]
      },
      {
        Effect   = "Allow"
        Action   = ["secretsmanager:GetSecretValue"]
        Resource = "arn:aws:secretsmanager:\${var.region}:\${var.account_id}:secret:healthcare/*"
      },
      {
        Effect   = "Allow"
        Action   = ["ses:SendEmail", "ses:SendRawEmail"]
        Resource = "arn:aws:ses:\${var.region}:\${var.account_id}:identity/*"
        Condition = {
          StringEquals = { "ses:Recipients": ["@\${var.internal_domain}"] }
        }
      }
    ]
  })
}

Notice what is not in this policy: no IAM management permissions, no ability to modify security groups, no ability to access other S3 buckets, no ability to access KMS keys for other services. The application can only do what it needs to do. This is least-privilege in practice, and it limits the blast radius of an application-level compromise significantly.

Building this infrastructure from scratch takes time to get right, and the details matter for HIPAA compliance. If you are planning a healthcare application on AWS and want a reference architecture review before you start building, we can help you evaluate your design against the requirements that auditors actually check.

Related service

Healthcare Software Development

HIPAA-compliant platforms, EMR integration, and care coordination tools for US home health agencies.

Learn more

Written by

Gaurang Ghinaiya
Gaurang Ghinaiya

Founder & CEO

Gaurang Ghinaiya is the Founder & CEO of Nexios Technologies. He is passionate about building innovative software solutions that drive business growth. With years of experience in technology leadership, he guides teams toward excellence.

Let's talk

Have a project in mind?

Tell us about your project below, or pick another way to reach us. Average response time: under 4 business hours.