HIPAA-Compliant Healthcare Cloud · Lesson 2 of 6
PHI Encryption: At-Rest & In-Transit on AWS
PHI Encryption on AWS
HIPAA requires that Protected Health Information (PHI) be encrypted at rest and in transit. AWS provides the building blocks; this article shows how to wire them together correctly.
The Two Encryption Requirements
At Rest: PHI stored in databases, S3, EBS volumes must be encrypted
In Transit: PHI moving between services or to/from users must be over TLS
Common HIPAA violations:
✗ Unencrypted S3 bucket with lab results
✗ Database with encryption disabled
✗ HTTP endpoint (no TLS) for patient portal
✗ Storing encryption keys in application code or environment variablesAWS KMS — Key Management
AWS Key Management Service (KMS) manages your encryption keys. You never see the raw key material.
Bash
# Create a customer-managed key for PHI
aws kms create-key \
--description "PHI encryption key" \
--key-usage ENCRYPT_DECRYPT \
--key-spec SYMMETRIC_DEFAULT \
--tags TagKey=purpose,TagValue=phi-encryption
# Create an alias
aws kms create-alias \
--alias-name alias/phi-key \
--target-key-id <key-id>Key policy — restrict who can use the key:
JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Allow healthcare app to use key",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789:role/HealthcareAppRole"
},
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "*"
}
]
}S3 — Encrypting Medical Files
Bash
# Create an S3 bucket with server-side encryption enforced
aws s3api create-bucket \
--bucket my-phi-bucket \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1
# Enable SSE-KMS (server-side encryption with KMS key)
aws s3api put-bucket-encryption \
--bucket my-phi-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "alias/phi-key"
},
"BucketKeyEnabled": true
}]
}'
# Block all public access
aws s3api put-public-access-block \
--bucket my-phi-bucket \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
# Enforce HTTPS only (deny HTTP)
aws s3api put-bucket-policy \
--bucket my-phi-bucket \
--policy '{
"Statement": [{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-phi-bucket/*", "arn:aws:s3:::my-phi-bucket"],
"Condition": {"Bool": {"aws:SecureTransport": "false"}}
}]
}'Using from .NET:
C#
var s3 = new AmazonS3Client();
// Upload — encryption happens automatically at S3
await s3.PutObjectAsync(new PutObjectRequest
{
BucketName = "my-phi-bucket",
Key = $"patients/{patientId}/records/{fileName}",
InputStream = fileStream,
ContentType = "application/pdf"
// SSE-KMS applied automatically by bucket policy
});
// Generate a pre-signed URL (expires in 15 minutes)
var url = await s3.GetPreSignedURLAsync(new GetPreSignedUrlRequest
{
BucketName = "my-phi-bucket",
Key = $"patients/{patientId}/records/{fileName}",
Expires = DateTime.UtcNow.AddMinutes(15),
Protocol = Protocol.HTTPS // always HTTPS
});RDS — Encrypted Database
Enable encryption when creating the RDS instance — it cannot be enabled after creation without a snapshot restore.
Bash
aws rds create-db-instance \
--db-instance-identifier phi-database \
--db-instance-class db.t3.medium \
--engine postgres \
--master-username admin \
--master-user-password "$(aws secretsmanager get-random-password --query 'RandomPassword' --output text)" \
--storage-encrypted \
--kms-key-id alias/phi-key \
--no-publicly-accessible \
--vpc-security-group-ids sg-xxxxxxxx \
--db-subnet-group-name my-private-subnet-groupKey flags:
--storage-encrypted— enables encryption at rest--kms-key-id— use your customer-managed KMS key--no-publicly-accessible— database is only accessible from your VPC
Secrets Manager — No Credentials in Code
Bash
# Store database credentials
aws secretsmanager create-secret \
--name "phi-db-credentials" \
--secret-string '{
"username": "app_user",
"password": "...",
"host": "phi-database.xxx.rds.amazonaws.com",
"port": 5432,
"dbname": "healthapp"
}'
# Enable automatic rotation (every 30 days)
aws secretsmanager rotate-secret \
--secret-id phi-db-credentials \
--rotation-rules AutomaticallyAfterDays=30Retrieve in .NET using Managed Identity:
C#
// Use SDK — no credentials needed when running on EC2/ECS with an IAM role
var secretsManager = new AmazonSecretsManagerClient();
var response = await secretsManager.GetSecretValueAsync(new GetSecretValueRequest
{
SecretId = "phi-db-credentials"
});
var creds = JsonSerializer.Deserialize<DbCredentials>(response.SecretString)!;
var connectionString = $"Host={creds.Host};Database={creds.Dbname};" +
$"Username={creds.Username};Password={creds.Password}";TLS Everywhere — Encryption in Transit
Internal services:
• RDS: set require_ssl = 1 in parameter group
• ElastiCache: enable in-transit encryption when creating the cluster
• S3: bucket policy denies non-HTTPS requests (already set above)
• Service-to-service: use VPC private endpoints, no public internet
External (user-facing):
• ALB/API Gateway: TLS 1.2 minimum, TLS 1.3 preferred
• Certificates: ACM (AWS Certificate Manager) — free, auto-renewed
• HSTS: enforce HTTPS in browser via headerC#
// In ASP.NET Core — enforce HTTPS
app.UseHsts();
app.UseHttpsRedirection();
// Configure TLS in Kestrel
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(https =>
{
https.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
});
});CloudTrail — Audit Every KMS Key Use
Bash
# Enable CloudTrail for all regions
aws cloudtrail create-trail \
--name phi-audit-trail \
--s3-bucket-name my-cloudtrail-bucket \
--is-multi-region-trail \
--enable-log-file-validation \
--kms-key-id alias/phi-key # encrypt the audit logs too
aws cloudtrail start-logging --name phi-audit-trailCloudTrail records every KMS Decrypt call — meaning you can see exactly who decrypted which PHI and when.
HIPAA Encryption Checklist
Storage:
☐ S3 buckets: SSE-KMS enabled, public access blocked, HTTPS enforced
☐ RDS: encryption enabled at creation, using customer KMS key
☐ EBS volumes: encrypted (set account default)
☐ Backups: RDS automated backups inherit encryption
Transit:
☐ ALB/API Gateway: TLS 1.2+, certificate from ACM
☐ RDS: SSL required for all connections
☐ S3: HTTPS-only bucket policy
☐ Internal services: within VPC, private subnets
Keys:
☐ Customer-managed KMS key for PHI (not AWS-managed)
☐ Key policy restricts access to least privilege
☐ No credentials in code, environment variables, or repos
☐ All secrets in Secrets Manager with rotation enabled
Audit:
☐ CloudTrail enabled in all regions with log file validation
☐ CloudTrail logs encrypted and shipped to immutable S3 bucket
☐ KMS key usage logged (automatic with CloudTrail data events)Key Takeaways
- KMS customer-managed keys give you full control and audit trail of every encryption/decryption operation
- S3 encryption is meaningless without also blocking public access and enforcing HTTPS
- RDS encryption must be set at creation — you cannot enable it on an existing unencrypted instance
- Secrets Manager eliminates credentials from code and configs — use IAM roles to authorize access
- CloudTrail + KMS data events creates the audit trail HIPAA requires: who accessed what PHI and when