PHI Encryption: At-Rest & In-Transit on AWS
Implement HIPAA-compliant PHI encryption on AWS: KMS for key management, S3 server-side encryption, RDS encryption, Secrets Manager, TLS everywhere, and audit trails with CloudTrail.
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.
# 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:
{
"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
# 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:
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.
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
# 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:
// 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 header// 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
# 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
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.