Back to blog
healthcarebeginner

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.

Asma HafeezApril 17, 20265 min read
healthcarehipaaawsencryptionsecurityphi
Share:𝕏

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 variables

AWS 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-group

Key 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=30

Retrieve 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 header
C#
// 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-trail

CloudTrail 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

  1. KMS customer-managed keys give you full control and audit trail of every encryption/decryption operation
  2. S3 encryption is meaningless without also blocking public access and enforcing HTTPS
  3. RDS encryption must be set at creation — you cannot enable it on an existing unencrypted instance
  4. Secrets Manager eliminates credentials from code and configs — use IAM roles to authorize access
  5. CloudTrail + KMS data events creates the audit trail HIPAA requires: who accessed what PHI and when

Enjoyed this article?

Explore the learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.